КулЛиб - Классная библиотека! Скачать книги бесплатно 

С# 2005 для "чайников" [Стефан Рэнди Дэвис] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]
ББК 32.973.26-018.2.75
Д94
УДК 681.3.07
Компьютерное издательство "Диалектика"
Зав. редакцией С.Н. Тригуб
Перевод с английского канд. техн. наук И.В. Красикова, А.А. Мраморнова
Под редакцией канд. техн. наук И.В. Красикова
По общим вопросам обращайтесь в издательство "Диалектика" по адресу:
info@dialektika.com,
http://www.dialektika.com
115419, Москва, а/я 783; 03150, Киев, а/я 152
Дэвис, Стефан Рэнди, Сфер, Чак.
Д94
С# 2005 для "чайников".: Пер. с англ. — М . : ООО "И.Д. Вильяме", 2008. — 576 с . :
ил. — Парал. тит. англ.
ISBN 978-5-8459-1068-4 (рус.)
Даже если вы никогда не имели дела с программированием, эта книга поможет вам
освоить с нуля язык С#. Вы сможете писать на нем программы любой степени сложно­
сти. Если вы уже знакомы с каким-либо иным языком программирования, тогда процесс
изучения С# только упростится, но наличие опыта программирования — условие совер­
шенно необязательное.
Книга познакомит вас не только с типами, конструкциями и операторами языка С#,
но и с ключевыми концепциями объектно-ориентированного программирования, реали­
зованными в этом языке, который в настоящее время представляет собой один из наибо­
лее приспособленных для создания программ для Windows-среды.
Если вы в начале большого пути в программирование — смелее покупайте эту книгу:
она послужит вам отличным путеводителем, который облегчит вам первые шаги на этом
длинном, но очень увлекательном пути.
ББК 32.973.26-018.2.75
Все названия программных продуктов являются зарегистрированными торговыми марками соответст­
вующих фирм.
Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было
форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирова­
ние и запись на магнитный носитель, если на это нет письменного разрешения издательства JOHN
WILEY&Sons, Inc.
Copyright © 2008 by Dialektika Computer Publishing.
Original English language edition Copyright © 2006 by Wiley Publishing, Inc., Indianapolis, Indiana.
All rights reserved including the right of reproduction in whole or in part in any form. This translation is pub­
lished by arrangement with Wiley Publishing, Inc.
Wiley, the Wiley Publishing logo, For Dummies, the Dummies Man logo, A Reference for the Rest of Us!, The
Dummies Way, Dummies Daily, The Fun and Easy Way, Dummies.com, and related trade dress are trademarks or
registered trademarks of John Wiley & Sons, b e , and/or its affiliates in the United States and other countries, and
may not be used without written permission. All other trademarks are the property of their respective owners. Wiley
Publishing, Inc., is not associated with any product or vendor mentioned in this book.
ISBN 978-5-8459-1068-4 (рус)

©

ISBN 0-7645-9704-3 (англ.)

перевод, оформление, макетирование
© by Wiley Publishing, Inc., 2006

Компьютерное изд-во "Диалектика", 2008,

Оглавление
7

Об авторах

1

Введение

19

Часть I. Создание ваших первых программ на С#

27

Глава 1. Создание вашей первой Windows-программы на С#

29

Глава 2. Создание консольного приложения на С#

47

Часть II. Основы программирования в С#

55

Глава 3. Объявление переменных-значений

57

Глава 4. Операторы

73

Глава 5. Управление потоком выполнения

85

Часть III. Объектно-основанное программирование

из

Глава 6. Объединение данных — классы и массивы

115

Глава 7. Функции функций

141

Глава 8. Методы класса

177

Глава 9. Работа со строками в С#

199

Часть IV. Объектно-ориентированное программирование

223

Глава 10. Что такое объектно-ориентированное программирование

225

Глава 11. Классы

231

Глава 12. Наследование

261

Глава 13. Полиморфизм

283

Часть V. За базовыми классами

309

Глава 14. Интерфейсы и структуры

311

Глава 15. Обобщенное программирование

339

Часть VI. Великолепные д е с я т к и

373

Глава 16. Десять наиболее распространенных ошибок компиляции

375

Глава 17. Десять основных отличий С# и С++

385

Часть VII. Д о п о л н и т е л ь н ы е главы

391

Глава 18. Эти исключительные исключения

393

Глава 19. Работа с файлами и библиотеками

419

Глава 20. Работа с коллекциями

445

Глава 2 1 . Использование интерфейса Visual Studio

487

Глава 22. С# по дешевке

525

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

565

6

Оглавление

Содержание
Об авторах

17

Введение

19

Часть I. Создание ваших первых программ на С#

27

Глава 1. Создание вашей первой Windows-программы на С#

29

Введение в машинные языки, С# и платформу .NET
Что такое программа?
Что такое С#?
Что такое .NET?
Что такое Visual Studio 2005? Visual С#?
Создание Windows-приложения на языке С#
Создание шаблона
Компиляция и запуск вашей первой программы Windows Forms
Украшение программы
Учим форму трудиться
Проверка конечного продукта
Программисты на Visual Basic 6.0, берегитесь!

29
30
30
31
32
32
33
36
37
42
43
44

,

Глава 2. Создание консольного приложения на С#

47

Создание шаблона консольного приложения
Создание исходной программы
Пробная поездка
Создание реального консольного приложения
Изучение шаблона консольного приложения
Схема программы
Комментарии
Тело программы

47
47
49
49
51
51
51
52

Часть II. Основы программирования в С#

55

Глава 3. Объявление переменных-значений

57

Объявление переменной
Что такое int
Правила объявления переменных
Вариации на тему int
Представление дробных чисел
Работа с числами с плавающей точкой
Объявление переменной с плавающей точкой
Более точное преобразование температур

57
58
59
59
60
61
62
63

Ограничения переменных с плавающей точкой
Десятичные числа — комбинация'целых и чисел с плавающей точкой
Объявление переменных типа decimal
Сравнение десятичных, целых чисел и чисел с плавающей точкой
Логичен ли логический тип?
Символьные типы
Тип char
.
Специальные символы
Тип string
Что такое тип-значение?
Сравнение string и char
Объявление числовых констант
Преобразование типов

63
64
64
65
65
66
66
66
67
67
68
69
70

Глава 4. Операторы

73

Арифметика
Простейшие операторы
Порядок выполнения операторов
Оператор присваивания
Оператор инкремента
Логично ли логическое сравнение?
Сравнение чисел с плавающей точкой
Составные логические операторы
Тип выражения
Вычисление типа операции
Типы при присваивании
Немного экзотики — тернарный оператор

73
73
74
75
76
77
78
79
80
80
82
83

Глава 5. Управление потоком выполнения

85

Управление потоком выполнения
Оператор if
Инструкция else
Как избежать else
Вложенные операторы if

86
86
89
90
90

Циклы
Цикл while
Цикл do...while
Операторы break и continue
Цикл без счетчика
Правила области видимости
Цикл for
Пример
Зачем нужны разные циклы
Вложенные циклы
Конструкция switch
Оператор goto

8

93
93
98
98
99
103
104
104
105
106
109
111

Содержание

Часть III. Объектно-основанное программирование

и з

Глава 6. Объединение данных — классы и массивы

115

Классы
Определение класса
Что такое объект
Доступ к членам объекта
Ссылки
Классы, содержащие классы
Статические члены класса
Определение константных членов-данных

115
116
117
117
120
122
123
124

Массивы С#
Зачем нужны массивы
Массив фиксированного размера
Массив переменного размера
Массивы объектов
Конструкция foreach
Сортировка массива объектов

124
125
125
127
130
133
134

Глава 7. Функции функций

141

Определение и использование функции
Использование функций в ваших программах
Аргументы функции
Передача аргументов функции

141
143
149
150

Передача функции нескольких аргументов
Соответствие определений аргументов их использованию
Перегрузка функции
Реализация аргументов по умолчанию
Передача в функцию типов-значений
Возврат значений из функции
Возврат значения оператором return
Возврат значения посредством передачи по ссылке
Когда какой метод использовать
Определение функции без возвращаемого значения
Передача аргументов в программу
Передача аргументов из приглашения DOS
Передача аргументов из окна
Передача аргументов в Visual Studio 2005

150
152
153
154
156
162
162
163
163
166
167
169
170
173

Глава 8. Методы класса

177

Передача объекта в функцию
Определение функций объектов и методов
Определение функций — статических членов
Определение метода
Полное имя метода
Обращение к текущему объекту
Ключевое слово this

177
179
179
181
182
183
185

Содержание

9

Когда t h i s используется явно
Что делать при отсутствии this
Помощь от Visual Studio — автоматическое завершение
Справка по встроенным функциям системной библиотеки
Помощь при использовании ваших собственных функций и методов
Внесение дополнений в справочную систему
Генерация XML-документации

185
188
190
191
192
193
197

Глава 9. Работа со строками в С#

199

Основные операции над строками
Объединение неразделимо!
Сравнение строк
Сравнение без учета регистра
Использование конструкции switch
Считывание ввода пользователя
Разбор числового ввода
Обработка последовательности чисел
Управление выводом программы
Использование методов Trim() и Pad()
Использование функции конкатенации
Использование функции Split()
Форматирование строки

200
200
201
205
205
206
207
210
212
212
215
217
218

Часть IV. Объектно-ориентированное программирование

223

Глава 10. Что такое объектно-ориентированное программирование

225

Объектно-ориентированная концепция №1 — а б с т р а к ц и я
Приготовление блюд с помощью функций
Приготовление "объектно-ориентированных" блюд
Объектно-ориентированная концепция №2 — классификация
Зачем нужна классификация
Объектно-ориентированная концепция №3 — удобный интерфейс
Объектно-ориентированная концепция №4 — управление доступом
Поддержка объектно-ориентированных концепций в С#

225
226
226
227
227
228
229
229

Глава 11. Классы

231

Ограничение доступа к членам класса
Пример программы с использованием открытых членов
Прочие уровни безопасности
Зачем нужно управление доступом
Методы доступа
Пример управления доступом
Выводы
Определение свойств класса
Конструирование объектов посредством конструкторов
Конструкторы, предоставляемые С#

231
232
235
235
236
237
242
242
244
244

10

Содержание

Конструктор по умолчанию
Создание объектов
Выполнение конструктора в отладчике
Непосредственная инициализация объекта — конструктор по умолчанию
Конструирование с инициализаторами
Перегрузка конструкторов
(
Устранение дублирования конструкторов
Фокусы с объектами

246
247
249
252
252
253
256
260

Глава 12. Наследование

261

Наследование класса
Зачем нужно наследование
Более сложный пример наследования
ЯВЛЯЕТСЯ или СОДЕРЖИТ
Отношение ЯВЛЯЕТСЯ
Доступ к BankAccount через содержание
Отношение СОДЕРЖИТ
Когда использовать отношение ЯВЛЯЕТСЯ, а когда — СОДЕРЖИТ
Поддержка наследования в С#
Изменение класса
.
Неверное преобразование времени выполнения
Ключевые слова is и as
Наследование и конструктор
Вызов конструктора по умолчанию базового класса
Передача аргументов конструктору базового класса
Обновленный класс BankAccount
Деструктор

261
263
264
267
267
268
269
270
270
270
271
272
274
274
276
278
281

Глава 13. Полиморфизм

283

Перегрузка унаследованного метода
Простейший случай перегрузки функции
Различные классы, различные методы
Сокрытие метода базового класса
Вызов методов базового класса
Полиморфизм
Что неверно в стратегии использования объявленного типа
Использование is для полиморфного доступа к скрытому методу
Объявление метода виртуальным
Абстракционизм в С#
Разложение классов
Голая концепция, выражаемая абстрактным классом
Как использовать абстрактные классы
Создание абстрактных объектов невозможно
Создание иерархии классов
Опечатывание класса

283
284
284
285
289
291
292
293
294
297
297
302
302
304
304
308

Содержание

11

309

Часть V. За базовыми классами
Глава 14. Интерфейсы и структуры

311

Что значит МОЖЕТ_ИСПОЛЬЗОВАТЬСЯ_КАК
Что такое интерфейс
Краткий пример
Пример программы, использующей отношение МОЖЕТ_ИСПОЛЬЗОВАТЬСЯ_КАК
Создание собственного интерфейса
Предопределенные интерфейсы
Сборка воедино
Наследование интерфейса
Абстрактный интерфейс
Структуры С# и их отличия от классов
Структуры С#
Конструктор структуры
Методы структур
Пример применения структуры
Унификация системы типов
Предопределенные типы структур
Унификация системы типов с помощью структур
Упаковка типов-значений

311
312
313
315
315
316
317
323
323
326
327
329
329
330
333
333
334
337

Глава 15. Обобщенное программирование

339

Необобщенные коллекции
Необобщенные коллекции
Использование необобщенных коллекций
Обобщенные классы
Обобщенные классы безопасны
Обобщенные классы эффективны
Использование обобщенных коллекций
Понятие
Использование List
Создание собственного обобщенного класса
Очередь с приоритетами
Распаковка пакета
Функция Main()
Написание обобщенного кода
Обобщенная очередь с приоритетами
Незавершенные дела
Обобщенные методы
Обобщенные методы в необобщенных классах
Обобщенные методы в обобщенных классах
Ограничения для обобщенного метода
Обобщенные интерфейсы
Обобщенные и необобщенные интерфейсы
Использование (необобщенной) фабрики классов
Построение обобщенной фабрики

340
340
341
343
343
344
344
345
345
347
348
352
353
355
356
358
360
362
363
363
364
364
365
366

12

Содержание

Объявление пространств имен
Важность пространств имен
Доступ к классам с использованием полностью квалифицированных имен
Директива using
Использование полностью квалифицированных имен
Объединение классов в библиотеки
Создание проекта библиотеки классов
Создание классов для библиотеки
Создание проекта драйвера

Хранение данных в файлах
Использование StreamWriter
Повышение скорости чтения с использованием StreamReader

422
424
425
426
427
430
430
431
432
434
435
440

Г л а в а 20. Работа с к о л л е к ц и я м и

445

Обход каталога файлов
Написание собственного класса коллекции: связанный список
Пример связанного списка
Зачем нужен связанный список
Обход коллекций: итераторы
Доступ к коллекции: общая задача
Использование foreach
Обращение к коллекциям как к массивам: индексаторы
Формат индексатора
Пример программы с использованием индексатора
Блок итератора
Итерация месяцев
Что такое коллекция
Синтаксис итератора
Блоки итераторов произвольного вида и размера
Где надо размещать итераторы

445
451
452
461
461
462
464
465
465
465
469
473
474
475
476
479

Г л а в а 21. И с п о л ь з о в а н и е интерфейса Visual Studio

487

Настройка расположения окон
Состояния окон
Скрытие окна
Перестановка окон
Наложение окон
Модные штучки
Работа с Solution Explorer
Упрощение жизни с помощью проектов и решений
Отображение проекта
Добавление класса
Завершение демонстрационной программы
Преобразование классов в программу
Как должен выглядеть код
Помогите мне!
F1

487
488
490
490
491
493
493
494
495
497
498
501
502
506
506

14

Содержание

Предметный указатель
Поиск
Дополнительные возможности
Автоперечисление членов
Отладка
Жучки в программе: а дустом не пробовали?
Пошаговая отладка
Главное — вовремя остановиться
Стек вызовов
Я сделал это!

507
509
510
511
512
512
514
517
520
523

Глава 22. С# по дешевке

525

Работа без сети — но не без платформы .NET
Получение бесплатных компонентов
Обзор цикла разработки
Программирование на С# в программе SharpDevelop
Изучение SharpDevelop
Сравнение возможностей SharpDevelop и Visual Studio
Получение справочной информации
Настройка программы SharpDevelop
Добавление инструмента для запуска отладчика
Запуск отладчика из SharpDevelop
Отсутствующие возможности отладчика
Программирование на С# в TextPad
Создание класса документов . CS для языка С#
Добавление собственных инструментов: Build С# Debug
Настройка инструмента для компиляции финальной версии
Объяснение опций настройки инструментов Debug и Release
Работа над ошибками компиляции
Настройка остальных инструментов
Тестирование с помощью программы NUnit
Запуск программы NUnit
Тестирование
Написание тестов NUnit
Исправление ошибок в проверяемой программе
Написание исходного текста Windows Forms без Form Designer
Это всего лишь код
Работа в стиле визуального инструмента
Частичные классы
Самостоятельное написание
Убедитесь, что пользователи смогут запустить вашу программу
Visual Studio для бедных

526
526
527
528
528
529
530
531
531
532
534
534
537
538
540
541
545
545
548
548
549
550
557
559
559
560
561
562
563
564

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

565

Содержание

15

Часть VI. Великолепные д е с я т к и

373

Глава 16. Десять наиболее распространенных ошибок компиляции

375

The name 'memberName' does not exist in the class or namespace 'className'
Cannot implicitly convert type 'x' into 'y'
'className.memberName' is inaccessible due to its protection level
Use of unassigned local variable 'n'
Unable to copy the file 'programName.exe' to 'programName.exe'. The process cannot...
'subclassName.methodName' hides inherited member 'baseclassName.methodName'.
Use the new keyword if hiding was intended
'subclassName' : cannot inherit from sealed class 'baseclassName'
'className' does not implement interface member 'methodName'
'methodName' : not all code paths return a value
} expected

375
377
379
380
380

Глава 17. Десять основных отличий С# и С++

385

Отсутствие глобальных данных и функций
Все объекты размещаются вне кучи
Переменные-указатели запрещены
Обобщенные классы С# и шаблоны С++
Никаких включаемых файлов

386
386
387
387
388

Не конструирование, а инициализация
Корректное определение типов переменных
Нет множественного наследования
Проектирование хороших интерфейсов
Унифицированная система типов

388
389
389
389
389

Часть VII. Д о п о л н и т е л ь н ы е главы

391

Глава 18. Эти исключительные исключения

393

381
382
383
383
384

Старый способ обработки ошибок
Возврат индикатора ошибки
Чем плохи коды ошибок
Использование механизма исключений для сообщения об ошибках
Пример
Создание собственного класса исключения
Использование нескольких catch-блоков
Как исключения протекают сквозь пальцы
Регенерация исключения
Как реагировать на исключения
Перекрытие класса Exception

393
395
398
400
402
405
406
408
411
412
413

Глава 19. Работа с файлами и библиотеками

419

Разделение одной программы на несколько исходных файлов
Разделение единой программы на сборки
Объединение исходных файлов в пространства имен

419
421
422

Содержание

13

Стефан Р. Дэвис (Stephen R. Davis) (более известный по второму имени — Ренди)
живет со своей женой и сыном недалеко от Далласа, штат Техас. Он и его семейство на­
писали множество книг, включая С + + для чайников ( С + + For Dummies) и С + + Weekend
Crash Course. Стефан работает в фирме L-3 Communications.
Чак С ф е р (Chuck Sphar) ушел из подразделения Microsoft, работающего над до­
кументацией по языку С++, в 1997 году после шести лет тяжелой работы главным тех­
ническим писателем. Две его последние публикации были посвящены объектноориентированному программированию для Мае и библиотеке классов M F C . В настоя­
щее время он заканчивает роман о древнем Риме ( a g a i n s t r o m e . c o m ) и работает
с программированием в среде .NET. Пожелания и мелкие замечания можно отсылать
Чаку по адресу c s h a r p @ c h u c k s p h a r . com.

Пэм ( Р а т ) и маме — Чак Сфер.

Я хотел бы поблагодарить Клодет Мур (Claudette Moore) и Дебби Маккенна (Debbie
McKenna), которые заставили меня написать эту книгу. Я также хочу поблагодарить
Ренди Дэвиса (Randy Davis) за его готовность передать своего "младенца" парню, кото­
рого он не знал. Я считаю, что это очень тяжело, и надеюсь, что был достаточно коррек­
тен, дополняя и расширяя первое издание его превосходной книги.
Должен также выразить благодарность прекрасным людям в издательстве Wiley, и в ча­
стности редактору Кейти Фелтман (Katie Feltman) и редактору проекта Киму Даросетту
(Kim Darosett). Ким сумел поддержать меня в новой ипостаси — автора для чайников.
Я также хотел бы поблагодарить Криса Боуера (Chris Bower) за его техническую консуль­
тацию и превосходное знание языка С#, Джона Эдвардса (John Edwards), которому книга
обязана целостностью и согласованностью, а также художникам, специалистам по рекламе
и другим людям, создавшим из моих файлов реальную книгу.
Выражаю сердечную благодарность Пэм за ее постоянную поддержку и помощь
(much enabling). Она мне помогает во всем.
Чак Сфер.

Издательский дом "Вильяме" благодарит Ерофеева Сергея и Кущенко Сергея за боль­
шой вклад в подготовку издания книги.

Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше
мнение и хотим знать, что было сделано нами правильно, что можно было сделать лучше
и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые дру­
гие замечания, которые вам хотелось бы высказать в наш адрес.
Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумаж­
ное или электронное письмо, либо просто посетить наш Web-сервер и оставить свои за­
мечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравит­
ся или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши книги
более интересными для вас.
Посылая письмо или сообщение, не забудьте указать название книги и ее авторов,
а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обязатель­
но учтем его при отборе и подготовке к изданию последующих книг. Наши координаты:
E-mail: inf o @ w i l l i a m s p u b l i s h i n g . com
WWW:

h t t p : //www. w i l l i a m s p u b l i s h i n g . c o m

Информация для писем из:
России:
115419, Москва, а/я 783
Украины: 03150, Киев, а/я 152

Введение
Язык программирования С# является мощным, относительно новым потомком более
ранних языков С, С++ и Java. Программирование на нем доставляет много удовольствия,
в чем можно будет убедиться при работе с этой книгой.
Язык С# был создан Microsoft как главная часть ее инициативы .NET. Возможно, из
соображений политики компания Microsoft направила спецификации языка С# в комитет
международных стандартов ассоциации ЕСМА (произносится как "эк-ма") летом 2000
года, задолго до внедрения платформы .NET. Теоретически любая компания может раз­
работать свою собственную версию языка С#, предназначенную для работы в любой
операционной системе и на любой машине, большей, чем калькулятор.
Когда вышло первое издание настоящей книги, компилятор языка С# Microsoft был
единственным, и ее инструментальный пакет Visual Studio .NET предлагал единственную
возможность программирования на языке С#. С тех пор, однако, Visual Studio претерпел
два существенных изменения — появилась версия Visual Studio 2003 и, совсем недавно,
Visual Studio 2005. И, по крайней мере, еще два игрока вступили в игру С#.
В настоящее время можно писать и компилировать программы на языке С# на мно­
жестве машин, работающих под управлением Unix, при помощи реализаций Mono или
Portable .NET платформы .NET и языка С#.
Mono (www. g o - m o n o . com) является программным проектом с открытым исход­
ным кодом, финансируемым компанией Novell Corporation. Версия 1.1.8 вышла
В июне 2005 года. Хотя проект Mono и отстает от платформы .NET компании Mi­
crosoft (версию 1.1 Microsoft выпустила пару лет назад), он быстро развивается.
Проект Portable .NET фирм Southern Storm Software и DotGNU (www. d o t g n u . o r g /
p n e t . h t m l ) также является проектом с открытым исходным кодом. Во время
написания этой книги текущей версией проекта Portable .NET была 0.7.0.
Оба проекта предназначены для выполнения программ С# в Windows и различных
операционных системах семейства Unix, включая Linux и Macintosh компании Apple. Ко­
гда писалась эта книга, проект Portable .NET работал на большем количестве платформ,
в то время как проект Mono гордится более полной реализацией платформы .NET. Так
что выбор между ними может быть затруднен, в зависимости от вашего проекта, плат­
формы и целей. (Книги по программированию для этих платформ уже становятся дос­
тупны. Посетите сайт www. a m a z o n . com.)
Программное обеспечение с открытым исходным кодом создается сотрудни­
чающими группами программистов-добровольцев и обычно является бесплат­
ным для всех.
Переносимость языка С# и других языков платформы .NET выходит далеко за рамки
настоящей книги. Но можно ожидать, что в течение нескольких лет программы С# для
Windows, которые можно научиться создавать по этой книге, будут работать на различ­
ном аппаратном обеспечении и для всех типов операционных систем, что соответствует
требованиям для языка Java компании Sun Microsystems — о работе на любой машине.
Это, несомненно, хорошая вещь, даже для Microsoft. Переносимость — вопрос, над ко-

торым в настоящее время идет интенсивная работа, так что нет никаких сомнений, что
все трудности и препятствия на пути к истинной универсальной переносимости языка С#
будут преодолены. Но этот путь является уже не только путем Microsoft.
Однако в настоящий момент пакет Visual Studio компании Microsoft содержит наибо­
лее развитые версии языка С# и платформы .NET, а также набор инструментов к ним с
богатыми возможностями для программирования.
Если вам нужен только С#, то в одной из дополнительных глав вы узнаете, как
практически бесплатно написать код С#. (Вы потеряете множество удобств,
включая отличные средства визуального дизайна, обеспечиваемые Visual Studio
2005, но сможете создавать код Windows и без них — в особенности такой про­
стой код, как рассматриваемый в этой книге.)

Хотя в версию 2.0 языка С# был внесен ряд изменений, в основном С# остается прак­
тически таким же, как и в предыдущей версии. В этой книге рассматриваются следую­
щие значительные нововведения.
S

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

S

Обобщенное программирование является важным нововведением! Новые воз­
можности позволяют создавать более обобщенный и гибкий код, что является
мечтой любого программиста. Из главы 15, "Обобщенное программирование", вы
узнаете, как создавать более простой код с улучшенной безопасностью типов
с помощью обобщенного программирования.

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

Цель книги заключается в объяснении языка программирования С#, но для реального
написания программ необходима специальная среда кодирования. Мы уверены, что
большинство читателей будет использовать Microsoft Visual Studio, хотя предусмотрены
и другие варианты. Основывая книгу на Visual Studio, мы попытались сделать долю Visu­
al Studio минимально необходимой. Можно было бы просто сказать: "Запускайте свою
программу каким угодно образом", но вместо этого мы говорим: "Запускайте свою про­
грамму С# в Visual Studio нажатием клавиши ". Мы хотим, чтобы вы могли сосре­
доточиться на самом языке С#, а не на том, как работают простые вещи.
Вполне понятно, что многие читатели, если не большинство, захотят использовать
С# для создания графических приложений Windows, поскольку язык С# является мощ­
ным средством разработки подобных программ, но это всего лишь одна из областей

20

Введение

применения С#. Данная же книга должна в первую очередь обращать внимание на С#,
как на язык программирования. Графические программы Windows будут кратко рас­
смотрены в первой главе, но вы должны хорошо понять основы С#, прежде чем пере­
ходить к программированию для Windows. Также вполне понятно, что некоторые
опытные пользователи будут применять С# для создания сетевых, распределенных
приложений. Однако из-за издательских ограничений невозможно включить эту тему
в данную книгу. В книге С# для чайников распределенное программирование не рас­
сматривается. В ней совсем кратко рассмотрена платформа .NET — по той простой
причине, что могущество языка С# во многом исходит из библиотек классов .NET
Framework, которые используются этим языком.

Для того чтобы просто запустить программы, сгенерированные С#, нужна, как мини­
мум, общеязыковая исполняющая среда (Common Language Runtime — CLR). Visual Stu­
dio 2005 копирует систему CLR на вашу машину во время процедуры установки. В каче­
стве альтернативы можно загрузить весь пакет .NET, включая компилятор языка С# и
множество других полезных инструментов, зайдя на Web-сайт компании Microsoft по ад­
ресу h t t p : / / m s d n . m i c r o s o f t . c o m . Ищите там набор инструментальных средств
разработки программного обеспечения .NET (Software Development Toolkit — SDK).
Большинство программ, приведенных в этой книге, можно при необходимости
создавать и в среде Visual Studio 2003. Исключениями являются программы,
содержащие новые возможности, доступные только в языке С# 2.0, прежде все­
го обобщения и блоки итераторов. Имеется также более дешевая версия систе­
мы Visual Studio 2005 — С# Express 2005, и другие недорогие альтернативы,
рассматриваемые в дополнительных главах.

Kaк использовать книгу
При создании настоящей книги авторами преследовалась цель сделать ее максималь­
но легкой в использовании, поскольку изучение нового языка и так достаточно трудное.
Зачем же излишне его усложнять? Книга разделена на части. В первой части представле­
но введение в программирование на С# с использованием Visual Studio. В ней пошагово
излагается создание двух различных типов программ. Авторы настоятельно рекоменду­
ют начать с этой части и прочесть данные две главы, прежде чем перейти к другим час­
тям книги. Даже если вы программировали ранее, базовая структура программы, создан­
ная в первой части, постоянно применяется во всей книге.
Главы в частях со второй по пятую являются самостоятельными. Они написаны так,
чтобы можно было открыть книгу на любой из них и начать чтение. Если вы новичок
в программировании, то должны полностью прочесть вторую часть, прежде чем идти да­
лее. Но если просто возвращаетесь назад, чтобы освежить свою память по некоторой оп­
ределенной теме, у вас не возникнет никаких проблем при переходе к разделу, и вам не
нужно будет повторно перечитывать предыдущие 20 страниц.
И, конечно же, книгу завершают традиционная часть о "великолепных десятках"
и дополнительные главы; много интересного можно найти и на компакт-диске, прила­
гаемом к книге.

Введение

21

Ниже приведено краткое описание каждой части книги.

Часть I, "Создание ваших первых программ на С # "
В этой части шаг за шагом рассматривается написание минимального графического
приложения Windows с использованием интерфейса Visual Studio 2005. В ней также по­
казывается, как создать базовую структуру консольной программы С#, которая исполь­
зуется в других частях книги.

Ч а с т ь II, " О с н о в ы п р о г р а м м и р о в а н и я в С # "
На базовом уровне пьесы Шекспира — это всего лишь набор слов, связанных вместе.
С этой же точки зрения 90% любой программы С#, которую вы когда-либо напишете,
состоит из создания переменных, выполнения арифметических действий и управления
ходом выполнения программы. Во второй части внимание уделяется этим основным
операциям.

Ч а с т ь III, " О б ъ е к т н о - о с н о в а н н о е п р о г р а м м и р о в а н и е "
Одно дело — объявлять переменные где-либо в программе, добавлять и убирать их.
И совсем другое — создавать реальные программы для реальных людей. В третьей части
внимание уделяется тому, как организовывать данные так, чтобы их было легче исполь­
зовать при создании программы.

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

Часть V, "За базовыми классами"
После того как самолет оторвется от земли, он должен куда-нибудь лететь. Изучение клас­
сов и основ объектно-ориентированного программирования — это только начало. В данной
части сделан следующий шаг: в ней представлены структуры, интерфейсы и обобщения, от­
крывающие доступ к более мощным объектно-ориентированным концепциям.

Ч а с т ь VI, " В е л и к о л е п н ы е д е с я т к и "
Язык С# силен в поиске ошибок в ваших программах — иногда кажется, что он даже
слишком хорошо указывает на недостатки. Однако верите вы в это или нет, но С# все же
пытается принести вам пользу. Каждая проблема, им обнаруженная, могла бы привести к
другим проблемам, которые вам пришлось бы находить и локализовывать самостоятельно.

22

Введение

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

О п р и л а г а е м о м CD-ROM
На прилагаемом компакт-диске содержится масса хороших вещей. Прежде всего на
нем можно найти все исходные тексты из этой книги. Кроме того, на компакт-диске со­
держится набор полезных утилит. Утилита SharpDevelop не рекомендуется для полно­
масштабной разработки коммерческих программ, но она весьма полезна для написания
небольших приложений или быстрого внесения изменений, чтобы не ждать, пока загру­
зится Visual Studio. Она полностью подходит для компиляции всех исходных текстов дан­
ной книги. Редактор TextPad представляет собой существенно усиленную версию стан­
дартного Блокнота. Он предоставляет прекрасную дешевую платформу для программиро­
вания на С#. Инструмент тестирования NUnit, очень популярный среди программистов на
С#, проводит проверку вашего кода легче, чем из Visual Studio, SharpDevelop или TextPad.
Не пренебрегайте компакт-диском и имеющимися на нем программами.
И, конечно, не забудьте о файле ReadMe, содержащем всю наиболее свежую ин­
формацию.

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

Данной пиктограммой выделены места, которые могут сохранить много ва­
шего времени и усилий.

Это необходимо запомнить, так как это важно.

Это также следует запомнить. Иначе оно настигнет вас тогда, когда вы меньше
всего ожидаете, и создаст одну из действительно трудно находимых ошибок.
Данная пиктограмма указывает код, который можно найти на прилагаемом к
этой книге компакт-диске. Эта возможность предназначена, чтобы избавить
вас от лишнего набора, если ваши пальцы начали дрожать. Но не злоупот­
ребляйте ею — вы лучше поймете С#, если будете набирать текст программ
самостоятельно.

Введение

23

Чтобы помочь вам, в книге используется несколько соглашений. Термины, которые
не являются "настоящими словами", такие как имена переменных, напечатаны таким
шрифтом. Листинги программ выделены из текста следующим образом:
use System;
n a m e s p a c e MyNameSpace

{

public

}

class

MyClass

{
}

Каждый листинг сопровождается ясным и понятным пояснением. Полные исходные
тексты программ помещены на прилагаемый компакт-диск, в отличие от небольших
фрагментов.
Наконец, вы увидите стрелки, как, например, во фразе: "Выберите команду меню
F i l e ^ O p e n W i t h O N o t e p a d " . Это означает, что необходимо выбрать меню File. Затем из
появившегося раскрывающегося меню выбрать O p e n W i t h , и наконец, из следующего
подменю выбрать N o t e p a d .

Очевидно, что первым шагом должно быть изучение языка С# (в идеале используя
для этого книгу С# 2005 для чайников, конечно). Вы можете потратить несколько меся­
цев на написание простых программ С#, прежде чем сделать следующий шаг — освоить
создание приложений Windows. Вам придется потратить еще много месяцев на прило­
жения Windows, прежде чем вы начнете создавать программы, предназначенные для
распространения через Интернет.
Тем временем вы можете поддерживать свои знания языка С# несколькими способами.
Прежде всего, обратитесь к официальному источнику h t t p : / / m s d n . m i c r o s o f t . com/
msdn. Кроме того, на различных Web-сайтах для программистов имеется обширный ма­
териал по языку С#, включая живые обсуждения разных вопросов — от того, как сохра­
нить исходный файл, и до сравнения свойств детерминистической и недетерминистиче­
ской сборки мусора. Вот список нескольких больших сайтов по С#:

•S

http://msdn.microsoft.com,

который

направит

вас

на

соответствующие

сайты групп разработчиков, включая С# и платформу .NET;
S
S

I

h t t p : / / b l o g s . m s d n . c o m / c s h a r p f aq, блог "Часто задаваемые вопросы по С#";
http://msdn.microsoft.com/vcsharp/team/blogs,

который

содержит

личные блоги членов группы разработки С#;
•S w w w . c s 2 t h e m a x . c o m .
Один из авторов книги поддерживает Web-сайт www. c h u c k s p h a r . com, содержа­

щий ряд часто задаваемых вопросов (FAQ). Если вы столкнетесь с чем-то, чего не смо-

24

Введение

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

Введение

25

Часть I

Создание ваших первых
программ на С#

Вы должны пройти длинный путь, прежде чем овладеете языком С#,
так что немного отвлекитесь, прежде чем идти по нему. В первой части
вы попробуете на вкус программирование графики Windows, пошагово
создавая базовое приложение Windows при помощи интерфейса Visual
Studio 2005. В этой части также будет показана разработка базовой
структуры С# для демонстрационных программ, с которыми вы встре­
титесь в настоящей книге.

Глава 1

Создание вашей первой
Windows-программы на С#
В этой главе...
> Что такое программа? Что такое С#?
> Создание Windows-программы
> Настройка Visual Studio 2005

этой главе будет немного рассказано о компьютерах, машинных языках, языке
С# и Visual Studio 2005. Далее будет приведено пошаговое описание создания
очень простой Windows-программы, написанной на С#.

Компьютер является удивительно быстрым, но невероятно глупым служащим. Компью­
теры будут делать все, о чем их попросят (в разумных пределах, конечно), и сделают это
чрезвычайно быстро, так как они постоянно становятся все быстрее и быстрее. Во время
написания этих строк процессор обычного персонального компьютера может выполнять
миллиард команд в секунду. Да, вы правильно поняли — именно "ард", а не'"он".
К сожалению, компьютеры не понимают ничего похожего на человеческий язык. Вы,
конечно, можете возразить: "Мой телефон позволяет позвонить моему другу, стоит мне
всего лишь произнести его имя. А я знаю, что моим телефоном управляет крошечный
компьютер. Значит, компьютер может говорить по-человечески". Но на самом деле ваши
слова понимает компьютерная программа, а не сам компьютер.
Язык, который воспринимают компьютеры, называют машинным языком. Люди мо­
гут писать на нем, но это чрезвычайно трудно и приводит к частым ошибкам.
По историческим причинам машинный язык известен как ассемблер. В про­
шлом каждый изготовитель предоставлял программу, называемую ассембле­
ром, которая преобразовывала специальные слова в отдельные машинные ко­
манды. Таким образом, вы могли бы написать нечто действительно загадочное,
наподобие MOV АХ, СХ (между прочим, это реальная команда процессора
Intel). Ассемблер преобразовал бы эту команду в шаблон битов, соответствую­
щих единичной машинной команде.

сайте w w w . g o t d o t n e t . c o m / t e a m / l a n g ) . Однако С# является флагманским языком во
флоте .NET. С# всегда будет первым языком, с помощью которого можно получить доступ
к каждой новой возможности платформы .NET.
Платформа предыдущего поколения представляла собой смесь инструментов
с загадочными названиями. Платформа .NET обновляет и объединяет их все
в пакете Visual Studio 2005 с большей концентрацией на технологиях сети Ин­
тернет и баз данных, новейших версиях Windows и серверах .NET. Платформа
.NET вместо частных форматов Microsoft поддерживает развивающиеся стан­
дарты связи, такие как XML и SOAP. И, в заключение, платформа .NET под­
держивает такую модную вещь, как службы Web (Web Services).

Ч т о т а к о е Visual Studio 2005? Visual C # ?
Вы, безусловно, задаете очень много вопросов. Первым "визуальным" языком от
Microsoft был Visual Basic, под кодовым названием Thunder ("Гром"). Первым попу­
лярным языком программирования, основанным на С, был Visual С++. Как и Visual
Basic, он был назван "визуальным" из-за встроенного графического интерфейса поль­
зователя (graphical user interface — GUI), который включил все, что необходимо для раз­
работки отличных программ на С++.
В конечном итоге Microsoft упаковала все свои языки в единую среду — Visual Studio.
Так как Visual Studio 6.0 начала немного устаревать, разработчики с нетерпением ожидали
выхода седьмой версии пакета. Незадолго до выпуска Microsoft решила переименовать его
в Visual Studio .NET, чтобы подчеркнуть связь новой среды разработки с платформой .NET.
На первый взгляд это звучало как маркетинговый ход, но при более тщательном рас­
смотрении оказалось, что пакет Visual Studio .NET отличался от своих предшественни­
ков совсем немного — но достаточно для того, чтобы обеспечить новое имя. Visual Stu­
dio 2005 является наследником исходного пакета Visual Studio .NET. Более мощные воз­
можности пакета Visual Studio анализируются в дополнительных главах.
Компания Microsoft назвала свою реализацию языка Visual С#. Фактически,
Visual С# является не более чем компонентом С# пакета Visual Studio. С#есть
С#, независимо от того, входит он в Visual Studio или нет.
Хорошо, на этом все. Больше никаких вопросов.

Чтобы помочь вам с С# и Visual Studio, в этом разделе шаг за шагом рассматривается
создание простой Windows-программы. Программы Windows обычно называются при­
ложениями Windows, WinApps или приложениями WinForms для краткости.
Поскольку целью настоящей книги является рассмотрение языка С#, ее, по су­
ществу, нельзя считать ни книгой по Web-программированию, ни книгой по ба­
зам данных, ни книгой о программировании для Windows. В частности, визу­
альное программирование Windows Forms рассматривается только в этой главе.
То есть вы всего лишь немного попробуете это на вкус.

32

Часть I. Создание ваших первых программ на С#

Люди и компьютеры решили прийти к компромиссу. Программисты создают свои
программы на языке, который не так свободен, как человеческая речь, но намного более
гибок и легок в использовании, чем машинный язык. Такие языки, благодаря которым
достигается компромисс (например, С#), называются компьютерными языками высокого
уровня. (Хотя термин высокий является весьма относительным.)

Что такое программа?
Что такое программа? В известном смысле, программа Windows является исполняе­
мым файлом, запускаемым двойным щелчком на его пиктограмме. Например, версия
Microsoft Word, которая применялась для написания этой книги, является программой.
Вы называете такую программу исполняемой. Имена исполняемых программных файлов
обычно заканчиваются расширением . е х е .
Но программа на самом деле — это нечто большее. Исполняемая программа состоит
из одного или нескольких исходных файлов. Файл программы С# является текстовым
файлом, содержащим последовательность команд С#, которые записываются вместе со­
гласно правилам грамматики языка С#. Этот файл называют исходным, возможно, из-за
того, что он служит источником расстройства и беспокойства программиста.

Что такое С # ?
Язык программирования С# — один из тех промежуточных языков, которые исполь­
зуются программистами для создания исполняемых программ. Он занимает нишу между
мощным, но сложным С++, и легким в использовании, но ограниченным Visual Basic —
во всяком случае, в версии 6.0 и более ранних. (Новейшее воплощение Visual Basic
.NET — язык, во многих отношениях похожий на С#. Но как лидирующий язык плат­
формы .NET, именно С# имеет тенденцию первым представлять наиболее новые воз­
можности.) Файл программы С# имеет расширение . c s .
Некоторые считают, что "до-диез" — это то же, что и "ре-бемоль", но вы не
должны называть этот новый язык таким именем — по крайней мере в преде­
лах слышимости Редмонда, штат Вашингтон.
ку С# присущи следующие характеристики.
"ибкость: программы С# могут выполняться как на вашей машине, так и переда­
т ь с я по сети и выполняться на удаленном компьютере.
Мощность: язык С# имеет фактически тот же набор команд, что и язык С++, но
:о сглаженными ограничениями.
1егкость в использовании: С# изменяет команды, ответственные за большинство
>шибок в С++, так что вы потратите гораздо меньше времени на поиск этих ошибок.
визуальная ориентированность: библиотека кода .NET, применяемая языком С# для
iHorax его возможностей, предоставляет помощь, необходимую для быстрого созда­
йся сложных визуальных форм с раскрывающимися списками, окнами с закладками,
:группированными кнопками, полосами прокрутки и фоновыми изображениями.
Мужественность к Интернету: язык С# играет основную роль в системе .NET,
юторая является текущим подходом компании Microsoft к программированию для
Vindows и Интернета. .NET произносится как дот-нет.

30

Насть I. Создание ваших первых программ на С#

I / Безопасность: любой язык, предназначенный для использования в Интернете,
должен включать серьезную защиту против злобных хакеров.
В заключение стоит отметить, что язык С# является неотъемлемой частью платфор­
мы .NET.

Что т а к о е .NET?
Инициатива .NET появилась несколько лет назад в качестве стратегии Microsoft сде­
лать всемирную сеть доступной простым смертным, таким как вы, например. Сегодня
эта инициатива означает гораздо больше и включает в себя все, что делает Microsoft.
В частности, она является новым способом программирования для Windows. Эта плат­
форма предоставляет основанный на С язык — С#, а также простые визуальные инстру­
менты, благодаря которым Visual Basic стал таким популярным. Краткое историческое
описание поможет вам увидеть корни языка С# и платформы .NET.
Программирование для Интернета традиционно было очень трудным на более старых
языках наподобие С и С++. Компания Sun Microsystems в ответ на эту проблему создала
язык программирования Java. Для этого компания Sun взяла грамматику языка С++, сде­
лала ее немного более дружественной и ориентировала на распределенную разработку.
Когда программисты говорят "распределенный", они имеют в виду географи­
чески рассредоточенные компьютеры, которые выполняют программы, об­
щающиеся друг с другом — во многих случаях через Интернет.
Когда компания Microsoft занялась Java несколько лет назад, она столкнулась с ком­
панией Sun на почве юриспруденции из-за изменений, которые она хотела сделать в язы­
ке. В результате Microsoft пришлось в какой-то степени отказаться от Java и начать ис­
кать способы конкурировать с этим языком.
Отказ от Java был к лучшему, потому что Java имел серьезную проблему: хотя он и
является мощным языком, но вы должны написать вашу программу полностью на языке
Java, чтобы получить все его преимущества. В Microsoft имеется достаточное количество
разработчиков и написано слишком много миллионов строк исходного кода, так что
компания Microsoft должна была придумать некоторый способ поддержки множества
языков. Так появилась платформа .NET.
Платформа .NET представляет собой структуру, во многом сходную с библиотеками
языка Java, поскольку язык С# подобен Java. Java является не только языком, но и об­
ширной библиотекой кода. Точно так же и С# в действительности нечто намного боль­
шее, чем просто ключевые слова и синтаксис языка С#. Это еще и полностью объектноориентированная библиотека, содержащая тысячи программных элементов, упрощаю­
щих любой вид программирования, который только можно представить. Начиная с баз
данных, ориентированных на работу в Интернете, и заканчивая криптографией и скром­
ным диалоговым окном Windows.
Microsoft могла бы утверждать, что платформа .NET намного превосходит пакет Webинструментов компании Sun, основанный на Java, но не в этом дело. В отличие от Java, в
платформе .NET от вас не требуется переписывать уже имеющиеся программы. Программист
на Visual Basic может добавить всего несколько строк, чтобы "познакомить" существующую
программу с Web (это означает, что программа будет "знать", как получить данные из Интер­
нета). Платформа .NET поддерживает все распространенные языки Microsoft и более сорока
других языков, написанных третьими компаниями (самый последний список находится на

Глава 1. Создание вашей первой Windows-программы на С#

31

Кроме введения в Windows Forms, эта программа служит проверкой вашей среды
Visual Studio. Это всего лишь тест. Если бы это действительно было программой для
Windows... Впрочем, это и есть программа для Windows. Если вы сможете успешно на­
писать, скомпоновать и выполнить эту программу, ваша среда Visual Studio настроена
правильно, и вы готовы к созданию программ любой сложности.

Создание ш а б л о н а
Написание приложений Windows с нуля является, как известно, достаточно трудным
процессом. С многочисленными дескрипторами и контекстами создание даже простой
программы для Windows вызывает бесчисленные проблемы.
Visual Studio 2005 вообще и С# в частности значительно упрощают задачу по созда­
нию базового приложения WinApp. Честно говоря, придется даже немного разочаро­
ваться, так как вы не будете с волнением создавать его вручную.
Поскольку Visual С# специально создан для работы в Windows, он может защитить от
многих сложностей написания программ для Windows с нуля. Кроме того, Visual Studio
2005 включает в себя мастер приложений (Application Wizard), который формирует шаб­
лоны программ.
Обычно шаблоны программ фактически ничего не делают — по крайней мере, ничего
полезного. Однако они избавляют от начальных трудностей. Некоторые шаблоны про­
грамм достаточно сложны. Вы будете поражены тем, насколько много возможностей
имеет мастер приложений.
После завершения установки Visual Studio 2005 выполните следующие действия для
создания шаблона.
1. Для запуска Visual Studio 2005 выберите команду меню StartoAII Programs^

Microsoft Visual Studio 2005^ Microsoft Visual Studio 2005, как показано
1.1.

на рис.

После похрипывания процессора и поскрипывания диска перед вами появится
рабочий стол Visual Studio. Теплее, уже теплее...
2. Выберите в меню команду F i l e ^ N e w ^ Project, как показано на рис. 1.2.
3. Visual Studio откроет диалоговое окно N e w Project, как продемонстрировано на
рис. 1.3.
Проект является набором файлов, которые компонуются пакетом Visual Studio
для создания единой программы. Вы будете создавать исходные файлы С#,
имеющие расширение . CS. Расширение файла проекта — CSPROJ.

4. В панели Project Types выберите Visual С#, подпункт Windows. В панели

Templates щелкните на пиктограмме Windows Application.
Если вы сразу же не увидите пиктограмму требующегося шаблона, не волнуйтесь.
Возможно, необходимо немного прокрутить ползунок в панели T e m p l a t e s .
Пока не щелкайте на кнопке ОК.

Глава 1. Создание вашей первой Windows-программы на С#

33

Рис. 1.1. Вот какую сеть мы плетем, когда задумываем написать программу на С#

34

Часть I. Создание ваших первых программ на С#

Рис. 1.3. Мастер приложений Visual Studio только и ждет, чтобы создать для
вас программу Windows
5. В строке ввода Name введите название своего проекта (или оставьте пред­
ложенное по умолчанию).
Мастер приложений создаст папку, в которой сохранит различные файлы, вклю­
чая первоначальные исходные файлы проекта С#. В качестве имени этого катало­
га мастер приложений использует название, которое вы ввели в поле N a m e . Ис­
ходным названием по умолчанию является W i n d o w s A p p l i c a t i o n l . Если вы
уже создавали новый проект, то начальным названием может быть WindowsApp l i c a t i o n 2 , W i n d o w s A p p l i c a t i o n 3 и т.д.
В данном примере можно использовать предложенные по умолчанию название
и расположение для этого нового каталога: My D o c u m e n t s \ V i s u a l S t u d i o
P r o j e c t s \ W i n d o w s A p p l i c a t i o n l . Я помещаю свои реальные программы
там же, но для настоящей книги было изменено заданное по умолчанию располо­
жение на более короткий путь к файлу. Чтобы изменить заданное по умолчанию
расположение, выберите команду меню T o o l s ^ O p t i o n s 1 * P r o j e c t s and Solutions 1 *
General. Укажите новое р а с п о л о ж е н и е — С : \ C # P r o g r a m s для этой книги —

в окне Visual Studio Projects Location и щелкните на кнопке О К . (Вы можете
одновременно создать новый каталог в диалоговом окне Project Location. Щелк­
ните на пиктограмме папки с маленьким солнышком в верхней части диалогового
окна. Каталог уже может существовать, если вы устанавливали примеры про­
грамм с компакт-диска).
6. Щелкните на кнопке О К .
Индикатор диска несколько секунд помигает, прежде чем в центре экрана откро­
ется форма F o r m l .

Глава 1. Создание вашей первой Windows-программы на С#

35

Компиляция и запуск вашей первой
п р о г р а м м ы Windows Forms
После того как мастер приложений загрузит шаблон программы, она откроется в ре­
жиме проектирования. Вы должны преобразовать эту пустую исходную программу С#
в приложение Windows, просто чтобы удостовериться, что сгенерированный мастером
приложений шаблон не содержит ошибок.
Процесс преобразования исходного файла С# в живое приложение Win­
dows называется построением (или компиляцией). Если в исходном файле
содержатся ошибки, они будут обнаружены программой Visual С# в про­
цессе компиляции.
Чтобы скомпилировать и запустить вашу первую программу Windows Forms, выпол­
ните следующие действия.
1. Выберите в меню пункт BuildBuild projectname (где projectname— это
название наподобие W i n d o w s A p p l i c a t i o n l или MyProject)
Должно открыться окно Output. Если оно не открылось, вы можете при желании это
сделать до начала компиляции. Выберите пункт V i e w ^ O t h e r W i n d o w s ^ O u t p u t . За­
тем Build. В окне Output прокручивается ряд сообщений. Последнее сообщение
должно быть следующего вида: B u i l d : 1 s u c c e e d e d , 0 f a i l e d , 0 s k i p ­
p e d (или чем-то очень похожим на это). Это компьютерный эквивалент выражения
"все в порядке". Если вы не используете окно Output, то должны увидеть сообщение
B u i l d s u c c e e d e d или B u i l d f a i l e d в статусной строке прямо над меню Start.
На рис. 1.4 показано, на что похожа программа Visual Studio после компиляции
программы Windows, завершенной с окном Output. Не беспокойтесь насчет рас­
положения окон. Вы можете перемещать их так, как вам необходимо. Важными
составляющими являются окна F o r m s D e s i g n e r и Output. Вкладка окна проекти­
рования обозначена " F o r m l . c s
[Design]".
2. Теперь вы можете запустить программу, выбрав в меню пункт D e b u g s
Start Without D e b u g g i n g .
Программа запустится и откроет окно, которое выглядит точно так же, как и окно

F o r m s Designer, что проиллюстрировано на рис. 1.5.
В терминах языка С# такое окно называется формой. Форма имеет границу и
строку заголовка вдоль верхней границы с маленькими кнопками Minimize,

Maximize и Close.
3. Щелкните на маленькой кнопке C l o s e в верхнем правом углу рамки для за­
вершения программы.
Вот видите! Программирование на С# не такое уж и трудное.
Эта начальная программа являлась проверкой установки вашего пакета. Если у вас все
получилось, следовательно, ваш пакет Visual Studio находится в хорошей форме и готов
к программам, рассматривающимся в оставшейся части книги.
Обновите свое резюме, упомянув в нем, что вы являетесь программистом при­
ложений для Windows. В крайнем случае — одного приложения точно...

36

Часть I. Создание ваших первых программ на С#

Окно вывода

Окно Solution Explorer

Рис. 1.4. Первоначальный шаблон программы Windows не очень впечатляет

Рис. 1.5. Шаблон приложения Windows
работает, но он не убедит вашу супру­
гу, что пакет Visual Studio 2005 стоит
затраченных денег

Украшение программы
Заданная по умолчанию программа Windows не очень впечатляет, но вы можете не­
много ее улучшить. Вернитесь к Visual Studio и выберите окно со вкладкой " F o r m l . cs

[Design] " (рис. 1.4). Это окно F o r m s Designer.
Forms Designer является мощным средством, дающим возможность "раскрасить"
вашу программу в форме. Когда вы закончите, выберите команду меню Build, и F o r m s
Designer создаст код С#, необходимый для построения приложения С#, с такой же кра­
сивой формой, как вы нарисовали.

Глава 1. Создание вашей первой Windows-программы на С#

37

В этом разделе представлены несколько новых возможностей F o r m s D e s i g n e r , кото­
рые упрощают программирование Windows Forms. Вы узнаете, как скомпилировать при­
ложение с двумя полями текстового ввода и кнопкой. Пользователь может ввести чтонибудь в одном поле (обозначенном как S o u r c e ) , но не может в другом ( T a r g e t ) . Когда
пользователь щелкнет на кнопке С о р у , программа скопирует текст из поля S o u r c e в по­

ле Target. Это все.
Размещение э л е м е н т о в управления на ф о р м е
Помеченные окна, которые составляют пользовательский интерфейс Visual Studio,
называются окнами документов (document windows) и окнами управления (control win­
dows). Окна документов предназначены для создания и редактирования документов, та­
ких, например, как исходные файлы С#, составляющие программу С#. Окна управления,
подобные окну Solution Explorer, показанному на рис. 1.4, предназначены для целей
управления в Visual Studio в процессе программирования. Больше информации об окнах,
меню и других возможностях Visual Studio содержится в дополнительных главах.
Все небольшие безделушки типа кнопок и текстовых полей известны как элементы
управления (controls). Как программист Windows, вы используете эти инструментальные
средства для создания графического интерфейса пользователя, что обычно является наи­
более трудной частью программы Windows. В окне F o r m s D e s i g n e r эти инструменты
живут в окне управления T o o l b o x .
1
Если ваше окно T o o l b o x не открыто, выберите команду меню V i e w ^ T o o l b o x . На рис. 1.6
показан пакет Visual Studio с открытым в правой части экрана окном T o o l b o x .

Рис. 1.6. Окно Toolbox битком набито интересиьши элементами управления
Не волнуйтесь, если ваши окна расположены не так, как на рис. 1.6. Например,
ваше окно T o o l b o x может быть слева, справа или посередине. Вы можете пе­
ремещать любое из окон по рабочему столу как хотите. Как это делать, объяс­
няется в дополнительных главах.

38

Часть I. Создание ваших первых программ на С#

В окне T o o l b o x есть различные разделы, включая Data, C o m p o n e n t s и W i n d o w s
Forms. Эти разделы, обычно называемые закладками, просто организовывают элементы
управления, чтобы вы не запутались в них. Окно T o o l b o x содержит множество элемен­
тов управления, к которым вы можете добавлять свои собственные.
Щелкните на знаке "плюс" рядом с надписью C o m m o n Controls (или All W i n d o w s
Forms), чтобы открыть расположенные ниже элементы, как показано на рис. 1.6. Вы бу­
дете использовать эти элементы управления для размещения на вашей форме. Полоса
прокрутки справа дает возможность перемещаться вверх и вниз по элементам управле­
ния, которые перечислены в окне T o o l b o x .
Элемент управления можно разместить где угодно на форме методом перетаскивания
и опускания. Чтобы использовать окно T o o l b o x для создания двух текстовых полей и кноп­
ки, нужно выполнить следующие действия.
1. Захватите мышью элемент управления Textbox. переместите его на форму,
которая обозначена как F o r m l , и отпустите кнопку мыши.
Возможно, вам придется пролистать окно T o o l b o x . После того как вы перетащите
требуемый элемент управления, на форме появится текстовое поле. В нем может
содержаться текст t e x t B o x l . Это название, которое назначено мастером проек­
тирования данному конкретному элементу управления. (Кроме свойства N a m e ,
элемент управления имеет свойство T e x t , которое не обязательно должно соот­
ветствовать свойству N a m e ) . Щелкая и перетаскивая углы текстового поля, мож­
но изменять его размеры.
Текстовое поле можно сделать только шире. Его нельзя сделать более высо­
ким, потому что по умолчанию эти текстовые поля являются однострочными.
Небольшая указывающая вправо стрелка на текстовом поле позволяет изме­
нить эту настройку, но не обращайте на нее внимания, пока не прочтете допол­
нительные главы.
2. Снова захватите мышью элемент управления Textbox и поместите его под
первым текстовым полем.
Обратите внимание на появившиеся тонкие синие направляющие линии, которые
помогают выровнять второе текстовое поле с другими элементами управления.
Это отличная новая возможность.
3. Теперь захватите мышью элемент управления Button и поместите его под
двумя текстовыми полями.
Под текстовыми полями появится кнопка.
4. Изменяйте размеры формы и перемещайте элементы управления, исполь­
зуйте направляющие линии, пока форма не станет привлекательной.
Полученная форма показана на рис. 1.7. Ваша форма может выглядеть не­
сколько иначе.

Управление с в о й с т в а м и
Теперь самая важная проблема в вашем приложении — это обозначение кнопки.
Название b u t t o n l не очень наглядно. В первую очередь вы должны изменить
именно его.

IM на С #

Глава /. Создание вашей первой Windows-программы на С#

39

Рис. 1.7. Первоначальный вид формы
Каждый элемент управления имеет набор свойств, которые определяют его внешний
вид и то, как он раббтает. Получить доступ к этим свойствам можно с помощью окна
Properties. Для изменения свойств различных элементов управления выполните сле­
дующие действия.
1. Выберите кнопку, щелкнув на ней мышью.

2. Вызовите окно Properties с помощью команды меню V i e w ^ Properties

Window.
Кнопка имеет несколько наборов свойств: вверху перечислен набор свойств
внешнего вида, ниже — свойства поведения и несколько других. Вы должны из­
менить свойство T e x t , которое находится в группе A p p e a r a n c e . (Чтобы отсорти­
ровать свойства в алфавитном порядке, а не по категориям, щелкните на обозна­
ченной буквами AZ пиктограмме в верхней части окна.)
3. В окне Properties выберите поле в правой колонке рядом со свойством
Text. Введите в него текст Сору и нажмите клавишу .
Эти настройки проиллюстрированы на рис. 1.8 в окне Properties и в результи­
рующей форме. Кнопка теперь помечена как С о р у .

Рис. 1.8. Окно Properties позволяет управлять свойствами элементов
4. Измените начальное содержимое элементов управления T e x t b o x . Выберите
верхнее текстовое поле и повторите третий шаг, введя текст З д е с ь
зователь

вводит

поля, введя текст Сюда п р о г р а м м а

40

поль­

т е к с т . Сделайте то же самое для нижнего текстового
скопирует

текст.

Часть I. Создание ваших первых программ на С#

Благодаря этим надписям пользователь будет знать, что делать после запуска
программы. Ничто не сбивает с толку пользователей сильнее, чем запутанное
диалоговое окно.
5. Аналогично изменение свойства Text формы изменяет текст в заголовке
окна. Щелкните где-нибудь на форме, введите новое название в свойство
Text и нажмите клавишу .
В данном примере установим название заголовка "Приложение копирования текста".
6. При изменении свойств формы щелкните на свойстве AcceptButton (в группе
Misc окна Properties), а затем — в пустом месте справа от свойства Accept­
Button для указания, какая кнопка должна реагировать, когда пользователь
нажмет клавишу . В данном случае выберите buttonl.
Надпись " С о р у " является текстом на этой кнопке, но ее название остается b u t t o n l . Его тоже можно изменить при желании. Это свойство N a m e элемента
F o r m , т.е. свойство формы, а не кнопки.
7. Выберите нижнее текстовое поле и прокрутите группу свойств Behavior,
пока не доберетесь до свойства Readonly. Установите его значение равным
True путем выбора из раскрывающегося списка, как показано на рис. 1.9.

Рис. 1.9. Присвоение текстовому полю значения "только для чтения" не
дает возможности редактировать это поле при выполнении программы
8. Щелкните на кнопке Save в панели инструментов Visual Studio для сохра­
нения своей работы.
Во время работы пользуйтесь иногда кнопкой S a v e , чтобы не потерять слиш­
ком много, если ваша собака вдруг зацепит кабель питания компьютера. У несохраненных файлов изображена звездочка на вкладке в верхней части окна

Forms Designer.
Компиляция п р и л о ж е н и я
Выберите команду меню B u i l d ^ B u i l d Windows Application 1, чтобы перекомпилиро­
вать приложение. Этот шаг скомпилирует новое приложение Windows с формой, кото­
рую вы только что создали. В окне Output вы должны увидеть сообщение 1 s u c ­
ceeded, 0 f a i l e d , 0 s k i p p e d .

наС#

Глава 1. Создание вашей первой Windows-программы на С#

41

Теперь запустите программу, выбрав команду меню Debug^Start Without Debug­
ging. Запущенная программа откроет форму, которая похожа на ту, которую вы редакти­
ровали, как показано на рис. 1.10. Вы можете вводить текст в верхнее текстовое поле, но
не можете в нижнее (если вы не забыли изменить свойство ReadOnly).

Рис. 1.10. Окно программы выгля­
дит так же, как и форма, которую
вы только что скомпилировали

Учим форму трудиться
Программа выглядит правильно, но она ничего не делает. Если вы щелкнете на кноп­
ке Сору, ничего не случится. Пока что вы установили только свойства из группы Ap­
pearance, — свойства, которые контролируют внешний вид элементов управления. Те­
перь выполните следующие действия, чтобы кнопка Сору действительно могла копиро­
вать текст из одного текстового поля в другое.
1. В окне Forms Designer снова выберите кнопку Сору.
2. В окне Properties щелкните на пиктограмме маленькой молнии над спи­
ском свойств, чтобы открыть новый набор свойств.
Эти свойства называются событиями элементов управления. Они определяют,
что должен делать элемент управления при выполнении программы.
Вы должны установить событие Click. Оно указывает, что кнопка должна делать,
, когда пользователь щелкнет на ней. Это вполне логично.
3. Дважды щелкните на событии Click и посмотрите, как все изменится.
Режим Design— один из двух различных способов обзора приложения. Другим
способом является режим Code, в котором показан исходный код С#, созданный
незаметно для вас мастером проектирования. Пакет Visual Studio "знает", что для
того, чтобы программа могла переносить текст, вы должны ввести код на языке С#.
Вместо пиктограммы молнии можно просто дважды щелкнуть на самой кнопке
в окне Forms Designer.
При установке события Click экран в Visual Studio переключается в режим
Code и создается новый метод. Этому методу присваивается описательное имя
b u t t o n l _ C l i c k ( ) . Когда пользователь щелкнет на кнопке Сору, этот метод
выполнит фактическое перемещение текста из источника textBoxl в приемник
textBox2.

42

Часть I. Создание ваших первых программ на С#

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

Данный метод просто копирует свойство T e x t из поля t e x t B o x l в поле t e x t B o x 2 .
4. Поскольку кнопка buttonl теперь обозначена " С о р у " , переименуйте метод с
помощью команды меню Refactor. Дважды щелкните на названии but­

tonl Click в окне C o d e . Выберите команду Ref a c t o r s Rename. В поле
New

Name

введите

CopyClick.

Нажмите

дважды

клавишу



(следите при этом за полями в диалоговом окне).
Для текста элемента управления необходимо ясно отображать его цели.
Появившееся в пакете Visual Studio 2005 меню Refactor является самым надеж­
ным способом для выполнения некоторых модификаций кода. Например, ручное
изменение названия метода b u t t o n l _ C l i c k привело бы к потере ссылки на ме­
тод где-нибудь в коде, который Visual Studio сгенерировала для вас.
Второе диалоговое окно операции переименования показывает, что именно изме­
нится: метод и любые ссылки на него в комментариях, текстовых строках или
других местах в коде. Вы можете снять отметки в верхней панели, чтобы эти эле­
менты не изменялись. В нижней панели P r e v i e w C o d e C h a n g e s можно увидеть,
что и как изменится. Используйте меню Refactor, чтобы уберечься от большого
количества работы, часто ведущей к ошибкам.
5. Добавьте следующую строку кода к методу C o p y C l i c k ( ) :
textBox2.Text

=

textBoxl.Text;

Обратите внимание на то, как язык С# пытается вам помочь в процессе ввода
текста. На рис. 1.11 показан экран во время ввода последней части приведен­
ной выше строки. Раскрывающийся список свойств для текстового поля дает
возможность вспомнить, какие свойства доступны и как они используются.
Эта функция автозавершения очень помогает во время программирования.
(Если список автозавершения не появляется, нажмите комбинацию клавиш
).
6. Выберите команду меню B u i l d ^ B u i l d WindowsApplicationl для добавле­
ния к программе нового метода.

Проверка конечного п р о д у к т а
Выберите команду меню D e b u g o S t a r t Without D e b u g g i n g , чтобы выполнить про­
грамму в последний раз. Введите какой-нибудь текст в исходное текстовое поле и затем
щелкните на кнопке С о р у . Текст волшебным образом скопируется в приемное текстовое
поле, как показано на рис. 1.12. Весело повторяйте этот процесс, вводя какой угодно
текст и копируя его, пока не устанете от этого.
Возвращаясь к процессу создания приложения, вы можете быть поражены тем, на­
сколько это все основано на рисунках и работе с мышью. Захват элементов управления,
размещение их в форме, установка свойств — и всего лишь одна строка кода С#!

Глава 1. Создание вашей первой Windows-программы на С#

43

Рис. 1.11. Функция автозавершения отображает названия свойств во время набора текста

Рис. 1.12. Это работает!
Вы можете поспорить, что программа делает не так уж много, но позвольте с
этим не согласиться. Посмотрите некоторые из более ранних книг по програм­
мированию для Windows, написанные до появления мастера приложений, и вы
увидите, как много часов заняло бы написание даже такого простого приложе­
ния, как это.

П р о г р а м м и с т ы на Visual Basic 6.0, б е р е г и т е с ь !
Для программистов на Visual Basic 6.0, которые есть среди вас, все это, возможно,
кажется слишком приземленным. Действительно, F o r m s D e s i g n e r работает почти так
же, как и в поздних версиях Visual Basic. Однако .NET F o r m s D e s i g n e r , применяемый
языком Visual С#, намного более мощный, чем его двойник из Visual Basic 6.0. Плат- I
форма .NET и язык С# (а также и язык Visual Basic .NET в этом отношении) используют
библиотеку подпрограмм .NET, которая является более мощной, обширной и целостной,
чем старая библиотека Visual Basic. Кроме того, платформа .NET поддерживает разра-

44

Часть I. Создание ваших первых программ на С#

ботку распределенных программ для сети, так же как и программ, применяющих множе­
ство языков, что не умеет Visual Basic. Но главное усовершенствование F o r m s Designer,
используемого языками С# и Visual Basic .NET, по сравнению с предшественником Vis­
ual Basic 6.0, заключается в том, что весь код, сгенерированный для вас, является кодом,
который можно легко изменять. В Visual Basic 6.0 вы должны были довольствоваться
тем, что давал вам мастер проектирования.

Глава 1. Создание вашей первой Windows-программы на С#

45

Глава 2

Создание консольного
приложения на С#
> Создание шаблона простого консольного приложения
> Изучение шаблона простого консольного приложения
> Составные части шаблона простого консольного приложения

аже начинающие программисты на С# в состоянии писать программы для
Windows. Не верите? Тогда обратитесь к главе 1, "Создание вашей первой
Windows-программы на С#". Однако изучение основ С# лучше проводить, не отвлекаясь
на графический пользовательский интерфейс, а создавая так называемые консольные при­
ложения, для которых требуется писать существенно меньше кода и которые значитель­
но проще понимать.
В этой главе Visual Studio понадобится для создания шаблона консольного приложе­
ния. Затем вручную этот шаблон будет несколько упрощен. Полученная в результате за­
готовка будет применяться для множества программ, рассматриваемых в данной книге.
Основное предназначение настоящей книги — помочь вам понять С#. Вы не сможете
создать, например, красивую трехмерную игру без знания языка С#.

Создание шаблона консольного
приложения
Описанные далее действия предусматривают использование Visual Studio. Если
вы работаете не в Visual Studio, а в другой среде программирования, то должны
обратиться к ее документации либо просто ввести исходный текст в вашей сре­
де разработки С#.

Создание и с х о д н о й п р о г р а м м ы
Выполните следующие шаги для создания консольного приложения С#.
1. Воспользуйтесь командой меню F i l e ^ N e w ^ P r o j e c t для формирования но­
вого проекта.
Visual Studio откроет окно с пиктограммами, представляющими различные типы
приложений, которые вы можете создать.

2. Выберите в окне New Project пиктограмму Console Application и щелкни­
те на ней.
Убедитесь, что в панели Project T y p e s вы выбрали Visual С# и Windows, иначе
Visual Studio может создать неверный проект — например, приложения на язы­
ке программирования Visual Basic или Visual С++. Затем щелкните на пикто­

грамме Console Application в панели Templates.
Visual Studio требует создания проекта перед тем, как вы сможете начать вводить
исходный текст вашей программы. Проект представляет собой что-то вроде кор­
зины, в которой хранятся все файлы, необходимые для разработки программы.
Когда вы даете компилятору задание построить программу, он пересматривает
эту корзину в поисках файлов, требуемых для сборки программы.
По умолчанию для вашего первого консольного приложения будет предложено
имя C o n s o l e A p p l i c a t i o n l , но в этот раз измените его на C o n s o l e A p p T e m p l a t e . В последующих главах книги вы сможете открывать шаблон, сохранять
его под новым именем и сразу иметь все необходимое для начала работы.
По умолчанию место для хранения проектов находится в папке, спрятанной глу­
боко в папке My D o c u m e n t s . Лично я предпочитаю размещать свои программы
не там, где меня заставляют, а там, где мне хочется. В главе 1, "Создание вашей
первой Windows-программы на С#", было показано, как изменить место хранения
проектов, предлагаемое по умолчанию, на С: \ C # P r o g r a m s (если вы хотите уп­
ростить себе работу с этой книгой).
2. Щелкните на кнопке ОК.
Немного пошуршав диском, Visual Studio сгенерирует файл P r o g r a m , c s . (Если
вы посмотрите в окно Solution E x p l o r e r , то увидите и другие файлы. Пока просто
игнорируйте их существование. Если окно Solution E x p l o r e r отсутствует на экра­
не, его можно вызвать командой V i e w O S o l u t i o n E x p l o r e r . ) Расширение исход­
ных файлов С # — .CS. Имя P r o g r a m — это имя по умолчанию, присваиваемое
файлу программы.
Содержимое вашего первого консольного приложения выглядит следующим образом:
using

...

namespace

{

class

{

ConsoleAppTemplate

Program

static

void Main(string[]

args)

{

}

I
Вдоль левой границы окна вы увидите несколько маленьких плюсов (+) и ми­
нусов (-) в квадратиках. Щелкните на знаке + возле u s i n g . . .. Этим вы от­
кроете область кода — эта весьма удобная возможность Visual Studio позволя­
ет уменьшать неразбериху на экране, сворачивая области кода и пряча их долой
с глаз программиста (но не компилятора!). После раскрытия области кода вы
увидите такие директивы:

48

Часть I. Создание ваших первых программ на С#

using S y s t e m ;
using S y s t e m . C o l l e c t i o n s . G e n e r i c ;
using S y s t e m . T e x t ;
Области кода помогают сфокусироваться на коде, с которым вы работаете, скрывая
код, который в данный момент не представляет интерес. Некоторые блоки кода, такие
как блок пространств имен, блок классов, методов и т.п., получают значки + / - автома­
тически, без директивы # r e g i o n . Вы можете включить в исходный текст собственные
сворачиваемые области, добавляя директиву # r e g i o n над интересующей частью кода, ко­
торую хотите иметь возможность сворачивать, и # e n d r e g i o n после нее. Это позволит
дать имя вашей области, например, что-то вроде P u b l i c m e t h o d s . Обратите внимание,
что такие имена могут включать пробелы. Кроме того, области могут быть вложены одна в
другую (еще одно преимущество над Visual Basic), но не могут перекрываться.
В настоящий момент вам нужна только одна директива u s i n g S y s t e m . Можно уб­
рать остальные; если вам будет не хватать какой-то из них, компилятор не преминет со­
общить об этом.

Пробная поездка
Чтобы преобразовать исходный текст программы на С# в выполнимую программу,
воспользуйтесь командой меню B u i l d ^ B u i l d C o n s o l e A p p T e m p l a t e . Visual Studio отве­
тит следующим сообщением:
-Build s t a r t e d : P r o j e c t :
Debug Any CPU Csc.exe

/noconfig

ConsoleAppTemplate,

/nowarn

Configuration:

( a n d much m o r e )

Compile c o m p l e t e - - 0 e r r o r s , 0 w a r n i n g s
ConsoleAppTemplate - > С : \ C # P r o g r a m s \ . . . ( a n d m o r e ) = = B u i l d :
1 s u c c e e d e d or u p - t o - d a t e , 0 f a i l e d , 0 s k i p p e d = =
Главное во всем этом — 1

s u c c e e d e d в последней строке.

Это общее правило в программировании: "succeeded" — это хорошо, "failed" —
плохо.

Для запуска программы воспользуйтесь командой меню D e b u g s S t a r t W i t h o u t D e ­
bugging. Программа выведет на экран черное консольное окно и тут же завершится. По­
хоже, что она просто ничего не делает. Кстати, так оно и есть на самом деле. Шаблон —
это всего лишь пустая оболочка.

Создание реального консольного
приложения
Отредактируйте файл P r o g r a m . c s , чтобы он выглядел следующим образом:
using S y s t e m ;
namespace C o n s o l e A p p T e m p l a t e
{ // Фигурные с к о б к и

Глава 2. Создание консольного приложения наС#

49

// Класс Program — о б ъ е к т ,
p u b l i c c l a s s Program

содержащий наш к о д

{
// Это - н а ч а л о программы
/ / Каждая программа и м е е т м е т о д M a i n ( )
s t a t i c void Main(string[] args)

{
/ / Немного к о д а , чтобы программа х о т ь ч т о - т о д е л а л а
/ / Приглашение в в е с т и имя п о л ь з о в а т е л я
C o n s o l e . W r i t e L i n e ( " П о ж а л у й с т а , в в е д и т е ваше и м я : " ) ;
// Считывание в в о д и м о г о имени
s t r i n g sName = C o n s o l e . R e a d L i n e О ;
// Приветствие п о л ь з о в а т е л я с и с п о л ь з о в а н и е м в в е д е н н о г о имени
Console.WriteLine("Добрый день, " + sName);
/ / Ожидание п о д т в е р ж д е н и я п о л ь з о в а т е л я
C o n s o l e . W r i t e L i n e ( " Н а ж м и т е < E n t e r > для " +
"завершения
программы...");
Console.Read();
/ / Код M a i n ( ) н а э т о м з а к а н ч и в а е т с я
} // Конец функции M a i n ( )
} // Конец к л а с с а Program
} // Конец п р о с т р а н с т в а имен C o n s o l e A p p T e m p l a t e
Не волнуйтесь о тексте, находящемся в программе после двойной или тройной I
косой черты (// или / / / ) , и не беспокойтесь, если вы введете пару лишних I
пробелов или пустых строк. Однако обращайте внимание на строчные и про- I
писные буквы.
Для того чтобы превратить исходный текст из файла P r o g r a m . c s в выполнимую I
программу C o n s o l e A p p T e m p l a t e . e x e , примените команду меню B u i l d s Build C o n ­

soleAppTemplate.
Чтобы запустить программу из Visual Studio 2005, воспользуйтесь командой меню
D e b u g ^ S t a r t Without D e b u g g i n g . При этом вы увидите черное консольное окно, в котором будет выведено приглашение ввести ваше имя (вам может потребоваться активизировать окно, щелкнув на нем мышью). После того как вы введете свое имя, программа
поприветствует вас и выведет надпись Нажмите < E n t e r > д л я з а в е р ш е н и я п р о г р а м м ы . . . . Нажатие клавиши приведет к закрытию окна.
Эту же программу можно выполнить из командной строки DOS. Для этого откройте
окно DOS и введите следующее:
CD \ C # P r o g r a m s \ C o n s o l e A p p T e m p l a t e \ b i n \ D e b u g
Теперь введите C o n s o l e A p p T e m p l a t e для запуска программы. Вывод программы
на экран будет точно таким же, как только что описано. Вы можете также перейти в пап­
ку \ C # P r o g r a m s \ C o n s o l e A p p T e m p l a t e \ b i n \ D e b u g в Проводнике Windows и за­
пустить программу двойным щелчком на файле C o n s o l e A p p T e m p l a t e . е х е .
Для того чтобы открыть окно DOS, попробуйте воспользоваться командой ме­
ню T o o l s ' ^ C o m m a n d W i n d o w . Если эта команда недоступна в вашем меню
Visual Studio T o o l s , воспользуйтесь командой меню S t a r t s Ail P r o g r a m s 1 ^

Microsoft Visual Studio 2005Visual Studio T o o l s o V i s u a l Studio 2005 C o m ­
mand Prompt.
50

Часть I. Создание ваших первых программ на С#

I
|
1
I
|

В последующих подразделах будет рассмотрено, как работает созданное консольное
приложение.

Схема программы
Базовая схема всех консольных приложений начинается со следующего кода:
using
using
using

System;
System.Collections.Generic;
System.Text;

namespace

ConsoleAppTemplate

Программа начинает выполнение с первой строки, идущей после названия функции
Main (), и заканчивается ее закрывающей фигурной скобкой. Пока что это все, что
можно сказать по этому поводу.
Список директив u s i n g может находиться непосредственно до или после строки
namespace C o n s o l e A p p T e m p l a t e {. Порядок не имеет значения. В своем прило­
жении вы можете использовать множество разных вещей из .NET. О том, что такое про­
странство имен и зачем нужна директива u s i n g , будет рассказано в одной из дополни­
тельных глав.

Комментарии
Шаблон содержит массу строк, к которым добавляются другие строки, выглядящие
следующим образом:
// Стартовая т о ч к а программы
public s t a t i c v o i d M a i n ( s t r i n g [ ]

args)

Первую строку этого фрагмента С# игнорирует — это строка комментария.
Любая строка, начинающаяся с символов // или / / / , представляет собой
обычный текст, предназначенный для человека и полностью игнорируемый
С#. Пока что / / и / / / рассматриваются как эквивалентные символы начала
комментария.
Зачем включать в программу строки, которые будут проигнорированы компилято­
ром? Потому что комментарии помогают понять текст программы. Исходный текст — не

Глава 2. Создание консольного приложения на С#

51

такая уж легкая для понимания штука. Помните, что язык программирования — это ком­
промисс между тем, что понимает человек, и что понимает компьютер? Комментарии
помогут вам при написании кода, а особенно тем (и это можете быть вы сами через ка­
кое-то время), кто будет заниматься вашей программой и пытаться понять ее логику. До­
бавление пояснений в программу сделает эту работу намного проще.
Не экономьте на комментариях. Комментируйте исходный текст сразу и часто.
Это поможет вам и другим программистам легче разобраться, для чего предна­
значена та или иная инструкция С# в исходном тексте.

Т е л о программы
Ядро программы находится в блоке исходного текста, помеченного как M a i n ( ) :
/ / Приглашение в в е с т и имя п о л ь з о в а т е л я
C o n s o l e . W r i t e L i n e ( " П о ж а л у й с т а , в в е д и т е ваше и м я : " ) ;
/ / Считывание в в о д и м о г о и м е н и
s t r i n g sName = C o n s o l e . R e a d L i n e ( ) ;
// Приветствие п о л ь з о в а т е л я с использованием в в е д е н н о г о
C o n s o l e . W r i t e L i n e ( " Д о б р ы й д е н ь , " + sName),•

имени

Вы можете сэкономить массу времени, воспользовавшись при вводе новой
возможностью — Code Snippets (фрагменты кода), которая облегчает ввод рас­
пространенных инструкций, наподобие C o n s o l e . W r i t e L i n e . Нажмите ком­
бинацию клавиш , а затем для появления раскрывающегося
меню Code Snippets. Прокрутите его до cw и нажмите клавишу . Visual
Studio вставит в тело программы инструкцию C o n s o l e . W r i t e L i n e () с точ­
кой ввода между скобками.
Если у вас имеется ряд сокращений типа c w , f o r и i f , которые легко запомнить,
воспользуйтесь еще более быстрым методом: введите cw и нажмите клавишу .
(Попробуйте также выделить несколько строк исходного текста, и нажать клавиши
, а затем . Выберите из списка if — и выделенный текст окажется
внутри конструкции i f . ) Более того, вы можете даже создавать собственные фрагменты.
Программа начинает работу с первой инструкции С#: C o n s o l e . W r i t e L i n e . Эта
команда выводит на экран строку П о ж а л у й с т а , в в е д и т е ваше и м я : .
Следующая инструкция считывает вводимую пользователем строку и сохраняет ее
в переменной с именем s N a m e (о переменных будет рассказано в главе 3, "Объявление
переменных-значений"). В последней строке выполняется объединение строк Добрый
д е н ь , с введенным именем пользователя, а также вывод получившейся в результате
объединения строки на экран.
Последние строки заставляют компьютер ожидать, пока пользователь не нажмет кла­
вишу . Эти строки обеспечивают приостановку выполнения программы, чтобы
было время просмотреть на экране результаты ее работы:
/ / Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
C o n s o l e . W r i t e L i n e ( " Н а ж м и т е < E n t e r > для " +
"завершения п р о г р а м м ы . . . " ) ;
Console.Read();

52

Часть I. Создание ваших первых программ на С#

В зависимости от того, как именно запущена программа, данный фрагмент может
оказаться очень важным. В Visual Studio это можносделать двумя способами. Если ис­
пользуется команда D e b u g ^ S t a r t , Visual Studio закрывает окно программы сразу же по
ее завершении. То же происходит и при запуске программы двойным щелчком на пикто­
грамме файла в Проводнике Windows.
Вне зависимости от того, каким образом запускается программа, ожидание на­
жатия пользователем клавиши перед завершением программы решает
все проблемы.
Теперь вы можете удалить из шаблона строки от первого C o n s o l e . W r i t e L i n e до
предпоследнего, и получите пустой, чистый метод M a i n () для использования в качестве
шаблона для последующих консольных приложений. Но не убирайте последние инст­
рукции C o n s o l e . W r i t e L i n e и C o n s o l e . Read. Они понадобятся вам в ваших кон­
сольных приложениях.

Глава 2. Создание консольного приложения на С#

53

Часть ІІ

Основы программирования
вС#

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

Глава 3

Объявление переменных-значений
> Создание места для хранения информации — переменные С#
> Использование целых чисел
> Работа с дробными числами
> Объявление других типов переменных
> Работа с числовыми константами
> Изменение типов

аиболее фундаментальной из всех концепций программирования является кон­
цепция переменной. Переменная С# похожа на небольшой ящик, в котором мож­
но хранить разные вещи (в частности, числа) для последующего применения. Термин пе­
ременная пришел из мира математики. Например, математик может сказать следующее:
Эта запись означает, что, начиная с этого момента, математик, используя п, подразу­
мевает 1 — т.е. пока он не изменит п на что-то другое (число, уравнение и т.д.).
Значение термина переменная в программировании не сильно отличается от его знаI чения в математике. Программист на С# может написать:

|

Тем самым он определит "вещь" п и присвоит ей значение 1. Начиная с этого места
программы переменная п имеет значение 1 до тех пор, пока программист не заменит его
некоторым другим числом.
К сожалению для программистов, С# накладывает ряд ограничений на переменные —
ограничений, с которыми не сталкиваются математики.

Когда математик говорит: "и равно 1", это означает, что термин п эквивалентен 1.
Математик свободен в своем выборе переменных — например, он может сказать или на­
писать следующее:
2

х = у +2у+1
Если к = у + 1 ,

то х = к

2

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

тие "у плюс 1", представляя собой что-то вроде сокращенной записи. Если вы хотите разо­
браться в этом более детально и точно — обратитесь к учебникам по математике.
Программист должен быть гораздо педантичнее в использовании терминологии. На­
пример, программист на С# может написать следующий код:

Первая его строка означает: "Выделим небольшое количество памяти компьютера
и назначим ему имя п " . Этот шаг аналогичен, например, абонированию почтового ящика
в почтовом отделении и наклейке на него ярлыка. Вторая строка гласит: "Сохраним зна­
чение 1 в переменной п, тем самым заменяя им предыдущее хранившееся в ней значе­
ние". При использовании аналогии с почтовым ящиком это звучит как: "Откроем ящик, I
выбросим все, что там было, и положим в него 1".
Знак равенства (=) называется оператором присваивания.

Математик говорит: "и равно 1". Программист на С# выражается более точно:
"Сохраним значение 1 в переменной п " . Операторы С# указывают компьютеру,
что именно вы хотите сделать. Другими словами, операторы — это глаголы,
а не существительные. Оператор присваивания берет значение справа от него и
сохраняет его в переменной, указанной слева от него.

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

= 1;
= 1.1;
= House;
= "Ну и г л у п о с т ь ! "

Приведенные строки приравнивают переменную п к разносортным вещам, и матема­
тик об этом абсолютно не беспокоится.
С# и приблизительно не столь гибок. В нем каждая переменная имеет фиксированный
тип. Когда вы абонируете почтовый ящик, вы выбираете ящик интересующего вас раз­
мера. Если вы выбрали ящик "для целых чисел", не стоит надеяться, что туда сможет
поместиться строка.
В примере в предыдущем разделе вы выбрали ящик, созданный для работы с целыми
числами — С# называет их i n t . Целые числа — это числа, применяющиеся для пере­
числения (1, 2, 3 и т.д.), а также 0 и отрицательные числа
1, -2, - 3 . . .
Перед тем как использовать переменную, ее надо объявить. После того как вы
объявили переменную как i n t , в нее можно помещать и извлекать из нее це­
лые значения, что продемонстрировано в следующем примере:
// О б ъ я в л я е м
i n t П;

58

переменную

п

Часть II. Основы программирования в С#

// Объявляем переменную m и и н и ц и а л и з и р у е м ее з н а ч е н и е м 2
int m = 2 ;
// Присваиваем з н а ч е н и е , х р а н я щ е е с я в т, п е р е м е н н о й п
п = т,Первая строка после комментария является объявлением, которое создает небольшую
область в памяти с именем п, предназначенную для хранения целых значений. Началь­
ное значение п не определено до тех пор, пока этой переменной не будет присвоено не­
которое значение. Второе объявление не только объявляет переменную m типа i n t , но
и инициализирует ее значением 2.
Термин инициализировать означает присвоить начальное значение. Инициали­
зация переменной заключается в первом присваивании ей некоторого значения.
Вы ничего не можете сказать о значении переменной до тех пор, пока она не
будет инициализирована.
Последняя строка программы присваивает значение, хранящееся в m (равное 2), пере­
менной п. Переменная п будет хранить значение 2, пока ей не будет присвоено новое зна­
чение (в частности, она не потеряет свое значение при присваивании его переменной т).

Правила о б ъ я в л е н и я п е р е м е н н ы х
Вы можете выполнить инициализацию переменной как часть ее объявления:
// Объявление п е р е м е н н о й т и п а
// начального з н а ч е н и я 1
int 0 = 1 ;

int

с п р и с в а и в а н и е м ей

Это эквивалентно помещению 1 в ящик i n t в момент его аренды, в отличие от его
вскрытия и помещения туда 1 позже.
Инициализируйте переменные при их объявлении. Во многих, но не во всех,
случаях С# инициализирует переменные за вас, но рассчитывать на это нельзя.

Вы можете объявить переменную в любом месте (ну, или почти в любом) программы.
Однако вы не можете использовать переменную до того, как она будет объявлена, и при­
сваивать ей какие-либо значения. Так, следующие два присваивания некорректны:
// Это п р и с в а и в а н и е н е в е р н о , п о с к о л ь к у п е р е м е н н о й m не
/ / присвоено з н а ч е н и е п е р е д е е и с п о л ь з о в а н и е м
int m;
n = m;
// Следующее п р и с в а и в а н и е н е к о р р е к т н о в с и л у т о г о , ч т о
// переменная р не была о б ъ я в л е н а до ее и с п о л ь з о в а н и я
р = 2;
'int р;
И последнее — нельзя дважды объявить одну и ту же переменную.

Вариации на т е м у int
Большинство простых переменных имеют тип i n t . Однако С# позволяет настраивать
целый тип для конкретных случаев.

Глава 3. Объявление переменных-значений

59

Все целочисленные типы переменных ограничены хранением только целых чисел, но!
диапазоны этих чисел различны. Например, переменная типа i n t может хранить только!
целые числа из диапазона примерно от -2 миллиардов до 2 миллиардов.
Два миллиарда сантиметров — это больше, чем диаметр Земли. Но если этой величи-В
ны вам не хватает, С# имеет еще один целочисленный тип, называемый 1опдИ
(сокращение от l o n g i n t ) , который может хранить гораздо большие числа ценой уве-ш
личения размера "ящика": он занимает 8 байт (64 бит) в отличие от 4-битового i n t .
В С# имеются и другие целочисленные типы, показанные в табл. 3.1.

Как будет рассказано позже, фиксированные значения — такие как 1 — тоже имеют
тип. По умолчанию считается, что простая константа наподобие 1 имеет тип i n t . Константы, отличные от i n t , должны явно указывать свой т и п — так, например, 123U
(обратите внимание на U) — это константа типа u i n t — беззнакового целого.
Большинство целых значений — знаковые (signed), т.е. они могут представлять наряду с положительными и отрицательные значения. Беззнаковые (unsigned) целые числа
могут представлять только неотрицательные значения, но зато их диапазон представле
ния удваивается по сравнению с соответствующими знаковыми типами. Как видно из
табл. 3.1, имена большинства беззнаковых типов образуются из знаковых путем добав
ления префикса и.

1

Целых чисел хватает для большинства вычислений. Я считал так до 6 класса и даже
не думал, что существуют какие-то другие числа. Я до сих пор не могу забыть свое по-'
трясение в 6 классе, когда учительница рассказала о дробных числах.
Множество вычислений требуют применения дробных чисел, которые никак не могут
быть точно представлены целыми числами. Общее уравнение для конвертации темпера­
туры в градусах Фарегнейта в температуру в градусах Цельсия демонстрирует это:

Напомним, что автор учился в американской школе. — Примеч. пер.

60

Часть II. Основы программирования в С#

// Преобразование т е м п е р а т у р ы 4 5 ° F
int nFahr = 4 1 ;
int n C e l s i u s = ( n F a h r - 3 2 ) * ( 5 / 9 ) ;
Для некоторых значений данное уравнение работает совершенно корректно. Напри­
мер, 41 °F равен 5°С. "Правильно, Девис!" — сказала бы мне учительница в 6 классе.
Попробуем теперь другое значение, например 100°F. Приступим к вычислениям:
100-32 = 6 8 ; 68 (5/9) дает 37. " Н е т , — сказала бы учительница,— правильный от­
вет — 37.78". И даже это не совсем верно, так как в действительности правильный от­
вет—37.777..., где 7 повторяется до бесконечности, но, увы, невозможно написать бес­
конечную книгу.
Тип i n t может представлять только целые числа. Целый эквивалент числа
37.78 — 37. При этом, для того чтобы разместить число в целой переменной,
дробная часть числа отбрасывается — такое действие называется усечением
(truncation).
Усечение — совсем не то же самое, что округление (rounding). Усечение от­
брасывает дробную часть, а при округлении получается ближайшее целое
значение. Так, усечение 1.9 даст 1, а округление — 2.
Для температур 37 может оказаться вполне достаточно. Вряд ли ваша одежда при
37.78°С будет существенно отличаться от одежды при 37°С. Но для множества, если не
большинства, приложений такое усечение неприемлемо.
На самом деле все еще хуже. Тип i n t не в состоянии хранить значение 5/9 и преоб­
разует его в 0. Соответственно, данная формула будет давать нулевое значение n C e l ­
sius для любого значения n F a h r . Поэтому даже такой непритязательный человек, как
я, сочтет это неприемлемым.
На прилагаемом к книге компакт-диске в каталоге C o n v e r t T e m p e r a t u r e W i t h R o u n d O f f имеется программа, использующая целочисленное
преобразование температур. Пока что вы можете не разобраться со всеми ее
деталями, но можете посмотреть на уравнение преобразования и запустить
программу C o n v e r t T e m p e r a t u r e W i t h R o u n d O f f . e x e , чтобы увидеть
результаты ее работы.

Ограничения, накладываемые на переменные типа i n t , для многих приложений не­
приемлемы. Обычно главным препятствием является не диапазон возможных значений
(двух квинтиллионов 64-битового l o n g хватает, пожалуй, для подавляющего большин­
ства задач), а невозможность представления дробных чисел.
В некоторых ситуациях нужны числа, которые могут иметь ненулевую дробную
часть, и которые математики называют действительными числами (real numbers). Все­
гда находятся люди, удивляющиеся такому названию — неужели целые числа — не­
действительные?

? в С#

Глава 3. Объявление переменных-значений

61

Обратите внимание на сказанное о том, что действительное число может
иметь ненулевую дробную часть — т.е. число 1.5 является действительным так
же, как и число 1.0. Например, 1.0 + 0.1 = 1.1. Просто при чтении оставшейся
части этой главы все время не забывайте о наличии точки.
К счастью, С# прекрасно понимает, что такое действительные числа. Они могут быть
с плавающей точкой и с так называемым десятичным представлением. Гораздо более
распространена плавающая точка.

Объявление переменной с плавающей точкой
Переменная с плавающей точкой может быть объявлена так, как показано в следую­
щем примере:
float

f

=

1.0;

После того как вы объявите переменную как f l o a t , она остается таковой при всех
естественных для нее операциях.
В табл. 3.2 рассматриваются использующиеся в С# типы с плавающей точкой. Все
переменные этих типов — знаковые (т.е. не существует такой вещи, как переменная
с плавающей точкой, не способная представлять отрицательные значения).

Вы можете решить, что тип f l o a t — это тип по умолчанию для переменных с {
плавающей точкой, но на самом деле типом по умолчанию является d o u b l e .
Если вы не определите явно тип для, скажем, 12.3, С# сделает его d o u b l e .
Столбец точности в табл. 3.2 указывает количество значащих цифр, которые может
представлять такая переменная. Например, 5/9 на самом деле равно 0.555... с бесконечной последовательностью пятерок. Однако переменная типа f l o a t имеет точность не
более 6 цифр, что означает, что все цифры после шестой могут быть проигнорированы. I
Таким образом, 5/9 , будучи выражено в виде f l o a t , может выглядеть как
0 . 5555551457382
Не забывайте, что все цифры после шестой пятерки ненадежны.
На самом деле тип f l o a t имеет 6.5 значащих цифр. Дополнительные полциф­
ры получаются из-за того, что точность представления чисел с плавающей точ­
82

кой связана с такой функцией, как 10'°

. Впрочем, вряд ли это должно сильно

вас интересовать.
То же число 5/9 может выглядеть при использовании типа d o u b l e следующим образом:
0.55555555555555557823
Тип d o u b l e имеет 15-16 значащих цифр.

62

Часть II. Основы программирования в С#

Числа с плавающей точкой в С# по умолчанию имеют точность d o u b l e , так
что применяйте везде тип d o u b l e , если только у вас нет веских причин по­
ступить иначе. Однако используете ли вы d o u b l e или f l o a t — все равно
ваша программа будет считаться программой, работающей с числами с пла­
вающей точкой.

Более точное п р е о б р а з о в а н и е т е м п е р а т у р
Вот как выглядит формула преобразования температур в градусах Фаренгейта в гра­
дусы Цельсия при использовании переменных с плавающей точкой:
dCelsius =

(dFahr - 3 2 . 0 )

*

(5.0 / 9 . 0 ) ;

На прилагаемом к книге компакт-диске имеется d o u b l e - в е р с и я программы
преобразования температур C o n v e r t T e m p e r a t u r e W i t h F l o a t .

Приведенный далее пример показывает результат работы программы ConvertTem­
peratureWithFloat:
Введите т е м п е р а т у р у в г р а д у с а х Ф а р е н г е й т а : 1 0 0
Температура в г р а д у с а х Ц е л ь с и я р а в н а : 3 7 . 7 7 7 7 7 7 7 7 7 7 7 7 8
Press Enter to t e r m i n a t e . . .

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

Перечисление
Нельзя использовать числа с плавающей точкой для перечисления. Некоторые струк­
туры С# должны быть подсчитаны (1, 2, 3 и т.д.). И всем известно, что числа 1.0, 2.0, 3.0
можно применять для подсчета количества точно так же, как и 1, 2, 3, но С# ведь этого
не знает. Например, при указанной выше точности чисел с плавающей точкой откуда С#
знать, что вы не сказали в действительности 1.000001?
Находите ли вы эту аргументацию убедительной или нет — но вы не можете исполь­
зовать числа с плавающей точкой для подсчета количества.

Сравнение чисел
Вы должны быть очень осторожны при сравнении чисел с плавающей точкой. На­
пример, 12.5 может быть представлено как 12.500001. Большинство людей не волнуют
такие мелкие добавки в конце числа, но компьютер понимает их буквально, и для С#
12.500000 и 12.500001 — это разные числа.
Так, если вы сложите 1.1 и 1.1, вы можете получить в качестве результата 2.2 или
2.200001. И если вы спросите: "Равно ли значение dDouv l e v a r i a b l e . 2.2?", то можете
получить совсем не тот ответ, который ожидаете. Такие вопросы вы должны переформу­
лировать, например, так: "Отличается ли абсолютное значение разности d D o u v l e V a r i ­
able и 2.2 менее чем на 0.000001?", другими словами, равны ли два значения с некото­
рой допустимой ошибкой.

Глава 3. Объявление переменных-значений

63

Процессоры Pentium используют небольшой трюк, который несколько снижает уш
занную неприятность,— при работе они применяют специальный формаа
в котором для числа с плавающей точкой выделяется 80 бит. При округлении тако|
го числа к 64-битовому почти всегда получается результат, который вы ожидаете.

Скорость вычислений
Процессоры х86, использующиеся на старых компьютерах под управление]!
Windows, выполняли действия над целыми числами существенно быстрее, чем над чис|
лами с плавающей точкой. В настоящее время эта проблема так остро не стоит.
Отношение скоростей работы процессора Pentium III при простом (пожалуй, слиш]
ком простом) тесте, состоящем в 300000000 сложений и вычитаний целых чисел и чисем
с плавающей точкой, оказалось равным примерно 3 к 1. То есть вместо одного сложешя
d o u b l e можно сделать три сложения i n t . (Вычисления с применением умножения
и делений могут привести к другим результатам.)

Ограниченность диапазона
В прошлом переменные с плавающей точкой могли представлять значительно боль­
ший диапазон чисел, чем целые. Сейчас диапазон представления целых чисел существ
венно вырос — стоит вспомнить о типе l o n g .
Даже простой тип f l o a t способен хранить очень большие числа, но числа
значащих цифр у него ограничено примерно шестью. Например, 1 2 3 4 5 6 7 8 9F
означает то же, что и 1 2 3 4 5 6 0 0 0 F . (О том, что такое F в приведенных заган
сях, вы узнаете немного позже.)

Как уже объяснялось в предыдущих разделах, и целые, и десятичные числа имеют
свои недостатки. Переменным с плавающей точкой присущи проблемы, связанные с во­
просами округления из-за недостаточной точности представления, целые переменные не
могут представлять числа с дробной частью. Бывают ситуации, когда совершенно необ­
ходимо иметь возможность получить лучшее из обоих миров, а именно числа, которые:
•S подобно числам с плавающей точкой, способны иметь дробную часть;
S

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

К счастью, в С# есть такой тип чисел, называющийся d e c i m a l . Переменная типа
28
28
d e c i m a l в состоянии представлять числа от 10" до 10 — вполне достаточный диапа­
зон значений! И все это делается без проблем, связанных с округлением.

О б ъ я в л е н и е п е р е м е н н ы х т и п а decimal
Переменные типа d e c i m a l объявляются и используются так же, как и переменные
других типов:

64

Часть II. Основы программирования в С#

decimal m l ;
decimal m2 = 1 0 0 ;
decimal m3 = 100M;

//
//
//

Хорошо
Лучше
Идеально

Объявление ml выделяет память для переменной ml без ее инициализации. Пока вы
не присвоите значение этой переменной, оно будет неопределенным. В этом нет особой
проблемы, так как С# не позволит вам использовать переменную без начального при­
своения ей какого-либо значения.
Второе объявление создает переменную т2 и инициализирует ее значением 100. В этой
ситуации неприятным моментом оказывается то, что 100 имеет тип i n t . Поэтому С# вы­
нужден конвертировать i n t в d e c i m a l перед инициализацией. К счастью, С# понимает,
чего именно вы добиваетесь, и выполняет эту инициализацию для вас.
Лучше всего использовать такое объявление, как объявление переменной тЗ с кон­
стантой 10ОМ типа d e c i m a l . Буква М в конце числа указывает, что данная константа
имеет тип d e c i m a l , так что никакого преобразования не требуется.

Сравнение д е с я т и ч н ы х , ц е л ы х ч и с е л
и чисел с п л а в а ю щ е й т о ч к о й
Создается впечатление, что числа типа d e c i m a l имеют лишь одни достоинства и ли­
шены недостатков, присущих типам i n t и d o u b l e . Переменные этого типа обладают
широким диапазоном представления и не имеют проблем округления.
Однако у чисел d e c i m a l есть свои неприятности. Во-первых, поскольку они имеют
дробную часть, они не могут использоваться в качестве счетчиков, например, в циклах, о ко­
торых пойдет речь в главе 5, "Управление потоком выполнения".
Вторая проблема не менее серьезна, и заключается в том, что вычисления с этим ти­
пом чисел гораздо медленнее, чем с простыми целыми числами или даже с числами
с плавающей точкой. В уже упоминавшемся тесте с 300000000 сложений и вычитаний
работа с числами d e c i m a l примерно в 50 раз медленнее работы с числами i n t . Это от­
ношение становится еще хуже для более сложных операций. Кроме того, большинство
математических функций, таких как синус или возведение в степень, не имеют версий
для работы с числами d e c i m a l .
Понятно, что числа типа d e c i m a l наиболее подходят для финансовых приложений, где
исключительно важна точность, но само количество вычислений относительно невелико.

Логичен ли логический tnun?
И наконец, о переменных логического типа. Тип b o o l имеет только два значения —
true и f a l s e . Это не ш у т к а — целый тип переменных придуман для работы только
с двумя значениями.
Ранее программисты на С и С++ использовали нулевое значение переменной
типа i n t для обозначения f a l s e и ненулевое — для обозначения t r u e . В С#
этот фокус не проходит.
Переменная типа b o o l объявляется следующим образом:
bool t h i s I s A B o o l

= true;

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

Глава 3. Объявление переменных-значений

65

не можете преобразовать b o o l в i n t (чтобы, скажем, f a l s e превратилось в 0) или
в s t r i n g (чтобы f a l s e стало " f a l s e " ) .

Программа, которая в состоянии заниматься только вычислениями, могла бы устроить
разве что математиков, страховых агентов и военных ( д а - д а — первые вычислительные
машины были созданы для расчета таблиц артиллерийских стрельб). Однако в большинст­
ве приложений программы должны работать не только с цифрами, но и с буквами.
С# рассматривает буквы двумя различными путями — как отдельные символы типа
c h a r и как строки символов типа s t r i n g .

Т и п char
Переменная типа c h a r способна хранить только один символ. Символьная константа
выглядит как символ, окруженный парой одинарных кавычек:
char с

=

' а' ;

Вы можете хранить любой символ из латинского алфавита, кириллицы, арабского,
иврита, японских катаканы и хираганы и массы японских, китайских или корейских ие­
роглифов.
Кроме того, тип c h a r может использоваться в качестве счетчика, т.е. его можно
применять в циклах, о которых вы узнаете в главе 5, "Управление потоком выполнения".
У символов нет никаких проблем, связанных с округлением.
Переменные типа c h a r не включают информации о шрифтах, так что в пере­
менной c h a r может храниться, например, вполне корректный иероглиф, но
при выводе его без использования соответствующего шрифта вы увидите на
экране только мусор.

Специальные символы
Некоторые символы являются непечатными в том смысле, что вы ничего не увидите
при выводе их на экран или на принтер. Наиболее очевидным примером такого символа
1
является пробел '
(кавычка, пробел, кавычка). Другие символы не имеют буквенного
эквивалента — например, символ табуляции. Для указания таких символов С# использу­
ет обратную косую черту, как показано в табл. 3.3.

66

Часть II. Основы программирования в С#

Тип string
Еще одним распространенным типом переменных является s t r i n g . Приведенные да­
лее примеры показывают, как объявляются и инициализируются переменные этого типа.
// Объявление с о т л о ж е н н о й и н и ц и а л и з а ц и е й
string s o m e S t r i n g l ;
someStringl = " Э т о с т р о к а " ;
// Инициализация при о б ъ я в л е н и и
string s o m e S t r i n g 2 = " Э т о с т р о к а " ;
Константа типа s t r i n g , именуемая также строковым литералом, представляет собой
набор символов, окруженный двойными кавычками. Символы в строке могут включать
специальные символы, показанные в табл. 3.3. Строка не может быть перенесена на но­
вую строку в исходном тексте на С#, но может содержать символ новой строки, как по­
казано в следующем примере:
// Неверная з а п и с ь с т р о к и
string s o m e S t r i n g = " Э т о с т р о к а
и это с т р о к а " ;
// А вот так - в е р н о
string s o m e S t r i n g =

"Это с т р о к а \ п и э т о с т р о к а " ;

При выводе на экран при помощи вызова C o n s o l e . W r i t e L i n e вы увидите текст,
размещенный в двух строках:
Это строка
и это строка
Строка не является ни перечислимым типом, ни типом-значением — в процессоре не
существует встроенного типа строки. К строкам применим только один распространенный
оператор — оператор сложения, который просто объединяет две строки в одну, например:
string s = "Это п р е д л о ж е н и е . " + " И э т о т о ж е . " ;
Приведенный код присваивает строке s значение:
"Это предложение . И э т о т о ж е ."
Строка без символов, записанная как "" (пара двойных кавычек), является
корректной строкой для типа s t r i n g , и называется пустой строкой. Пустая
строка отличается от нулевого символа ' \ 0 ' и от строки, содержащей любое
количество пробелов ("

").

Кстати, все остальные типы данных в этой главе — типы-значения (value types).
Строковый тип типом-значением не является.

Все инструкции С# должны быть реализованы как машинные команды процессо­
р а — процессора Intel в случае PC. Эти процессоры также имеют собственную
концепцию переменных. Например, процессор Intel содержит восемь внутренних
хранилищ, именуемых регистрами, каждый из которых может хранить одно зна­
чение i n t . Не вдаваясь в детали функционирования процессора, можно сказать,
что все типы, описываемые в данной главе, за исключением d e c i m a l и s t r i n g ,
являются встроенными для процессора. Таким образом, существует машинная

Глава 3. Объявление переменных-значений

67

команда, суть которой в следующем: прибавить один i n t к другому. Имеется I
аналогичная команда и для сложения d o u b l e .
Кроме того, переменные описанных типов (опять же, за исключением s t r i n g ) I
имеют фиксированную длину. Переменные типа с фиксированной длиной всегда за-1
нимают одно и то же количество памяти. Так что при присваивании а = Ь С# может!
поместить значение b в а без каких-либо дополнительных мер, разработанных для I
типов переменной длины. Эта характеристика дает этим типам переменных имя ти-1
пы-значения.
Типы i n t , d o u b l e , b o o l и и х "близкие родственники" наподобие беззнаково-1
го i n t являются встроенными типами. Встроенные типы переменных и тип I
d e c i m a l известны также как типы-значения. Тип s t r i n g не относится ни I
к тем ни к другим.
Типы, о которых речь пойдет в главе 6, "Объединение данных — классы и массивы",
определяемые программистом и известные как ссылки, не являются ни встроенными, ни
типами-значениями. Тип s t r i n g является ссылочным типом, хотя компилятор С# рас­
сматривает его специальным образом в силу его широкой распространенности.

Сравнение string и char
Хотя строки имеют дело с символами, тип s t r i n g существенно отличается от типа
c h a r . Понятно, что имеются некоторые тривиальные отличия. Так, символ помещается
в одинарные кавычки, а строка — в двойные. Кроме того, тип c h a r — это всегда один
символ, так что следующий код не имеет с м ы с л а — ни в плане сложения, ни в плане
конкатенации:
char cl =
char с2 =
char сЗ =

' а ' ;
' Ь 1 ;
cl + с 2 ;

На самом деле этот код почти компилируем, но его смысл существенно отли­
чается от того, который мы ему приписываем. С# преобразует cl и с2 в значе­
ния типа i n t , представляющие собой числовые значения соответствующих
символов, после чего складывает полученные значения. Ошибка возникает при
попытке сохранить полученный результат в с З , так как при размещении значе­
ния типа i n t в переменной меньшего размера c h a r данные могут быть поте­
ряны. В любом случае, эта операция не имеет смысла.
С другой стороны, строка может быть любой длины. Таким образом, конкатенация
двух строк вполне осмысленна:
string si = " а " ;
s t r i n g s2 = " b " ;
s t r i n g s3 = si + s2;

//

Результат



"ab"

В качестве части своей библиотеки С# определяет целый ряд строковых операций,
которые будут описаны в главе 9, "Работа со строками в С # " .

68

Часть II. Основы программирования в С#

Соглашения по именованию
Программирование и так достаточно сложно, чтобы делать его еще сложнее.
Чтобы код на С# было легче читать, обычно используются определенные со­
глашения по именованию переменных, которым желательно следовать, чтобы код был
понятен другим программистам.
Имена всех объектов, кроме переменных, начинаются с прописной буквы,
а имена переменных— со строчной. Делайте эти имена как можно более ин­
формативными (зачастую это приводит к тому, что имена состоят из нескольких
слов). Слова должны начинаться с прописной буквы, и лучше, если между ними не
будет символов подчеркивания — например, t h i s I s A L o n g V a r i a b l e N a m e .
Первая буква имени переменной указывает ее тип. Большинство таких букв три­
виальны — f для f l o a t , d для d o u b l e , s для s t r i n g и так далее. Единственным
нарушающим правило символом является п для i n t . Есть еще одно исключение —
по традиции, уходящей в программирование на Фортране, отдельные буквы i, j и к
также используются как распространенные имена переменных типа i n t .
Венгерская запись постепенно выходит из моды, по крайней мере в кругах программи­
стов .NET. Тем не менее я все еще остаюсь ее поклонником, поскольку она позволяет
мне знать тип каждой переменной в программе, не обращаясь к ее объявлению.
В последних версиях Visual Studio вы можете просто подвести курсор к переменной
и получить информацию о ее типе в окне подсказки, что делает венгерскую запись менее
полезной. Однако вместо того чтобы встревать в "религиозные войны" по поводу того
или иного способа именования, выберите тот, который вам по душе, и следуйте ему.

В жизни очень мало абсолюта, но он присутствует в С#: любое выражение имеет зна­
чение и тип. В объявлении наподобие i n t п легко увидеть, что переменная п имеет тип
int. Разумно предположить, что тип результата вычисления п + 1 также i n t . Но что
можно сказать о типе константы 1?
Тип константы зависит от двух вещей: ее значения и наличия необязательной буквы
в конце. Любое целое величиной до примерно 2 миллиардов (см. табл. 3.1) рассматрива­
ется как i n t . Числа, превышающие это значение, трактуются как l o n g . Любые числа
с плавающей точкой рассматриваются как d o u b l e .
В табл. 3.4 показаны константы, объявленные как имеющие конкретные типы, т.е.,
в частности, с буквенными дескрипторами в конце. Строчные эти буквы или пропис­
ные—значения не имеет, например записи lu и 1U равноценны.

Глава 3. Объявление переменных-значений

69

Окончание табл. 3.'
Константа

Тип

1. 0

double

1 . OF

float



decimal

true

bool

false

bool

1

а

1

char

•\п'
1

c h a r (СИМВОЛ НОВОЙ с т р о к и )

\xl23

"a

1

string"

c h a r ( с и м в о л с ш е с т н а д ц а т е р и ч н ы м ч и с л о в ы м з н а ч е н и е м 123)
string
s t r i n g ( п у с т а я строка)

Человек не рассматривает числа, используемые для счета, как разнотипные. Напри
мер, нормальный человек (не программист на С#) не станет задумываться, глядя на чис­
ло 1, знаковое оно или беззнаковое, "короткое" или "длинное". Хотя для С# все эти типы
различны, даже он понимает, что все они тесно связаны между собой. Например, в при
веденном далее фрагменте исходного текста величина типа i n t преобразуется в l o n g :
i n t nValue = 10;
long lvalue;
l v a l u e = nValue;

//

Это п р и с в а и в а н и е

корректно

Переменная типа i n t может быть преобразована в l o n g , поскольку любое значение
типа i n t может храниться в переменной типа l o n g и оба типа представляют собой чис­
ла, пригодные для перечислений. С# выполняет такое преобразование автоматически,
без каких-либо комментариев.
Однако обратное преобразование может вызвать проблемы. Например, приведенный
далее фрагмент исходного текста содержит ошибку:
long lvalue = 10;
i n t nValue;
nValue = l v a l u e ;

// Неверно!

Некоторые значения, которые могут храниться в переменной l o n g , не помещаются
в переменной типа i n t (ну, например, 4 миллиарда). С# в такой ситуации генерирует
сообщение об ошибке, поскольку в процессе преобразования данные могут быть утеря­
ны. Ошибку такого рода обычно довольно сложно обнаружить.
Но что, если вы точно знаете, что такое преобразование вполне допустимо? Напри­
мер, несмотря на то что переменная l v a l u e имеет тип l o n g , в данной конкретной про­
грамме ее значение не может превышать 10 0. В этом случае преобразование перемен­
ной l v a l u e типа l o n g в переменную n V a l u e типа i n t совершенно корректно.
Вы можете пояснить С#, что отлично понимаете, что делаете, посредством оператора
приведения типов:
long lvalue = 10;
i n t nValue;
nValue = ( i n t ) l v a l u e ;

70

// Все в порядке

Часть II. Основы программирования в С#

Глава 3. С

При приведении вы размещаете имя требующегося типа в круглых скобках непосред­
ственно перед преобразуемым значением. Приведенная выше запись гласит: "Не волнуй­
ся и преобразуй l v a l u e в тип i n t — я знаю, что делаю, и всю ответственность беру на
себя". Конечно, такое утверждение может показаться излишне самоуверенным, но зачастую оно совершенно справедливо.
Перечислимые числа могут быть преобразованы в числа с плавающей точкой автома­
тически, но обратное преобразование требует использования оператора приведения ти­
пов, например:
double dValue = 1 0 . 0 ;
long l v a l u e = ( l o n g ) d V a l u e ;
Все приведения к типу d e c i m a l и из него нуждаются в применении оператора при­
ведения типов. В действительности все числовые типы могут быть преобразованы в дру­
гие числовые типы с помощью такого оператора. Однако ни b o o l , ни s t r i n g не могут
быть непосредственно приведены ни к какому иному типу.
Встроенные функции С# могут преобразовывать числа, символы или логиче­
ские переменные в их строковые "эквиваленты". Например, вы можете преоб­
разовать значение t r u e типа b o o l в строку " t r u e " ; однако такое преобразо­
вание нельзя рассматривать как непосредственное. Эти два значения — совер­
шенно разные вещи.

Глава 3. Объявление переменных-значений

71

Глава4

Операторы
> Выполнение арифметических действий
I Логические операции
> Составные логические операторы

атематики создают переменные и выполняют над ними различные дейст­
вия, складывая их, умножая, а иногда — представьте себе — даже интегри­
руя. В главе 3, "Объявление переменных-значений", описано, как объявлять и опреде­
лять переменные, но там ничего не говорится о том, как их использовать после объяв­
ления, чтобы получить что-то полезное. В этой главе рассматриваются операции, ко­
торые могут быть произведены над переменными. Для выполнения операций требуют­
ся операторы, такие как +, -, =, < или &. Здесь речь пойдет об арифметических,
^логических и других операторах.

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

Простейшие о п е р а т о р ы
•С большинством из этих операторов вы должны были познакомиться еще в школе.
Они перечислены в табл. 4.1. Обратите внимание, что в программировании для обозна­
чения умножения используется звездочка (*), а не крестик (х).

Большинство этих операторов называются бинарными, поскольку они выполняются
над двумя значениями: одно из них находится с левой стороны от оператора, а другое —
с правой. Единственным исключением является унарный минус, который столь же прост,
как и остальные рассматриваемые здесь операторы:
i n t nl = 5;
i n t n2 = - n l ;

//

Теперь

значение п2 равно

-5

Значение -п представляет собой отрицательное значение п.
Оператор деления по модулю может быть вам незнаком. Деление по модулю анало- 1
гично получению остатка после деления. Так, 5%3 равно 2 ( 5 / 3 = 1, остаток 2), а^25%3 I
равен 1 ( 2 5 / 3 = 8, остаток 1).
Строгое определение оператора % выглядит как х= ( х / у ) *у + ( х % у ) .

Арифметические операторы (кроме деления по модулю) определены для всех типов
переменных. Оператор же деления по модулю не определен для чисел с плавающей точ- I
кой, поскольку при делении значений с плавающей точкой не существует остатка.

Порядок выполнения операторов
Значение некоторых выражений может оказаться непонятным. Например, рассмот- |
рим выражение:
int П = 5 * 3 + 2 ;
Что имел в виду написавший такую строку программист? Что надо умножить 5 на 3,
а затем прибавить 2? Или сначала сложить 3 и 2, а результат умножить на 5?

|

С# обычно выполняет операторы слева направо, так что результатом приведен- I
ного примера будет значение, равное 17.

С# вычисляет значение п в представленном далее выражении, сначала деля 24 на 6,
а затем деля получившееся значение на 2 :
int

п

=

24/6/2;

Однако у операторов есть своя иерархия, приоритеты, или проще — свой порядок
выполнения. С# считывает все выражение и определяет, какие операторы имеют наи­
высший приоритет и должны быть выполнены до операторов с меньшим приоритетом. I
Например, приоритет умножения выше, чем сложения. Во многих книгах изложению
этого вопроса посвящены целые главы, но сейчас не стоит забивать этим ни главу, ни
вашу голову.
Никогда не полагайтесь на то, что вы (или кто-то иной) помните приоритеты I
операторов. Явно указывайте подразумеваемый порядок выполнения выраже­
ния посредством скобок.
Значение следующего выражения совершенно очевидно и не зависит от приоритета
операторов:
int п =

74

(7

%

3)

*

(4+

( 6 / 3 ) ) ;

Часть //. Основы программирования в С# [

Скобки перекрывают приоритеты операторов, явно указывая, как именно компилятор
должен интерпретировать выражение. С# ищет наиболее вложенную пару скобок и вы­
числяет выражение в ней — в данном случае это 6 / 3 , что дает значение 2. В результате
получается:
int п = (7 % 3)

*

(4 + 2) ;

// 2 = 6 / 3

Затем С# продолжает поиск скобок и вычисляет значения в них, что приводит к выражению:
int n = 1 * 6 ;

/ / 6 = 4 + 2,

1 = 7 % 3

Так что в конечном счете получается:
int П = 6;
Правило "всегда используйте скобки" имеет, пожалуй, одно исключение. Лично мне
с этим сложно примириться, но многие программисты опускают скобки в выражениях
наподобие приведенного ниже, поскольку очевидно, что умножение имеет более высо­
кий приоритет, чем сложение:
int п = 7 + 2

*

3;

//

То ж е ,

что и 7 +

( 2 * 3 )

Оператор п р и с в а и в а н и я
С# унаследовал одну интересную концепцию от С и С++: присваивание является би­
нарным оператором, возвращающим значение аргумента справа от него. Присваивание
имеет тот же тип, что и оба аргумента (типы которых должны быть одинаковы).
Этот новый взгляд на присваивание никак не влияет на выражения, с которыми вы
уже сталкивались:
П

= 5 * 3;
В данном примере 5 * 3 = 15 и имеет тип i n t . Оператор присваивания сохраняет это

int-значение справа в i n t - п е р е м е н н о й слева и возвращает значение 1 5 . То, что он воз­
вращает значение, позволяет, например, сохранить это значение еще в одной перемен­
ной, т.е. написать:
m= п = 5 * 3;
При наличии нескольких присваиваний они выполняются справа налево. В приведен­
ном выше выражении правый оператор присваивания сохраняет значение 15 в перемен­
ной п и возвращает 15, после чего левый оператор присваивания сохраняет значение 15
в переменной m и возвращает 15 (это возвращенное значение в данном примере больше
никак не используется).
Такое странное определение присваивания делает корректным такой причудли­
вый фрагмент, как показанный ниже (хотя я и предпочитаю воздерживаться от по­
добных вещей):
int n;
int m;
П = m = 2 ;
Старайтесь избегать цепочек присваиваний, поскольку они менее понятны человеку,
читающему исходный текст программы. Всего, что может запутать человека, читающего
исходный текст вашей программы (включая и лично вас), следует избегать. Любые неяс­
ности ведут к ошибкам. Огромная часть программирования — от правил языка и его
конструкций до соглашений по именованию переменных и рекомендаций, основанных
на опыте программистов — нацелены на одно: устранение ошибок в программах.

[лава 4. Операторы

75

С# добавляет ко множеству простейших операторов небольшое подмножество опера­
торов, построенных на основе других бинарных операторов. Например, выражение

П +=

1;

эквивалентно следующему:

П = П +

1 ;

Такие операторы присваивания существуют почти для всех бинарных операторов. I
В табл. 4.2 показаны наиболее распространенные составные операторы присваивания.

Т а б л и ц а 4.2. Составные операторы присваивания
Оператор

Значение

В табл. 4.2 опущено два более сложных составных оператора присваивания, =.
Операторы побитового сдвига, на которых они основаны, будут рассмотрены позже
в этой главе.

Оператор инкремента
Среди всех сложений, выполняемых в программах,добавление 1 к переменной —
наиболее распространенная операция:
n = n +

1;

// У в е л и ч е н и е п на

1

С# позволяет записать такую операцию сокращенно:
n +=

1;

// У в е л и ч е н и е п на

1

Но, оказывается, и это недостаточно кратко, и в С# имеется еще более краткое обозначе­
ние этого действия — оператор инкремента:
++п;

// Увеличение п на 1

Все три приведенных выражения функционально эквивалентны, т.е. все они увеличи­
вают значение п на 1.
Оператор инкремента достаточно странен, но еще больше странности добавляет ему
то, что на самом деле имеется два оператора инкремента: + + п и п + + . Первый, + + п , на­
зывается префиксным, а второй, п + + , — постфиксным. Разница между ними достаточно
тонкая, но очень важная.
Вспомните, что каждое выражение имеет тип и значение. В следующем фрагменте
исходного текста и + + п , и п++ имеют тип int:

int П;
П = 1;
int р = ++П;
76

Часть II. Основы программирования в С#

n = 1;
int m = n + + ;
Чему равны значения p и m после выполнения этого фрагмента? (Подсказка: можно
выбирать 1 или 2.) Оказывается, значение р равно 2, а значение m — 1. То есть значение
выражения + + п — это значение п после увеличения, а значение п + + равно значению п
до увеличения. Значение самой переменной п в обоих вариантах равно 2.
Эквивалентные

операторы

декремента—

п- -

и

— п—

используются

для

замены выражения n = n - 1 . Они работают точно так же, как и операторы инкремента.

Откуда взялся оператор инкремента?
Причина появления оператора инкремента лежит в туманном прошлом —
в наличии в 1970-х годах в машине PDP-8 машинной команды инкремента.
Язык С, прямой предок С#, в свое время создавался для применения именно на этих
машинах. Наличие соответствующей машинной команды позволяло уменьшить ко­
личество машинных команд при использовании п + + вместо п=пч-1. В то время эко­
номия даже нескольких машинных команд давала существенный выигрыш во вре­
мени работы.
В настоящее время компиляторы гораздо интеллектуальнее, и нет никакой разницы,
написать ли в программе п + + или п = п + 1 . Однако программисты — люди привычки,
так что оператор инкремента благополучно дожил до сегодняшних дней, и увидеть
в программе на С или С++ выражение п = п + 1 практически нереально.
Кроме того, чаще всего программистами используется постфиксная версия оператора.
Впрочем, это дело вкуса.. ?

С# предоставляет к услугам программиста также целый ряд логических операторов
сравнения, показанных в табл. 4.3. Эти операторы называются логическими сравнениями
(logical comparisons), поскольку они возвращают результат сравнения в виде значения
true или f a l s e , имеющего тип b o o l .
• Вот примеры использования логических сравнений:
int m = 5;
int n = 6;
bool b = m > n;
В этом примере переменной b присваивается значение f a l s e , поскольку 5 не боль­
ше, чем 6.

2

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

Глава 4. Операторы

77

Т а б л и ц а 4.3. Л о г и ч е с к и е операторы сравнения
Оператор...

...возвращает true, если...

а

=.= Ь

а имеет то же значение, что и ь

а



а больше ь

а

>= b

а больше или равно ь
а меньше ь

а

< ь
2;
b = 3 . 0 > 2.0;
1
> 'b' ;
b = 'a
b =

1

A'
1

b =• ' A

<

'a';

<

'b' ;



//
//
//
//
//
//
//
//
//

b = 10M > 12M;

true
true
f a l s e - позже в алфавитном порядке
о з н а ч а е т "больше"
true
- п р о п и с н о е 'А' меньше
строчного ' а '
true
- в с е п р о п и с н ы е буквы меньше в с е х
строчных
false

Операторы сравнения всегда дают в качестве результата величину типа b o o l . Опера­
торы сравнения, отличные от ==, неприменимы к переменным тира s t r i n g (не волнуй­
тесь, С# предлагает другие способы сравнения строк).

Сравнение чисел с плавающей точкой
Сравнение двух чисел с плавающей точкой может легко оказаться не вполне коррект­
ным, так что тут нужна особая осторожность. Рассмотрим следующее сравнение:
float
float
fl =
f2 =
bool
fl =
f2 =
bool

fl;
f2;
10;
fl /
Ы =
9;
fl /
b2 =

3;
(3

*

f2)

==

fl;

3;
(3

*

f2)

==

fl;

Обратите внимание, что в пятой и восьмой строках примера сначала содержится опе­
ратор присваивания =, а затем оператор сравнения ==. Это — разные операторы.
С# сначала выполняет логическое сравнение, а затем присваивает его результат пере­
менной слева от оператора присваивания.
Единственное

отличие

между

вычислениями

Ы

и

Ь2

состоит

в

исходном

значении f 1. Так чему же равны значения Ы и Ь2? Очевидно, что значение Ы равно
t r u e : 9/3 равно 3, 3*3 равно 9, 9 равно 9. Никаких проблем!
Значение Ы не столь очевидно: 10/3 равно 3.3333.... 3.3333...*3 равно 9.9999.... Но
равны ли числа 9.9999... и 10? Это зависит от того, насколько сообразительны ваши

78

Часть II. Основы программирования в С#

компилятор и процессор. При использовании процессора типа Pentium С# недостаточно
умен для того, чтобы понять, что Ы надо присвоить значение t r u e .
Для сравнения f 1 и f 2 можно воспользоваться функцией для вычисления аб­
солютного значения следующим образом:
Math.abs(fl-f2*3.0)

type3

(Здесь стрелка означает "дает".) Типы t y p e l и t y p e 2 должны быть совместимы с опе­
ратором о р .

80

Часть II. Основы программирования в С#

Большинство операторов могут иметь несколько вариантов. Например, оператор ум­
ножения может быть следующих видов:
int
uint
long
float
decimal
double

* int
* uint
* long
* float
* decimal
* double

•=>


•=>
•=>



int
uint
long
float
decimal
double

Таким образом, 2 * 3 использует i n t * i n t версию оператора * и дает в результате
int 6.

Неявное преобразование типов
Все хорошо, просто и понятно, если умножать две переменные типа i n t или две пе­
ременные типа f l o a t . Но что получится, если типы аргументов слева и справа будут
различны? Что, например, произойдет в следующей ситуации:
int n l = 1 0 ;
double d2 = 5 . 0 ;
double d R e s u l t = nl

*

d2 ;

Во-первых, в C# нет оператора умножения i n t * d o u b l e . C# может просто сгенери­
ровать сообщение об ошибке и предоставить разбираться с проблемой программисту.
Однако он пытается понять намерения программиста и помочь ему. У С# есть операторы
умножения i n t * i n t и d o u b l e * d o u b l e . С# мог бы преобразовать d2 в значение i n t ,
но такое преобразование привело бы к потере дробной части числа (цифр после десятич­
ной точки). Поэтому вместо этого С# преобразует nl в значение типа d o u b l e и исполь­
зует оператор умножения d o u b l e * d o u b l e . Это действие известно как неявное повы­
шение типа (implicit promotion).
Такое повышение называется неявным, поскольку С# выполняет его автоматически,
и является повышением, так как включает естественную концепцию высоты типа. Список
операторов умножения был приведен в порядке повышения— от i n t до d o u b l e , или
от i n t до d e c i m a l — от типа меньшего размера к типу большего размера. Между ти­
пами с плавающей точкой и d e c i m a l неявное преобразование не выполняется. Преоб­
разование из более емкого типа, такого как d o u b l e , в менее емкий, такой как i n t , на­
зывается понижением (demotion).
Повышение иногда называют преобразованием вверх (up conversion), а понижение —
преобразованием вниз (down conversion).
Неявные понижения запрещены. В таких случаях С# генерирует сообщение об
ошибке.

Явное преобразование т и п а
Но что, если С# ошибается? Если на самом деле программист хотел выполнить целое
умножение?
Вы можете изменить тип любой переменной с типом-значением с помощью оператора
приведения типа (cast), который представляет собой требуемый тип, заключенный в скобки,
и располагаемый непосредственно перед приводимой переменной или выражением.

Глава 4. Операторы

81

Таким образом, в следующем выражении используется оператор умножения i n t * i n t :
int nl
double
double

= 10;
d2 = 5 . 0 ;
nResult =

nl

*

(int)d2;

Приведение d2 к типу i n t известно как явное понижение (explicit demotion) или noi
нижающее приведение (downcast). Понижение является явным, поскольку программю!
явно объявил о своих намерениях.
Вы можете осуществить приведение между двумя любыми типами-значениями, нем
висимо от их взаимной высоты.
Избегайте неявного преобразования типов. Делайте все изменения типов!
значений явными с помощью оператора приведения.

Оставьте л о г и к у в покое
С# не позволяет преобразовывать другие типы в тип b o o l или выполнять преобразо­
вание типа b o o l в другие типы.

Т и п ы при присваивании
Все сказанное о типах выражений применимо и к оператору присваивания.
Случайные несоответствия типов, приводящие к генерации сообщений об
ошибках, обычно происходят в операторах присваивания, а не в точке действи­
тельного несоответствия.
Рассмотрим следующий пример умножения:
i n t n l = 10;
i n t П2 = 5 . 0

*

nl;

Вторая строка этого примера приведет к генерации сообщения об ошибке, связанной
с несоответствием типов, но ошибка произошла при присваивании, а не при умножении.
Вот что произошло: для того чтобы выполнить умножение, С# неявно преобразовал nl
в тип d o u b l e . Затем С# выполнил умножение двух значений типа d o u b l e , получив в
результате значение того же типа d o u b l e .
Типы левого и правого аргументов оператора присваивания должны совпадать, но
тип левого аргумента не может быть изменен. Поскольку С# не может неявно понизить
тип выражения, компилятор генерирует сообщение о том, что он не может неявно пре­
образовать тип d o u b l e в i n t .
При использовании явного приведения никаких проблем не возникнет:
int nl
i n t n2

=
=

10;
(int) (5.0

*

nl) ;

(Скобки необходимы, потому что оператор приведения имеет очень высокий приоритет.)
Такой исходный текст вполне работоспособен, так как явное понижение разрешено.
Здесь значение nl будет повышено до d o u b l e , выполнено умножение, а результат типа
d o u b l e будет понижен до i n t . Однако в этой ситуации надо подумать о душевном здо­
ровье программиста, поскольку написать просто 5 * п 1 было бы проще как для програм­
миста, так и для С#.

82

Часть II. Основы программирования в С#

Большинство операторов имеют два аргумента, меньшинство —- один. И только один
оператор — тернарный — имеет три аргумента. Лично я считаю, что это ненужная экзо­
тика. Вот формат этого оператора:
Выражение т и п а b o o l ? Выражение1

:

Выражение2

А это пример его применения:
int а = 1 ;
int Ь = 2 ;
int nMax =

(a>b)

? а

:

Ь;

Если а больше Ь (условие в скобках), значение выражения равно а. Если а не больше Ь,
значение выражения равно Ь.
Выражения 1 и 2 могут быть любой сложности, но это должны быть истинные выра­
жения— они не могут содержать объявлений или других инструкций, не являющихся
выражениями.
3

Тернарный оператор непопулярен по следующим причинам.
Он не является необходимым. Использование оператора i f , описанного в гла­
ве 5, "Управление потоком выполнения", дает тот же эффект, и его легче понять.
На тернарный оператор накладываются дополнительные ограничения. На­
пример, выражения 1 и 2 должны быть одного и того же типа. Это приводит
к следующему:
int а = 1 ;
double b = 0 . 0 ;
i n t nMax = ( a > b ) ? а : b;
Такой исходный текст не будет компилироваться, несмотря на то что в конечном
итоге nMax будет иметь значение а. Поскольку а и b должны быть одного и того
же типа, а будет повышено до d o u b l e , чтобы соответствовать Ь. Тип результи­
рующего значения оператора ?:

оказывается d o u b l e , и этот тип должен быть

понижен до i n t перед присваиванием:
int а = 1 ;
double b = 0 . 0 ;
i n t nMax;
/ / Можно п о с т у п и т ь
nMax = ( i n t ) ( ( a > b )
/ / . . . или т а к :
nMax = ( a > b ) ? а :

так:
? а : b) ;
(int)b;

Увидеть тернарный оператор в реальной программе — большая редкость.

3

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

Глава 4. Операторы

83

Глава 5

Управление потоком выполнения
> Что делать, если...
> Цикл w h i l e
> Цикл f o r
> Конструкция s w i t c h

ассмотрим следующую простую программу:
using S y s t e m ;
namespace H e l l o W o r l d
(
public

class

Program

{
/ / С т а р т о в а я т о ч к а программы
s t a t i c void Main(string[] args)
{
/ / Приглашение д л я в в о д а и м е н и
C o n s o l e . W r i t e L i n e ( " В в е д и т е ваше и м я : " ) ;
/ / Считывание в в е д е н н о г о и м е н и
s t r i n g sName = C o n s o l e . R e a d L i n e ( ) ;
// П р и в е т с т в и е с и с п о л ь з о в а н и е м в в е д е н н о г о имени
Console.WriteLine("Привет, " + sName);
/ / Ожидание п о д т в е р ж д е н и я п о л ь з о в а т е л я
C o n s o l e . W r i t e L i n e ( " Н а ж м и т е для "
"завершения
программы...");
Console.Read();

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

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

Основой возможности принятия решения в С# является оператор i f :
if

i

(Условие)
II

Этот

код

выполняется,

если

Условие

истинно

}
//
//

Этот код выполняется вне з а в и с и м о с т и от
истинности
Условия
Непосредственно за оператором if в круглых скобках содержится некоторое услов­
ное выражение типа b o o l (см. главу 4, "Операторы"), после чего следует код, заклю­
ченный в фигурные скобки. Если условное выражение истинно (имеет значение true),
программа выполняет код, заключенный в фигурных скобках. Если нет — этот код про­
граммой опускается.
Работу оператора if проще понять, рассматривая конкретный пример:
// Гарантируем, ч т о а - н е о т р и ц а т е л ь н о :
/ / Е с л и а меньше 0 . . .
i f ( а < 0)
{
// ...присваиваем э т о й переменной значение 0
а = 0;

}

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

О п е р а т о р if
Рассмотрим небольшую программу, вычисляющую проценты. Пользователь вводит
вклад и проценты, и программа подсчитывает сумму, получаемую по итогам года (это не
слишком сложная программа). Вот как такие вычисления выглядят на С#:
// В ы ч и с л е н и е суммы в к л а д а и п р о ц е н т о в
decimal mlnterestPaid;
mlnterestPaid = mPrincipal * (mlnterest / 100);
/ / В ы ч и с л е н и е общей суммы
decimal mTotal = m P r i n c i p a l + m l n t e r e s t P a i d ;
В первом уравнении величина вклада m P r i n c i p a l умножается на величину про­
центной ставки m l n t e r e s t (деление на 1 0 0 связано с тем, что пользователь вводит ве­
личину ставки в процентах). Получившаяся величина увеличения вклада сохраняется
в переменной m l n t e r e s t P a i d , а затем суммируется с основным вкладом и сохраняет­
ся в переменной m T o t a l .

86

Часть II. Основы программирования в С#

Программа должна предвидеть, что данные вводит всего лишь человек, ко­
торому свойственно ошибаться. Например, ошибкой должны считаться от­
рицательные величины вклада или процентов (конечно, в банке хотели бы,
чтобы это было не так...), и в приведенной далее программе C a l c u l a t e I n t e r e s t , имеющейся на прилагаемом компакт-диске, выполняются соот­
ветствующие проверки.
// C a l c u l a t e l n t e r e s t
// Вычисление величины н а ч и с л е н н ы х п р о ц е н т о в д л я д а н н о г о
// вклада. Е с л и п р о ц е н т н а я с т а в к а или в к л а д о т р и ц а т е л ь н ы ,
// г е н е р и р у е т с я с о о б щ е н и е об о ш и б к е .
using S y s t e m ;
namespace C a l c u l a t e l n t e r e s t
(
public

class

Program

{
public

s t a t i c void Main(string[]

args)

{
/ / Приглашение д л я в в о д а в к л а д а
C o n s o l e . W r i t e ( " В в е д и т е сумму в к л а д а : " ) ;
string sPrincipal = Console.ReadLine();
decimal mPrincipal =
Convert.ToDecimal(sPrincipal);
// Убеждаемся, что вклад не отрицателен
if ( m P r i n c i p a l < 0)
{
C o n s o l e . W r i t e L i n e ( " В к л а д н е может "
"быть о т р и ц а т е л ь н ы м " ) ;
m P r i n c i p a l = 0;

}
/ / Приглашение д л я в в о д а п р о ц е н т н о й с т а в к и
C o n s o l e . W r i t e ( " В в е д и т е процентную с т а в к у : " ) ;
s t r i n g s l n t e r e s t = Console . ReadLine О ;
decimal mlnterest =
Convert.ToDecimal(slnterest);
// Убеждаемся, что процентная ставка не
// отрицательна
if ( m l n t e r e s t < 0)
{

C o n s o l e . W r i t e L i n e ( " П р о ц е н т н а я с т а в к а не "
"может быть о т р и ц а т е л ь н а " ) ;
m l n t e r e s t = О,-

}

О

/ / Вычисляем сумму величины п р о ц е н т н ы х
// начислений и вклада
decimal mlnterestPaid;
mlnterestPaid = mPrincipal * (mlnterest / 100);
/ / В ы ч и с л е н и е общей суммы
d e c i m a l mTotal = m P r i n c i p a l + m l n t e r e s t P a i d ;
/ / Вывод р е з у л ь т а т о в
C o n s o l e . W r i t e L i n e ( ) ; // skip a l i n e
Console.WriteLine("Вклад = " + mPrincipal);

Глава 5. Управление потоком выполнения

87

Console.WriteLine("Проценты = "+mInterest+"%");
Console.WriteLine();
C o n s o l e . W r i t e L i n e ( " Н а ч и с л е н н ы е проценты = "
+ mlnterestPaid);
C o n s o l e . W r i t e L i n e ( " О б щ а я сумма = " + m T o t a l ) ;
/ / Ожидание р е а к ц и и п о л ь з о в а т е л я
C o n s o l e . W r i t e L i n e ( " Н а ж м и т е < E n t e r > для "
"завершения п р о г р а м м ы . . . " ) ;
Console.Read();

}
}

}
Программа C a l c u l a t e l n t e r e s t начинает свою работу с предложения пользовате!

лю ввести величину вклада. Это предложение выводится с помощью функции W r i t e - •
L i n e ( ) , которая выводит значение типа s t r i n g н а консоль.
Всегда точно объясняйте пользователю, чего вы от него хотите. Если возмож-В
но, укажите также требуемый формат вводимых данных. Обычно на неинфор-И
мативные приглашения наподобие одного символа > пользователи отвечают»
совершенно некорректно.
В программе для считывания всего пользовательского ввода до нажатия клавиши!
в переменную типа s t r i n g используется функция R e a d L i n e ( ) . Поскольку!
программа работает с величиной вклада как имеющей тип d e c i m a l , введенную строку!
следует преобразовать в переменную типа d e c i m a l , что и делает функция Con­
v e r t . T o D e c i m a l ( ) . Полученный результат сохраняется в переменной m P r i n c i p a l .
Команды R e a d L i n e ( ) , W r i t e L i n e () и T o D e c i m a l () служат примерами вы- I
зовов функций. Вызов функции делегирует некоторую работу другой части про­
граммы, именуемой функцией. Детально вызов функций будет описан в главе 7,
"Функции функций", но приведенные здесь примеры очень просты и понятны.
Если же вам что-то не ясно в вызовах функций, потерпите немного, и все будет
объяснено детально.
В следующей строке выполняется проверка переменной m P r i n c i p a l . Если она от­
рицательна, программа выводит сообщение об ошибке. Те же действия производятся и
для величины процентной ставки. После этого программа вычисляет общую сумму так,
как уже было описано в начале раздела, и выводит конечный результат посредством не­
скольких вызовов функции W r i t e L i n e ( ) .
Вот пример вывода программы при корректном пользовательском вводе:
В в е д и т е сумму в к л а д а : 1 2 3 4
Введите процентную с т а в к у : 21
Вклад
Проценты

= 1234
= 21%

Начисленные проценты = 2 5 9 . 1 4
Общая сумма
= 1493.14
Нажмите < E n t e r > д л я з а в е р ш е н и я

программы...

88

Часть II. Основы программирования в С#

А так выглядит вывод программы при ошибочном вводе отрицательной величины
процентной ставки:
Введите сумму в к л а д а : 1 2 3 4
Введите п р о ц е н т н у ю с т а в к у
Процентная с т а в к а н е может
Вклад
Проценты

г-12.5
быть о т р и ц а т е л ь н а

= 1234
= 0%

Начисленные п р о ц е н т ы = 0
Общая сумма
= 12 34
Нажмите < E n t e r > д л я з а в е р ш е н и я п р о г р а м м ы . . .
Отступ внутри блока if повышает удобочитаемость исходного текста. С# иг­
норирует все отступы, но для человека они весьма важны. Большинство редак­
торов для программистов автоматически добавляют отступ при вводе операто­
ра i f . Для включения автоматического отступа в Visual Studio выберите ко­
манду меню T o o l s O O p t i o n s , затем раскройте узел T e x t Editor, потом С # ,
а в конце щелкните на вкладке T a b s . На ней включите флаг S m a r t Indenting
и установите то количество пробелов на один отступ, которое вам по душе. Ус­
тановите то же значение и в поле T a b S i z e .

Инструкция else
Некоторые функции должны проверять взаимоисключающие условия. Например,
в приведенном далее фрагменте исходного текста в переменной m a x сохраняется наи­
большее из двух значений а и Ь:
// Сохраняем н а и б о л ь ш е е из д в у х з н а ч е н и й а и b
// в п е р е м е н н о й m a x
int max;
// Если а больше Ь . . .
if (а > Ь)

(

I

// . . . с о х р а н я е м з н а ч е н и е
max = а,-

.

а в переменной max

- '

"

// Если а меньше или р а в н о Ь . . .
if (а Ь)

I

I'

/ / Управление о п е р а т о р о м g o t o п е р е д а е т с я
// расположенному за меткой e x i t L a b e l
goto e x i t L a b e l ;

коду,

// Некоторый программный к о д
exitLabel:
// Управление п е р е д а е т с я в э т у т о ч к у
Оператор g o t o крайне непопулярен по той же причине, по которой он является
очень мощным средством — в силу его полной неструктурированности. Отслеживание
переходов в нетривиальных случаях, превышающих несколько строк кода, — крайне не­
благодарная задача. Это именно тот случай, когда говорят о "соплях" в программе.
Вокруг применения g o t o ведутся почти "религиозные войны". Доходит до
критики С# просто за то, что в нем есть этот оператор. Но на самом деле в нем
нет ничего ужасного или демонического. Другое дело, что его применения сле­
дует избегать, если в этом нет крайней нужды.

Глава 5. Управление потоком выполнения

111

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

Глава 6

Объединение данных классы и массивы
> Введение в классы С#
> Хранение данных в объектах
> Ссылки на объекты
> Создание массивов объектов

ы можете свободно объявлять и использовать все встроенные типы данных —
такие как i n t , d o u b l e или b o o l — для хранения информации, необходимой
вашей программе. Для ряда программ таких простых переменных вполне достаточно, но
большинству программ требуется средство для объединения связанных данных в акку­
ратные пакеты.
Некоторым программам надо собрать вместе данные, связанные логически, но
имеющие разные типы. Например, приложение, работающее со списками студентов,
должно хранить разнотипную информацию о них — имя, год рождения, успеваемость
и т.п. Логически рассуждая, имя студента может иметь тип s t r i n g , год рождения —
int или s h o r t , средний б а л л — d o u b l e . Такой программе необходима возможность
объединить эти разнотипные переменные в единую структуру под именем S t u d e n t .
К счастью, в С# имеется структура, известная как класс, которая предназначена для об­
легчения группирования таких разнотипных переменных.
В других случаях программам требуются наборы однотипных объектов. Возьмем для
примера программу, которая должна усреднять успеваемость. Тип d o u b l e — естест­
венный кандидат для представления индивидуальной успеваемости студента, но для того
чтобы представлять успеваемость группы студентов, необходим тип, который является
набором d o u b l e . Для таких целей в С# существуют массивы.
И наконец, реальной программе для работы с информацией о студентах могут пона­
добиться как классы, так и массивы, причем объединенные в одно целое — массив сту­
дентов. С# позволяет получить желаемое.

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

Аналитик скорее всего скажет, что "класс отображает концепцию из предметной об
ласти задачи в программу". Предположим, например, что ваша з а д а ч а — построения
имитатора дорожного движения, который должен смоделировать улицы, перекрестка
шоссе и т.п.
Любое описание такой задачи должно включать термин транспортное средство
Транспортные средства обладают определенной максимальной скоростью движении
имеют вес, и некоторые из них оснащены прицепами. Таким образом, имитатор дорож
ного движения должен включать класс V e h i c l e , у которого должны иметься свойств
наподобие d T o p S p e e d , n W e i g h t и b T r a i l e r .
Поскольку классы — центральная концепция в программировании на С#, они буи
гораздо детальнее рассмотрены в главах части IV, "Объектно-ориентированное
программирование"; здесь же описаны только азы.

Определение класса
Пример класса V e h i c l e может выглядеть следующим образом:
public

class

Vehicle

{
public
public
public
public

s t r i n g sModel;
s t r i n g sManufacturer;
i n t nNumOfDoors;
i n t nNumOfWheels;

//
//
//
//

Название модели
Производитель
Количество дверей
Количество колес

}
Определение класса начинается словами p u b l i c

c l a s s , за которыми идет имя

класса (в данном случае — V e h i c l e ) .
Как и все имена в С#, имена классов чувствительны к регистру. С# не имея
никаких правил для именования классов, но неофициальная традиция гласит
что имена классов начинаются с прописной буквы.
За именем класса следует пара фигурных скобок, внутри которых могут быть не
сколько членов (либо ни одного). Члены класса представляют собой переменные, образующие часть класса. В данном примере класс V e h i c l e начинается с члена s t r i n g
s M o d e l , который содержит название модели транспортного средства. Второй член в
этом примере — s t r i n g

sManuf a c t u r e r , а последние два члена содержат количест-

во дверей и колес в транспортном средстве.
Как и в случае обычных переменных, делайте имена членов максимально информативными. Хотя я и добавил к именам членов комментарии, они не являются обязательными с точки зрения С#. Обо всем должны говорить имена переменных.
Модификатор p u b l i c перед именем класса делает класс доступным для всей про-!
граммы. Аналогично, модификаторы p u b l i c перед именами членов также делают их
доступными для всей программы. Возможны и другие модификаторы, но более подроб­
но о доступности речь пойдет в главе 11, "Классы".
Определение класса должно описывать свойства объектов решаемой задачи. Сделать
это прямо сейчас вам будет немного сложновато, поскольку вы не знаете, в чем именно
состоит задача, так что просто следите за ходом изложения.

116

Часть III. Объектно-основанное программирование

Глав,

Что т а к о е о б ъ е к т
Объект класса объявляется аналогично встроенным объектам С#, но не идентично им.
Термин объект используется для обозначения "вещей". Да, это не слишком
полезное определение, так что приведем несколько примеров. Переменная типа
i n t является объектом i n t . Автомобиль является объектом V e h i c l e .
Вот фрагмент кода, создающий автомобиль, являющийся объектом V e h i c l e :
Vehicle myCar;
myCar = new V e h i c l e () ;
В первой строке объявлена переменная myCar типа V e h i c l e , так же как вы можете
объявить переменную n S o m e t h i n g класса i n t (да, класс является типом, и все объекты
С# определяются как классы). Команда new V e h i c l e ()

создает конкретный объект

типа V e h i c l e и сохраняет его местоположение в переменной myCar. Оператор n e w
("новый") не имеет ничего общего с возрастом автомобиля — он просто выделяет новый
блок памяти, в котором ваша программа может хранить свойства автомобиля myCar.
В терминах С# m y C a r — это объект класса V e h i c l e . Можно также сказать,
что m y C a r — экземпляр класса V e h i c l e . В данном контексте экземпляр
(instance) означает "пример" или "один из". Можно использовать этот термин
и как глагол, и говорить об инстащировании V e h i c l e — это именно то, что
делает оператор new.
Сравните объявление myCar с объявлением переменной num типа i n t :
int num;
num = 1;
В первой строке объявлена переменная num типа i n t , а во второй созданной пере­
менной присваивается значение типа i n t , которое вносится в память по месту располо­
жения переменной num.
Переменная встроенного типа num и объект my С а г хранятся в памяти поразному. Константа 1 не занимает память, поскольку и процессор, и С# знают,
что такое 1. Но процессор понятия не имеет о такой концепции, как V e h i c l e .
Выражение

new V e h i c l e

выделяет

память,

необходимую

для

описания

транспортного средства, понятного процессору, С#, и вообще — всему миру.

Доступ к ч л е н а м о б ъ е к т а
Каждый объект класса V e h i c l e имеет собственный набор членов. Приведенное да­
лее выражение сохраняет число 1 в члене nNumberOf D o o r s объекта, на который ссы­
лается myCar:
myCar. nNumberOf D o o r s = 1;
Каждый оператор С# имеет не только значение, но и тип. Объект myCar явля­
ется объектом типа V e h i c l e . Переменная V e h i c l e .nNumberOf D o o r s име­
ет тип i n t (вернитесь к определению класса V e h i c l e ) . Константа 1 также
имеет тип i n t , так что типы константы с правой стороны от оператора при­
сваивания и переменной с левой стороны соответствуют друг другу.

Глава 6. Объединение данных - классы и массивы

117

Аналогично, в следующем фрагменте кода сохраняются ссылки на строки strinJ
описывающие модель и производителя myCar:
m y C a r . s M a n u f a c t u r e r = "BMW";
myCar.sModel = " I s e t t a " ;
( I s e t t a — небольшой автомобиль, который производился в 1950-х годах и имел одя
дверь впереди.)

Пример объектно-основанной программы
Программа V e h i c l e D a t a O n l y очень проста и делает следующее:
определяет класс V e h i c l e ;
создает объект myCar;
указывает свойства myCar;
получает значения свойств myCar и выводит их на экран.
Вот код программы V e h i c l e D a t a O n l y :

//

VehicleDataOnly

/ / С о з д а е т о б ъ е к т V e h i c l e , з а п о л н я е т е г о члены и н ф о р м а ц и е й ,
// в в о д и м о й с к л а в и а т у р ы , и выводит ее на э к р а н
using System;
namespace

VehicleDataOnly

{
public

class

Vehicle

{
public
public
public
public

s t r i n g sModel;
s t r i n g sManufacturer;
i n t nNumOfDoors,i n t nNumOf Wheels,-

/ / Модель
// Производитель
// Количество дверей
// Количество колес

}
public

class

Program

{
/ / Начало программы
s t a t i c void
Main(string[] args)
{
/ / Приглашение п о л ь з о в а т е л ю
C o n s o l e . W r i t e L i n e ( " В в е д и т е информацию

о

машине");

// Создание экземпляра V e h i c l e
Vehicle
myCar = new V e h i c l e ( ) , / / В в о д информации д л я ч л е н о в к л а с с а
Console.Write("Модель = " ) ;
string s = Console.ReadLine();
myCar.sModel = s;

118

Часть III. Объектно-основанное программировав

/ / Можно п р и с в а и в а т ь з н а ч е н и я н е п о с р е д с т в е н н о
Console.Write("Производитель = " ) ;
myCar.sManufacturer
= Console.ReadLineО;
/ / Остальные данные имеют тип i n t
Console.Write("Количество дверей = " ) ;
s = Console.ReadLine();
myCar.nNumOfDoors
= Convert.ToInt32(s),Console.Write("Количество колес = " ) ;
s = Console.ReadLine();
myCar.nNumOfWheels = C o n v e r t . T o I n t 3 2 ( s ) ;
// Вывод п о л у ч е н н о й информации
C o n s o l e . W r i t e L i n e ( " \ п В а ш а машина: " ) ;
Console.WriteLine(myCar.sManufacturer
+ " " +
myCar.sModel);
C o n s o l e . W r i t e L i n e ( " c " + myCar.nNumOfDoors +
" дверями, "
+
"на " + m y C a r . n N u m O f W h e e l s
+
" колесах") ;
/ / Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
C o n s o l e . W r i t e L i n e ( " Н а ж м и т е < E n t e r > для " +
" з а в е р ш е н и я п р о г р а м м ы . . . ") ;
Console.Read();

Листинг программы начинается с определения класса V e h i c l e .
Определение класса может находиться как до, так и после класса P r o g r a m —
это не имеет значения. Однако вам нужно принять какой-то один стиль и сле­
довать ему.
Программа создает объект myCar класса V e h i c l e , а затем заполняет все его поля
информацией, вводимой пользователем с клавиатуры. Проверка корректности входных
данных не производится. Затем программа выводит введенные данные на экран в немно­
го ином формате.
Вывод этой программы выглядит следующим образом:
Введите информацию о машине
Модель
= Metropolitan
Производитель = N a s h
Количество д в е р е й = 2
Количество к о л е с
= 4
Ваша м а ш и н а :

Nash M e t r o p o l i t a n
с 2 дверями, на 4 к о л е с а х
Нажмите < E n t e r > д л я з а в е р ш е н и я

программы...

Глава В, Объединение данных - классы и массивы

119

Вызов R e a d ( ) , в отличие от R e a d L i n e ( ) , оставляет курсор сразу за введен!
ной строкой. Это приводит к тому, что ввод пользователя находится на той nil
строке, что и приглашение. Кроме того, добавление символа новой стромГ
' \ п ' создает пустую строку без необходимости вызывать W r i t e L i n e ( ) .

О т л и ч и е м е ж д у объектами
Заводы в Детройте в состоянии выпускать множество автомобилей и отслеживать кажду»|
выпущенную машину и при этом не путать их. Аналогично, программа может создать не­
сколько объектов одного и того же класса, как показано в следующем фрагменте:
V e h i c l e c a r l = new V e h i c l e О ;
carl.sManufacturer = "Studebaker";
carl.sModel = "Avanti";
/ / Следующий к о д никак н е в л и я е т
V e h i c l e c a r 2 = new V e h i c l e ( ) ;
c a r 2 . s M a n u f a c t u r e r = "Hudson";
car2.sModel = "Hornet";

на

carl

Создание объекта c a r 2 и присваивание ему имени и производителя никак не влияй]
н а объект c a r l .
Возможность различать объекты одного класса очень важна при программировании,
Объект может быть создан, с ним могут быть выполнены различные действия — и он
всегда выступает как единый объект, отличный от других подобных ему объектов.

Ссылки
Оператор "точка" и оператор присваивания — единственные два оператора, опреде­
ленные для ссылочных типов. Рассмотрим следующий фрагмент исходного текста:
/ / С о з д а н и е н у л е в о й ссылки
V e h i c l e yourCar;
// Присваивание значения ссылке
y o u r C a r = new V e h i c l e ( ) ;
// И с п о л ь з о в а н и е точки для обращения к ч л е н у
yourCar.sManufacturer = "Rambler";
// Создание новой ссылки, к о т о р а я у к а з ы в а е т на тот же
V e h i c l e yourSpousalCar = yourCar;

объект

В первой строке создается объект y o u r C a r , причем без присваивания ему значения,
Такая неинициализированная ссылка называется нулевым объектом (null object). Любые
попытки использовать неинициализированную ссылку приводят к немедленной генера­
ции ошибки, которая прекращает выполнение программы.
Компилятор С# может перехватить большинство попыток использования не
инициализированной ссылки и сгенерировать предупреждение в процессе ком
пиляции программы. Если вам каким-то образом удалось провести компьютер
то обращение к неинициализированной ссылке при выполнении программь
приведет к ее аварийному останову.
Второе выражение создает новый объект V e h i c l e и присваивает его ссылочной пе-1
ременной y o u r C a r . И последняя строка кода присваивает ссылке y o u r S p o u s a l C a r l

120

Часть III. Объектно-основанное программирование

ссылку y o u r C a r . Как показано на рис. 6.1, это приводит к тому, что y o u r S p o u s a l C a r
ссылается на тот же объект, что и y o u r C a r .

Рис. 6.1. Взаимоотношения между дву­
мя ссылками на один и тот же объект
Эффект от следующих двух вызовов одинаков:
// Создание вашей машины
Vehicle y o u r C a r = new V e h i c l e () ;
yourCar. s M o d e l = " K a i s e r " ;
// Эта машина п р и н а д л е ж и т и вашей ж е н е
Vehicle y o u r S p o u s a l C a r = y o u r C a r ;
// Изменяя о д н у машину, вы и з м е н я е т е и д р у г у ю
yourSpousalCar. sModel = "Henry J " ;
C o n s o l e . W r i t e L i n e ( " В а ш а машина - " + y o u r C a r . s M o d e l ) ;
Выполнение данной программы приводит к выводу на экран названия модели
Henry J, а не K a i s e r . Обратите внимание, что y o u r S p o u s a l C a r не указывает на
yourCar— вместо этого просто и y o u r S p o u s a l C a r , и y o u r C a r указывают,на один
и тот же объект.
Кроме того, ссылка y o u r S p o u s a l C a r будет корректна, даже если окажется
"потерянной" (например, при выходе за пределы области видимости), как показано
в следующем фрагменте:
// Создание вашей машины
Vehicle y o u r C a r = new V e h i c l e () ;
yourCar. s M o d e l = " K a i s e r " ;
// Эта машина п р и н а д л е ж и т и вашей ж е н е
Vehicle y o u r S p o u s a l C a r = y o u r C a r ;
// Когда о н а з а б и р а е т с е б е вашу м а ш и н у . . .
yourCar = n u l l ;
// yourCar теперь ссылается на "нулевой
// объект"
// . . . y o u r S p o u s a l C a r с с ы л а е т с я на в с е ту же машину
C o n s o l e . W r i t e L i n e ( " В а ш а машина - " + y o u r S p o u s a l C a r . s M o d e l ) ;
Выполнение этого фрагмента исходного текста выводит на экран сообщение
"Ваша машина - K a i s e r " несмотря на то, что ссылка y o u r C a r стала недейст­
вительной.
Объект перестал быть достижимым по ссылке y o u r C a r . Но он не будет пол­
ностью недостижимым, пока не будут "потеряны" или обнулены обе ссылки —
и yourCar, и y o u r S p o u s a l C a r .
После этого — вернее будет сказать, в некоторый непредсказуемый момент после
этого — сборщик мусора (garbage collector) С# вернет память, использованную ранее под

Глава 6. Объединение данных - классы и массивы

121

объект, все ссылки на который утрачены. Дополнительные сведения о сборке мусора бу-1
дут приведены в конце главы 12, "Наследование".

Классы, содержащие классы
Члены класса могут, в свою очередь, быть ссылками на другие классы. Например!
транспортное средство имеет двигатель, свойствами которого являются, в частности!
мощность и рабочий объем. Можно поместить эти параметры непосредственно в клася
V e h i c l e следующим образом:
public class Vehicle
{
// Модель
p u b l i c s t r i n g sModel;
public
s t r i n g s M a n u f a c t u r e r ; // П р о и з в о д и т е л ь
// К о л и ч е с т в о д в е р е й
p u b l i c i n t nNumOfDoors;
public
i n t nNumOfWheels;
// К о л и ч е с т в о к о л е с
p u b l i c i n t nPower;
// Мощность д в и г а т е л я
public double displacement;
// Р а б о ч и й о б ъ е м

}

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

class

Motor

{
public
public

i n t nPower;
double displacement;

/ / Мощность
/ / Рабочий объем

}
Вы можете внести этот класс в класс V e h i c l e следующим образом:
public
{

class

public
public
public
public
public

}

Vehicle

s t r i n g sModel;
s t r i n g sManufacturer;
i n t nNumOfDoors;
i n t nNumOfWheels;
Motor motor;

//
//
//
//

Модель
Производитель
Количество дверей
Количество колес

Соответственно, создание s o n s C a r теперь выглядит так:
/ / Сначала с о з д а е м д в и г а т е л ь
M o t o r l a r g e r M o t o r = new M o t o r ( ) ;
l a r g e r M o t o r . n P o w e r = 23 0;
largerMotor.displacement = 4.0;
// Теперь с о з д а е м автомобиль
V e h i c l e s o n s C a r = new V e h i c l e ( ) ;
sonsCar.sModel = "Cherokee S p o r t " ;
sonsCar.sManfacturer = "Jeep";
sonsCar.nNumOfDoors = 2;
sonsCar.nNumOfWheels = 4;
// Присоединяем д в и г а т е л ь к автомобилю
sonsCar.motor = largerMotor;

122

Насть III. Объектно-основанное программирование

Доступ к рабочему объему двигателя из V e h i c l e можно получить в два этапа, как
показано в приведенном фрагменте:
Motor m = s o n s C a r . m o t o r ;
Console.WriteLine("Рабочий объем равен

"

+ m.displacement);

Однако можно получить эту величину и непосредственно:
Console.Writeline ("Рабочий объем равен " +
sonsCar.motor.displacement);
Влюбом случае доступ к значению d i s p l a c e m e n t осуществляется через класс M o t o r .
Этот фрагмент взят из исходного текста программы V e h i c l e A n d M o t o r ,
которую можно найти на прилагаемом компакт-диске.

Статические ч л е н ы к л а с с а
Большинство членов-данных описывают отдельные объекты. Рассмотрим следующий
класс Саг:
public c l a s s

Car

(
public

string

sLicensePlate;

//

Номерной

знак

автомобиля

I
Номерной знак является свойством объекта, описывающим каждый автомобиль
и уникальным для каждого автомобиля. Присваивание номерного знака одному автомо­
билю не меняет номерной знак другого:
Car c o u s i n s C a r = new Car () ;
cousinsCar. s L i c e n s e P l a t e = " X Y Z 1 2 3 " ;
Car yourCar = new Car () ;
yourCar. s L i c e n s e P l a t e = " A B C 7 8 9 " ;
Однако имеются и такие свойства, которые присущи всем автомобилям. Например,
количество выпущенных автомобилей является свойством класса Саг, но не отдельного
объекта. Свойство класса помечается специальным ключевым словом s t a t i c , как пока­
зано в следующем фрагменте исходного текста:
public class Car
{
public s t a t i c
int nNumberOfCars; / / К о л и ч е с т в о выпущенных а в т о м о б и л е й
public s t r i n g s L i c e n s e P l a t e ;
/ / Номерной з н а к а в т о м о б и л я

}
Обращение к статическим членам выполняется не посредством объекта, а через сам
класс, как показано в следующем фрагменте исходного текста:
// Создание н о в о г о о б ъ е к т а к л а с с а Саг

Car newCar = new C a r () ;
newCar.sLicensePlate = "ABC123";
// Увеличиваем к о л и ч е с т в о

а в т о м о б и л е й на

1

Car.nNumberOf Cars++ ;

(пава 6. Объединение данных - классы и массивы

123

О б р а щ е н и е к ч л е н у о б ъ е к т а n e w C a r . s L i c e n s e P l a t e в ы п о л н я е т с я посред
ством объекта n e w C a r , в то время как обращение к (статическому) члену
C a r . n N u m b e r O f C a r s о с у щ е с т в л я е т с я с п о м о щ ь ю и м е н и к л а с с а . В с е объект
типа С а г совместно используют один и тот же член n N u m b e r O f C a r s .

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

Program

{
//

Число

public
public

дней

в

году

i n t

const
s t a t i c

(включая

nDaysInYear

void

високосный

=

год)

366;

Main(string[]

args)

{
//

Это

int[]

массив



о

них

будет

nMaxTemperatures

for(int

index

=

0;

=

index

рассказано

new
<

немного

позже

int[nDaysInYear];

nDaysInYear;

index++)

{
//

Вычисление

средней

температуры

для

каждого

дня

года

Константу n D a y s I n Y e a r можно использовать везде в вашей программе вместо
ла

366.

Константные

переменные

очень

полезны,

так

как

позволяют

заме

"магические числа" ( в данном с л у ч а е — 3 6 6 ) описательным именем n D a y s I n Y e a r , *
повышает удобочитаемость программы и облегчает ее сопровождение.
С# имеет еще один способ объявления констант. Возможно, вам больше поим
вится использовать н е модификатор c o n s t , а модификатор r e a d o n l y :
public

readonly

i n t

nDaysInYear

=

366;

Как и при применении модификатора c o n s t , после того как вы присвоите конст
инициализирующее значение, оно не может быть изменено нигде в программе. Xотя
причины этого совета носят слишком технический характер, чтобы описывать их в
стоящей книге, при объявлении констант предпочтительно использовать модификат
readonly.
Для констант имеется собственное соглашение по именованию. Вместо именован
их так же, как и переменных (как в примере с n D a y s I n Y e a r ) многие программы
предпочитают использовать прописные буквы с разделением слов подчеркиваниями
D A Y S _ I N _ Y E A R . Такое соглашение ясно отделяет константы от обычных переменных.

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

124

Часть III. Объектно-основанное программирова

одна конструкция для хранения множества объектов, например, коллекции старинных
автомобилей Билла Гейтса. Встроенный класс A r r a y представляет собой структуру, ко­
торая может содержать последовательности однотипных элементов (чисел типа i n t ,
double, объекты V e h i c l e и т.п.).

Зачем нужны массивы
Рассмотрим задачу определения среднего из десяти чисел с плавающей точкой. Каж­
дое из 10 чисел требует собственной переменной для хранения значения типа d o u b l e
(усреднение целых чисел может привести к ошибке округления, описанной в главе 3,
"Объявление переменных-значений"):
double
double
double
double
double
double
double
double
double
double

dO
dl
d2
d3
d4
d5
d6
d7
d8
d9

=
=
=
=
=
=
=
=
=
=

5;
2;
7;
3.5;
6.5;
8;
1;
9;
1;
3;

Теперь нужно просуммировать все эти значения и разделить полученную сумму на 10:
double dSum = dO + dl + d2 + d3 + d4 +
d5 + d6 + d7 + d8 + d 9 ;
double dAverage = dSum / 1 0 ;
Перечислять все элементы — очень утомительно, даже если их всего 10. А теперь
представьте, что вам надо усреднить 10000 чисел...

Массив фиксированного размера
К счастью, вам не нужно отдельно именовать каждый из элементов. С# предоставляет
в распоряжение программиста массивы, которые могут хранить последовательности зна­
чений. Используя массив, вы можете переписать приведенный выше фрагмент следую­
щим образом:
double[]

dArray =

{5,

2,

7,

3.5,

6.5,

8,

1,

9,

1,

3} ;

Класс A r r a y использует специальный синтаксис, который делает его более
удобным в применении. Двойные квадратные скобки [] указывают на способ
доступа к отдельным элементам массива:
dArray[0]
dArray[1]

с о о т в е т с т в у е т dO
соответствует dl

Нулевой элемент массива соответствует do, первый — dl и так далее.
Номера элементов массива — 0, 1, 2, ... — известны как их индексы.

(пава 6. Объединение данных
- классы и массивы

125

В С# индексы массивов начинаются с 0, а не с 1. Таким образом, элемент
с индексом 1 не является первым элементом массива. Не забывайте об этом!
Использование d A r r a y было бы слабым улучшением, если бы в качестве индекса массива нельзя было использовать переменную. Применять цикл f o r существенно проще, чем записывать каждый элемент вручную, что и демонстрирует программа F i x e d A r r a y A v e r a g e .
11
'FixedArrayAverage
//
Усреднение
массива
чисел
// использованием цикла
namespace
FixedArrayAverage
using

размера

фиксированного

с

System;

public

class

Program

{
public

static

double[]

void

Main(string[]

dArray =
{5, 2, 7, 3 . 5 ,

6.5,

8,

args)

1,

9,

1,

3};

// Н а к о п л е н и е суммы э л е м е н т о в
// м а с с и в а в п е р е м е н н о й dSum
d o u b l e d S u m = 0 ,f o r ( i n t i = 0; i < 10; i + + \

I

dSum =

dSum +

dArray[i];

// Вычисление среднего значения
d o u b l e d A v e r a g e = dSum / 1 0 ;
Console.WriteLine(dAverage);
// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения программы.. . " ) ;
Console.Read();

Программа начинает работу с инициализации переменной d S u m значением 0. Затем
программа циклически проходит по всем элементам массива d A r r a y и прибавляет га
к d S u m . По окончании цикла сумма всех элементов массива хранится в d S u m . Раздели!
ее на количество элементов массива, получаем искомое среднее значение.

Проверка границ массива
Программа F i x e d A r r a y A v e r a g e должна циклически проходить по массиву из 10
элементов. К счастью, цикл разработан так, что проходит ровно по 10 элементам мас­
сива. Ну а если была бы допущена ошибка и проход был бы сделан не по 10 элементам, а по иному их количеству? Следует рассмотреть два основных случая.

126

Часть III. Объектно-основанное программирование

Что произойдет при выполнении 9 итераций? С# не трактует такую ситуацию как
ошибочную. Если вы хотите рассмотреть только 9 из 10 элементов, то как С# может
указывать вам, что именно вам нужно делать? Конечно, среднее значение при этом
будет неверным, но программе это неизвестно.
Что произойдет при выполнении 11 (или большего количества) итераций?
В этом случае С# примет свои меры и не позволит индексу выйти за дозволенные пре­
делы, чтобы вы не смогли случайно переписать какие-нибудь важные данные
впамяти. Чтобы убедиться в этом, измените сравнение в цикле f o r , заменив 10 на 11:
for ( i n t i = 0 ; i < 1 1 ; i + + )
При выполнении программы вы получите диалоговое окно со следующим сообщени­
ем об ошибке:
IndexOutOfRangeException was u n h a n d l e d
Index was o u t s i d e t h e b o u n d s o f t h e a r r a y .
Здесь C# сообщает о происшедшей н е п р и я т н о с т и — исключении I n d e x O u t O f ­
R a n g e E x c e p t i o n , из названия которого и из поясняющего текста становится понят­
на причина ошибки, — выход индекса за пределы допустимого диапазона. (Кроме
этого, выводится детальная информация о том, где именно и что произошло, но пока
то вы не настолько знаете С#, чтобы разобраться в этом.)

Массив переменного размера
Массив, используемый в программе F i x e d A r r a y A v e r a g e , сталкивается с двумя
серьезными проблемами:
его размер фиксирован и равен 10 элементам;
что еще хуже, значения этих элементов указываются непосредственно в тексте
программы.
Значительно более гибкой была бы программа, которая могла бы считывать переменной
количество значений, вводимое пользователем, ведь она могла бы работать не только с
определенными в программе F i x e d A r r a y A v e r a g e значениями, но и с другими мно­
жествами значений.
Формат объявления массива переменного размера немного отличается от объявления
«ива фиксированного размера:
doublet] d A r r a y = n e w d o u b l e [ N ] ;
Здесь N — количество элементов в выделяемом массиве.
Модифицированная версия программы V a r i a b l e A r r a y A v e r a g e позво­
ляет пользователю указать количество вводимых значений. Поскольку про­
грамма сохраняет введенные значения, она может не только вычислить
среднее значение, но и вывести результат в удобном виде.
;// V a r i a b l e A r r a y A v e r a g e
//Вычисление с р е д н е г о з н а ч е н и я м а с с и в а , р а з м е р к о т о р о г о
//указывается п о л ь з о в а т е л е м в о в р е м я р а б о т ы п р о г р а м м ы .
[// Накопление в в е д е н н ы х д а н н ы х в м а с с и в е п о з в о л я е т
[//обращаться к н и м н е о д н о к р а т н о , в ч а с т н о с т и , д л я г е н е р а ц и и
[//привлекательно в ы г л я д я щ е г о в ы в о д а н а э к р а н .

Гюва 6, Объединение данных - классы и массивы

127

namespace

VariableArrayAverage

{
using

System;

public

class

Program

{
public

s t a t i c

void

Main(string[]

args)

{
// Сначала считывается количество чисел типа double,
// которое пользователь намерен ввести для усреднения
Console.Write("Введите количество усредняемых чисел:
");
s t r i n g sNumElements = C o n s o l e . R e a d L i n e О ;
i n t numElements = Convert.ToInt32(sNumElements);
Console.WriteLine();
//

Объявляем

double[]
//

массив

dArray

Накапливаем

for

(int

i

=

=

необходимого

new

значения

0;

i

<

размера

double[numElements];
в

массиве

numElements;

i++)

{
// Приглашение пользователю для ввода чисел
C o n s o l e . W r i t e ( " В в е д и т е ч и с л о типа d o u b l e №" +
( i + 1) + " : ") ;
s t r i n g sVal = C o n s o l e . R e a d L i n e ( ) ;
double dValue = Convert.ToDouble(sVal);
//

}

Вносим

dArray[i]

число
=

в

массив

dValue;

// Суммируем
'numElements'
/ / п е р е м е н н о й dSum
d o u b l e dSum = 0;
for

(int

i

=

0;

i

<

значений

numElements,-

из

массива

в

i++)

{
dSum

=

dSum

+

dArray[i];

}
// Вычисляем среднее
d o u b l e d A v e r a g e = dSum

/

numElements;

/ / Выводим р е з у л ь т а т н а э к р а н
Console.WriteLine();
Console.Write(dAverage
+ " является средним

из

("

+ d A r r a y [ 0 ] ) ,for

(int

i

=

1;

i

<

numElements,-

Console.Write("

+

i++)

{
"

+

dArray[i]);

}
Console.WriteLine(")

128

/

"

+

numElements);

Часть III. Объектно-основанное программирован

// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();

Вот как выглядит вычисление среднего для чисел от 1 до 5:
Введите

количество

усредняемых

Введите
Введите
Введите
Введите
Введите

число
число
число
число
число

double
double
double
double
double

типа
типа
типа
типа
типа

№1:
№2:
№3:
№4:
№5:

чисел:

5

1
2
3
4
5

3 является с р е д н и м и з
( 1 + 2 + 3 + 4 + 5 )
/5
Нажмите < E n t e r > д л я з а в е р ш е н и я п р о г р а м м ы . . .
Программа V a r i a b l e A r r a y A v e r a g e начинается с вывода приглашения пользова­
телю указать количество значений, которые будут введены далее и которые требуется
усреднить. Введенное значение сохраняется в переменной n u m E l e m e n t s типа i n t .
В представленном примере здесь оказывается введено число 5.
Затем программа выделяет память для нового массива d A r r a y с указанным количе­
ством элементов. В данном случае программа это делает для массива, состоящего из пя­
ти элементов типа d o u b l e . Программа выполняет n u m E l e m e n t s итераций цикла, счи­
тывая вводимые пользователем значения и заполняя ими массив.
После того как пользователь введет указанное им ранее число данных, программа ис­
пользует тот же алгоритм, что и в программе F i x e d A r r a y A v e r a g e для вычисления
среднего значения последовательности чисел.
В последней части генерируется вывод среднего значения вместе с введенными чис­
лами в привлекательном виде (по крайней мере с моей точки зрения).
Этот вывод не так уж и прост, как может показаться. Внимательно проследите,
как именно программа выводит открывающую скобку, знаки сложения, числа
последовательности и закрывающую скобку.
Программа V a r i a b l e A r r a y A v e r a g e , возможно, не удовлетворяет вашим пред­
ставлениям о гибкости. Может статься, что вам бы хотелось позволить пользовате­
лю вводить числа, а после ввода какого-то очередного числа дать команду вычис­
лить среднее значение введенных чисел. Кроме массивов, С# предоставляет про­
граммисту и другие типы коллекций; некоторые из них могут при необходимости
увеличивать или уменьшать свой размер. В главе 15, "Обобщенное програм­
мирование", вы познакомитесь с такими альтернативами массивам.

Свойство Length
В программе V a r i a b l e A r r a y A v e r a g e для заполнения массива использован
цикл f o r :

Глава 6.

Объединение данных - классы и массивы

129

//

Объявляем

doublet]
//

массив

dArray

Накапливаем

for

(int

i

=

=

необходимого

new

значения

0;

i

<

размера

double[numElements];
в

массиве

numElements;

i++)

{
// Приглашение пользователю для ввода чисел
C o n s o l e . W r i t e ( " В в е д и т е ч и с л о типа d o u b l e №"
+
(i + 1) + " : »,),,;
s t r i n g sVal = C o n s o l e . R e a d L i n e О ;
double dValue
=
Convert.ToDouble(sVal);
//

Вносим

число

dArray[i]

=

в

массив

dValue;

}
Массив d A r r a y о б ъ я в л е н к а к и м е ю щ и й д л и н у n u m E l e m e n t s . Таким о б р а з о м , по­
н я т н о , п о ч е м у ц и к л в ы п о л н я е т и м е н н о n u m E l e m e n t s и т е р а ц и й д л я п р о х о д а п о массиву.
Вообще г о в о р я , не с л и ш к о м - т о и у д о б н о т а с к а т ь п о в с ю д у в м е с т е с м а с с и в о м пер(
м е н н у ю , в к о т о р о й х р а н и т с я е г о д л и н а . Но, к с ч а с т ь ю , э т о н е я в л я е т с я н е и з б е ж н ы м и
у м а с с и в а е с т ь с в о й с т в о L e n g t h , к о т о р о е с о д е р ж и т е г о д л и н у , т а к ч т о d A r r a y . Lengtkl
в данном случае содержит то же значение, что и n u m E l e m e n t s .
Таким о б р а з о м , п р е д п о ч т и т е л ь н е е и с п о л ь з о в а т ь т а к о й в и д ц и к л а f o r :
//

Накапливаем

for

(int

i

=

0;

значения
i

<

в

массиве

dArray.Length;

i++)

О т л и ч и е массивов фиксированной и переменной д л и н ы
С п е р в о г о в з г л я д а б р о с а е т с я в г л а з а , н а с к о л ь к о о т л и ч а е т с я с и н т а к с и с м а с с и в о в фик|
сированной и переменной длины:
double[]
doublet]

dFixedLengthArray =
{5,2,7,3.5,6.5,8,1,9,1,3};
d V a r i a b l e L e n g t h A r r a y = new d o u b l e [ 1 0 ] ;

Однако C# п ы т а е т с я с э к о н о м и т ь в а ш и у с и л и я , а п о т о м у п о з в о л я е т И с п о л ь з о в а т ь ита|
кой код для выделения памяти для массива с его одновременной инициализацией:
doublet]
dFixedLengthArray =
new d o u b l e [ 1 0 ]
{5,
2,
7,
3.5,

6.5,

8,

1,

9,

1,

3 } ;

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

П р о г р а м м и с т а м о ч е н ь ч а с т о п р и х о д и т с я р а б о т а т ь с н а б о р а м и п о л ь з о в а т е л ь с к и х объ­
е к т о в ( к л а с с о в ) . С к а ж е м , п р о г р а м м е д л я у н и в е р с и т е т а т р е б у е т с я н е к а я с т р у к т у р а , описы-]
вающая студентов, например, такая:
public

class

Student

{
public
public

string
double

sName;
dGPA;
//

Средний

балл

}
130

Часть III. Объектно-основанное программирование

Этот простейший класс содержит только имя студента и его успеваемость. В следующем
фрагменте объявляется массив из n u m ссылок на объекты типа S t u d e n t :
Student []

students

=

new

Student[num];

Выражение n e w S t u d e n t [ n u m ]

объявляет н е массив объектов S t u d e n t ,

а массив ссылок на объекты S t u d e n t .

Итак, каждый элемент s t u d e n t s [ i ] представляет собой ссылку на нулевой объект,
так как С# инициализирует новые, неопределенные объекты значением n u l l . Можно
сформулировать это и иначе, сказав, что ни один из элементов s t u d e n t s [ i ] после вы­
полнения рассматриваемого оператора n e w не ссылается на объект типа S t u d e n t . Так
что сначала вы должны заполнить массив следующим образом:
for

(int

i

=

0;

i

<

students. Length;

=

new

i + +)

{
students[i]

Student();

}
Теперь в программе можно вводить свойства отдельных студентов таким образом:
students [ i ]

=

new

students [ i ] . sName
students [ i ] . dGPA

Student ();
=

=

"My N a m e " ;
dMyGPA;

Все это можно увидеть в программе A v e r a g e S t u d e n t G P A , которая полу­
чает информацию о студентах и вычисляет их среднюю успеваемость.

// A v e r a g e S t u d e n t G P A
// Вычисляет среднюю
using S y s t e m ;
namespace

успеваемость

AverageStudentGPA

множества

студентов.

.

f
public

class

Student

{
public
public

string
sName;
d o u b l e dGPA;

//

Средний

балл

}
public
{

class

public

Program

s t a t i c

void

Main(string[]

args)

{
// Определяем количество студентов
Console.WriteLine("Введите
количество
студентов");
string s = Console.ReadLine();
int nNumberOfStudents = Convert.ToInt32(s);
// Выделяем массив объектов S t u d e n t
Student[]

students

// Заполняем массив
f o r ( i n t i = 0; i <

=

new

Student[nNumberOfStudents];

students.Length;

Глава 6. Объединение данных - классы и массивы

i++)

131

{

//
//
//
Co

Приглашение для ввода информации. Единица
прибавляется в связи с тем,
что индексация
м а с с и в о в в С# н а ч и н а е т с я с н у л я
n s o l e . W r i t e ( " В в е д и т е имя
студента
"
+ ( i + 1) + " : ") ;
s t r i n g sName = C o n s o l e . R e a d L i n e О ;

Console.Write("Введите
средний балл студента:
s t r i n g sAvg = C o n s o l e . R e a d L i n e О ;
d o u b l e dGPA = C o n v e r t . T o D o u b l e ( s A v g ) ;
// Создаем объект
Student thisStuden
thisStudent.sName
thisStudent.dGPA
//

Добавляем

students[i]

на основе введенной
t = new S t u d e n t ( ) ;
= sName;
= dGPA;

созданный
=

объект

в

" ) ;

информации

массив

thisStudent;

}
//
Усредняем успеваемость
d o u b l e dSum = 0 . 0 ;
for

(int

i

=

0;

i

<

студентов

students.Length;

i++)

{
dSum

+=

students[i].dGPA;

}
double

dAvg

=

dSum/students.Length;

// Выводим в ы ч и с л е н н о е
Console.WriteLine();

значение

C o n s o l e . W r i t e L i n e ( " С р е д н я я у с п е в а е м о с т ь по
"
+
students.Length
+ " студентам - " + dAvg);
//

Ожидаем

подтверждения

пользователя

Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();

}

}

}

П р о г р а м м а п р е д л а г а е т п о л ь з о в а т е л ю в в е с т и к о л и ч е с т в о р а с с м а т р и в а е м ы х студен
п о с л е ч е г о с о з д а е т м а с с и в с о о т в е т с т в у ю щ е г о р а з м е р а , э л е м е н т а м и к о т о р о г о являю
ссылки н а объекты типа S t u d e n t .
П о с л е э т о г о п р о г р а м м а в х о д и т в ц и к л f o r , в к о т о р о м п р о и с х о д и т з а п о л н е н и е масс
ва. П о л ь з о в а т е л ю п р е д л а г а е т с я в в е с т и и м я к а ж д о г о с т у д е н т а и е г о с р е д н и й б а л л —
данные используются для создания объекта S t u d e n t .
П о с л е з а п о л н е н и я м а с с и в а п р о г р а м м а в х о д и т в о в т о р о й ц и к л , в к о т о р о м усредняй]
с я у с п е в а е м о с т ь с т у д е н т о в , а п о с л е в ы ч и с л е н и й п о л у ч е н н ы й с р е д н и й б а л л выводите]
на экран.

132

Часть III. Объектно-основанное программирована

Вот как выглядит типичный результат работы программы:
Введите

количество

студентов

3
Введите
Введите
Введите
Введите
Введите
Введите

имя с т у д е н т а
средний балл
имя
студента
средний балл
имя с т у д е н т а
средний балл

Средняя

успеваемость

Нажмите



для

1:
Randy
студента:
2:
Jeff
студента:
3:
Carrie
студента:
по

3

3.0
3.5
4.0

студентам

завершения

-

3.5

программы...

Имя ссылочной переменной лучше делать состоящим из одного слова, как, на­
пример, s t u d e n t . В имя переменной желательно каким-то образом включить
имя класса, как, например, b a d S t u d e n t , g o o d S t u d e n t и т.п. Имя массива
(или другой коллекции) предпочтительнее делать простым и очевидным, таким
как s t u d e n t s , p h o n e N u m b e r s

или p h o n e N u m b e r s I n M y P a l m P i l o t . Как

обычно, этот совет всего лишь отражает личное мнение а в т о р а — С# совер­
шенно безразлично, какие именно имена вы будете давать вашим переменным.

Рассмотрим е щ е р а з , к а к и м е н н о в ы ч и с л я е т с я с р е д н я я у с п е в а е м о с т ь с т у д е н т о в :
public

class

Student

{
public
public

string
double

sName;
dGPA;
//

Средний

балл

)
public

class

Program

(
public

s t a t i c

void

Main(string[]

args)

{
// ...
Создаем массив
...
// Усредняем успеваемость
d o u b l e dSum = 0 . 0 ;
for

(int

i

=

0;

i

<

students.Length;

i++)

{
dSum

+=

students[i].dGPA;

}

}

d o u b l e dAvg = dSum / s t u d e n t s . L e n g t h ;
// . . . Прочие д е й с т в и я с массивом . . .
Цикл f o r проходит по всем элементам массива.
Переменная s t u d e n t s . L e n g t h содержит количество элементов в массиве.

Глава 6.

Объединение данных - классы и массивы

133

С# предоставляет программистам особую конструкцию f o r e a c h , которая спроекти­
рована специально для итеративного прохода по контейнерам, таким как массивы. Ош
работает следующим образом:
// Усредняем успеваемость
d o u b l e dSum = 0 . 0 ;
foreach

(Student

stud

in

students)

{
dSum +=

stud.dGPA;

}

d o u b l e dAvg = dSum / s t u d e n t s . L e n g t h ;
При первом входе в цикл из массива выбирается первый объект типа S t u d e n t и со­
храняется в переменной s t u d . При каждой последующей итерации цикл f o r e a c h вы­
бирает из цикла и присваивает переменной s t u d очередной элемент массива. Управле­
ние покидает цикл f o r e a c h , когда все элементы массива оказываются обработанными.
Обратите внимание, что в выражении f o r e a c h нет никаких индексов. Это позволяет
существенно снизить вероятность появления ошибки в программе.
Программистам на С, С++ или J a v a цикл f o r e a c h покажется на первый взгляд
неудобным, однако этот уникальный оператор С# (точнее, .NET) — простей­
ший способ организации циклической обработки всех элементов массива.
На самом деле цикл f o r e a c h мощнее, чем можно представить из приведенного
примера. Кроме массивов, он работает и с другими видами коллекций (о которых
рассказывается, например, в главе 15, "Обобщенное программирование"). Кроме
того, f o r e a c h в состоянии работать и с многомерными массивами (т.е. масси­
вами массивов), но эта тема выходит за рамки настоящей книги.

Сортировка элементов в массиве — весьма распространенная программистская зада-'
ча. То, что массив не может расти или уменьшаться, еще не означает, что его элементы
не могут перемещаться, удаляться или добавляться. Например, обмен местами двух эле­
ментов типа S t u d e n t в массиве может быть выполнен так, как показано в следующем
фрагменте исходного текста:
S t u d e n t temp = s t u d e n t s [i] ;
s t u d e n t s [i]
= students [j];
students[j]
= temp;

//

Сохраняем

i-го

студента

Здесь сначала во временной переменной сохраняется ссылка на объект в i-ой позиции
массива s t u d e n t s , чтобы она не была потеряна при обмене, затем ссылка в i-ой пози­
ции заменяется ссылкой в j-ой позиции. После этого в j-ую позицию помещается ранее
сохраненная во временной переменной ссылка, которая изначально находилась в i-ОЙ по­
зиции. Происходящее схематично показано на рис. 6.2.
Некоторые коллекции данных более гибки, чем массивы, и поддерживают до­
бавление и удаление элементов. С такими коллекциями вы познакомитесь в
главе 15, "Обобщенное программирование".

734

Часть III. Объектно-основанное программирование

Рис. 6.2. "Обмен двух объектов" на самом де­
ле означает "обмен ссылок на два объекта"
Приведенная ниже программа демонстрирует,

к а к и с п о л ь з о в а т ь возмож­

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

Он не с л и ш к о м

эффективен

и плохо подходит для сортировки больших массивов с тысячами элементов,
но зато очень прост и вполне п р и м е н и м для н е б о л ь ш и х массивов.
// S o r t S t u d e n t s
// Демонстрационная
// о б ъ е к т о в
using S y s t e m ;
namespace

программа

для

сортировки

массива

SortStudents

(
class

Program

{
public

s t a t i c

void

Main(string[]

args)

{
// Создание
Student[]
st
students[0]
students[1]
students[2]
students[3]
students[4]

массива студентов
u d e n t s = new S t u d e n t [ 5 ] ;
= Student.NewStudent("Homer",
0);
= Student.NewStudent("Lisa",
4.0);
= Student.NewStudent("Bart",
2.0);
= Student.NewStudent("Marge",
3.0);
= Student.NewStudent("Maggie",
3.5);

Глава ft Объединение данных — классы

и массивы

135

/ / Вывод н е о т с о р т и р о в а н н о г о
списка:
Console.WriteLine("До
сортировки:");
OutputStudentArray(students);
// Сортируем с п и с о к с т у д е н т о в в с о о т в е т с т в и и с их
// у с п е в а е м о с т ь ю
(первыми в списке идут студенты с
//
лучшей
успеваемостью)
Console.WriteLine("\пСортировка
списка\п");
Student.Sort(students);
/ / Вывод о т с о р т и р о в а н н о г о
списка
Console.WriteLine("Отсортированный
OutputStudentArray(students);

список:");

//
Ожидаем п о д т в е р ж д е н и я
пользователя
Console.WriteLine("Нажмите для " +
"завершения
программы...");
Console.Read();

}
// O u t p u t S t u d e n t A r r a y - выводит информацию о в с е х
//
студентах
в массиве
public
s t a t i c
void OutputStudentArray(Student []
students)

{
foreach(Student

s

in

students)

{
Console.WriteLine(s.GetString());

}
} }
//

Student

//

-

описание

студента,

включающее

его

имя

и

успеваемость

class

Student

{
public

string

sName;

public

double

dGrade

//
//

NewStudent
объект

public

-

s t a t i c

=

0.0;

возвращает

Student

новый

инициализированный

NewStudent(string
double

sName,
dGrade)

{
S t u d e n t s t u d e n t = new S t u d e n t ( ) ;
s t u d e n t . s N a m e = sName;
student.dGrade = dGrade;
return
student;

}
//
//

GetString
строку

public

string

-

преобразует

текущий

объект

типа

Student

в

GetString()

{

136

Часть III. Объектно-основанное программирован»»

strin
s +=
s +=
s +=
retur

g s = "" ;
dGrade;
" - " ;
sName;
n
s;

}
//
//
//

Sort - сортировка массива студентов
убывания их у с п е в а е м о с т и при помощи
пузырьковой сортировки

public

s t a t i c

void

Sort(Student[]

в порядке
алгоритма

students)

{
bool
//

bRepeatLoop;

Цикл

выполняется

до

полной

сортировки

списка

do

{
// Этот флаг принимает значение t r u e при наличии
// хотя бы одного объекта не в порядке с о р т и р о в к и
bRepeatLoop =
false;
// Цикл по в с е м у с п и с к у
f o r ( i n t i n d e x = 0;
index
index++)

студентов
<
(students.Length

-

1);

{
//
//
if

Если два студента находятся в списке в неверном
порядке...
(students[index].dGrade
<
students[index +
1].dGrade)

{
//
...меняем их местами...
Student to = s t u d e n t s [ i n d e x ] ;
S t u d e n t from = s t u d e n t s [ i n d e x +
students[index]
= from;
s t u d e n t s [ i n d e x + 1]
= to;

1];

//
. . . и присваиваем флагу значение t r u e ,
чтобы
// программа выполнила очередной проход по
// списку
(итерации продолжаются,
пока список не
//
будет
полностью отсортирован)
bRepeatLoop = t r u e ;

}

}

}

while

(bRepeatLoop);

}
}
Рассмотрим вывод данной программы, просто чтобы убедиться в ее работоспособности:
До с о р т и р о в к и :

О - Homer
4 - Lisa
2 - Bart

Глава 6.

Объединение данных - классы и массивы

137

3 3.5

Marge
- Maggie

Сортировка

списка

Отсортированный
4 - Lisa
3.5
- Maggie
3 - Marge
2 - Bart
0 - Homer
Нажмите < E n t e r >

список:

для

завершения

программы...

Чтобы сберечь ваше и мое время, создание списка из пяти студентов просто закоди­
ровано непосредственно в программе. Метод N e w S t u d e n t () выделяет память и созда­
ет новый объект типа S t u d e n t , инициализирует его и возвращает вызывающей фун*
ции. Для вывода информации о всех студентах в списке используется функция OutputStudentArray().
Затем программа вызывает функцию S o r t ( ) . После сортировки программа повтор»
ет процесс вывода списка, чтобы вы могли убедиться, что теперь он упорядочен.
Само собой, ключевым моментом в программе S o r t S t u d e n t s является мето]
S o r t ( ) . Этот алгоритм выполняет проходы по списку до тех пор, пока список не буда
полностью отсортирован, и при каждом таком проходе сравнивает два соседних объем
массива. Если они находятся в неверном порядке, функция обменивает их местами и от
мечает этот факт в специальной переменной-флаге, которая затем используется в уел»
вии цикла, указывая, полностью ли отсортирован массив. На рис. 6.3-6.6 показано, и
выглядит список студентов после каждого прохода.

HOMER

0

LISA

4

LISA

4

BART

2

BART

2

MARGE

3

MARGE

3

MAGGIE

3,5

MAGGIE

3,5

HOMER

0

Рис. 6.3. Список студен­
тов до сортировки

-*•— Homer проделал свой путь в конец списка

Рис. 6.4. Список студентов после первого прохода

Рис. 6.5. Список студентов после второго прохода
В конечном итоге лучшие студенты, как пузырьки в воде, "всплывают" в верх списка
в то время как наихудшие "тонут" и падают на дно. Потому такая сортировка и называ
ется
пузырьковой.

138

Часть III. Объектно-основанное программирование

Рис. 6.6. Список студентов после предпоследнего
прохода оказывается полностью отсортирован.
Последний проход завершает сортировку, так
как никаких изменений при нем не вносится
Ключевым моментом любой функции сортировки является возможность пере­
упорядочения элементов внутри массива, присваивая ссылку из одного элемен­
та другому. Заметим, что такое присваивание не создает копии объекта, а сле­
довательно, является очень быстрой операцией.

Глава 6.

Объединение данных - классы и массивы

139

Глава 7

Функции функций
> Определение функции
> Передача аргументов в функцию
> Получение результата из функции
> Передача аргументов программе

рограммисты нуждаются в возможности разбить большую программу на бо­
лее мелкие части, с которыми легче совладать. Например, программы, содер­
жащиеся в предыдущих главах, уже достигли своего предельного размера, с которым как
с единым целым может справиться один человек.
С# позволяет разделить код на куски, известные как функции. Правильно спроектиро­
ванные и реализованные функции существенно облегчают работу по написанию слож­
ных программ.

Рассмотрим следующий пример:
class

Example

public i n t n l n t ;
public s t a t i c i n t n S t a t i c I n t
public v o i d MemberFunction()
{

}

Console.WriteLine("Это

/ / H e
статический член
// Статический член
/ / H e статическая функция

функция-член" ) ;
^

public

static

void

ClassFunction()

//

Статическая

функция

{
Console.WriteLine("Это

функция

класса");

}
Элемент n l n t является членом-данными, с которыми вы познакомились в главе 6,
"Объединение д а н н ы х — классы и массивы". Однако элемент M e m b e r F u n c t i o n () для вас
нов. Он известен как функция-член, представляющая собой набор кода С#, который может
быть выполнен с помощью ссылки на имя этой функции. Честно говоря, такое определение
смущает даже меня самого, так что лучше рассмотреть, что такое функция, на примерах.
Примечание: отличие между статическими и нестатическими функциями крайне
важно. Частично эта тема будет раскрыта в данной главе, но более подробно об этом

речь пойдет в главе 8, "Методы класса", где будет введен термин метод, очень распрестраненный в объектно-ориентированных языках программирования наподобие С# л
обозначения нестатических функций классов.
В следующем фрагменте присваиваются значения члену объекта n l n t и члену класш
(статическому члену) n S t a t i c I n t :
Example
example
example.nlnt
Example.nStaticInt

new
1;
= 2;

Example();
// Создание объекта
// И н и ц и а л и з а ц и я ч л е н а с
// использованием объекта
// И н и ц и а л и з а ц и я ч л е н а с
// использованием класса

Практически аналогично в приведенном далее фрагменте происходит обращение (путем
вызова) к функциям M e m b e r F u n c t i o n ( ) и C l a s s F u n c t i o n ( ) :
Example e x a m p l e = new E x a m p l e ( ) ; // Создание о б ъ е к т а
example.MemberFunction();
// Вызов функции-члена
// с у к а з а н и е м о б ъ е к т а
Example.ClassFunction();
// Вызов функции к л а с с а с
// указанием класса
// Приведенные далее строки не компилируются
example.ClassFunction();
// Нельзя обращаться к функции
// к л а с с а с у к а з а н и е м о б ъ е к т а
E x a m p l e . M e m b e r F u n c t i o n ( ) ; // Нельзя обращаться к функции// члену с у к а з а н и е м к л а с с а
Отличие между функциями класса (статическими) и функциями-членами
(нестатическими), или методами, отражает различие между переменными клас­
са (статическими) и переменными-членами (нестатическими), описанное ранее!
в главе 6, "Объединение данных — классы и массивы".
Выражение e x a m p l e . M e m b e r F u n c t i o n ( ) передает управление коду, содержаще
муся внутри функции. Процесс вызова E x a m p l e . C l a s s F u n c t i o n () практически та
кой же. В результате выполнения приведенного выше фрагмента кода получается сл
дующий вывод на экран:
Это
Это

функция-член
функция класса

После того как функция завершает свою работу, она передает управление в точку
из которой она была вызвана.
Я включаю круглые скобки при указании функций в т е к с т е — наприме
M a i n () — чтобы функции было легче распознать. Без этого упоминания их
в тексте можно легко спутать с переменными или классами.
В приведенном примере код функций не делает ничего особенного, кроме вывода на!
экран единственной строки, но в общем случае функции выполняют различные сложные
операции, такие как вычисление математических функций, объединение строк, сорти­
ровка массивов или отправка электронных писем, словом, сложность решаемых функ­
циями задач ничем не ограничена. Функции могут быть любого размера и любой степени
сложности, но все же лучше, если они будут небольшими по размеру, так как с такими
функциями гораздо легче работать.

142

Часть III. Объектно-основанное программирование

В этом разделе в качестве демонстрации того, как разумное определение функций
может помочь сделать программу проще для написания и понимания, будет взята моно­
литная программа C a l c u l a t e l n t e r e s t T a b l e из главы 5, "Управление потоком вы­
полнения", и разделена на несколько функций. Такой процесс переделки рабочего кода
при сохранении его функциональности называется рефакторингом, и Visual Studio 2005
обеспечивает удобное меню Refactor, которое автоматизирует большинство распростра­
ненных задач рефакторинга.
Определение функций и их вызовы будут детально рассмотрены немного позже
в этой главе, а пока считайте данный пример просто кратким обзором.

Чтение комментариев при опущенном программном коде должно способство­
вать пониманию намерений программиста. Если это не так — значит, вы плохо
комментируете ваши программы. (И наоборот, если вы не можете, опустив
большинство комментариев, понять, что делает программа на основании имен
функций, значит, вы недостаточно ясно именуете функции и/или делаете их
слишком большими).
"Скелет" программы C a l c u l a t e l n t e r e s t T a b l e выглядит следующим образом:
public

static

void

Main ( s t r i n g []

args)

{
// Приглашение в в е с т и начальный вклад.
// Если вклад о т р и ц а т е л е н , г е н е р и р у е т с я сообщение об
// о ш и б к е .
// Приглашение для ввода процентной с т а в к и .
// Если п р о ц е н т н а я с т а в к а о т р и ц а т е л ь н а , г е н е р и р у е т с я
// сообщение об ошибке.
/ / П р и г л а ш е н и е для, в в о д а к о л и ч е с т в а л е т .
// Вывод в в е д е н н ы х д а н н ы х .
/ / Цикл п о в в е д е н н о м у к о л и ч е с т в у л е т .
while(nYear =

Возврат

return

при

вводе

корректного

значения

0)
введенного

значения

mValue;

}
// В п р о т и в н о м с л у ч а е г е н е р и р у е т с я
// сообщение об ошибке
Console.WriteLine(sPrompt
+

Глава 7. Функции функций

и

выводится

145

" не может иметь о т р и ц а т е л ь н о е з н а ч е н и е " ) ;
Console.WriteLine("Попробуйте
еще р а з " ) ;
Console.WriteLine();

}

}

//
//
//

O u t p u t l n t e r e s t T a b l e - для заданных значений вклада,
п р о ц е н т н о й с т а в к и и с р о к а г е н е р и р у е т и выводит на
экран таблицу роста вклада

// Реализация раздела 3 основной программы
public

s t a t i c

void

OutputlnterestTable(decimal
decimal
decimal

mPrincipal,
mlnterest,
mDuration)

{
for

(int

nYear

=

1;

nYear

для " +
"завершения программы. . . " ) ;
Console.Read();
}
// A v e r a g e A n d D i s p l a y - усредняет два числа и выводит
// результат с использованием переданных меток
public s t a t i c
void AverageAndDisplay(string si,
double dl,
s t r i n g s2,
d o u b l e d2)
{
double dAverage = (dl + d2) / 2 ;
Console.WriteLine("Среднее "
+ si
+ ", равной " + dl
+ ", и "
+ s2
+ ", равной " + d2
+ ", равно "
+ dAverage);

Вот как выглядит вывод этой программы на экран:
Среднее оценки 1 , равной 3 . 5 , и оценки 2 , равной 4 ,
Нажмите < E n t e r > для завершения программы. . .

равно 3 . 7 5

Функция A v e r a g e A n d D i s p l a y () объявлена с несколькими аргументами в том по­
рядке, в котором они в нее передаются.
Как обычно, выполнение программы начинается с первой команды после M a i n ( ) .
Первая строка после M a i n ( ) , не являющаяся комментарием, вызывает функцию A v e r ­
a g e A n d D i s p l a y ( ) , передавая ей две строки и два значения типа d o u b l e .
Функция A v e r a g e A n d D i s p l a y ()

вычисляет среднее переданных значений типа

double, dl и d 2 , переданных в функцию вместе с их именами (содержащимися в пере­
менных si и s 2 ) , и сохраняет полученное значение в переменной d A v e r a g e .
Изменение значений аргументов внутри функции может привести к ошибкам.
Разумнее присвоить эти значения временным переменным и модифицировать
уже их.

Глава 7. Функции функций

151

Соответствие определений аргументов их использованию
Каждый аргумент в вызове функции должен соответствовать определена
функции как в смысле типа, так и в смысле порядка. Приведенный далее ш
ходный текст некорректен и вызывает ошибку в процессе компиляции.
// A v e r a g e W i t h C o m p i l e r E r r o r
using
System;
namespace

-

эта

версия

не

компилируется!

Example

{
public

class

Program

{
public

static

void

Main(string[]

args)

{
// Обращение к ф у н к ц и и - ч л е н у
AverageAndDisplay("оценки 1",

"оценки

2",

3.5,

4.0);

// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
" з а в е р ш е н и я п р о г р а м м ы . . . ") ;
Console.Read();

}
// A v e r a g e A n d D i s p l a y - у с р е д н я е т д в а ч и с л а и выводит
// р е з у л ь т а т с и с п о л ь з о в а н и е м переданных меток
public
static
void AverageAndDisplay(string si,
double dl,
s t r i n g s2,
d o u b l e d2)

{

}

d o u b l e d A v e r a g e = ( d l + d 2) / 2;
Console.WriteLine("Среднее "
+
+ ", р а в н о й " +
+ ", и "
+
+ ", р а в н о й " +
+ ", р а в н о "
+

si
dl
s2
d2
dAverage);

}

C# обнаруживает несоответствие типов передаваемых функции аргументов с аргу­
ментами в определении функции. Строка " о ц е н к и 1 " соответствует типу s t r i n g
определении функции; однако согласно определению функции вторым аргумента
должно быть число типа d o u b l e , в то время как при вызове вторым аргументом фуш
ции оказывается строка s t r i n g .
Это случилось потому, что я просто обменял местами второй и третий аргумент!
функции. И это как раз то, за что я не люблю компьютеры — именно за то, что они по
нимают все совершенно буквально.
Чтобы исправить ошибку, достаточно поменять местами второй и третий аргументы
вызове функции A v e r a g e A n d D i s p l a y ( ) .

152

Часть III. Объектно-основанное программирован

Перегрузка функции
Ъ одном к л а с с е м о ж е т б ы т ь д в е ф у н к ц и и с о д н и м и т е м же и м е н е м — при
условии

различия

их

Это

аргументов.

явление

называется

перегрузкой

(overloading) и м е н и ф у н к ц и и .
Вот п р и м е р п р о г р а м м ы , д е м о н с т р и р у ю щ е й перегрузку.

// A v e r a g e A n d D i s p l a y O v e r l o a d e d - эта версия демонстрирует
// возможность перегрузки функции вычисления и вывода
// среднего значения
using

System;

namespace

AverageAndDisplayOverloaded

{
public

class

Program

{
public

static

void

Main(string[]

args)

{

// Вызов первой функции-члена
A v e r a g e A n d D i s p l a y ( " м о е й оценки", 3 . 5 ,

"твоей оценки",- 4 . 0 ) ;
Console.WriteLine();

// Вызов второй функции-члена
AverageAndDisplay(3.5,

4.0);

// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите



для

"

+

"завершения программы...");
Console.Read();

// A v e r a g e A n d D i s p l a y - вычисление среднего значения двух
// чисел и вывод его на экран с переданными функции
// метками этих чисел
public
void

static
AverageAndDisplay(string
string

{
double dAverage = (dl + d2) / 2 ;
Console.WriteLine("Среднее

"

si,
s2,

+

double
double

dl,
d2)

si

+ ", равной " + d l ) ;
Console.WriteLine("и

"

+

s2

+ ", равной " + d2
+ ", равно "
}
public static void AverageAndDisplay(double dl,

+ dAverage);

double

d2)

{
double

dAverage

Глава 7. Функции функций

=

(dl

+

d2)

/

2;

153

Console.WriteLine("среднее
+ " и "
+ " равно

"
"

+ dl
+ d2
+ dAverage);

В программе определены две версии функции A v e r a g e A n d D i s p l a y ( ) . Программ
вызывает одну из них после другой, передавая им соответствующие аргументы. С# в col
стоянии определить по переданным функции аргументам, какую из версий следует ви|
звать, сравнивая типы передаваемых значений с определениями функций. Программ
корректно компилируется и выполняется, выводя на экран следующие строки:
Среднее
и твоей

моей оценки, равной 3.5
оценки, равной 4, равно

Среднее
Нажмите

3.5 и 4


равно 3.75
для завершения

3.75

программы...

Вообще говоря, С# не позволяет иметь в одной программе две функции с одинаковым!
именами. В конце концов, как тогда он сможет разобраться, какую из функций следует вызш
вать? Но дело в том, что С# в имя функции во внутреннем представлении компилятора вклш
чает не только имя функции, но и количество и типы ее аргументов. Поэтому С# в состояв!
отличить функцию A v e r a g e A n d D i s p l a y ( s t r i n g , d o u b l e , s t r i n g , d o u b l e ) отфуш
ции A v e r a g e A n d D i s p l a y ( d o u b l e , d o u b l e ) . Если рассматривать эти функции с ихаи
гументами, становится очевидным, что они разные.

Реализация аргументов по умолчанию
Зачастую оказывается желательно иметь две (или более) версии функции, имеюищ
следующие отличия.
Одна из функций представляет собой более сложную версию, обеспечи
вающую большую гибкость, но требующую большого количества аргу
ментов от вызывающей программы, причем некоторые из них для пользо
вателя могут быть просто непонятны.
Под пользователем функции зачастую подразумевается программист, пр
меняющий ее в своих программах, так что пользователь функции и полк
ватель готовой программы — это разные люди. Еще один термин, прим
няемый для обозначения такого рода пользователя — клиент.
Вторая версия функции гораздо проще для применения, она работает
же, как и первая, в которой часть аргументов принимает некоторые п
допределенные значения по умолчанию.
Такое поведение легко реализуется с использованиемперегрузки функций.
Рассмотрим следующую пару функций D i s p l a y R o u n d e d D e c i m a l ( ) :

//
//
//

154

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

же
с

Часть III. Объектно-основанное программирован®

using

System;

namespace

F u n c t i o n s W i t h D e f a u l tA r g u m e n t s

(
public

class

Program

{
public

s t a t i c

void

Main(string[]

args)

// Вызов ф у н к ц и и - ч л е н а
11
C o n s o l e . W r i t e L i n e ( " {0} ,
DisplayRoundedDecimal(12.345678M,

3));

// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();

}
//

DisplayRoundedDecimal

//

decimal

в

строку

с

-

преобразует

определенным

значение

количеством

типа
значащих

// цифр
public

s t a t i c

string

DisplayRoundedDecimal(decimal
int

mValue,

nNumberOfSignificantDigits)

{
// Сначала округляем число до у к а з а н н о г о количества
// значащих цифр
decimal mRoundedValue =
decimal.Round(mValue,
nNumberOfSignificantDigits);
// и п р е о б р а з у е м е г о в с т р о к у
string s = Convert.ToString(mRoundedValue);
return

s,-

}
public
s t a t i c
string
DisplayRoundedDecimal(decimal

mValue)

{
// Вызываем D i s p l a y R o u n d e d D e c i m a l ( d e c i m a l ,
int)
с
// у к а з а н и е м к о л и ч е с т в а значащих цифр по умолчанию
string s = DisplayRoundedDecimal(mValue,
2);
return
s;

Функция D i s p l a y R o u n d e d D e c i m a l ( )

п р е о б р а з у е т з н а ч е н и е т и п а d e c i m a l в зна­

чение типа s t r i n g с о п р е д е л е н н ы м к о л и ч е с т в о м з н а ч а щ и х ц и ф р п о с л е д е с я т и ч н о й точ­
ки. Поскольку ч и с л а т и п а d e c i m a l ч а с т о п р и м е н я ю т с я в ф и н а н с о в ы х р а с ч е т а х , наибо­
лее распространенными б у д у т в ы з о в ы э т о й ф у н к ц и и со в т о р ы м а р г у м е н т о м , р а в н ы м 2.
В анализируемой п р о г р а м м е э т о п р е д у с м о т р е н о , и в ы з о в D i s p l a y R o u n d e d D e c i m a l ()
с одним а р г у м е н т о м о к р у г л я е т з н а ч е н и е э т о г о а р г у м е н т а до д в у х ц и ф р п о с л е д е с я т и ч н о й

'та 7. Функции функций

155

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

второй

аргумента функции.
Обратите внимание, что версия фунющи D i s p l a y R o u n d e d D e c i m a l ( d e c i m a l
в действительности осуществляет вызов функции D i s p l a y R o u n d e d D e c i m a l
( d e c i m a l , i n t ) . Такая практика позволяет избежать ненужного дублировав
кода. Обобщенная версия функции может использовать существенно большее
количество аргументов, которые ее разработчик может даже не включить док|
кументацию.
Аргументы по умолчанию не просто сберегают силы ленивого программна
Программирование — работа, требующая высочайшей степени концентрат
и излишние аргументы функции, для выяснения назначения и рекомендуем
значений которых необходимо обращаться к документации, затрудняют про
граммирование, приводят к перерасходу времени и повышают вероятное!
внесения ошибок в код. Автор функции хорошо понимает взаимосвязи меж!
аргументами функции и способен обеспечить несколько корректных перегру
женных версий функции, более дружественных к клиенту.
Примечание для программистов на Visual Basic и C/C++: в С# единственный способ
реализации аргументов по умолчанию — это перегрузка функций. С# не позволяет иметь
необязательные аргументы.

Передача в функцию типов-значений
Базовые типы переменных, такие как i n t , d o u b l e , d e c i m a l , известны как т и п
значения. Переменные таких типов могут быть переданы в функцию одним из двух спо
собов. По умолчанию эти переменные передаются в функцию по значению (by valuе
альтернативная форма — передача по ссылке.
Программисты часто не совсем точны в употреблении терминов. Если речь идет ти
пах-значениях, то когда программист говорит о "передаче переменной в функцию", это
обычно означает "передача значения переменной в функцию".

Передача по значению
В отличие от ссылок на объекты, переменные с типами-значениями наподобие id
или d o u b l e обычно передаются в функцию по значению, т.е. функции передается зна
чение, содержащееся в этой переменной, но не сама переменная.
При такой передаче изменение значения соответствующей переменной внутри
функции не вызовет изменения значения переданной переменной в вызывающей
программе, что и демонстрируется следующей программой:
// PassByValue - программа для демонстрации
// передачи аргумента по значению
using
System;
namespace

семантики

PassByValue

{
public

class

Program

{

156

Часть III.

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

// Update - функция п ы т а е т с я модифицировать з н а ч е н и я
// аргументов,
переданные ей; обратите внимание, что
// функции в к л а с с е м о г у т быть о б ъ я в л е н ы в любом п о р я д к е
public s t a t i c v o i d U p d a t e ( i n t i,
double d)
{

i = 10;
d = 2 0.0;

}
public

s t a t i c

void

Main(string []

args)

{
// Объявляем и и н и ц и а л и з и р у е м , д в е п е р е м е н н ы е
int i = 1;
double d = 2.0;
Console.WriteLine("Перед вызовом U p d a t e ( i n t , d o u b l e ) : " ) ;
C o n s o l e . W r i t e L i n e ( " i = " + i + ", d = " + d ) ;
// Вызываем функцию
U p d a t e ( i , d) ;
// О б р а т и т е в н и м а н и е — з н а ч е н и я 1 и 2 . 0 не и з м е н и л и с ь
Console . W r i t e L i n e ("После вызова U p d a t e ( i n t , d o u b l e ) :") ,C o n s o l e . W r i t e L i n e ( " i = " + i + ", d = " + d) ;
// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();

Выполнение программы дает такой вывод на экран:
Перед в ы з о в о м U p d a t e ( i n t , d o u b l e ) :
i=1, d = 2
После в ы з о в а U p d a t e ( i n t , d o u b l e ) :
i = 1, d = 2
Нажмите < E n t e r > д л я з а в е р ш е н и я п р о г р а м м ы . . .
Вызов U p d a t e () передает функции значения 1 и 2.0, а не ссылки на переменные i и.
Таким образом, изменение их значений в функции никак не влияет на значения исходных
переменных в вызывающей программе.

Передача по с с ы л к е
Передача функции переменных с типами-значениями по ссылке имеет ряд преиму­
ществ — этот метод передачи используется, в частности, когда вызывающая программа
хочет предоставить функции возможность изменять значение передаваемой в качестве
аргумента переменной. Приведенная далее программа P a s s B y R e f e r e n c e демонстри­
рует эту возможность.
С# дает программисту возможность передачи аргументов по ссылке с ис­
пользованием ключевых слов r e f и o u t . Слегка измененная программа
P a s s B y V a l u e демонстрирует, как это делается.

Глава 7.

Функции функций

157

// PassByReference - программа для
// передачи аргумента по ссылке
using
System;
namespace

демонстрации

семантики

PassByValue

{
public

class

Program

{
// Update - функция пытается модифицировать значения
// аргументов,
переданные ей; обратите внимание,
на
// передачу аргументов как ref и out
public s t a t i c void Update(ref
int i,
out double d)

{
i =
d =

10;
20.0;

}
public

s t a t i c

void

Main(string[]

args)

{
// Объявляем две переменные и одну инициализируем
i n t i = 1;
double
d;
Console.WriteLine("Перед вызовом " +
"Update(ref
int,
out double):");
Console.WriteLine("i = " + i +
", d н е и н и ц и а л и з и р о в а н а " ) ;

'

// Вызываем функцию
Update(ref i,
out d);
//
//

Обратите внимание
равно
20.0



значение

Console.WriteLine("После

вызова

i

равно

10,

значение

d

"

"Update (ref
int,
out double):"),C o n s o l e . W r i t e L i n e ( " i = " + i + ", d = " + d ) ;
/ / Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для
"завершения

"

+

программы...");

Console.Read();

Ключевое слово r e f указывает С#, что в функцию следует передать ссылку на i, а не
просто значение этой переменной. Следовательно, изменения, выполненные с этой переменной, оказываются экспортированы обратно вызывающей программе.
Подобно этому, ключевое слово o u t говорит: "Передай ссылку на эту переменную,
но можешь никак не заботиться о ее значении, поскольку оно все равно не будет исполь­
зоваться и будет перезаписано в процессе работы функции". Это ключевое слово годится
тогда, когда переменная применяется исключительно для того, чтобы вернуть значение
вызывающей программе.

158

Часть III. Объектно-основанное программирование

Выполнение рассмотренной программы P a s s B y R e f е г е п с е приводит к следующе­
му выводу на экран:
Перед в ы з о в о м U p d a t e ( r e f i n t , o u t d o u b l e ) :
i = 1, d н е и н и ц и а л и з и р о в а н а
После в ы з о в а U p d a t e ( r e f i n t , o u t d o u b l e ) :
i = 10, d = 20
Нажмите < E n t e r > д л я з а в е р ш е н и я п р о г р а м м ы . . .
Аргумент, передаваемый как o u t , всегда считается передаваемым так же как
r e f , т.е. писать r e f

o u t — это тавтология. Кроме того, при передаче аргу­

ментов по ссылке вы должны всегда передавать только переменные — переда­
ча литеральных значений, например просто числа 2 вместо переменной типа
i n t , в этом случае приводит к ошибке.
Обратите внимание, что начальные значения i и d переписываются в функции U p ­
d a t e d . После возврата в функцию M a i n () эти переменные остаются с измененными
в функции U p d a t e () значениями. Сравните это поведение с программой P a s s B y ­
Value, где внесенные изменения не сохраняются при выходе из функции.

Не передавайте переменную по с с ы л к е д в а ж д ы
Никогда не передавайте по ссылке одну и ту же переменную дважды в одну
функцию, поскольку это может привести к неожиданным побочным эффек­
там. Описать эту ситуацию труднее, чем просто продемонстрировать при­
мер программы. Внимательно взгляните на функцию U p d a t e () в приве­
денном листинге.
// PassByRef e r e n c e E r r o r - д е м о н с т р а ц и я п о т е н ц и а л ь н о
/ / ошибочной с и т у а ц и и п р и в ы з о в е ф у н к ц и и с п е р е д а ч е й
// аргументов по с с ы л к е
using S y s t e m ;
namespace

PassByRef e r e n c e E r r o r

{

public

class

Program

{
// Update - э т а функция п ы т а е т с я изменить з н а ч е н и я
// переданных ей а р г у м е н т о в
public s t a t i c v o i d D i s p l a y A n d U p d a t e ( r e f
int nVarl,
ref
i n t nVar2)

{
Console.WriteLine("Начальное
nVarl);
nVarl = 10;

значение

nVarl

-

"

+

Console.WriteLine("Начальное
nVar2);
n V a r 2 = 2 0;

значение

nVar2

-

"

+

}
public

static

void

Main(string[]

args)

{

Глава 7.

Функции функций

159

// О б ъ я в л я е м и и н и ц и а л и з и р у е м переменную
i n t п = 1;
C o n s o l e . W r i t e L i n e ( " П е р е д вызовом " +
"Update(ref n,
ref n ) : " ) ;
Console.WriteLine("n = " + n);
Console.WriteLine();
// Вызываем функцию
DisplayAndUpdate(ref

n,

ref

n);

// Обратите внимание на то, как изменяется значение
// - не т а к , к а к ожидалось от э т о й п е р е м е н н о й и
// функции, в которую она была п е р е д а н а
Console.WriteLine();
Console.WriteLine("После вызова " +
"Update(ref n,
ref n ) : " ) ;
C o n s o l e . W r i t e L i n e ( " n = " + n) ;

п

// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения программы.. . " ) ;
Console.Read();

}
}
В этом примере функция U p d a t e ( r e f i n t , r e f i n t ) объявлена как функцию
с двумя целыми аргументами, передаваемыми по ссылке — ив этом нет ничего некор
ректного или необычного. Проблема возникает тогда, когда в функцию передается одна
и та же переменная как в качестве первого, так и второго аргумента. Внутри функции
происходит изменение переменной n V a r l , которая ссылается на переменную п, от ее
начального значения 1 до значения 10. Затем происходит изменение переменной nVar
:
которая тоже ссылается на переменную п, от ее начального значения 1 до значения 20
и побочное действие заключается в том, что переменная п теряет ранее присвоенное
значение 10 и получает новое значение 20.
Это видно из приведенного ниже вывода программы на экран:
Перед вызовом U p d a t e ( r e f n,
ref n ) :
n = 1
Начальное
Начальное

значение
значение

nVarl
nVar2

-

1
10

После вызова U p d a t e ( r e f n,
ref
n = 20
Нажмите < E n t e r > д л я з а в е р ш е н и я

n):
программы...

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

160

Часть III. Объектно-основанное программированы

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

Почему некоторые аргументы используются только д л я возврата значений?
С# по мере возможности старается защитить программиста от всех глупостей, которые
он может вольно или невольно сделать. Одна из этих глупостей — программист может
забыть проинициализировать переменную перед ее первым применением (особенно час­
то это случается с переменными-счетчиками). С# генерирует ошибку, когда вы пытае­
тесь использовать объявленную, но не инициализированную переменную:
int n V a r i a b l e ;
Console.WriteLine("Это
n V a r i a b l e = 1;
Console.WriteLine("А

ошибка

это

-

нет

"

+,nVariable);
"

+

nVariable);

Однако C# не в состоянии отслеживать переменные в функции:
void

SomeFunction(ref

int

nVariable)

{
Console.WriteLine("Ошибка

или

нет?

"

+

nVariable);

},

Откуда функция S o m e F u n c t i o n () может знать, была ли переменная n V a r i a b l e
инициализирована перед вызовом функции? Это невозможно. Вместо этого С# отсле­
живает переменные при вызове функции — например, вот такой вызов функции при­
ведет к сообщению об ошибке:
int n U n i n i t i a l i z e d V a r i a b l e ;
SomeFunction(ref n U n i n i t i a l i z e d V a r i a b l e ) ;
Если бы C# позволил осуществить такой вызов, то функция S o m e F u n c t i o n () полу­
чила бы ссылку на неинициализированную переменную (т.е. на мусор ( g a r b a g e ) ) в па­
мяти. Ключевое слово o u t позволяет функции и вызывающему ее коду договориться
о том, что передаваемая по ссылке переменная может быть не инициализирована —
функция обещает не использовать ее значение до тех пор, пока оно не будет какимлибо образом присвоено самой функцией. Следующий фрагмент исходного текста
компилируется без каких-либо замечаний:
int n U n i n i t i a l i z e d V a r i a b l e ;
SomeFunction(out n U n i n i t i a l i z e d V a r i a b l e ) ;
Передача инициализированной переменной как o u t - а р г у м е н т а также является кор­
ректным действием:
int n l n i t i a l i z e d V a r i a b l e = 1;
SomeFunction(out n l n i t i a l i z e d V a r i a b l e ) ;
В этом случае значение переменной n l n i t i a l i z e d V a r i a b l e будет просто проиг­
норировано функцией S o m e F u n c t i o n ( ) , но никакой опасности для программы это
не представляет.

Глава 7. Функции функций

161

Многие реальные операции создают некоторые значения, которые должны быть воз
вращены тому, кто вызвал эту операцию. Например, функция s i n () получает аргумент
и возвращает значение тригонометрической функции "синус" для данного аргумент
Функция может вернуть значение вызывающей функции двумя способами. Наиболее
распространенный — с помощью команды r e t u r n ; второй способ использует возмож
ности передачи аргументов по ссылке.

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

class

Example

{
public

static

double

Average(double

dl,

double

d2)

{
double dAverage =
return
dAverage;

(dl

+

d2)

/

2;

}
public

static

void

TestO

{
d o u b l e vl = 1.0;
d o u b l e v2 = 3 . 0 ;
double dAverageValue = Average(vl, v2);
C o n s o l e . W r i t e L i n e ( " С р е д н е е д л я " + vl
+ " и " + v2 + " р а в н о "
+ dAverageValue);
// Такой метод также вполне работоспособен
C o n s o l e . W r i t e L i n e ( " С р е д н е е д л я " + vl
+ " и " + v2 + " р а в н о "
+ Average(vl, v2));

}
}
Прежде всего обратите внимание, что функция объявлена как p u b l i c s t a t i c double
A v e r a g e () — тип d o u b l e перед именем функции указывает на тот факт, что функция Av­
e r a g e () возвращает вызывающей функции значение типа d o u b l e .
Функция A v e r a g e () использует имена dl и d2 для значений, переданных ей в ка­
честве аргументов. Она создает переменную d A v e r a g e , которой присваивает среднее
значение этих переменных. Затем значение, содержащееся в переменной d A v e r a g e ,
возвращается вызывающей функции.
Программисты иногда говорят, что "функция возвращает d A v e r a g e " . Это не­
корректное сокращение. Говорить, что передается или возвращается dAver­
a g e или иная переменная — неточно. В данном случае вызывающей функции
возвращается значение, содержащееся в переменной d A v e r a g e .
Вызов A v e r a g e () из функции T e s t () выглядит точно так же, как и вызов любой
другой функции; однако значение типа d o u b l e , возвращаемое функцией A v e r a g e d ,
сохраняется в переменной d A v e r a g e V a l u e .

162

Часть III. Объектно-основанное программирование]

Функция, которая возвращает значение (как, например, A v e r a g e ( ) ) , не может
завершиться просто по достижении закрывающей фигурной скобки, поскольку
С# совершенно непонятно, какое же именно значение должна будет вернуть эта
функция? Для этого обязательно наличие оператора r e t u r n .

Возврат значения посредством передачи по ссылке
Функция может также вернуть одно или несколько значений вызывающей программе
с помощью ключевых слов r e f или o u t . Рассмотрим пример U p d a t e () из раздела
"Передача по ссылке" данной главы:
// U p d a t e - функция пытается модифицировать значения
//аргументов, переданные ей; обратите внимание, на
// передачу аргументов как r e f и o u t
public static void Update (ref int i, out double d)
{
i = 10;
d = 20.0;
}
Эта функция объявлена как v o i d , так как она не возвращает никакого значения вы­
зывающей функции. Однако поскольку переменная i объявлена как r e f , а переменная
d— как o u t , любые изменения значений этих переменных, выполненные в функции
Update ( ) , сохранятся при возврате в вызывающую функцию. Другими словами, значе­
ния этих переменных вернутся вызывающей функции.

Когда какой метод использовать
Вы можете задуматься: "Функция может возвращать значение как с использованием
оператора r e t u r n , так и посредством переменных, переданных по ссылке. Так какой же
метод мне лучше применять в своих программах?" В конце концов, ту же функцию Av­
erage () вы могли написать и так:
public c l a s s

{

Example

// Примечание: параметр, передаваемый как ' o u t ' ,
// сделать последним в списке
public s t a t i c v o i d A v e r a g e ( d o u b l e 1 , d o u b l e d 2 ,
out double dResults)

лучше

{
dResults = (dl + d2) / 2;
}

public

static

void

Test()

{
double vl = 1.0;
d o u b l e v2 = 3 . 0 ;
double
dAverageValue;
Average(dAverageValue,
vl,
v2);
C o n s o l e . W r i t e L i n e ( " С р е д н е е " + vl
+ " и " + v2 + " равно "
+ dAverageValue);
}
}

Глава 7. Функции функций

163

Обычно

значение

вызывающей

функции

возвращается

с

помощью

оператЛ

r e t u r n , а не посредством o u t - а р г у м е н т а , хотя обосновать преимущество такого по;
хода очень трудно.
Возврат значения посредством передачи аргумента по ссылке иногда требу!
дополнительных действий, которые будут описаны в главе 14, "Интерфейсы!
структуры". Однако обычно эффективность — не главный фактор при прин|
тии решения о способе возврата значения из функции.

•^S^

Как правило, "метод o u t " используется, если требуется вернуть из функции несши
ко значений — например, как в следующей функции:
public

class

Example

{
public
static
void AverageAndProduct(double dl, double d2,
out double dAverage,
out double dProduct)

{
}

}

dAverage
dProduct

=
=

( d l + d2)
dl * d 2 ;

/ 2 ;

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

Нулевая ссылка и ссылка на ноль
Ссылочные переменные, в отличие от переменных типов-значений, при создании ини­
циализируются значением по умолчанию n u l l . Однако нулевая ссылка (т.е. ссылка,
инициализированная значением n u l l ) — это не то же, что ссылка на ноль. Например,
две следующие ссылки совершенно различны:
class
{

Example

int

nValue,-

}
// Создание нулевой ссылки r e f l
Example
refl;
// Создание ссылки на нулевой объект
E x a m p l e r e f 2 = new E x a m p l e О;
r e f 2 . n V a l u e = 0;
Переменная r e f l пуста, как мой бумажник. Она указывает в "никуда", т.е. не указы­
вает ни на какой реальный объект. Ссылка же r e f 2 указывает на вполне конкретный
объект, значение которого равно нулю.
Возможно, эта разница станет понятнее после следующего примера:
string
string

164

si;
s2 =

"";

Часть III. Объектно-основанное программирована

fj

По сути, возникает аналогичная с и т у а ц и я — si указывает на нулевой объект, в то
время как s2 — на пустую строку (на сленге программистов пустая строка иногда
называется нулевой строкой). Это очень существенное отличие, как становится ясно
из следующего исходного текста:
// T e s t namespace

тестовая
Test

программа

(
using
System;
public
class
Program
{
public

s t a t i c

void

Main(string []

strings)

{
Console.WriteLine("Эта программа исследует " +
"функцию T e s t S t r i n g ( ) " ) ;
Console.WriteLine();
Example e x a m p l e O b j e c t = new E x a m p l e ( ) ;
Console.WriteLine("Передача
нулевого
объекта:");
string s = null;
exampleObject.TestString(s);
Console.WriteLine();
// Теперь п е р е д а е м в функцию нулевую
(пустую)
строку
Console.WriteLine("Передача пустой с т р о к и : " ) ;
exampleObject.TestString("");
Console.WriteLine() ;
// Наконец,
передаем реальную строку
Console.WriteLine("Передача реальной строки:");
exampleObject.TestString("test
string");
Console.WriteLine();
// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
" з а в е р ш е н и я п р о г р а м м ы . . . ") ;
C o n s o l e . R e a d () ,-

class

Example

{
public

void

TestString(string

sTest)

{
//
//
if
{

Проверка,
не нулевой
быть п е р в о й ! )
(sTest == n u l l )

ли

Console.WriteLine("sTest
return;

объект

==

(эта

проверка

должна

n u l l " ) ;

}
//
//
//
//

Мы знаем,
что s T e s t не указывает на нулевой объект,
но в с е еще может у к а з ы в а т ь на пустую с т р о к у .
Проверяем,
не указывает ли s T e s t на нулевую
(пустую)
строку

if

(String.Compare(sTest,

{

11

Console . WriteLine (
return;

Глава 7. Функции функций

sTest

"")
-

==

0)

ссылка

на

пустую

строку");

165

{

}

}

// Строка не пустая, выводим ее

} Console.WriteLine("sTest указывает на:

'"

+

sTest +

" ' " ) ;

Функция T e s t S t r i n g O использует сравнение s T e s t = = n u l l для проверки, не на
нулевой ли объект указывает ссылка. Для проверки, не указывает ли ссылка на пустую
строку, функция T e s t S t r i n g () использует функцию C o m p a r e () (функция Com­
p a r e () возвращает 0, если переданные ей строки равны. В главе 9, "Работа со стро­
ками в С # " , вы более детально познакомитесь со сравнением строк).
Вот как выглядит вывод этой программы на экран:
Эта программа и с с л е д у е т функцию T e s t S t r i n g O
Передача нулевого объекта:
sTest == n u l l
Передача пустой строки:
s T e s t - ссылка на пустую с т р о к у
Передача реальной строки:
sTest указывает на:
'test string'
Нажмите < E n t e r > д л я з а в е р ш е н и я п р о г р а м м ы . . .

Определение функции без возвращаемого значения
Выражение p u b l i c s t a t i c d o u b l e A v e r a g e О объявляет функцию A v e r a g e d
как возвращающую значение типа d o u b l e . Однако бывают функции, не возвращающие
ничего. Ранее вы сталкивались с примером такой ф у н к ц и и — A v e r a g e A n d D i s p l a y О,
которая выводила вычисленное среднее значение на экран, ничего не возвращая вызываю­
щей функции. Вместо того чтобы опустить в объявлении такой функции тип возвращаемо­
го значения, в С# указывается v o i d :
public

void

AverageAndDisplay(double,

double)

Ключевое слово v o i d , употребленное вместо имени типа, по сути, означает отсут­
ствие типа, т.е. указывает, что функция A v e r a g e A n d D i s p l a y () ничего не возвраща­
ет вызывающей функции. (В С# любое объявление функции обязано указывать возвра­
щаемый тип, даже если это v o i d . )
Функция, которая не возвращает значения, программистами называется voidфункцией, по использованному ключевому слову в ее описании.

Функции, не являющиеся v o i d - ф у н к ц и я м и , возвращают управление вызывающей
функции при выполнении оператора r e t u r n , за которым следует возвращаемое вызы­
вающей функции значение. Поскольку v o i d - ф у н к ц и я не возвращает никакого значе­
ния, выход из нее осуществляется посредством оператора r e t u r n без какого бы то ни
было значения либо (по умолчанию) при достижении закрывающей тело функции фи­
гурной скобки.
Рассмотрим следующую функцию D i s p l a y R a t i o ( ) :
public

class

Example

{

166

Часть III. Объектно-основанное программирование

public

{

//
if

static

void

DisplayRatio(double
double

Если знаменатель
( d D e n o m i n a t o r ==

равен
0.0)

dNumerator,
dDenominator)

0...

{
// . . . в ы в е с т и сообщение об ошибке и в е р н у т ь
// у п р а в л е н и е вызывающей функции . . .
C o n s o l e . W r i t e L i n e ( " З н а м е н а т е л ь не может быть
// Выход из функции
return;

нулем");

}
// Эта ч а с т ь функции в ы п о л н я е т с я т о л ь к о в том с л у ч а е ,
// к о г д а з н а м е н а т е л ь не р а в е н нулю
double dRatio = dNumerator / dDenominator;
Console.WriteLine("Отношение " + dNumerator
+ " к " + dDenominator
+ " равно " + d R a t i o ) ;
} // Если з н а м е н а т е л ь не р а в е н нулю, выход из функции
}
// в ы п о л н я е т с я з д е с ь
Функция D i s p l a y R a t i o () начинает работу с проверки, не равно ли значение d D e ­
n o m i n a t o r нулю.
Если значение d D e n o m i n a t o r равно нулю, программа выводит сообщение об
ошибке и возвращает управление вызывающей функции, не пытаясь вычислить
значение отношения. При попытке вычислить отношение произошла бы ошибка
целения на 0 с аварийным остановом программы в результате.
Если значение d D e n o m i n a t o r не равно нулю, программа выводит на экран значение
отношения. При этом закрывающая фигурная скобка после вызова функции W r i t e ­
L i n e () является закрывающей скобкой функции D i s p l a y R a t i o () и, таким обра­
зом, представляет собой точку возврата из функции в вызывающую программу.

Взгляните на любое консольное приложение в этой книге. Выполнение программы
всегда начинается с функции M a i n ( ) . Рассмотрим аргументы в следующем объявлении
функции M a i n ( ) :
public

static

void

Main ( s t r i n g []

args)

{
//

}

...

Исходный т е к с т программы

...

Функция M a i n () — статическая функция класса P r o g r a m , определенная Visual Stu­
dio AppWizard. Она не возвращает значения и принимает в качестве аргументов массив
объектов типа s t r i n g . Что же это за строки?
Для запуска консольного приложения пользователь вводит имя программы в команд­
ной строке. При необходимости он может ввести в этой же строке после имени програм­
мы дополнительные аргументы. Вы видели это при использовании команд наподобие
copy m y F i l e С : \ m y D i r , копирующей файл m y F i l e в каталог С : \ m y D i r .

Глава 7. Функции функций

167

Как показано в приведенной далее демонстрационной программе D i s p l a y
A r g u m e n t s , массив объектов s t r i n g , передаваемый в качестве параметра
в функцию M a i n ( ) , и представляет собой аргументы текущего вызова про
граммы.
//

DisplayArguments

using

-

вывод

аргументов,

переданных

программе

System;

namespace

DisplayArguments

{
class

{

Test

public

// Класс,
содержащий функцию
// называться Program
s t a t i c

void

Main(string[]

Main(),

не

обязан

args)

{
//
Количество аргументов
C o n s o l e . W r i t e L i n e ( " У программы
args.Length);
// Это а р г у м е н т ы :
i n t nCount = 0;
foreach
(string
arg

in

{0}

аргументов",

args)

{
Console.WriteLine("Аргумент
nCount++,

{о}
arg);

{l}",

}
/ / Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения программы...");

}

}

Console.Read();
r e t u r n 0;
//
//
//
//

Другие программы,
запущенные в
консольном окне,
могут проверить
возвращаемое значение;
ненулевое
з н а ч е н и е обычно о з н а ч а е т ошибку

это

Обратите внимание, что функция M a i n ( ) может возвращать значение несмотря 4
то, что она объявлена как v o i d , как и везде в настоящей книге. Однако она непременно
должна быть объявлена как s t a t i c .
Программа начинает свою работу с вывода длины массива a r g s . Это значение соот­
ветствует количеству аргументов, переданных функции. Затем программа циклически|
проходит по всем элементам массива a r g s , выводя каждый его элемент на экран.
Ниже приведен пример вывода данной программы (в первой строке показан вид ко]
мандной строки, введенной при запуске программы):
DisplayArguments



argl

arg2

У программы 3 аргументов
Аргумент 0 /с
Аргумент 1
- argl
Аргумент 2
- arg2
Нажмите



для

завершения

программы...

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

168

Часть III. Объектно-основанное программирова,

параметра, и все они являются строками. Первый параметр — "/ с " . Обратите внимание,
что он обрабатывается и передается в массив a r g s точно так же, как и все остальные ар­
гументы. Только сама программа может определить, что именно означает каждый из ар­
гументов и как он влияет на работу программы.

Передача аргументов из приглашения DOS
Чтобы запустить программу D i s p l a y A r g u m e n t s из приглашения DOS, выполните
следующие шаги.
1. Выберите
команду
S t a r t s P r o g r a m s ^ A c c e s s o r i e s ^ C o m m a n d Prompt
(ПускО П р о г р а м м ы ^ С т а н д а р т н ы е ^ К о м а н д н а я строка).
После этого вы должны увидеть на экране черное окно с мигающим курсором ря­
дом с приглашением С: \ > (приглашение может включать каталог).
2. Перейдите в каталог, содержащий проект D i s p l a y A r g u m e n t s , введя ко­
манду cd

\C#Programs\DisplayArguments\bin\Debug.

(По умолчанию корневой каталог для демонстрационных программ из данной
книги — С # P r o g r a m s . Если выбранный вами каталог другой, введите его.)
Приглашение изменится и примет вид С : \ C # P r o g r a m s \ D i s p l a y A r g u m e n t s \
bin\Debug>.

Если что-то идет не т а к — воспользуйтесь средствами Windows для поиска
нужной папки. В Проводнике Windows щелкните правой кнопкой мыши на
папке С : \ C # P r o g r a m s и выберите в раскрывающемся меню команду Search
(Найти...), как показано на рис. 7.2.

Puc. 7.2. Проводник Windows позволяет легко найти нужную папку на жестком диске

ирование

Глава 7. Функции функций

169

В п о я в и в ш е м с я диалоговом окне введите D i s p l a y A r g u m e n t s . е х е и щелкните
кнопке

Search (Найти). Н а й д е н н ы е имена ф а й л о в появятся в правой части диалого
Search Result (Результаты поиска), как показано н а рис. 7.3. Проигнор

вого окна

руйте файл D i s p l a y A r g u m e n t s . e x e в каталоге o b j \ D e b u g ; вам нужен файл
в каталоге b i n \ D e b u g . Теперь, после того как вы н а ш л и точное расположение файл
и его полное имя, вернитесь в консольное окно и перейдите в требуемую папку.

Рис. 7.3. Файл найден; имя соответствующей папки можно найти
в правой части диалогового окна
Обычно Visual Studio 2005 размещает выполнимые файлы в подкаталоге bin
D e b u g ; однако это может быть подкаталог b i n \ r e l e a s e или любой другойесли вы измените конфигурацию Visual Studio.
Windows позволяет использовать в именах файлов и папок пробелы, одна
DOS работать с ними не умеет. В версиях Windows до Windows ХР вам мож
потребоваться взять имя с пробелами в кавычки. Например, можно перейти
каталог с именем My S t u f f с помощью команды наподобие
cd

\"Му

Stuff"

3. В строке приглашения, которая теперь выглядит как С:\C#Programs
DisplayArguments\bin\Debug>, введите команду
displayarguments



argl

arg2.

При этом должна выполниться программа D i s p l a y A r g u m e n t s . е х е , и ее в
вод будет таким, как показано на рис. 7.4. Заметим, что на выполнение програ
мы не влияет, какими буквами — строчными или прописными — вы набрали
имя, а кроме того, вы можете опустить и расширение . е х е .

Передача аргументов из окна
Вы можете запустить программу наподобие D i s p l a y A r g u m e n t s . e x e , введя

имя в командной строке окна Command Prompt (Командная строка). Программу мо
но также запустить и с использованием интерфейса Windows, дважды щелкнув на
имени в окне или в Проводнике Windows.

170

Часть III. Объектно-основанное программирова

Рис. 7.4. Выполнение программы DisplayArguments. ехе из при­
глашения DOS приводит к выводу информации о ее аргументах
Как показано на рис. 7.5, двойной щелчок на имени D i s p l a y A r g u m e n t s приводит
к запуску программы без передачи ей аргументов.

Puc. 7.5. В Проводнике Windows вы можете запустить про­
грамму посредством двойного щелчка на ее имени
Перетаскивание и отпускание одного или нескольких файлов на пиктограмму D i s ­
p l a y A r g u m e n t s . е х е в Проводнике Windows приводит к выполнению программы,
аналогичному вводу в командной строке D i s p l a y A r g u m e n t s

имена

файлов. Одно­

временное перетягивание и отпускание файлов a r g l . t x t и a r g 2 . t x t н а пиктограмму
D i s p l a y A r g u m e n t s дает результат, показанный на рис. 7.6.
Для того чтобы перетащить несколько
нажмите клавишу и выберите
показано на рис. 7.6. Теперь нажмите
файлов и отпустите их на пиктограмму

Глава 7. Функции функций

файлов, выберите в списке первый файл,
остальные требующиеся вам файлы, как
кнопку мыши, перетяните все множество
приложения D i s p l a y A r g u m e n t s .

171

Puc. 7.6. Windows позволяет перетащить и отпустить файлы на пик­
тограмму консольного приложения
Вывод программы D i s p l a y A r g u m e n t s в этом варианте запуска показан на рис. 7.7.

Puc. 7.7. Перетаскивание файлов на пиктограмму приложения дает тот
же эффект, что и указание их имен в командной строке

172

Часть III. Объектно-основанное программирование

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

Ф у н к ц и я W r i t e L i n e ()
Вы могли заметить, что функция WriteLine (), использовавшаяся в рассматривае­
мых программах, представляет собой не более чем вызов функции класса Console:
Console.WriteLine("Это — вызов ф у н к ц и и " ) ;
Функция WriteLine () — одна из множества предопределенных функций, предос­
тавляемых библиотекой .NET. Console — предопределенный класс, предназначен­
ный для использования в консольных приложениях.
Аргументом функции WriteLine (), применявшимся в примерах выше, является
строка string. Оператор + позволяет программисту собрать эту строку из несколь­
ких строк или строк и переменных встроенных типов, например, так:
string s = "Маша";
Console. WriteLine ("Меня зовут " + s +
" и мне " + 3 + " г о д а " ) ;
В результате вы увидите выведенную на экран строку "Меня зовут Маша и мне
Ев'года".

Второй вид функции WriteLine () допускает наличие более гибкого множества ар­
гументов, например:
Console .WriteLine ("Меня зовут { о } и мне {l}
"Маша", 3 ) ;

года",

Первый аргумент такого вызова называется форматной строкой. В данном примере
строка "Маша" вставляется вместо символов {0} — ноль указывает на первый аргу­
мент после командной строки. Целое число 3 вставляется в позицию, помеченную как
{l}. Этот вид функции более эффективен, поскольку конкатенация строк не так про­
ста, как это звучит, и не столь эффективна.
Кроме того, в этом варианте в форматной строке может использоваться ряд управляю­
щих элементов, которые указывают, как именно должны выводиться аргументы функ­
ции WriteLine (). Вы познакомитесь с ними в главе 9, "Работа со строками в С#".

Передача аргументов в Visual Studio 2005
Для того чтобы запустить программу в Visual Studio 2005, сначала удостоверьтесь,
что она собрана без ошибок. Выберите команду меню Builds Build имя программы
и убедитесь в отсутствии в окне Output сообщений об ошибках. Корректное сообщение
в этом окне должно выглядеть как
Build: 1 s u c c e e d e d , 0 f a i l e d , 0 s k i p p e d
Если в окне Output вы видите что-то другое — ваша программа не запустится.
Выполнить программу без передачи аргументов — дело одного щелчка. После того как
программа успешно собрана, выберите команду меню Debug^Start Debugging (или на­
жмите клавишу ) или Debug^Start Without Debugging (клавиши ) и полу­
чите желаемое.

Глава 7. Функции функций

173

По умолчанию Visual Studio выполняет программу, не передавая ей аргументов. Ее:
это не то, что вам нужно, вы должны указать Visual Studio, какие аргументы следует
редавать. Для этого выполните такие шаги.
1. Откройте окно Solution Explorer, для чего воспользуйтесь командой меш!

V i e w : Solution Explorer.
Окно Solution Explorer содержит описание вашего решения. Решение состоит из
одного или нескольких проектов. Каждый проект описывает программу. Напрн
мер, проект D i s p l a y A r g u m e n t s гласит, что P r o g r a m . c s — один из файле
вашей программы, и что ваша программа является консольным приложенная
Проект также содержит описание других свойств, включая аргументы, испои
зуемые при запуске программы D i s p l a y A r g u m e n t s из Visual Studio.
2. Щелкните правой кнопкой мыши на DisplayArguments в Solution Eх
plorer и выберите из раскрывающегося меню команду Properties, как пока
зано на рис. 7.8.
При этом перед вами появится окно вида, представленного на рис. 7.9, в которой
можно указать множество различных настроек вашего проекта — только вот ж
лать этого без глубокого понимания, что именно вы настраиваете, ни в коем ели
чае не следует.

Puc. 7.8. Обращение к свойствам проекта посредством щелчка правой кноп­
кой мыши в Solution Explorer
3. На вкладке DisplayArguments выберите в списке вкладок в левой части]

Debug.
4. В поле Command Line Arguments группы Start Options введите аргументы,!
которые вы хотите передать в программу при запуске ее из Visual Studio.

174

Часть III. Объектно-основанное программирование

Puc. 7.9. Введите аргументы программы в поле Command Line Arguments
на вкладке Debug
5. Сохраните и закройте окно Properties, а затем выполните программу с помощью
команды меню Debug^Start.
Как показано на рис. 7.10, Visual Studio откроет окно DOS с ожидаемым резуль­
татом выполнения программы.

Puc. 7.10. Передача аргументов консольному приложению в Visual Studio

Глава 7. Функции функций

Единственным отличием между выводом программы из Visual Studio 2005 и из Я
мандной строки является отсутствие на экране строки с именем самой программы и si
реданными ей аргументами.

176

Часть III. Объектно-основанное программирование

Глава 8

Методы класса
> Передача объекта в функцию
> Преобразование функции класса в метод

> Что такое t h i s
> Генерация документации

осле статических функций, рассматривавшихся в главе 7, "Функции функций",
мы перейдем к нестатическим методам класса. Статические функции принад­
лежат всему классу, в то время как методы — экземплярам класса. Кстати, многие профаммисты предпочитают называть все одним словом -— либо методами, либо функциями,
не делая того различия между ними, на которое обращено ваше внимание здесь. Однако
имеющееся отличие между статическими и нестатическими функциями очень важно.

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

// P a s s O b j e c t
using S y s t e m ;
namespace

{

public

}

-

демонстрация

передачи

объекта

PassObject
class

public

Student

string

sName;

}
public c l a s s P r o g r a m
(
public s t a t i c v o i d M a i n ( s t r i n g [ ]
{

Student

student

=

new

args)

Student();

в

функцию

// П р и с в а и в а е м имя путем н е п о с р е д с т в е н н о г о
//
полю о б ъ е к т а
Console.WriteLine("Сначала:");
student.sName =
"Madeleine";
OutputName(student);

обращения

к

// Изменяем имя с и с п о л ь з о в а н и е м функции
Console.WriteLine("После
изменения:");
SetName(student,
"Willa");
OutputName(student);
//
Ожидаем п о д т в е р ж д е н и я
пользователя
Console.WriteLine("Нажмите для " +
" з а в е р ш е н и я п р о г р а м м ы . . . ") ;
Console.Read();

// OutputName
public
static

- Вывод имени с т у д е н т а
void OutputName(Student

student)

{
// Вывод т е к у щ е г о имени с т у д е н т а
Console.WriteLine("Student.sName
=
student.sName);

{o}",

}
// SetName - и з м е н е н и е имени с т у д е н т а
public
s t a t i c void SetName(Student
student,
string
sName)

{
student.sName

}

}

=

sName;

}

Программа создает объект s t u d e n t , в котором не содержится ничего, кроме име~
Она сначала присваивает имя непосредственно и выводит его с помощью функции Out
putName().
Затем программа изменяет имя посредством функции S e t N a m e ( ) . Поскольку все
объекты в С# передаются в функции по ссылке, изменения, внесенные в объект stud
d e n t в функции, остаются и после возврата из нее. Когда функция M a i n () опять вьзы
вает функцию для вывода имени студента, последняя выводит измененное имя, что вид
но из вывода программы на экран:
Сначала:
Student.sName = Madeleine
После изменения:
Student.sName = Willa.
Нажмите < E n t e r > для з а в е р ш е н и я

программы.

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

178

Часть III. Объектно-основанное программирован

Student s t u d e n t = n e w S t u d e n t ( ) ;
student.Name = " M a d e l e i n e " ;
OutputName ( s t u d e n t ) ;
Console. W r i t e L i n e ( s t u d e n t . N a m e ) ;
I // И с п р а в л е н н а я
public

static

функция

void

//

Все

еще

"Madeleine"

O u t p u t N a m e () :

OutputName (Student

student)

}
student

=

new

student.Name

Student();

=

//
//
//

He п р и в о д и т к изменению
объекта
s t u d e n t вне
OutputName()

"Pam";

}

Класс представляет собой набор элементов, описывающий объект или концепцию ре­
ального мира. Например, класс V e h i c l e может содержать данные о максимальной ско­
рости, максимальном разрешенном весе, количестве пассажирских мест и т.д. Однако
транспортное средство имеет и активные свойства: возможность тронуться с места, ос­
тановиться и т.п. Эти действия можно описать функциями, работающими с данными
транспортного средства. Эти функции представляют собой такую же часть класса V e h i ­
cle, как и его члены-данные.

Определение функций - статических членов
Например, вы можете переписать программу из предыдущего раздела не­
многоиначе:

// P a s s O b j e c t T o M e m b e r F u n c t i o n - д л я р а б о т ы
// используется с т а т и ч е с к а я ф у н к ц и я - ч л е н
UBing S y s t e m ;

с

полями

объекта

namespace PassObjectMemberToFunction
{
public

class

Student

(
public

string

sName;

// OutputName - в ы в о д и м е н и с т у д е н т а
public
static void OutputName(Student

student)

{
/ / Вывод т е к у щ е г о и м е н и с т у д е н т а
Console.WriteLine("Student.sName
=
student.sName);

{o}",

}
// SetName - и з м е н я е м имя с т у д е н т а
public s t a t i c v o i d S e t N a m e ( S t u d e n t

Глава ft Методы класса

student,

179

string

{

sName)

s t u d e n t . s N a m e = sName;

}

}
public

class

Program

{
public

static

void

Main(string[]

args)

{
Student

student

= new S t u d e n t ( ) ;

// П р и с в а и в а е м имя н е п о с р е д с т в е н н ы м обращением к
// объектуConsole .WriteLine("Сначала:");
student.sName = "Madeleine";
Student.OutputName(student);
// Теперь
функция
// принадлежит классу
// Student
// И з м е н я е м имя с помощью ф у н к ц и и
Console.WriteLine("После
изменения:");
Student.SetName(student, "Willa");
Student.OutputName(student);
// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();

}

}

По сравнению с программой P a s s O b j e c t данная программа имеет только одно важщ|
изменение: функции O u t p u t N a m e () и S e t N a m e () перенесены в класс S t u d e n t .
Из-за этого изменения функция M a i n () вынуждена обращаться к означенным фущ|
циям с указанием класса S t u d e n t . Эти функции теперь являются членами класса Stu-|
d e n t , а н е P r o g r a m , которому принадлежит функция M a i n ( )
Это маленький, но достаточно важный шаг. Размещение функции O u t p u t N a m e !
в классе приводит к повышению степени повторного использования: внешние функц
которым требуется выполнить вывод объекта на экран, могут найти функцию Output
N a m e () вместе с другими функциями в классе, следовательно, писать такие функ
для каждой программы, применяющей класс S t u d e n t , не требуется.
Указанное решение лучше и с философской точки зрения. Класс P r o g r a m не доля
беспокоиться о том, как инициализировать имя объекта S t u d e n t , или о том, как выпо:
нить вывод его имени на экран. Всю эту информацию должен содержать сам класс Stu
d e n t . Объекты отвечают сами за себя.
В действительности функция M a i n () не должна инициализировать объем
именем Madeleine непосредственно — в этом случае также следует использ<
вать функцию S e t N a m e ( ) .

180

Часть III. Объектно-основанное программирована

Внутри самого класса S t u d e n t одна функция-член может вызывать другую без яв­
ного указания имени класса. Функция S e t N a m e () может вызвать функцию O u t p u t feme ( ) , не указывая имени класса. Если имя класса не указано, С# считает, что вызвана
функция из того же класса.

Определение метода
Обращение к членам данных объекта — экземпляра класса — выполняется посредст­
вом указания объекта, а не класса:
Student s t u d e n t
student.sName

= new S t u d e n t () ;
= "Madeleine";

//
//

Создание экземпляра
Обращение к члену

Student

C# позволяет вызывать нестатические функции-члены аналогично:
student. S e t N a m e ( " M a d e l e i n e " ) ;
Следующий пример демонстрирует это:

//InvokeMethod
using S y s t e m ;
namespace

-

вызов

функции-члена

с

указанием

объекта

InvokeMethod

{
class S t u d e n t
{
// Информация
public
string
public
string
// SetName
public

-

void

об имени с т у д е н т а
sFirstName;
sLastName;
сохранение

информации

SetName(string

sFName,

об

имени

string

sLName)

{
sFirstName
sLastName

=
=

sFName;
sLName;

}
// ToNameString - п р е о б р а з у е т
// строку д л я вывода
public
s t r i n g ToNameString()

объект

класса

Stude

{
string
return s;

s

=

sFirstName

+

"

"

+

sLastName;

}
}
public

class

Program

{
public

s t a t i c

void

Main()

{
Student

student

=

new

Student();

student.SetName("Stephen",

Глава 8. Методы класса

"Davis");

181

Console.WriteLine("Имя студента
"
+ student.ToNameString());
// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
" з а в е р ш е н и я п р о г р а м м ы . . . ") ;
Console.Read();

}
}
}
Вывод данной программы состоит из одной строки:
Имя

студента

Stephen

Davis

Эта программа очень похожа на программу P a s s O b j e c t T o M e m b e r F u n c t i o n . B в
веденной версии используются нестатические функции для работы с именем и фамилией,
Программа начинает с создания нового объекта s t u d e n t класса S t u d e n t , после
го вызывает функцию S e t N a m e ( ) , которая сохраняет строки " S t e p h e n " и "Davi
в членах-данных s F i r s t N a m e и s L a s t N a m e . И наконец, программа вызывает с
цию-член T o N a m e S t r i n g ( ) , возвращающую имя студента, составленное из двух а
По историческим причинам, никак не связанным с С#, нестатические функц
члены называются методами. В книге будет использоваться термин метод]
нестатических функций-членов, и термин функция — для функций всех про
типов.

Некоторые

программисты

применяют

термины

метод

экземп

(нестатический) и метод класса (статический).
Вернемся вновь к методу S e t N a m e ( ) , предназначенному для изменения значений
лей объекта класса S t u d e n t . Какой именно объект модифицирует метод SetName!
Рассмотрим, как работает следующий пример:
S t u d e n t c h r i s t a = new S t u d e n t ( ) ; // Создаем двух совершенно
Student sarah
= new S t u d e n t ( ) ; // разных с т у д е н т о в
christa.SetName("Christa",
"Smith");
sarah.SetName("Sarah",
"Jones");
Первый вызов S e t N a m e () изменяет поля объекта c h r i s t a , а в т о р о й — объект
sarah.е
Программисты на С# говорят, что метод работает с текущим объектом. В пер
вом вызове текущим объектом является c h r i s t a , в о втором — s a r a h .

Полное имя метода
Имеется тонкая, но важная проблема, связанная с описанием имен методов. Рассмот
рим следующий фрагмент исходного текста:
public

class

Person

{
public

void

Address()

{
Console.WriteLine("Hi");

182

Часть III. Объектно-основанное программном

public
(

class

Letter

string sAddress;
// Сохранение а д р е с а
public

void

Address(string

sNewAddress)

}
sAddress = sNewAddress;

}
}
Любое обсуждение метода A d d r e s s () после этого становится неоднозначным. Ме­
тод A d d r e s s () класса P e r s o n не имеет ничего общего с методом A d d r e s s () класса
L e t t e r . Если кто-то скажет, что в этом месте нужен вызов метода A d d r e s s ( ) , то ка­
кой именно метод A d d r e s s () имеется в виду?
Проблема не в самих методах, а в описании. Метод A d d r e s s () как независимая са­
модостаточная сущность просто не с у щ е с т в у е т — существуют методы
Регson. A d d r e s s ( ) и L e t t e r . A d d r e s s ( ) . Путем добавления имени класса в начало
имени метода явно указывается, какой именно метод имеется в виду.
Это описание имени метода очень похоже на описание имени человека. К примеру,
в семье меня знают как Стефана (честно говоря, в семье меня зовут несколько иначе, но
это уже несущественные подробности). В семье больше нет Стефанов (по крайней мере
в моей семье), но вот на работе есть еще два Стефана.
Когда я обедаю с коллегами в компании, где нет этих других Стефанов, имя Стефан
однозначно относится ко мне. Но когда все оказываются на рабочих местах, чтобы избе­
жать неоднозначности, следует добавлять к имени и фамилию и обращаться к Стефану
Дэвису, Стефану Вильямсу или Стефану Лейе.
Таким образом, A d d r e s s () можно рассматривать как имя метода, а его класс — как
фамилию.

Рассмотрим следующий метод S t u d e n t . S e t N a m e ( ) :
class S t u d e n t
// Информация об и м е н и с т у д е н т а
public s t r i n g s F i r s t N a m e ;
public s t r i n g s L a s t N a m e ;
// SetName - с о х р а н е н и е и н ф о р м а ц и и
public v o i d S e t N a m e ( s t r i n g sFName,

об и м е н и
s t r i n g sLName)

{

sFirstName = sFName;
sLastName = sLName ;

}
}
public class Program
{

Шва 8. Методы класса
public static void Main()

183

{

S t u d e n t s t u d e n t l = new S t u d e n t ( ) ;
studentl.SetName("Joseph",
"Smith");
S t u d e n t s t u d e n t 2 = new S t u d e n t ( ) ;
student2.SetName("John",
"Davis");

}
}
Функция M a i n () использует метод S e t N a m e () для того, чтобы обновить поляом
ектов s t u d e n t l и s t u d e n t 2 . Но внутри метода S e t N a m e () нет ссылки ни на какя
объект типа S t u d e n t . Как уже было выяснено, метод работает "с текущим объектов
Но как он знает, какой именно объект — текущий?
Ответ прост. Текущий объект передается при вызове метода как неявный аргумент-I
например, вызов
studentl.SetName("Joseph",

"Smith");

эквивалентен следующему:
Student.SetName(studentl,
"Joseph",
"Smith");
// Это - э к в и в а л е н т н ы й вызов (однако э т о т вызов
// корректно скомпилирован)

не

будет

Я не хочу сказать, что вы можете вызвать метод S e t N a m e () двумя различными сп
собами, я просто подчеркиваю, что эти два вызова семантически эквивалентны. Объе
являющийся текущим — скрытый первый аргумент — передается в функцию так же,
и другие аргументы. Оставьте эту задачу компилятору.
А что можно сказать о вызове одного метода из другого? Этот вопрос иллюстриру
ся следующим фрагментом исходного текста:
public
class
Student
{
p u b l i c s t r i n g sFirstName,public
s t r i n g sLastName;
public void SetName(string sFirstName,
s t r i n g sLastName)
SetFirstName(sFirstName);
SetLastName(sLastName);
public

void

SetFirstName(string

sFirstName
public

void

sLastName

=

sName;

SetLastName(string
=

sName)

sName)

sName;

}
В вызове S e t F i r s t N a m e () не видно никаких объектов. Дело в том, что при вызок|
одного метода объекта из другого в качестве неявного текущего объекта передается ти
же объект, что и для вызывающего метода. Обращение к любому члену в методе объем
рассматривается как обращение к текущему объекту, так что метод знает сам, каком
именно объекту он принадлежит.

184

Часть III. Объектно-основанное программирование]

Ключевое слово this
В отличие от других аргументов, текущий объект в список аргументов функции не
попадает, а значит, программист не назначает ему никакого имени. Однако С# не остав­
ляет этот объект безымянным и присваивает ему не слишком впечатляющее имя t h i s ,
которое может пригодиться в ситуациях, когда вам нужно непосредственно обратиться к
текущему объекту.
t h i s — ключевое слово С#, так что оно не может использоваться в программе
ни для какой иной цели, кроме описываемой.

Итак, можно переписать рассматривавшийся выше пример следующим образом:
public

class

Student

(
public
public
public

string sFirstName;
string sLastName;
void SetName(string

sFirstName,

string

sLastName)

{

// Явная с с ы л к а на " т е к у щ и й о б ъ е к т "
// ключевого слова t h i s
this.SetFirstName (sFirstName) ;

с

применением

this.SetLastName (sLastName) ;
public

void

SetFirstName(string

this. sFirstName
public

void

=

sName;

SetLastName(string

this. sLastName

=

sName)

sName)

sName;

Обратите внимание на явное добавление ключевого слова t h i s . Добавление t h i s
к ссылкам на члены не привносит ничего нового, поскольку наличие t h i s подразумевается и
так. Однако когда M a i n () делает показанный ниже вызов, t h i s означает s t u d e n t l как в
функции S e t N a m e ( ) , так и в любом другом методе, который может быть вызван:
studentl. S e t N a m e ( " J o h n " ,

"Smith" ) ;

Когда this используется явно
Обычно явно использовать t h i s не требуется, так как компилятор достаточно разу­
мен, чтобы разобраться в ситуации. Однако имеются две распространенные ситуации,
когда это следует делать. Например, ключевое слово t h i s может потребоваться при
инициализации членов данных:
class

Person

(
public s t r i n g s N a m e ;
public i n t n I D ;
public v o i d I n i t ( s t r i n g

Глава 8. Методы класса

sName,

int

nID)

185

{

this.sName
this.nID

=

sName;

// Имена а р г у м е н т о в те же,
// что имена членов-данных

= nID;

}
}
Аргументы метода I n i t () носят имена s N a m e и n I D , которые совпадают с имев
ми соответствующих членов-данных. Это повышает удобочитаемость, поскольку сра
видно, в какой переменной какой аргумент следует сохранить. Единственная проблем!
том, что имя s N a m e имеется как в списке аргументов, так и среди членов-данных. Таи
ситуация оказывается слишком сложной для компилятора.
Добавление t h i s проясняет ситуацию, четко определяя, что именно подразумевая
под s N a m e . В

I n i t ()

имя

sName

обозначает аргумент функции, в то время и

t h i s . s N a m e — член объекта.
Ключевое слово t h i s требуется также при сохранении текущего объек
для последующего применения или использования в некоторой друп
функции. Рассмотрим следующую демонстрационную программу Ref а
enc ingThi s Exp1i с i tly:
// R e f e r e n c i n g T h i s E x p l i c i t l y
// использование t h i s
using
System;
namespace

-

программа

демонстрирует

явное

ReferencingThisExp1iсitly

{
public

class

Program

{
public

s t a t i c

void

Main(string[]

strings)

{
//
Создание студента
S t u d e n t s t u d e n t = new
student.Init("Stephen

Student();
Davis",
1234);

' // В н е с е н и е к у р с а в с п и с о к
Console.WriteLine("Внесение в список Stephen
"курса Biology 101");
student.Enroll("Biology
101");
// Вывод п р о с л у ш и в а е м о г о к у р с а
Console.WriteLine("Информация о
student.DisplayCourse();

"

+

студенте:");

// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для
"завершения

Davis

"

+

программы...");

Console.Read();

}
}
// S t u d e n t public
class

186

класс,
описывающий
Student

студента

Часть III. Объектно-основанное программировав

// Все студенты имеют имя и идентификатор
public
public

string
int

sName;
nID;

// Курс, прослушиваемый студентом
Courselnstance

// I n i t
public

-

courselnstance;

инициализация объекта

void

I n i t ( s t r i n g

sName,

int

nID)

{
this.sName
this.nID

=
=

sName;
nID;

courselnstance

=

null;

}
// E n r o l l - добавление в список
public

void

Enroll (string

sCourselD)

{
c o u r s e l n s t a n c e = new C o u r s e l n s t a n c e ( ) ;
courselnstance.Init(this,
sCourselD);

// Вывод имени студента и прослушиваемых курсов
public

void

Display-Course ()

{
Console . W r i t e L i n e (sName)
c o u r s e l n s t a n c e . D i s p l a y ()

;
;

// C o u r s e l n s t a n c e - объединение информации о студенте и
// прослушиваемом курсе
public

class

Courselnstance

{
public
public

Student
string

student;
sCourselD;

// I n i t - связь студента и курса
public

void

Init(Student

student,

string

sCourselD)

(
this.student
this.sCourselD

=
=

student;
sCourselD;

// D i s p l a y - вывод имени курса
public

void

D i s p l a y ()

{
Console.WriteLine(sCourselD);

km 8. Методы класса

187

В этой программе в объекте S t u d e n t имеются поля для имени и идентк
студента и один экземпляр курса (да, студент не очень ретивый...). Функция
создает экземпляр s t u d e n t , после чего вызывает функцию I n i t ( ) для его к
зации. В этот момент ссылка c o u r s e l n s t a n c e равна n u l l , поскольку студен
назначен ни один курс.
Метод E n r o l l ()

назначает студенту курс путем инициализации ссылю

s e l n s t a n c e новым объектом. Однако метод C o u r s e l n s t a n c e . I n i t ( ) пол
земпляр класса S t u d e n t в качестве первого аргумента. Какой экземпляр дол)
передан? Очевидно, что вы должны передать текущий объект класса S t u d e n
именно тот, ссылкой н а который является t h i s .

Что делать при отсутствии this
Смешивать статические функции классов и нестатические методы объектов
не из лучших, но тем не менее С# и здесь может прийти на помощь.
Чтобы понять, в чем состоит суть проблемы, давайте рассмотрим
щий исходный текст:

// M i x i n g F u n c t i o n s A n d M e t h o d s - совмещение функций
// м е т о д о в о б ъ е к т о в может п р и в е с т и к проблемам
using
System;
namespace

класса

и

MixingFunctionsAndMethods

{
public

class

Student

{
public
public

string
string

sFirstName;
sLastName;

// I n i t S t u d e n t - инициализация объекта s t u d e n t
public void InitStudent(string sFirstName,
string
sLastName)
this.sFirstName = sFirstName;
this.sLastName = sLastName;

// O u t p u t B a n n e r - вывод начальной
public
s t a t i c void OutputBanner()

строки

Console.WriteLine("Никаких
хитростей:");
// Console.WriteLine(? какой объект используется

public

void

OutputBannerAndName()

// Используется класс Student,
но
// не передаются никакие объекты
OutputBanner();

188

?);

статическому

методу

Часть III. Объектно-основанное программном

// Явная передача
OutputName(this);

объекта

}
// OutputName - вывод имени с т у д е н т а
public
s t a t i c void OutputName(Student

student)

{
// Здесь объект указан явно
Console.WriteLine("Имя
студента
{о}",
student.ToNameString());

}
II T o N a m e S t r i n g - п о л у ч е н и е и м е н и
public
s t r i n g ToNameString()

студента

{
//
//
//
ret

З д е с ь текущий о б ъ е к т у к а з а н н е я в н о ; можно
использовать
t h i s :
return this.sFirstName + " " + this.sLastName;
urn sFirstName + " " + sLastName;

}

}
public

class

Program

{
public

s t a t i c

void

Main(string[]

args)

{
S t u d e n t s t u d e n t = new S t u d e n t ( ) ;
student.InitStudent("Madeleine",
// Вывод з а г о л о в к а и и м е н и
Student.OutputBanner();
Student.OutputName(student);
Console.WriteLine();
// Вывод з а г о л о в к а и и м е н и еще
student.OutputBannerAndName();

"Cather");

раз

// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();

Следует начать с конца программы, с функции M a i n ( ) , чтобы вы лучше разглядели
имеющиеся проблемы. Программа начинается с создания объекта класса S t u d e n t
и инициализации его имени. Затем она хочет всего лишь вывести имя студента с не­
большим заголовком.
Функция M a i n () вначале выводит заголовок и сообщение с использованием стати­
ческих функций. Программа вызывает функцию O u t p u t B a n n e r () для вывода строки
заголовка и O u t p u t N a m e () для вывода имени студента. Функция O u t p u t B a n n e r ()

Глава 8. Методы класса

189

просто выводит строку на консоль. Функция O u t p u t N a m e () получает в качестве арг
мента объект класса S t u d e n t , так что она может получить и вывести имя студента.
Далее функция M a i n ( ) использует для решения той же задачи функцию объект
и вызывает s t u d e n t . O u t p u t B a n n e r A n d N a m e ( ) .
Метод s t u d e n t . O u t p u t B a n n e r A n d N a m e ( ) сначала вызывает статическую ф у н к
цию O u t p u t B a n n e r ( ) . При этом вызове, поскольку не указан класс, к которому принад
лежит функция, подразумевается, что она принадлежит тому же классу, что и вызывание
функция, т.е. классу S t u d e n t . После этого выполняется вызов O u t p u t N a m e ( ) . Это то
же функция класса, и точно так же устанавливается ее принадлежность классу S t u d
Однако эта функция требует передачи ей объекта класса S t u d e n t . Метод stu
d e n t . O u t p u t B a n n e r A n d N a m e ( ) передает е й в качестве этого аргумента t h i s .
Более интересная ситуация возникает при вызове T o N a m e S t r i n g () из Output
N a m e ( ) . Функция O u t p u t N a m e ( ) объявлена как s t a t i c и , таким образом, н е и м е е т
t h i s . У нее есть переданный ей объект класса S t u d e n t , который она и использует для
осуществления вызова.
Статическая функция не может вызывать нестатические методы без явного ука
зания объекта. Нет объекта — нет и вызова. В общем случае статическая функ
ция не может обратиться ни к одному нестатическому элементу класса. Однако
нестатические методы могут обращаться как к статическим, так и к нестатич
ским членам класса — данным и функциям.

В среде Visual Studio программисту доступна одна важная возможность, известные
как автозавершение (auto-complete). Когда вы вводите имя класса или объекта в вашем
исходном тексте, Visual Studio пытается предвидеть, какое имя класса или метода вы на
мерены ввести.
Описать автозавершение в Visual Studio проще на конкретном примере. Для демонст
рации того, как работает эта возможность Visual Studio, представлен следующий фраг
мент исходного текста из программы M i x i n g F u n c t i o n s A n d M e t h o d s :
// Вывод з а г о л о в к а и и м е н и
Student.OutputBanner();
Student.OutputName(student);
Console.WriteLine();
// Повторный вывод з а г о л о в к а и
student.OutputBannerAndName();

имени

Автозавершение— удобная вещь, но если вам требуется большая помощь, их
пользуйтесь командой Help^Index для вызова справки. Выводимый предметны,
указатель можно ограничить, например, только темами, связанными с С# или .NET
Воспользуйтесь этим предметным указателем так же, как вы пользуетесь предмет
ным указателем книги. К вашим услугам также команды поиска по текстам стате
справки и содержания справочной системы. Нужные вам статьи можно пометил
с тем, чтобы в будущем можно было быстро к ним возвращаться.

190

Часть III. Объектно-основанное программирован® Г.

Справка по встроенным функциям системной библиотеки
При использовании фрагмента программы M i x i n g F u n c t i o n s A n d M e t h o d s в каче­
стве примера, как только вы вводите в редакторе C o n s o l e . , Visual Studio сразу же вы­
водит список всех методов C o n s o l e . Стоит ввести W, как Visual Studio переходит в спи­
ске к первому методу, начинающемуся на W (а именно — W r i t e ( ) ) . Если вы нажмете
клавишу , то перейдете к методу W r i t e L i n e ( ) , а справа от списка появится справ­
ка по этому методу (рис. 8.1). В справке, в частности, указано, что имеется 19 перегру­
женных версий данного метода — естественно, каждая со своим набором аргументов.

Рис. 8.1. Автозавершение в Visual Studio позволяет правильно выбрать требуемый метод
Вы завершаете ввод имени W r i t e L i n e . Как только вы введете после имени откры­
вающую скобку, Visual Studio изменит выводимое окно подсказки — теперь в нем будут
показаны возможные аргументы функции (рис. 8.2).

Puc. 8.2. Visual Studio подсказывает, какие возможные аргументы может прини­
мать функция
Не нужно полностью вводить имя функции. Предположим, вы ввели W r i t e L ,
чего вполне достаточно, чтобы однозначно идентифицировать нужный вам ме­
тод. Не завершая ввода имени, введите открывающую скобку, и Visual Studio
завершит ввод имени метода за вас. Вы можете также воспользоваться клави­
шами для появления раскрывающегося меню автозавершения.

Глава 8. Методы класса

191

Справка по встроенным функциям системной библиотеки
При использовании фрагмента программы M i x i n g F u n c t i o n s A n d M e t h o d s в каче­
стве примера, как только вы вводите в редакторе C o n s o l e ., Visual Studio сразу же вы­
водит список всех методов C o n s o l e . Стоит ввести W, как Visual Studio переходит в спи­
ске к первому методу, начинающемуся на W (а именно — W r i t e ( ) ) . Если вы нажмете
клавишу , то перейдете к методу W r i t e L i n e ( ) , а справа от списка появится справ­
ка по этому методу (рис. 8.1). В справке, в частности, указано, что имеется 19 перегру­
женных версий данного метода — естественно, каждая со своим набором аргументов.

Рис. 8.1. Автозавершение в Visual Studio позволяет правильно выбрать требуемый метод
Вы завершаете ввод имени W r i t e L i n e . Как только вы введете после имени открывающую скобку, Visual Studio изменит выводимое окно подсказки — теперь в нем будут
показаны возможные аргументы функции (рис. 8.2).

Puc. 8.2. Visual Studio подсказывает, какие возможные аргументы может прини­
мать функция
Не нужно полностью вводить имя функции. Предположим, вы ввели W r i t e L ,
чего вполне достаточно, чтобы однозначно идентифицировать нужный вам ме­
тод. Не завершая ввода имени, введите открывающую скобку, и Visual Studio
завершит ввод имени метода за вас. Вы можете также воспользоваться клави­
шами для появления раскрывающегося меню автозавершения.
Л
' АВА

8. Методы класса

191

Можно щелкнуть мышью в левой части всплывающего окна справки для и
чтобы найти интересующую версию перегрузки функции W r i t e L i n e ().
описанием функции вы увидите описание ее первого аргумента (если такой
имеется). Функция W r i t e L i n e () имеет 19 вариантов перегрузки для разн
ных типов данных. Первый из них, который вы увидите во всплывающем он
не требует аргументов. Посредством клавиш

можнопрокручивать

список перегрузок.
Если воспользоваться версией W r i t e L i n e , которая в качестве первого аргумент
получает

форматную

строку,

то

как

только

будет

введена

строка,

например

" s o m e s t r i n g { 0 } " (использована сугубо в качестве примера), за которой вводится
запятая, как Visual Studio тут же ответит на эти действия описанием второго аргумент
функции, как показано на рис. 8.3.

Рис. 8.3. Visual Studio предоставляет информацию no каждому аргументу функции

Помощь при использовании ваших собственных
функций и методов
Visual Studio в состоянии помочь и при работе с собственными функциями (часта
помнить их и все их аргументы не менее сложно, чем для функций из .NET).
Продолжим анализируемый пример. Удалим строку " s o m e s t r i n g { о } " и за
ним вызов метода вызовом без аргументов: C o n s o l e . W r i t e L i n e О. В следуют
строке введем s t u d e n t . Как только будет введена точка после имени объекта, Vis
Studio откроет список членов объекта класса S t u d e n t . Если продолжить ввод и ввести
первую букву имени члена, Visual Studio тут же перейдет в списке к первому члену с
кой буквой, как показано на рис. 8.4. Будет также показано и объявление метода, что
вы могли вспомнить, как им пользоваться.
По пиктограмме слева от имени в списке автозавершения можно узнать, имеете ли
дело с членами-данными или с методами.
Гораздо проще определить это по цвету: члены-данные имеют цвет морс
волны, а методы — розовый.

192

Часть III. Объектно-основанное программирова

Рис. 8.4. Автозавершение в Visual Studio может работать и с пользовательскими
классами и методами
Вы можете встретиться с неизвестными вам методами, такими как E q u a l s или
GetHashCode. Эти методы все без исключения объекты получают от С# (совершенно
бесплатно) для технических целей.
Повторимся — ввод открывающей скобки позволяет Visual Studio автоматиче­
ски завершить ввод имени метода.

Та же возможность автозавершения работает и с функциями. Если вы введете имя
класса Student, за которым следует точка, Visual Studio откроет список членов этого
класса. Как только вы введете O u t p u t N , Visual Studio отреагирует на это списком аргу­
ментов функции OutputName ( ) , как показано на рис. 8.5.

Puc. 8.5. Visual Studio обеспечивает вас информацией независимо от того, работаете
вы с методами объекта или функциями класса

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

Глава 8. Методы класса

193

что делает функция O u t p u t N a m e ( ) , и не может предоставить программисту какой
либо описывающий ее текст. Но этой беде можно помочь — было бы желание.
Обычные комментарии в С# начинаются с двух косых черт — / /. Но Visual Stud
выделяет и понимает специальные комментарии, начинающиеся с трех косых черт///.

Это—

документирующие

комментарии,

снабжающие

Visual

Studio

дополнител

ной информацией, которая может использоваться функцией автозавершения.
Если быть точным, то впервые такая концепция появилась в языке Java, в кок
ром имелись специальные программы для сбора подобных комментариев в от
дельный файл справки. С# пошел несколько дальше и использует эти комма
тарии в своей динамической справочной системе, работающей в процессе ре
дактирования исходных текстов.
Документирующие комментарии могут содержать любую комбинацию элементом
показанных в табл. 8 . 1 .

Документирующие комментарии следуют правилам X M L 7 H T M L : команда начин»
ется с дескриптора < c o m m a n d > и заканчивается дескриптором
public
string
sFirstName;
///
/// Фамилия с т у д е н т а
/// < / s u m m a r y >
public

string

sLastName;

// I n i t S t u d e n t
///
/// И н и ц и а л и з а ц и я с т у д е н т а п е р е д е г о и с п о л ь з о в а н и е м .
/// < / s u m m a r y >
/// < p a r a m n a m e = " s F i r s t N a m e " > И м я с т у д е н т а < / р а г а т >
/// < p a r a m n a m e = " s L a s t N a m e " > Ф а м и л и я с т у д е н т а < / р а г а т >
public v o i d
I n i t S t u d e n t ( s t r i n g
sFirstName,
string
sLastName)
{'

this.sFirstName = sFirstName;
this.sLastName = sLastName;

// O u t p u t B a n n e r
///
///
///

Вывод з а г о л о в к а


public

s t a t i c

void

перед

выводом

информации

о

студенте

OutputBanner()

{
Console.WriteLine("Никаких
хитростей:");
// Console.WriteLine(? какой объект используется

?);

}
// O u t p u t B a n n e r A n d N a m e
/// < s u m m a r y >
/// Вывод з а г о л о в к а с п о с л е д у ю щ и м
/// < / s u m m a r y >
public

void

выводом

имени

студента

OutputBannerAndName()

{
// Используется класс S t u d e n t ,
но
// не передаются никакие объекты
OutputBanner();

статическому

методу

// Явная п е р е д а ч а о б ъ е к т а
OutputName(this,
5);

// O u t p u t N a m e
/// < s u m m a r y >
/// Выводит и м я с т у д е н т а н а к о н с о л ь
/// < / s u m m a r y >
/// < p a r a m n a m e = " s t u d e n t " > С т у д е н т ,
имя к о т о р о г о должно
///
быть
выведено
/// < p a r a m n a m e = " n I n d e n t " > K ^ H 4 e c T B O п р о б е л о в в
///
отступе
/// < r e t u r n s > B b m e f l e H H a f l
cTpoKa
public s t a t i c
string OutputName(Student
student,
int
nlndent)

{•

// Здесь объект указан явно
s t r i n g s = new S t r i n g ( '
',
nlndent);
s += S t r i n g . F o r m a t ( " И м я с т у д е н т а {О}",
student.ToNameString());
Console.WriteLine(s);
return
s;

}
//
ToNameString
///

///
///

Преобразует


///

CTpoKy

public

string

имя

студента
с

именем

в

строку

для

вывода

студента

ToNameString()

{
//
//
//
ret

З д е с ь текущий о б ъ е к т у к а з а н н е я в н о ; можно
использовать
t h i s :
return this.sFirstName + "
" + this.sLastName;
u r n sFirstName + " " + sLastName;

}

}
///
///


Класс,

///

использующий

класс

Student



public

class

Program

{
///
///


Стартовая

///



///

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

Сравнение без учета регистра
Метод C o m p a r e ( ) , использованный в функции I s T e r m i n a t e S t r i n g ( ) , рассмат­
ривает строки " E X I T " и " e x i t " как различные. Однако имеется перегруженная версия
функции C o m p a r e ( ) , которой передается три аргумента. Третий аргумент этой функции
указывает, следует ли при сравнении игнорировать регистр букв (значение t r u e ) или нет
(значение false). О перегрузке функций рассказывалось в главе 7, "Функции функций".
Следующая версия функции

I s T e r m i n a t e S t r i n g ()

возвращает значение t r u e ,

какими бы буквами не была введена команда завершения.
// I s T e r m i n a t e S t r i n g - в о з в р а щ а е т з н а ч е н и е t r u e , е с л и
// исходная с т р о к а с о о т в е т с т в у е т о д н о й из к о м а н д з а в е р ш е н и я
// программы
public s t a t i c b o o l I s T e r m i n a t e S t r i n g ( s t r i n g s o u r c e )

{


// Проверяет, р а в н а ли п е р е д а н н а я с т р о к а с т р о к а м exit
/ / или q u i t , н е з а в и с и м о о т р е г и с т р а и с п о л ь з у е м ы х б у к в
return ( S t r i n g . C o m p a r e ( " e x i t " , s o u r c e , t r u e ) = = 0 )
(String.Compare("quit", source, true) == 0 ) ;

)

Эта версия функции I s T e r m i n a t e S t r i n g ()

проще предыдущей версии с исполь­

зованием цикла. Ей не надо заботиться о регистре символов, и она может обойтись всего
лишь двумя условными выражениями, так как ей достаточно рассмотреть только два ва­
рианта команды завершения программы.
Обратите внимание, что приведенная версия функции I s T e r m i n a t e S t r i n g ()
не применяет оператор if. Вычисленное значение логического выражения
просто непосредственно передается пользователю, что позволяет избежать
применения if.

Использование конструкции switch
Мне не нравится этот способ, но вы можете использовать конструкцию s w i t c h для
поиска действий для конкретной строки. Обычно s w i t c h применяется для сравнения
значения переменной с некоторым набором возможных значений, однако эту конструк­
цию можно применять и для объектов s t r i n g . Вот как выглядит версия функции I s ­
TerminateString () с использованием конструкции s w i t c h .
//IsTerminateString - в о з в р а щ а е т з н а ч е н и е t r u e , е с л и
// исходная с т р о к а с о о т в е т с т в у е т о д н о й из к о м а н д з а в е р ш е н и я
// программы
public static b o o l I s T e r m i n a t e S t r i n g ( s t r i n g s o u r c e )

{
switch (source)
{
case "EXIT" :
case "exit" :

Глава 9. Работа со строками в С#

205

case "QUIT":
case "quit":
return true;

}
return

false;

}
Такой подход работает постольку, поскольку выполняется сравнение только пред­
определенного ограниченного количества строк. Цикл f o r ()

представляет собой сущ

ственно более гибкий подход, а применение функции C o m p a r e ( ) , нечувствительно!
к регистру, существенно повышает возможности программы по "пониманию" введенно­
го пользователем.

Считывание ввода пользователя
Программа может считывать ввод пользователя по одному символу, но тогда вы сами
должны заботиться о символах новой строки и прочих деталях. Более простым подходок
оказывается считывание строки с ее последующим разбором.
Ваша программа может считывать строки, как если бы это были массива
символов, с использованием оператора f o r e a c h или оператора индекса [],
Приведенный ниже исходный текст программы StringToCharAccess
демонстрирует данную методику.
// S t r i n g T o C h a r A c c e s s - о б р а щ е н и е
// если бы строка была массивом
using System;
namespace

к символам строки,

как

StringToCharAccess

{
public

class

Program

{
public

static void Main(string[]

args)

{
// С ч и т ы в а н и е строки с клавиатуры
Console.WriteLine("Введите произвольную " +
"строку с и м в о л о в . " ) ;
string sRandom = C o n s o l e . R e a d L i n e О ;
Console.WriteLine();
// Выводим стоку
Console.WriteLine("Вывод строки: " + s R a n d o m ) ;
Console.WriteLine();
// Выводим ее как последовательность символов
Console.Write("Вывод с использованием foreach: " ) ;
f o r e a c h ( c h a r с in s R a n d o m )
{
Console.Write(с);

}
C o n s o l e . W r i t e L i n e ( ) ; .// З а в е р ш е н и е с т р о к и
//

206

Пустая

строка-разделитель

Часть

III.

Объектно-основанное

программирован

Console.WriteLine();
/ / Вывод с т р о к и как п о с л е д о в а т е л ь н о с т и с и м в о л о в
Console.Write("Вывод с и с п о л ь з о в а н и е м for: " ) ;
f o r ( i n t i = 0; i < s R a n d o m . L e n g t h ; i + + )
{
Console.Write{sRandom[i]);

}

Console.WriteLine();

//

Завершение

строки

// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите д л я " +
"завершения п р о г р а м м ы . . . " ) ;
Console.Read();

Эта программа выводит некоторую совершенно случайную строку, вводимую поль­
зователем. Сначала строка выводится обычным способом с применением метода
WriteLine ( s t r i n g ) . Затем для вывода строки используется цикл f o r e a c h , который
выводит ее символы по одному. И наконец, для той же цели применяется цикл f o r на­
ряду с оператором индекса [ ]. В результате на экране получается примерно следующее:
Введите п р о и з в о л ь н у ю с т р о к у с и м в о л о в .
Вывод с т р о к и : d j n d f v c k e x f q y f z c n h j r f
Вывод с и с п о л ь з о в а н и е м

foreach:

Вывод с и с п о л ь з о в а н и е м

for:

Нажмите



для

djn dfv ckexfqyfz

djn dfv ckexfqyfz

завершения

cnhjrf

cnhjrf

программы...

Зачастую требуется убрать все пробельные символы с обоих концов строки (под тер­
мином пробельный символ (white space) подразумеваются символы, обычно не отобра­
жаемые на экране, например, пробел, символ новой строки или табуляции). Для этого
можно воспользоваться методом T r i m ():
// Удаляем п р о б е л ь н ы е с и м в о л ы
sRandom = s R a n d o m . T r i m () ;

с

концов

строки

S t r i n g . T r i m () возвращает новую строку. Применяя этот метод так, как по­
казано во фрагменте исходного текста выше, первоначальная версия строки с
пробельными символами оказывается потерянной и больше не используется.

Разбор числового ввода
Функция R e a d L i n e () используется для считывания объекта типа s t r i n g . Про­
грамма, которая ожидает числовой ввод, должна эту строку соответствующим образом
преобразовать в числа. С# предоставляет программисту класс C o n v e r t со всем необхо­
димым для этого инструментарием, в частности, методами для преобразования строки
в каждый из встроенных числовых типов. Так, следующий фрагмент исходного текста
считывает число с клавиатуры и сохраняет его в переменной типа int.

Глава ft Работа со строками в С#

207

string s = Console.ReadLine();
int n = C o n v e r t . T o I n t 3 2 ( s ) ;

/ / Д а н н ы е в в о д я т с я к а к строка,
// и п р е о б р а з у ю т с я в ч и с л о

Другие методы для преобразования еще более очевидны: T o D o u b l e (), ToFloat
ToBoolean().
Метод T o I n t 3 2 ( )

выполняет преобразование в 32-битовое знаковое цел

число (вспомните, что 32 бита — это размер обычного int), так что эта фуш
ция выполняет преобразование строки в число типа int; для преобразован!
строки в число типа l o n g используется функция То Int 6 4 ().
Когда функции преобразования попадается "неправильный" символ, она может ви
дать некорректный результат, так что вам следует убедиться, что строка содержит има|
но те данные, которые ожидаются.
Приведенная далее функция возвращает значение t r u e , если переданная ей crpoi
состоит только из цифр. Такая функция может быть вызвана перед функцией преобрал
вания строки в целое число, поскольку число может состоять только из цифр.
Вообще-то для чисел с плавающей точкой в строке может быть эта самая T«I
ка, а кроме того, перед числом может находиться знак минус — но сейчас»
терес представляют не эти частности, а сама идея.
Итак, вот эта функция:
//
//

IsAllDigits - возвращает
являются цифрами

public

static

bool

true,

если все символы строки

IsAllDigits(string

sRaw)

{
// У б и р а е м все л и ш н е е с концов строки. Если при этом в
// строке ничего не остается — значит, эта строка не
// представляет собой число
string s = s R a w . T r i m ( ) ; // И г н о р и р у е м п р о б е л ь н ы е символы
if ( s . L e n g t h == 0)

{
return

false;

}
// Циклически проходим по всем символам строки
f o r ( i n t i n d e x = 0; i n d e x < s . L e n g t h ; i n d e x + + )
{
// Наличие в строке символа, не являющегося цифрой,
// говорит о том, что это не число
if ( C h a r . I s D i g i t ( s [ i n d e x ] ) == false)
{
return false;

}
}
// Все в порядке:
return true;

строка

состоит

только из цифр

}
Функция I s A l l D i g i t s сначала удаляет все ненужные пробельные символы с обош|
концов строки. Если после этого строка оказывается пуста — значит, она состояла цели!
ком из пробельных символов и числом не является. Если строка остается не пустой,
функция проходит по всем ее символам. Если какой-то из символов оказывается не цщ

208

Часть

III.

Объектно-основанное

программирована

той, функция возвращает f a l s e , указывая, что переданная ей строка не является чис­
том. Возврат функцией значения t r u e означает, что все символы строки — цифры, так
что строка, по всей видимости, представляет собой некоторое числовое значение.
Следующая демонстрационная программа считывает вводимое пользовате­
лем число и выводит его на экран:

// IsAllDigits - д е м о н с т р а ц и о н н а я п р о г р а м м а ,
[// применение ф у н к ц и и I s A l l D i g i t s
using S y s t e m ;
namespace

иллюстрирующая

IsAllDigits

{
class P r o g r a m
{
public

static void Main(string[]

args)

{
// В в о д с т р о к и с к л а в и а т у р ы
Console.WriteLine("Введите целое ч и с л о " ) ;
string s = C o n s o l e . R e a d L i n e ( ) ;
// Проверка, может ли эта строка быть числом
if
(!IsAllDigits(s))

{
Console.WriteLine("Это

не

число!");

}
else
{
// П р е о б р а з о в а н и е строки в ц е л о е число
int n = I n t 3 2 . P a r s e ( s ) ;
// В ы в о д и м ч и с л о , у м н о ж е н н о е на 2
Console.WriteLine("2 * { о } = { l } " , n, 2 * n ) ;

}
// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
"завершения п р о г р а м м ы . . . " ) ;
Console.Read();

}
// IsAllDigits - в о з в р а щ а е т t r u e ,
// строки
// я в л я ю т с я ц и ф р а м и
public

static bool

если все символы

IsAllDigits(string

sRaw)

{
//
II

Тело
функции
было
краткости
опущено

рассмотрено

ранее

и

здесь

оно

для

Программа считывает строку, вводимую пользователем с клавиатуры, после чего вы­
полняет ее проверку с помощью функции I s A l l D i g i t s . Если функция возвращает
false, программа выводит пользователю предупреждающее сообщение. Если же нет,

Глава 9. Работа со строками в С #

209

программа преобразует строку в число с помощью функции I n t 3 2 . P a r s e ( ) , кош
представляет собой альтернативу C o n v e r t . T o I n t 3 2 ( ) . И наконец, программа выво
дит полученное число и его удвоенное значение (что должно доказывать корректность
преобразования строки в число).
Вот как выглядит пример вывода рассматриваемой программы:
Введите целое число
1АЗ
Это не ч и с л о !
Нажмите д л я

завершения

программы...

Можно просто попытаться использовать функцию класса C o n v e r t для вьш
нения преобразования строки в число, и обработать возможные исключен
генерируемые функцией преобразования. Однако имеется немалая вероятна
что при этом функция не сгенерирует исключения, а вернет некорректный
зультат — например, в приведенном выше примере с вводом в качестве чи
1АЗ вернет значение 1.

Обработка последовательности чисел
Зачастую программы получают в качестве вводимых данных строку, состоящую
нескольких чисел. Воспользовавшись методом S t r i n g . S p l i t ( ) , вы сможете лен
разбить строку на несколько подстрок, по одной для каждого числа, и работать с ним|
отдельности.
Функция S p l i t () преобразует единую строку в массив строк меньшего рази
с применением указанного символа-разделителя. Например, если вы скажете функвД
S p l i t ( ) , что следует использовать в качестве разделителя запятую, строка "1,2,
превратится в три строки — " 1", " 2" и " 3".
Приведенная далее демонстрационная программа применяет метод Split]
для ввода последовательности чисел для суммирования.

// ParseSequenceWithSplit
считывает последовательность
// разделенных запятыми ч и с е л , р а з д е л я е т ее на отдельные
// целые ч и с л а и суммирует их
namespace ParseSequenceWithSplit
{
using System;
class

Program

{
p u b l i c static v o i d M a i n ( s t r i n g []

args)

{
// Приглашение пользователю ввести последовательность
// целых чисел
Console.WriteLine("Введите
последовательность
целых"
+
" чисел, разделенных запятыми:");
// Считывание строки текста
string input = C o n s o l e . R e a d L i n e О ;
Console.WriteLine();

210

Часть

III.

Объектно-основанное

программирован*

// П р е о б р а з у е м с т р о к у в о т д е л ь н ы е п о д с т р о к и с
// и с п о л ь з о в а н и е м в к а ч е с т в е с и м в о л о в - р а з д е л и т е л е й
// з а п я т ы х и п р о б е л о в
1
1
c h a r [] c D i v i d e r s = { ' , ' ,
};
string[] segments = i n p u t . S p l i t ( c D i v i d e r s ) ;
// К о н в е р т и р у е м к а ж д у ю п о д с т р о к у в ч и с л о
int n S u m = 0;
f o r e a c h ( s t r i n g s in s e g m e n t s )

{

//
if

(Пропускаем пустые
( s . L e n g t h > 0)

подстроки)

{
// Пропускаем строки,
if ( I s A l l D i g i t s ( s ) )

не являющиеся числами

{
// Преобразуем строку в 32-битовое целое число
int n u m = I n t 3 2 . P a r s e ( s ) ;
Console.WriteLine("Очередное число = { о } " , n u m ) ;
// Д о б а в л я е мполученное
nSum += num;

число

в

сумму

}
// В ы в о д с у м м ы
Console.WriteLine("Сумма

=

{ о } " ,

nSum);

// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
" з а в е р ш е н и я п р о г р а м м ы . . . ") ;
Console.Read();

// I s A l l D i g i t s - в о з в р а щ а е т t r u e ,
// я в л я ю т с я ц и ф р а м и
public

static bool

если все символы строки

IsAllDigits(string

sRaw)

{
// Убираем все л и ш н е е с к о н ц о в с т р о к и . Е с л и п р и э т о м в
// строке н и ч е г о не о с т а е т с я - з н а ч и т , эта с т р о к а не
// п р е д с т а в л я е т собой ч и с л о
string s = s R a w . T r i m f ) ; // И г н о р и р у е м п р о б е л ь н ы е с и м в о л ы
if ( s . L e n g t h == 0)

{
return f a l s e ;

}
// Циклически проходим по всем символам строки
for (int i n d e x = 0; i n d e x < s . L e n g t h ; i n d e x + + )
{
// Наличие в строке с и м в о л а , не я в л я ю щ е г о с я цифрой,
// г о в о р и т о т о м , ч т о э т о не ч и с л о
if ( C h a r . I s D i g i t ( s [ i n d e x ] ) == f a l s e )

{

return

false;

tea 9. Работа со строками в С#

211

}
// Все в порядке:
return true;

}

строка

состоит

только из цифр

}
Программа P a r s e S e q u e n c e W i t h S p l i t начинает со считывания строки с клав]

ры. Затем методу S p l i t () передается массив символов c D i v i d e r s , представляю:
собой символы-разделители, использующиеся при отделении отдельных чисел в строке.
Далее программа циклически проходит по всем "подмассивам", созданным фук
S p l i t ( ) , применяя для этой цели цикл f o r e a c h . Программа пропускает все подстрЯ
нулевой длины, а для непустых строк вызывает функцию I s A l l D i g i t s () для тя
чтобы убедиться, что строка представляет собой число. Корректные строки преобри
ются в целые числа и суммируются с аккумулятором nSum. Некорректные числа щ
рируются (я предпочел не генерировать сообщения об ошибках).
Вот как выглядит типичный вывод данной программы:
Введите последовательность целых чисел, разделенных
1, 2 , а , 3, 4
Очередное число = 1
Очередное число = 2
Очередное число = 3
Очередное число = 4
С у м м а = 10
Нажмите



для

завершения

запятыми:

программы...

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

Управление выводом программы представляет собой важный аспект работы со ста
ками. Подумайте сами: вывод программы— это именно то, что видит пользователь. •
имеет значения, насколько элегантна внутренняя логика и реализация программы —Ж
вряд ли впечатлит пользователя; куда важнее для него корректность и внешнее предстш
ление выводимых программой данных.
Класс S t r i n g предоставляет программисту ряд методов для форматирования вьй
димой строки. В следующих разделах будут рассмотрены такие методы, как Trim
Pad(), PadRight(), PadLeft(), Substring() и Concat().

Использование методов Trim() и Pad()
Методом T r i m () можно воспользоваться для.удаления ненужных пробельных сю!
волов с обоих концов строки. Обычно этот метод применяется для удаления оробело)
при выравнивании выводимой строки.

212

Часть

III.

Объектно-основанное

программирован*

Еще один распространенный метод, часто используемый при форматировании —
функции Pad, которые добавляют к строке пробелы с тем, чтобы ее длина стала равной
некоторому предопределенному значению. Например, так вы можете добавить к строке
пробелы слева или справа, чтобы обеспечить выравнивание вывода по правому или ле­
вому краю.
В приведенной далее небольшой демонстрационной программе A l i g n O u t p u t для выравнивания списка имен применяются обе упомянутые функции.

// AlignOutput - в ы р а в н и в а н и е м н о ж е с т в а с т р о к д л я у л у ч ш е н и я
// внешнего в и д а в ы в о д а п р о г р а м м ы
namespace A l i g n O u t p u t
(

using S y s t e m ;
class P r o g r a m

{
public s t a t i c v o i d M a i n ( s t r i n g []

args)

{
string[]

names

=

{"Christa
",
"
Sarah",
"Jonathan",
"Sam",
" Schmekowitz " } ;
// Вывод и м е н в т о м в и д е , в к о т о р о м о н и п о л у ч е н ы
Console.WriteLine("Имена имеют разную д л и н у " ) ;
foreach(string

s

in n a m e s )

{
Console.WriteLine("Имя '{о}' до обработки", s);
}
C o n s o l e . W r i t e L i n e () ;
II Выравниваем с т р о к и по л е в о м у краю и д е л а е м их
// р а в н о й д л и н ы
string [] s A l i g n e d N a m e s = T r i m A n d P a d ( n a m e s ) ;
// Выводим о к о н ч а т е л ь н ы й р е з у л ь т а т на э к р а н
C o n s o l e . W r i t e L i n e ( " Т е же и м е н а в ы р о в н е н ы и и м е ю т
"одинаковую д л и н у " ) ;
foreach(string s in s A l i g n e d N a m e s )

"

+

C o n s o l e . W r i t e L i n e ( " И м я ' {0} ' п о с л е о б р а б о т к и " , s) ;

}
II Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
"завершения программы. . . " ) . ;
Console.Read();

}
Глава 9. Работа со строками в С#

213

// TrimAndPad - для данного массива строк удаляются
// пробелы с обоих сторон строки, после чего выполняется
// дополнение пробелов таким образом, чтобы все строки
// о к а з а л и с ь в ы р о в н е н ы с н а и б о л ь ш е й с т р о к о й в м а с с и в е
p u b l i c s t a t i c string[] T r i m A n d P a d ( s t r i n g [] strings)

{
// К о п и р у е м и с х о д н ы й м а с с и в в м а с с и в , с к о т о р ы м будем
// работать
string[] stringsToAlign = new String[strings.Length];
//
//

Удаляем ненужные
строки

f o r ( i n t i = 0;

пробелы

с

обоих

сторон

i < stringsToAlign.Length;

каждой
i++)

{
stringsToAlign[i]

= strings[i].Trim();

}
// Находим наибольшую длину
i n t n M a x L e n g t h = 0;
foreach(string

s

in

строки

в

массиве

stringsToAlign)

{
if

(s.Length

> nMaxLength)

{
nMaxLength

=

s.Length;

}
}
//

Выравниваем

f o r ( i n t i = 0;

все

строки

к

длине

самой длинной

i < stringsToAlign.Length;

i++)

{
stringsToAlign[i] =
stringsToAlign[i].PadRight(nMaxLength + 1 ) ;

}
// Возвращаем результат
return stringsToAlign;

}

}

вызывающей

функции

}

Демонстрационная программа A l i g n O u t p u t определяет массив имен, которые шеи
разные выравнивание и длину (вы можете переписать программу так, чтобы эти имена счип
вались с клавиатуры или из файла). Функция M a i n () сначала выводит эти имена на экран
том виде, в котором они получены программой. Затем вызывается функция TrimAndPad
существенно улучшающая внешний вид выводимых программой строк:
Имена имеют разную длину
Имя 'Christa
' до о б р а б о т к и
Имя '
S a r a h ' до обработки
Имя 'Jonathan' до обработки
И м я 'Sam' д о о б р а б о т к и
1
Имя ' Schmekowitz
до о б р а б о т к и

214

Часть

III.

Объектно-основанное

программировя

Тe же и м е н а в ы р о в н е н ы и и м е ю т о д и н а к о в у ю д л и н у
имя 'Christa
' после обработки
Вия 'Sarah
' после обработки
ta 'Jonathan
' после обработки
Имя 'Sam
' после обработки
Имя 'Schmekowitz ' п о с л е о б р а б о т к и
Нажмите < E n t e r > д л я з а в е р ш е н и я п р о г р а м м ы . . .
Метод T r i m A n d P a d ()

начинает с

создания

копии

переданного

ему массива

strings. В общем случае функция, работающая с переданными ей аргументами, долж­
на вернуть новые модифицированные значения, а не изменять переданные ей.
TrimAndPad () начинается с цикла, вызывающего T r i m () для каждого элемента
массива, чтобы удалить лишние пробельные символы с обоих концов строки. Затем вы­
полняется второй цикл, в котором происходит поиск самого длинного элемента массива.
И наконец, в последнем цикле для элементов массива вызывается метод P a d R i g h t ( ) ,
удлиняющий строки, делая их равными по длине.
Метод P a d R i g h t (10) увеличивает строку так, чтобы ее длина была как минимум
10 символов.

Например,

если длина исходной строки — 6

символов,

то

метод

PadRight (10) добавит к ней справа еще 4 пробела.
Метод T r i m A n d P a d () возвращает массив выровненных строк для вывода. Функция
lain () проходит по полученному списку строк, выводя их на экран. Вот и все.

Использование функции конкатенации
Зачастую программисты сталкиваются с задачей разбивки строки или вставки некоторой подстроки в середину другой строки. Заменить один символ другим проще всего
(помощью метода R e p l a c e ():
string s = " D a n g e r N o S m o k i n g " ;
а.Replace (s, ' ' , ' ! ' )
Этот

фрагмент исходного

текста преобразует начальную

строку в

"Danger!NoSmoking".

Замена всех вхождений одного символа (в данном случае — пробела) другим
(восклицательным знаком) особенно полезна при генерации списка элементов, разделен­
ии запятыми для упрощения разбора. Однако более распространенный и сложный слу­
чай включает разбиение единой строки на подстроки, отдельную работу с каждой под­
строкой с последующим объединением их в единую модифицированную строку.
Рассмотрим, например, функцию R e m o v e S p e c i a l C h a r s ( ) , которая уда­
ляет все встречающиеся специальные символы из передаваемой ей строки.
Демонстрационная программа R e m o v e W i t h S p a c e использует функцию
R e m o v e S p e c i a l C h a r s () для удаления из строки пробельных символов
(пробелов, табуляций и символов новой строки).
// RemoveWhiteSpace
определение
функции
// RemoveSpecialChars () ,
которая
может
удалять
из
//передаваемой е й с т р о к и п р о и з в о л ь н ы й п р е д о п р е д е л е н н ы й
// набор с и м в о л о в . В д а н н о й д е м о н с т р а ц и о н н о й п р о г р а м м е
/ / функция
используется
для
удаления
из
тестовой
строки
//
пробельных
символов
namespace R e m o v e W h i t e S p a c e

всех

'та 9. Работа со строками в С#

215
*

using

System;

public

class

Program

{
public

static void Main(string[]

args)

{
// Определение множества пробельных символов
c h a r t ] c W h i t e S p a c e = {' ', ' \ n ' , ' \ t ' } ;
// Начинаем работу со строкой, в которой имеются
// пробельные символы
s t r i n g s = " t h i s is a \ n s t r i n g " ;
Console.WriteLine("До:" + s ) ;
// Выводим строку с удаленными пробельными символами
Console.WriteLine("После : " +
RemoveSpecialChars(s, cWhiteSpace))
// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
" з а в е р ш е н и я п р о г р а м м ы . . . ") ;
Console.Read();

}
// R e m o v e S p e c i a l C h a r s - у д а л я е т из строки все указанные
// символы
public static string RemoveSpecialChars(string slnput,
c h a r t ] cTargets)

{
// В sOutput будет содержаться
string sOutput = slnput;
// Начинаем поиск пробельных
for(;;)

возвращаемая

строка

символов

{
// Ищем позиции искомых символов; если таковых в
// строке больше нет — выходим из цикла
int n O f f s e t = s O u t p u t . I n d e x O f A n y ( c T a r g e t s ) ;
if ( n O f f s e t
-1)

{
break;

}
// Разбиваем строку на две части — до найденного
// с и м в о л а и п о с л е н е г о
string sBefore = sOutput.Substring(0, n O f f s e t ) ;
string sAfter
= sOutput.Substring(nOffset + 1 ) ;
// и объединяем эти части, но уже без найденного
// символа
sOutput = String.Concat(sBefore, s A f t e r ) ;

216

Часть III.

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

I

Глав

return

sOutput;

Ключевой в этой демонстрационной программе является функция R e m o v e S p e c i a l ­
Chars ( ) . Она возвращает строку, которая представляет собой исходную строку, но с
удаленными вхождениями всех символов, содержащихся в массиве c T a r g e t s . Чтобы
лучше понять эту функцию, представьте, что ей передана строка " a b , c d , е " , а массив
специальных символов содержит единственный символ ' ,

1

.

Функция R e m o v e S p e c i a l C h a r s () начинается с входа в цикл, выход из которого
произойдет только тогда, когда в строке не останется ни одной запятой. Функция 1пdexOf Any () возвращает позицию первой найденной запятой (значение -1 указывает,
то ни одна запятая не найдена).
После первого вызова I n d e x O f A n y () возвращает 2 (позиция
1

1

1

а равна 0, позиция

b — 1, а позиция ' , ' — 2). Два следующих вызова функции разбивают строку на две
подстроки в указанном месте. Вызов S u b s t r i n g (0, 2) создает подстроку, содержа­
щую два символа,

начиная

с символа в позиции 0,

т.е.

"ab".

Второй

вызов

Substring (3) создает подстроку из символов с позиции 3 исходной строки и до ее
юнца, т.е. " c d , e " (+1 в вызове позволяет пропустить найденную запятую). Затем
функция Concat () объединяет эти подстроки вместе, создавая строку " a b e d , е".
Управление выполнением передается после этого в начало цикла. Очередная итераднл находит запятую в позиции 4, так что в результате получается строка " a b e d e " . По­
скольку в ней нет ни одной запятой, возвращаемая при последнем проходе позиция равиа-1.
Демонстрационная программа сначала выводит строку, содержащую пробельные
сиволы, затем использует функцию R e m o v e S p e c i a l C h a r s () для их удаления и вы­
водит получившуюся в результате строку:
|о: this is а
Wring
После :thisisastring
Зажмите д л я з а в е р ш е н и я п р о г р а м м ы . . .

Использование ф у н к ц и и SplitQ
В программе R e m o v e W h i t e S p a c e было продемонстрировано применение
методов C o n c a t ( ) и I n d e x O f ( ) ; однако использованный способ решения
поставленной задачи не самый эффективный. Стоит только немного поду­
мать, и можно получить существенно более эффективную функцию с ис­
пользованием уже знакомой функции S p l i t О. Соответствующая про­
грамма

имеется

на

прилагаемом

компакт-диске

в

каталоге

Remove-

W h i t e S p a c e W i t h S p l i t . Вот код функции R e m o v e S p e c i a l C h a r s () из
этой программы.
// RemoveSpecialChars - у д а л я е т из с т р о к и в с е у к а з а н н ы е
// символы
r a b l i c static s t r i n g R e m o v e S p e c i a l C h a r s ( s t r i n g s l n p u t ,
char[] cTargets)

ирование

toa ft Работа со строками в С#

217

// Разбиваем входную строку с использованием указанных
// символов в качестве р а з д е л и т е л е й
string[] sSubStrings = s l n p u t . S p l i t ( c T a r g e t s ) ;
// В sOutput будет содержаться
string sOutput = "";
// Цикл по всем подстрокам
foreach(string substring in

возвращаемая

строка

sSubStrings)

{
sOutput

=

String.Concat(sOutput,

substring);

}
return

sOutput;

}
В этой версии для разбиения входной строки на множество подстрок использу
функция S p l i t ()

с удаляемыми символами в качестве символов-разделителей,

скольку разделители не включаются в подстроки, создается эффект их удаления,
логика гораздо проще и менее подвержена ошибкам при реализации.
Цикл f o r e a c h в этой версии функции собирает части строки в единое целое. В
программы остается неизменным.

Класс S t r i n g предоставляет в распоряжение программиста метод Format О]
форматирования вывода, в основном — чисел. В своей простейшей форме Forma
позволяет вставлять строки, числа, логические значения в середину форматируа
строки. Рассмотрим, например, следующий вызов:
string myString =
S t r i n g . F o r m a t ( " { 0 } у м н о ж и т ь н а {l} р а в н о { 2 } " ,

2,

3,

2*3);

Первый аргумент F o r m a t () — форматная строка (строка формата). Элементы
в ней указывают, что и-ый аргумент, следующий за форматной строкой, должен
вставлен в этой точке. {0} означает первый аргумент (в данном случае — 2), {1
второй (3) и так далее.
В приведенном фрагменте получившаяся строка присваивается переменной myStr
и имеет следующий вид:
2

умножить

на

3

равно

6

Пока не указано иное, функция F o r m a t () использует формат по умолчанию для каж
типа аргумента. Для указания формата вывода можно размещать в фигурных скобках к
номера аргумента дополнительные модификаторы, которые показаны в табл. 9.1.

218

Часть

III.

Объектно-основанное

программирови

Окончание табл. 9.1

Все эти модификаторы могут показаться слишком запутанными, но вы все­
гда можете получить информацию о них в справочной системе С#. Чтобы
увидеть модификаторы в действии, взгляните на приведенную далее демон­
страционную программу O u t p u t F o r m a t C o n t r o l s , позволяющую ввести
не только число с плавающей точкой, но и модификатор формата, который
будет использован при выводе введенного числа обратно на экран.
//OutputFormatControls - п о з в о л я е т п о л ь з о в а т е л ю п о с м о т р е т ь ,
// как влияют м о д и ф и к а т о р ы ф о р м а т и р о в а н и я на в ы в о д ч и с е л .
//Модификаторы в в о д я т с я в п р о г р а м м у т а к ж е , к а к и ч и с л а — в
// процессе р а б о т ы п р о г р а м м ы
namespace O u t p u t F o r m a t C o n t r o l s
{
using S y s t e m ;
public c l a s s P r o g r a m

{
public s t a t i c v o i d M a i n ( s t r i n g []

args)

{
// Бесконечный цикл для ввода чисел, пока пользователь
// не введет вместо числа п у с т у ю строку, что является
// с и г н а л о м к о к о н ч а н и ю р а б о т ы п р о г р а м м ы

for(;;)
{
/ / В в о д ч и с л а и в ы х о д и з ц и к л а , е с л и в в е д е н а пустая]
// строка
C o n s o l e . W r i t e L i n e ( " В в е д и т е ч и с л о с п л а в а ю щ е й точкой")
string sNumber = C o n s o l e . R e a d L i n e О ;
if ( s N u m b e r . L e n g t h == 0)

{
break;

}
double dNumber = Double.Parse(sNumber);
// Ввод модификаторов форматирования, разделенных
// пробелами
Console.WriteLine("Введите модификаторы " +
"форматирования, разделенные " + ,
"пробелами");
chart] separator = {' ' } ;
string sFormatString = Console.ReadLine();
string[] sFormats = sFormatString.Split(separator) ;
// Цикл по введенным модификаторам
f o r e a c h ( s t r i n g s in s F o r m a t s )

{
if

(s.Length

!=

0)

{
// Создание управляющего элемента форматирования!
// из введенного модификатора
s t r i n g s F o r m a t C o m m a n d = " {0 : " + s + " } " ;
// В ы в о д числа с применением созданного
// управляющего элемента форматирования
Console.Write(
"Модификатор { о } дает ", sFormatCommand);
try
{
Console.WriteLine(sFormatCommand,

dNumber);

}
catch(Exception)
{
Console.WriteLine("");

}

}

}

}

220

}

}

}

Console.WriteLine();

// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
"завершения п р о г р а м м ы . . . " ) ;
Console.Read();

Часть

III.

Объектно-основанное

программировал

Программа O u t p u t F o r m a t C o n t r o l s считывает вводимые пользователем числа
сплавающей точкой в переменную d N u m b e r до тех пор, пока не будет введена пустая
прока — это является признаком окончания ввода. Обратите внимание, что программа
не выполняет никаких тестов для проверки корректности введенного числа с плавающей
точкой. Программа считает пользователя достаточно интеллектуальным и знающим, как
выглядят числа с плавающей точкой (довольно смелое допущение!).
Затем программа считывает ряд модификаторов форматирования, разделенных про­
белами. Каждый из них далее комбинируется со строкой { 0 } в переменной s F o r m a t ­
Command. Например,

если вы ввели N4,

программа

создаст

управляющий

элемент

{0: N4}. После чего введенное пользователем число выводится на экран с применением
этого элемента:
Console.WriteLine ( s F o r m a t C o m m a n d , d N u m b e r ) ;
В рассмотренном только что случае модификатора N4 команда по сути превращается в
tasole. W r i t e L i n e (" {О :N4 }" , d N u m b e r ) ;
жа ъытлядит типичный вывод программы на экран (полужирным шрифтом вы; делен ввод пользователя):
.Введите число с п л а в а ю щ е й т о ч к о й
1234 5 . 6 7 8 9
Введите м о д и ф и к а т о р ы ф о р м а т и р о в а н и я , р а з д е л е н н ы е п р о б е л а м и
СЕ F1 N0 0000000 .00000
Кодификатор {0:С} д а е т $ 1 2 , 3 4 5 . 6 8
Кодификатор {0:Е} д а е т 1 . 2 3 4 5 6 8 Е + 0 0 4
Кодификатор { 0 : F 1 } д а е т 1 2 3 4 5 . 7
Кодификатор {0:N0} д а е т 1 2 , 3 4 6
одификатор { 0 : 0 0 0 0 0 0 0 . 0 0 0 0 0 } д а е т 0 0 1 2 3 4 5 . 6 7 8 9 0
Введите число с п л а в а ю щ е й т о ч к о й
.12345
Заедите модификаторы ф о р м а т и р о в а н и я , р а з д е л е н н ы е п р о б е л а м и

ео.о%
Кодификатор { 0 : 0 0 . 0 % } д а е т 1 2 . 3 %
Идите число с п л а в а ю щ е й т о ч к о й
Заште д л я

завершения

программы...

Будучи примененным к числу 12345.6789, модификатор N0 добавляет в нужное место
пятую (часть N) и убирает все цифры после десятичной точки (часть 0), что дает стро­
ки, 346 (последняя цифра — результат округления, а не отбрасывания).
Аналогично, будучи примененным к числу 0.12345, модификатор

0 0 . 0 % даст

12.31 Знак % приводит к умножению числа на 100 и добавлению символа % к выводииу числу. 00.0 указывает, что в выводимой строке должно быть по меньшей мере две
фы слева от десятичной точки, и только одна — справа. Если тот же модификатор
применить к числу 0.01, будет выведена строка 0 1 . 0 % .
Непонятная конструкция t r y . . . c a t c h предназначена для перехвата всех по­
тенциальных ошибок при вводе некорректных чисел. Однако об этом расска­
зывается совсем в другой главе.

[ и з 9. Работа со строками в С#

221

Часть IV

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

Объектно-ориентированное программирование — термин, вызывающий
у программистов наибольший выброс адреналина в кровь. Так, объ­
ектно-ориентированным языком программирования является С + + —
и в этом его главное отличие от старого доброго С. К объектноориентированным языкам определенно относится и Java, как и еще
добрая сотня языков, придуманных за последний десяток лет. Но что
же это такое — объектно-ориентированный! Зачем это надо? и надо
ли вообще? стоит ли использовать это в своих программах?
В этой части вы столкнетесь с возможностями С#, которые делают
его объектно-ориентированным языком программирования. Объект­
но-ориентированное

программирование — это

не

просто работа

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

Глава 10

Что такое объектно-ориентированное
программирование
> Основы объектно-ориентированного программирования
> Абстракция и классификация
> Важность объектно-ориентированного программирования

этой главе будут даны ответы на два основных вопроса — какие концепции ле­
жат в основе объектно-ориентированного программирования и чем они отлича­
ются от уже рассмотренных концепций функционального программирования.

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

Это не просто пространные рассуждения. В повседневной жизни нас постоянно
преследуют стрессы. Чтобы уменьшить их число, мы начинаем обращать внима
ние только на события определенного масштаба. В объектно-ориентированно)
программировании уровень детализации, на котором вы работаете, называете
уровнем абстракции. И объяснить этот термин можно на примере абстрагирот
ния от подробностей внутреннего устройства микроволновой печи.
К счастью, ученые-кибернетики открыли объектную ориентированность и ряд других
концепций, снижающих уровень сложности, с которым должен работать программно
Использование абстракций делает программирование более простым и уменьшает коли
чество возможных ошибок. Именно в этом направлении как минимум полстолетия да
жется прогресс в программировании— работа со все более сложными концепциями а
все меньшим количеством ошибок.
Во время приготовления блюда многие смотрят на микроволновую печь просто как
на железный ящик. И пока ею можно управлять с помощью интерфейса, ее нельзя сло­
мать, "подвесить" или, что еще хуже, превратить готовящееся блюдо в угли.

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

Приготовление "объектно-ориентированных" блюд
Применяя объектно-ориентированный подход к приготовлению блюд, я первым де­
лом определяю объекты, используемые в задаче: сыр, бобы, чипсы и микроволновая
печь. После этого я начинаю моделировать их в программе, не задумываясь над деталя
ми их применения.
При этом я работаю (и думаю) на уровне базовых объектов. Главное, думать о том, что
приготовить блюдо, не волнуясь о деталях работы микроволновой печи — над этим уже подумали ее создатели (которым абсолютно нет дела до ваших кулинарных пристрастий).
После создания и проверки всех необходимых объектов можно переключиться на
следующий уровень абстракции, т.е. мыслить на уровне процесса приготовления закуски
не отвлекаясь на отдельные куски сыра или банку бобов. При таком подходе я легко пе­
реведу рецепт моего сына на язык С#.

226

Часть

IV.

Объектно-ориентированное

программировании]

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

Зачем вообще нужна эта классификация, это объектно-ориентированное программи­
рование? Ведь оно влечет за собой массу трудностей. Тем более, что уже имеется гото­
вый механизм функций. Зачем же что-то менять?
Иногда может показаться, что легче разработать и создать микроволновую печь спе­
циально для какого-то блюда и не строить универсальный прибор на все случаи жизни.
Тогда на лицевую панель не нужно было бы помещать никаких кнопок, кроме кнопки
СТАРТ. Блюдо всегда готовилось бы одинаковое время, и можно было бы избавиться от
всех этих бесполезных кнопок типа

ЛЕНИЯ. Все,

РАЗМОРОЗКА

или

ТЕМПЕРАТУРА ПРИГОТОВ­

что требовалось бы от такой печи, — это чтобы в нее помещалась одна та­

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

Чтобы сэкономить место, можно освободиться от этой глупой концепции — "микрон]
новая печь". Для приготовления закуски хватит и внутренностей печи. Тогда в инструкции
достаточно написать примерно следующее: "Поместите полуфабрикат в ящик. Соедини
красный и черный провод. Установите на трубе излучателя напряжение в 3000 вольт. Дол
появиться негромкий гул. Постарайтесь не стоять близко к установке, если хотите иметь ц
тей". Простая и понятная инструкция!
Но такой функциональный подход создает некоторые проблемы.
Слишком сложно. Нежелательно, чтобы фрагменты микроволновой печи па
мешивались с фрагментами закуски при разработке программы. Но поскольку ц
данном подходе нельзя создавать объекты и упрощать написание, работая с и
цым из них в отдельности, приходится держать в голове все нюансы каждого of
екта одновременно.
Не гибко. Когда-нибудь потребуется замена имеющейся микроволновой печщ
печь другого типа. Это делается без проблем, если интерфейс печи можно буд
оставить старым. Без четко очерченных областей действия, а также без разделен
интерфейса и внутреннего содержимого становится крайне трудно убрать стар
объект и поставить на его место новый.
Невозможно использовать повторно. Печи предназначены для приготовлен!
разных блюд. Вряд ли кому-то захочется создавать новую печь всякий раз при а
обходимости приготовить новое блюдо. Если задача уже решена, неплохо исши
зовать ее решение и в других программах.

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

228

Часть

IV.

Объектно-ориентированное

программирован

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

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

Итак, как же С# реализует объектно-ориентированное программирование? Впрочем,
это не совсем корректный вопрос. С# является объектно-ориентированным языком про-

Глава

10.

Что такое объектно-ориентированное программирование

229

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

языке,

вы

можете

написать

на

С#

программу,

не

являющуюся

объект!

ориентированной (например, вставив весь код Word в функцию M a i n O ) . Иногда
нужно писать и такие программы, но все же главное предназначение С# — создает
объектно-ориентированных программ.
С# предоставляет программисту следующие необходимые для написания объект!
ориентированных программ возможности.
Управляемый доступ. С# управляет обращением к членам класса. Ключе»
слова С# позволяют объявить некоторые члены открытыми для всех, а другие
защищенными или закрытыми. Подробнее эти вопросы рассматриваются в гла
ве 11, "Классы".
Специализация. С# поддерживает специализацию посредством механизма, i
вестного как наследование классов. Один класс при этом наследует члены друга
класса. Например, вы можете создать класс С а г , как частный случай класса Vi
h i c l e . Подробнее эти вопросы рассматриваются в главе 12, "Наследование".
Полиморфизм. Эта возможность позволяет объекту выполнить операцию так, как
это требуется для его корректного функционирования. Например, класс Rocket
унаследованный от V e h i c l e , может реализовать операцию S t a r t совершенно
иначе, чем С а г , унаследованный от того же V e h i c l e . По крайней мере, будем
надеяться, что это справедливо хотя бы по отношению к вашему автомобилю
хотя с некоторыми автомобилями никогда ни в чем нельзя быть уверенным...|
просьг

полиморфизма

рассматриваются

в

главах 13,

"Полиморфизм", 1

"Интерфейсы и структуры".

230

Часть

IV.

Объектно-ориентированное

программировал

Глава 11

Классы
У Защита класса посредством управления доступом
> Инициализация объекта с помощью конструктора
> Определение нескольких конструкторов в одном классе
> Конструирование статических членов и членов класса

ласе должен сам отвечать за свои действия. Так же как микроволновая печь не
должна вспыхнуть, объятая пламенем, из-за неверного нажатия кнопки, так
и класс не должен скончаться (или прикончить программу) при предоставлении не­
корректных данных.
Чтобы нести ответственность за свои действия, класс должен убедиться в корректно­
сти своего начального состояния и в дальнейшем управлять им так, чтобы оно всегда ос­
тавалось корректным. С# предоставляет для этого все необходимое.

Простые классы определяют все свои члены как p u b l i c . Рассмотрим программу
BankAccount, которая поддерживает член-данные b a l a n c e для хранения информа­
ции о балансе каждого счета. Сделав этот член p u b l i c , вы допускаете любого в святая
святых банка, позволяя каждому самому указывать сумму на счету.
Неизвестно, в каком банке храните свои сбережения вы, но мой банк и близко не на­
столько открыт и всегда строго следит за моим счетом, самостоятельно регистрируя ка­
ждое снятие денег со счета и вклад на счет. В конце концов, это позволяет уберечься от
всяких недоразумений, если вас вдруг подведет память.
Управление доступом дает возможность избежать больших и малых ошибок
в работе банка. Обычно программисты, привыкшие к функциональному про­
граммированию, говорят, что достаточно лишь определить правило, согласно
которому никакие другие классы не должны обращаться к члену b a l a n c e не­
посредственно. Увы, теоретически это, может быть, и так, но на практике такой
подход никогда не работает. Да, программисты начинают работу, будучи пере­
полненными благими намерениями, которые вскоре непонятно куда исчезают
под давлением сроков сдачи проекта...

Пример программы с использованием открытых членов
В приведенной демонстрационной программе класс B a n k A c c o u n t объяв
ляет все методы как p u b l i c , в то же время члены-данные nAccountNumb e r и d B a l a n c e сделаны p r i v a t e . Эта демонстрационная программам
корректна и не будет компилироваться, так как создана исключительно в дидактических целях.
// B a n k A c c o u n t - с о з д а н и е б а н к о в с к о г о счета с и с п о л ь з о в а н и е м
/ / п е р е м е н н о й т и п а d o u b l e д л я х р а н е н и я б а л а н с а с ч е т а (она
// объявлена как private, чтобы скрыть баланс от внешнего
// мира)
// Примечание: пока в программу не будут внесены
// исправления, она не будет компилироваться, так как
// функция Main() обращается к private-члену класса
// BankAccount.
using System;
namespace

BankAccount

{
public

class

Program

{
public

static void Main(string[]

args)

{
Console.WriteLine("В текущем состоянии эта " +
"программа не к о м п и л и р у е т с я . " ) ;
// Открытие банковского счета
Console.WriteLine("Создание объекта " +
"банковского с ч е т а " ) ;
BankAccount ba = new B a n k A c c o u n t ( ) ;
ba.InitBankAccount();
// Обращение к балансу при помощи метода Deposit()
// вполне корректно; Deposit() имеет право доступа ко
// всем членам-данным
ba.Deposit(10);
// Н е п о с р е д с т в е н н о е обращение к ч л е н у - д а н н ы м вызывает
// ошибку компиляции
C o n s o l e . W r i t e L i n e ( " З д е с ь вы п о л у ч и т е " +
"ошибку к о м п и л я ц и и " ) ;
ba.dBalance += 10;
// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
" з а в е р ш е н и я п р о г р а м м ы . . . ") ;
Console.Read();

}

}

// BankAccount - определение класса,
// простейший банковский счет
public class BankAccount

232

Часть

IV.

представляющего

Объектно-ориентированное

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

private s t a t i c i n t n N e x t A c c o u n t N u m b e r =
private i n t n A c c o u n t N u m b e r ;

10 0 0 ;

// хранение б а л а н с а в в и д е о д н о й п е р е м е н н о й типа d o u b l e
private d o u b l e d B a l a n c e ;
// Init - и н и ц и а л и з а ц и я б а н к о в с к о г о с ч е т а с н у л е в ы м
// б а л а н с о м и и с п о л ь з о в а н и е м о ч е р е д н о г о г л о б а л ь н о г о
// номера
public v o i d

InitBankAccount()

{
nAccountNumber =
dBalance = 0.0;

++nNextAccountNumber;

// G e t B a l a n c e - п о л у ч е н и е т е к у щ е г о б а л а н с а
public d o u b l e G e t B a l a n c e ( )

{
return d B a l a n c e ;

// Номер с ч е т а
public int G e t A c c o u n t N u m b e r ( )
{
return n A c c o u n t N u m b e r ;

}
public v o i d S e t A c c o u n t N u m b e r ( i n t

nAccountNumber)

{
this.nAccountNumber

= nAccountNumber;

// Deposit - п о з в о л е н л ю б о й п о л о ж и т е л ь н ы й в к л а д
public v o i d D e p o s i t ( d o u b l e d A m o u n t )

{
if

(dAmount

>

0.0)

{
dBalance

+= d A m o u n t ;

// Withdraw - вы м о ж е т е с н я т ь со с ч е т а л ю б у ю с у м м у , не
// превышающую б а л а н с ; ф у н к ц и я в о з в р а щ а е т р е а л ь н о с н я т у ю
// сумму
public d o u b l e W i t h d r a w ( d o u b l e

dWithdrawal)

{
if

(dBalance

0.0)

{
dBalance

+= d A m o u n t ;

// W i t h d r a w - вы м о ж е т е с н я т ь со с ч е т а л ю б у ю с у м м у , не
// превьшающую б а л а н с ; функция возвращает реально снятую
// сумму
public d o u b l e W i t h d r a w ( d o u b l e d W i t h d r a w a l )
if

( d B a l a n c e

=

с

${o}...",

mAmount;

Balance)

{
mAmountToWithdraw

=

Balance;

}
mBalance -= mAmountToWithdraw;
return
mAmountToWithdraw;

// S a v i n g s A c c o u n t
// п р о ц е н т о в
public

class

-

банковский

SavingsAccount

:

счет

с

начислением

BankAccount

{
public

decimal

mlnterestRate;

// SavingsAccount - процентная с т а в к а у к а з ы в а е т с я
// ч и с л о от 0 до 100
public
SavingsAccount(decimal
mlnitialBalance,

как

decimal
mlnterestRate)
:
base(mlnitialBalance)

{
this.mlnterestRate

=

mlnterestRate

// A c c u m u l a t e l n t e r e s t - н а ч и с л е н и е
public
void Accumulatelnterest()

/

100;

процентов

{
mBalance

//

=

Withdraw .-

Balance

снятие

Глава 13.Полиморфизм

+

со

(Balance

счета

*

mlnterestRate),-

произвольной

суммы,

не

295

//
//

превышающей
сумму

override

имеющейся

public

decimal

на

счету;

возвращает

Withdraw(decimal

снятую

mWithdrawal)

{
Console.WriteLine("SavingsAccount.Withdraw()...");
C o n s o l e . W r i t e L i n e ( " В ы з о в функции Withdraw базового
"класса
дважды. . . ' " ) ;
//
Снятие
1.50
base.Withdraw(1.5М);
//

Снятие

return

в

пределах

оставшейся

"

+

суммы

base.Withdraw(mWithdrawal);

}
}
public

class

Program

{
public

s t a t i c

void

MakeAWithdrawal(BankAccount
ba,
decimal
mAmount)

{
ba.Withdraw(mAmount);

}
public

s t a t i c

BankAccount

void

Вывод

args)

ba;

SavingsAccount
//

Main(string[]

sa;

баланса

Console.WriteLine("MakeAWithdrawal(ba,
. . . ) " ) ;
ba = n e w B a n k A c c o u n t ( 2 0 0 M ) ;;
MakeAWithdrawal(ba,
100M);
Console.WriteLine("Баланс BankAccount равен
{0:C}",
ba.Balance);
Console.WriteLine("MakeAWithdrawal(sa,
. . . ) " ) ;
sa = new S a v i n g s A c c o u n t ( 2 0 0 M ,
12);
MakeAWithdrawal(sa,
100M);
Console.WriteLine("Баланс
SavingsAccount
равен
{0:C}",
sa.Balance);
/ / Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для
"завершения

"

+

программы...");

Console.Read();

}
}

}
Вывод программы имеет следующий вид:

MakeAWithdrawal(ba,
... )
BankAccount.Withdraw()
с
$100...
Баланс BankAccount равен
$100.00
MakeAWithdrawal(sa,
...)
SavingsAccount.Withdraw()...

296

Часть IV. Объектно-ориентированное программирование

Вызов ф у н к ц и и W i t h d r a w б а з о в о г о к л а с с а д в а ж д ы . . .
BankAccount.Withdraw()
с
$ 1 . 5 . . .
BankAccount.Withdraw()
с
$100...
Баланс
SavingsAccount равен
$98.50
Нажмите < E n t e r > д л я з а в е р ш е н и я п р о г р а м м ы . . .
Метод W i t h d r a w ( )

п о м е ч е н в б а з о в о м к л а с с е B a n k A c c o u n t к а к v i r t u a l , в т о вре­

м я как в п о д к л а с с е о н п о м е ч е н к а к o v e r r i d e . М е т о д M a k e A W i t h d r a w a l ( )
изменений,

и

вывод

при

его

вызове

различен

из-за

того,

что

остается без

разрешение

вызова

ba. W i t h d r a w ( ) о с у щ е с т в л я е т с я н а о с н о в а н и и т и п а b a в о в р е м я в ы п о л н е н и я п р о г р а м м ы .
Для полного понимания того, как это работает, желательно пошагово пройти
п р о г р а м м у в отладчике Visual Studio 2005. Д л я этого соберите п р о г р а м м у как
обычно, а затем нажимайте клавишу для пошагового ее выполнения. Это
д о с т а т о ч н о в п е ч а т л я ю щ е е з р е л и щ е , к о г д а о д и н и т о т ж е в ы з о в п р и в о д и т в раз­
ные моменты к двум разным методам.
Б у д ь т е э к о н о м н ы п р и о б ъ я в л е н и и м е т о д о в в и р т у а л ь н ы м и . В с е и м е е т с в о ю це­
ну, т а к ч т о и с п о л ь з у й т е к л ю ч е в о е с л о в о v i r t u a l т о л ь к о п р и н е о б х о д и м о с т и .

Утка — в и д п т и ц ы . Т а к ж е к а к в о р о б е й и л и к о л и б р и . Л ю б а я п т и ц а п р е д с т а в л я е т ка­
кой-то п о д в и д п т и ц . Н о о б р а т н а я с т о р о н а м е д а л и в т о м , ч т о н е т п т и ц ы , к о т о р а я б ы л а б ы
птицей в о о б щ е . С т о ч к и з р е н и я п р о г р а м м и р о в а н и я э т о о з н а ч а е т , ч т о в с е о б ъ е к т ы B i r d
являются э к з е м п л я р а м и к а к и х - т о п о д к л а с с о в B i r d , н о н е и м е е т с я н и о д н о г о э к з е м п л я р а
класса B i r d . Т а к ч т о ж е т а к о е п т и ц а ? Э т о в с е г д а к а к о й - т о к о н к р е т н ы й в и д — п и н г в и н ,
курица или, к п р и м е р у , с т р а у с .
Различные т и п ы п т и ц и м е ю т м н о ж е с т в о о б щ и х с в о й с т в ( в п р о т и в н о м случае о н и б ы н е
были птицами), но н е т д в у х т и п о в , у к о т о р ы х бы о б щ и м и б ы л и в с е с в о й с т в а . Е с л и бы т а к и е
типы были, о н и б ы л и б ы о д и н а к о в ы м и т и п а м и , н и ч е м н е о т л и ч а ю щ и м и с я д р у г о т д р у г а .

Разложение классов
Люди с и с т е м а т и з и р у ю т о б ъ е к т ы , в ы д е л я я и х о б щ и е ч е р т ы . Ч т о б ы у в и д е т ь , к а к э т о
работает,

рассмотрим

два

класса—

HighSchool

и

University,

показанные

на

рис. 13.1. З д е с ь д л я о п и с а н и я к л а с с о в и с п о л ь з о в а н У н и ф и ц и р о в а н н ы й Я з ы к М о д е л и р о ­
вания (Unified M o d e l i n g L a n g u a g e , U M L ) , г р а ф и ч е с к и й я з ы к , о п и с ы в а ю щ и й к л а с с ы и и х
взаимоотношения д р у г с д р у г о м .
П о м н и т е — м а ш и н а Я В Л Я Е Т С Я т р а н с п о р т н ы м средством, н о С О Д Е Р Ж И Т м о т о р .

t

Как в и д н о на р и с . 1 3 . 1 , у ш к о л ы и у н и в е р с и т е т а м н о г о о б щ и х с в о й с т в . И у ш к о л ы ,

и у университета и м е е т с я о т к р ы т ы й м е т о д E n r o l l ( )
(зачисления в у ч е б н о е з а в е д е н и е ) .

для добавления объекта S t u d e n t

Оба класса имеют закрытый член n u m S t u d e n t s ,

в котором х р а н и т с я ч и с л о у ч а щ и х с я . Е щ е о д н о о б щ е е с в о й с т в о — в з а и м о о т н о ш е н и я уча­
щихся и у ч е б н ы х з а в е д е н и й : в у ч е б н о м з а в е д е н и и м о ж е т б ы т ь м н о г о у ч а щ и х с я , в то в р е м я

Глава 13. Полиморфизм

297

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

Рис.

13.1.

UML-описание

классов

HighSchool

и

University

В д о п о л н е н и е к с в о й с т в а м ш к о л ы у н и в е р с и т е т с о д е р ж и т м е т о д G e t G r a n t ()

и член-

данные nAvgSAT.
Р и с . 13.1 к о р р е к т н о о т о б р а ж а е т с и т у а ц и ю , н о б о л ь ш а я ч а с т ь и н ф о р м а ц и и дублирует­
с я . У м е н ь ш и т ь д у б л и р о в а н и е м о ж н о , е с л и п о з в о л и т ь к л а с с у U n i v e r s i t y унаследовать
б о л е е п р о с т о й к л а с с H i g h S c h o o l , к а к п о к а з а н о н а р и с . 13.2.

Рис.

13.2.

HighSchool
versity,
ные

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

упрощает

но

класс

привносит

класса
Uni

-

определен­

проблемы

К л а с с H i g h S c h o o l о с т а е т с я н е и з м е н н ы м , н о к л а с с U n i v e r s i t y п р и э т о м проще
о п и с а т ь . М о ж н о с к а з а т ь , ч т о U n i v e r s i t y — э т о к л а с с H i g h S c h o o l с ч л е н о м nAvgSAT и м е т о д о м G e t G r a n t ( ) . О д н а к о т а к о е р е ш е н и е и м е е т о д н у ф у н д а м е н т а л ь н у ю про­
блему — университет вовсе не школа со специальными свойствами.
В ы м о ж е т е с к а з а т ь : " Н у и ч т о ? Г л а в н о е , ч т о н а с л е д о в а н и е р а б о т а е т и э к о н о м и т наши
у с и л и я " . Д а , к о н е ч н о , э т о т а к , н о с к а з а н н о е в ы ш е — н е п р о с т о с т и л и с т и ч е с к а я тривиаль­
н о с т ь . Т а к о е н е в е р н о е п р е д с т а в л е н и е м о ж е т в в е с т и в з а б л у ж д е н и е п р о г р а м м и с т а как
с е й ч а с , т а к и в б у д у щ е м . В о д и н п р е к р а с н ы й д е н ь е м у , н е з н а к о м о м у с в а ш и м и фокусами,
п р и д е т с я ч и т а т ь и р а з б и р а т ь с я в в а ш и х и с х о д н ы х т е к с т а х , и т а к о е н е в е р н о е представле­
ние может привести к неправильному пониманию программы.
Кроме

того,

неверное

представление

может

привести

к

реальным

проблемам.

П р е д п о л о ж и м , ч т о в ш к о л е р е ш и л и в ы б и р а т ь л у ч ш е г о у ч е н и к а , и д л я э т о г о програм-

298

Часть IV. Объектно-ориентированное программирование

мист п р о с т о д о б а в л я е т в к л а с с H i g h S c h o o l м е т о д N a m e F a v o r i t e ( ) , у к а з ы в а ю щ и й
имя т а к о г о у ч е н и к а .
И вот — п р о б л е м а . В у н и в е р с и т е т е н е н а м е р е н ы о п р е д е л я т ь л у ч ш е г о с т у д е н т а , н о
метод N a m e F a v o r i t e ( )

оказывается

унаследованным.

Это

м о ж е т п о к а з а т ь с я не­

большой п р о б л е м о й — в к о н ц е к о н ц о в , э т о т м е т о д в к л а с с е U n i v e r s i t y м о ж н о про­
сто и г н о р и р о в а т ь .
Да, о д и н л и ш н и й м е т о д н е д е л а е т п о г о д ы , н о э т о е щ е о д и н к и р п и ч в с т е н е н е п о н и м а ­
ния. П о с т е п е н н о л и ш н и е ч л е н ы - д а н н ы е и м е т о д ы н а к а п л и в а ю т с я , и н а с т у п а е т м о м е н т ,
когда в а ш к л а с с у ж е н е в с о с т о я н и и в ы н е с т и т а к о й б а г а ж . Н е с ч а с т н ы й п р о г р а м м и с т у ж е
не понимает, к а к и е м е т о д ы " р е а л ь н ы " , а к а к и е — н е т .

UML Lite
У н и ф и ц и р о в а н н ы й Я з ы к М о д е л и р о в а н и я (Unified M o d e l i n g L a n g u a g e , U M L )
п р е д с т а в л я е т с о б о й в ы р а з и т е л ь н ы й я з ы к , с п о с о б н ы й я с н о о п р е д е л я т ь взаи­
моотношения о б ъ е к т о в в п р о г р а м м е . О д н о и з д о с т о и н с т в U M L з а к л ю ч а е т с я в т о м , ч т о
вы можете не з а в и с е т ь от к о н к р е т н о г о я з ы к а п р о г р а м м и р о в а н и я .
Ниже п е р е ч и с л е н ы о с н о в н ы е с в о й с т в а U M L .
Классы п р е д с т а в л е н ы п р я м о у г о л ь н и к а м и , р а з д е л е н н ы м и п о в е р т и к а л и н а т р и час­
ти. И м я к л а с с а у к а з ы в а е т с я в в е р х н е й ч а с т и п р я м о у г о л ь н и к а .
Члены-данные класса находятся в средней части, а методы — в нижней. М о ж н о
опустить с р е д н ю ю и л и н и ж н ю ю ч а с т ь п р я м о у г о л ь н и к а , е с л и в к л а с с е н е т ч л е н о в данных и л и м е т о д о в .
Члены с о з н а к о м п л ю с ( + ) п е р е д и м е н е м я в л я ю т с я о т к р ы т ы м и , с о з н а к о м м и н у с
(-) — з а к р ы т ы м и . В U M L о т с у т с т в у е т с п е ц и а л ь н ы й з н а к д л я з а щ и щ е н н ы х ч л е н о в ,
но некоторые программисты используют для обозначения таких членов символ #.
Закрытые ч л е н ы д о с т у п н ы т о л ь к о д л я д р у г и х ч л е н о в т о г о ж е к л а с с а ; о т к р ы т ы е
члены д о с т у п н ы в с е м к л а с с а м .
Метка { a b s t r a c t } п о с л е и м е н и у к а з ы в а е т а б с т р а к т н ы й к л а с с и л и м е т о д .
На самом деле U M L использует для этого иное обозначение, но так мне кажется
проще. В е д ь м ы и м е е м д е л о с у п р о щ е н н о й в е р с и е й — U M L

Lite.

Стрелка м е ж д у двумя классами представляет о т н о ш е н и е м е ж д у н и м и . Ч и с л о над лини­
ей означает м о щ н о с т ь — сколько э л е м е н т о в м о ж е т б ы т ь с к а ж д о г о к о н ц а стрелки.
Звездочка (*) означает произвольное число. Е с л и ч и с л о о п у щ е н о , по у м о л ч а н и ю пред­
полагается значение 1. Т а к и м о б р а з о м , на рис. 13.1 видно, ч т о о д и н университет м о ж е т
иметь сколько угодно студентов — они связаны о т н о ш е н и е м один-ко-многим.
Линия с б о л ь ш о й о т к р ы т о й и л и т р е у г о л ь н о й с т р е л к о й н а к о н ц е в ы р а ж а е т о т н о ш е ­
ние Я В Л Я Е Т С Я ( н а с л е д о в а н и е ) . С т р е л к а у к а з ы в а е т в и е р а р х и и к л а с с о в н а б а з о в ы й
класс. Д р у г и е т и п ы в з а и м о о т н о ш е н и й в к л ю ч а ю т о т н о ш е н и е С О Д Е Р Ж И Т , к о т о р о е
указывается л и н и е й с з а к р а ш е н н ы м р о м б и к о м с о с т о р о н ы в л а д е л ь ц а .

'

Имеются и д р у г и е п р о б л е м ы . П р и н а с л е д о в а н и и , п о к а з а н н о м н а р и с . 13.2, к а к в и д н о
и з схемы, к л а с с ы U n i v e r s i t y и H i g h S c h o o l и м е ю т о д н у и т у ж е п р о ц е д у р у зачисле­
ния. Как бы с т р а н н о э т о ни з в у ч а л о , б у д е м с ч и т а т ь , ч т о э т о т а к и е с т ь . П р о г р а м м а разра­
ботана, у п а к о в а н а и о т п р а в л е н а п о т р е б и т е л я м .

Глава 13. Полиморфизм

299

Н е с к о л ь к и м и м е с я ц а м и п о з ж е м и н и с т е р с т в о п р о с в е щ е н и я р е ш а е т и з м е н и т ь правила
з а ч и с л е н и я в ш к о л ы , ч т о , в с в о ю о ч е р е д ь , п р и в о д и т к и з м е н е н и ю п р о ц е д у р ы зачислена
и в университеты, что, конечно же, неверно.
К а к избежать у к а з а н н о й п р о б л е м ы ? П о н я т н о , ч т о ее к о р е н ь — в о т н о ш е н и я х класса
У н и в е р с и т е т не является ш к о л о й . О т н о ш е н и е С О Д Е Р Ж И Т также не будет работать — неу­
ж е л и университет с о д е р ж и т ш к о л у или ш к о л а — университет? К о н е ч н о ж е , нет. Решение»
к л ю ч а е т с я в т о м , ч т о и ш к о л а , и университет — это с п е ц и а л ь н ы е т и п ы у ч е б н ы х заведений.
Н а р и с . 13.3 п о к а з а н о б о л е е к о р р е к т н о е р е ш е н и е . Н о в ы й к л а с с S c h o o l содержит
о б щ и е с в о й с т в а д в у х т и п о в у ч е б н ы х з а в е д е н и й , в к л ю ч а я о т н о ш е н и я с о б ъ е к т а м и stu­
d e n t . Б о л е е т о г о , к л а с с S c h o o l д а ж е и м е е т м е т о д E n r o l l ( ) , х о т я о н и абстрактны!
поскольку и U n i v e r s i t y , и H i g h S c h o o l реализуют его по-разному.

Рис.
ны

13.3.

Классы

иметь общий

University и

базовый класс

HighSchool

долж­

School

Т е п е р ь к л а с с ы U n i v e r s i t y и H i g h S c h o o l н а с л е д у ю т о б щ и й б а з о в ы й класс. Каж­
дый из

них содержит свои уникальные члены:

H i g h S c h o o l — NameFavorite!),

a U n i v e r s i t y — G e t G r a n t О . К р о м е того, о б а класса п е р е к р ы в а ю т м е т о д E n r o l l ( ) ,
о п и с ы в а ю щ и й п р а в и л а з а ч и с л е н и я у ч а щ и х с я в р а з н ы е у ч е б н ы е з а в е д е н и я . По сути, здесь
в ы д е л е н о о б щ е е п у т е м с о з д а н и я б а з о в о г о к л а с с а и з д в у х с х о ж и х к л а с с о в , к о т о р ы е после
этого стали подклассами.
Введение класса S c h o o l имеет как м и н и м у м два больших преимущества.

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

Это изолирует один класс от изменений или дополнений в другой класс. Если
п о т р е б у е т с я , н а п р и м е р , в н е с т и д о б а в л е н и я в к л а с с U n i v e r s i t y , т о его новые
методы никак не повлияют на класс H i g h S c h o o l .
Процесс выделения общих свойств из схожих классов называется
классов

(factoring).

Это

важное

свойство

разложением

объектно-ориентированных

я з ы к о в про-1

г р а м м и р о в а н и я к а к п о о п и с а н н ы м в ы ш е п р и ч и н а м , т а к и с т о ч к и з р е н и я снижения!
избыточности.

300

Часть IV. Объектно-ориентированное программировать

Р а з л о ж е н и е к о р р е к т н о т о л ь к о в т о м с л у ч а е , к о г д а о т н о ш е н и я н а с л е д о в а н и я со­
ответствуют

действительности.

Можно

выделять

общие

свойства

классов

M o u s e и J o y s t i c k , п о с к о л ь к у о б а о н и п р е д с т а в л я ю т с о б о й у к а з а т е л ь н ы е уст­
ройства, но делать то же для классов M o u s e и D i s p l a y будет ошибкой.
Разложение о б ы ч н о п р и в о д и т к н е с к о л ь к и м у р о в н я м а б с т р а к ц и и . Н а п р и м е р , про­
грамма, о х в а т ы в а ю щ а я б о л е е ш и р о к и й к р у г ш к о л , м о ж е т и м е т ь с т р у к т у р у к л а с с о в , пока­
занную на р и с . 13.4.

Puc.

13.4. Разложение классов обычно дает дополнительные уровни в иерар­

хии

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

Как в и д и т е ,
Learning

и

внесено два новых класса между U n i v e r s i t y и

LowerLevel.

Например,

новый

класс

School:

HigherLearning

Higher-

делится

на

классы C o l l e g e и U n i v e r s i t y . Т а к а я м н о г о с л о й н а я и е р а р х и я — о б ы ч н о е и д а ж е же­
лательное я в л е н и е п р и р а з л о ж е н и и , с о о т в е т с т в у ю щ е м р е а л ь н о м у м и р у .
Заметим, о д н а к о , ч т о н и к а к о й т е о р и и р а з л о ж е н и я к л а с с о в н е с у щ е с т в у е т . Т а к , разло­
жение н а р и с . 13.4 м о ж н о с ч и т а т ь в п о л н е к о р р е к т н ы м , н о е с л и п р о г р а м м а в б о л ь ш е й
степени с в я з а н а с в о п р о с а м и а д м и н и с т р и р о в а н и я у ч е б н ы х з а в е д е н и й м е с т н ы м и властя­
ми, то более е с т е с т в е н н о й б у д е т и е р а р х и я к л а с с о в , п р е д с т а в л е н н а я на р и с . 13.5.

Puc.

13.5.

Разложение классов зависит от решаемой задачи

Глава 13. Полиморфизм

301

Голая концепция, выражаемая абстрактным классом
Вернемся в

очередной раз

к классу B a n k A c c o u n t .

Б о л ь ш и н с т в о м е т о д о в э т о г о к л а с с а не в ы з ы в а ю т п р о б л е м , п о с к о л ь к у о б а типа бан­
ковских счетов о д и н а к о в о их реализуют. О д н а к о правила снятия со счета оказывают
различными, так что вы должны реализовать S a v e i n g s A c c o u n t . W i t h d r a w ()

н е так,

к а к C h e c k i n g A c c o u n t . W i t h d r a w ( ) . Н о к а к в ы п р е д п о л а г а е т е р е а л и з о в а т ь BankAc­
count

.Withdraw()?

Д а в а й т е о б р а т и м с я за п о м о щ ь ю к б а н к о в с к о м у с л у ж а щ е м у . П р е д с т а в л я е т е э т о т диалог?
— К а к о в ы правила снятия денег со счета? — с п р а ш и в а е т е вы.
— С какого счета? Депозитного или чекового?
— Со с ч е т а , — о т в е ч а е т е в ы . — П р о с т о со с ч е т а .
П о л н о е непонимание в ответ.
П р о б л е м а в т о м , ч т о з а д а н н ы й в о п р о с н е и м е е т н и к а к о г о с м ы с л а . Н е б ы в а е т такой
в е щ и , к а к " п р о с т о с ч е т " . В с е с ч е т а ( в а н а л и з и р у е м о м п р и м е р е ) я в л я ю т с я л и б о депозит­
н ы м и , л и б о ч е к о в ы м и . К о н ц е п ц и я с ч е т а п р е д с т а в л я е т с о б о й а б с т р а к ц и ю , к о т о р а я объ­
е д и н я е т о б щ и е с в о й с т в а к о н к р е т н ы х с ч е т о в . О н а о к а з ы в а е т с я н е п о л н о й , п о с к о л ь к у в ней
недостает важного свойства W i t h d r a w ()

( е с л и н е м н о г о п о р а з м ы ш л я т ь , т о найдутся

и другие отсутствующие свойства).
Концепция B a n k A c c o u n t является абстракцией.

Как использовать абстрактные классы
Абстрактные классы используются для описания абстрактных концепций.
Абстрактный класс — э т о к л а с с с о д н и м и л и н е с к о л ь к и м и а б с т р а к т н ы м и методами.
Н а в е р н о е , э т о н е с л и ш к о м п р о я с н и л о с и т у а ц и ю ? Т о г д а в о т д о п о л н и т е л ь н о е пояснение:
абстрактный м е т о д — это метод, описанный при п о м о щ и ключевого слова a b s t r a c t .
Н и ч у т ь н е л е г ч е ? Т о г д а с л е д у ю щ е е д о б ь е т в а с о к о н ч а т е л ь н о : а б с т р а к т н ы й м е т о д н е име­
ет реализации.
Теперь рассмотрим урезанную демонстрационную программу.

//

Abstractlnheritance

//

абстрактным,

//

метода

namespace

-

класс

поскольку

в

BankAccount

нем

не

является

существует

реализации

Withdraw()
Abstractlnheritance

{
using

System;

//

AbstractBaseClass

//

котором

abstract

имеется

public

-

создадим

только

class

абстрактный

единственный

метод

класс,

в

Output()

AbstractBaseClass

{
//
//
//
abs

Output - абстрактный метод,
который выводит строку,
но только в подклассах,
которые перекрывают этот
метод
tract
public
void Output(string
sOutputString);

}
//

302

SubClassl

-

первая

конкретная

реализация

класса

Часть IV. Объектно-ориентированное программирование

//
AbstractBaseClass
public c l a s s
SubClassl

:

AbstractBaseClass

{
override

public

void

Output(string

sSource)

{
string

s

=

sSource.ToUpper();

Console.WriteLine("Вызов

SubClassl.Output()

из

{о}",

s) ;
}

}
//

SubClass2

//

-

еще

одна

конкретная

реализация

класса

AbstractBaseClass

public

class

SubClass2

:

AbstractBaseClass

{
override

public

void

Output(string

sSource)

{
string

s

=

sSource.ToLower();

n rs oo gl re a. W
c l a s sC o P
mriteLine("Вызов
s) ;

SubClass2.Output()

из

{0}",

{

public

s t a t i c

void

Test(AbstractBaseClass

ba)

{
ba.Output("Test");

}
public

s t a t i c

void

Main(string []

strings)

{
// Нельзя создать
объект класса AbstractBaseClass,
// п о с к о л ь к у он — а б с т р а к т н ы й . Если вы снимете
// комментарий со следующей с т р о к и ,
то С# сгенерирует
// сообщение об ошибке компиляции
// A b s t r a c t B a s e C l a s s ba
= new A b s t r a c t B a s e C l a s s ( ) ;
/ / Т е п е р ь п о в т о р и м наш э к с п е р и м е н т с к л а с с о м S u b c l a s s l
Console.WriteLine("Создание
объекта
SubClassl");
S u b C l a s s l s c l = new S u b C l a s s l ( ) ;
Test(scl) ;
// и к л а с с о м S u b c l a s s 2
Console.WriteLine("\пСоздание
объекта
SubClass2");
SubClass2
sc2 = new S u b C l a s s 2 ( ) ;
Test(sc2) ;
// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();
В программе с н а ч а л а о п р е д е л я е т с я к л а с с A b s t r a c t B a s e C l a s s с е д и н с т в е н н ы м аб­
страктным

методом

Output ().

Поскольку

он

объявлен

как

abstract,

метод

Output ( ) н е и м е е т р е а л и з а ц и и , т.е. т е л а м е т о д а .

Глава 13. Полиморфизм

303

К л а с с A b s t r a c t B a s e C l a s s н а с л е д у ю т д в а п о д к л а с с а : S u b C l a s s l и SubClass2
Оба — конкретные классы, так как перекрывают метод O u t p u t () "настоящими" мето
дами и не содержат собственных абстрактных методов.
К л а с с м о ж е т б ы т ь о б ъ я в л е н к а к а б с т р а к т н ы й н е з а в и с и м о от н а л и ч и я в нем аб
с т р а к т н ы х м е т о д о в . О д н а к о к о н к р е т н ы м к л а с с м о ж е т б ы т ь т о г д а и только т о
гда, к о г д а в с е а б с т р а к т н ы е м е т о д ы в с е х б а з о в ы х к л а с с о в в ы ш е н е г о сокрыть
(перекрыты) реальными методами.
М е т о д ы O u t p u t ( ) д в у х р а с с м а т р и в а е м ы х п о д к л а с с о в н е м н о г о р а з л и ч н ы — один и з
н и х п р е о б р а з у е т п е р е д а в а е м у ю е м у с т р о к у в в е р х н и й р е г и с т р , д р у г о й — в нижний. Вы
вод программы демонстрирует полиморфную природу класса A b s t r a c t B a s e C l a s s .
Создание
Вызов

объекта

Создание
Вызов

SubClassl

SubClassl.Output()
объекта

из

SubClass2.Output()

Нажмите



TEST

SubClass2
для

из

t e s t

завершения

программы...

А б с т р а к т н ы й м е т о д а в т о м а т и ч е с к и я в л я е т с я в и р т у а л ь н ы м , т а к ч т о добавлять
ключевое слово v i r t u a l к ключевому слову a b s t r a c t н е требуется.

Создание абстрактных объектов невозможно
О б р а т и т е в н и м а н и е е щ е на о д н у в е щ ь в р а с с м а т р и в а е м о й д е м о н с т р а ц и о н н о й прог р а м м е : нельзя создавать объект A b s t r a c t B a s e C l a s s , н о аргумент ф у н к ц и и Test()
объявлен

как

объект

класса A b s t r a c t B a s e C l a s s

дополнение крайне важно. Объекты S u b C l a s s l

или

одного

и SubClass2

из

его

подклассов. Это

м о г у т б ы т ь переданы

в функцию, поскольку оба являются конкретными подклассами A b s t r a c t B a s e C l a s s ,
З д е с ь и с п о л ь з о в а н о о т н о ш е н и е Я В Л Я Е Т С Я . Э т о о ч е н ь м о щ н а я м е т о д и к а , позволяющая
писать высоко обобщенные методы.

Д л я с о з д а н и я н о в о й и е р а р х и и н а с л е д о в а н и я м о ж н о т а к ж е и с п о л ь з о в а т ь ключ е в о е с л о в о v i r t u a l . Р а с с м о т р и м и е р а р х и ю к л а с с о в , п о к а з а н н у ю в приве­
денной далее демонстрационной программе I n h e r i t a n c e T e s t .
//

InheritanceTest

//

v i r t u a l

namespace

для

-

пример

создания

использования

новой

иерархии

ключевого

слова

классов

InheritanceTest

{
using.

System;

public

class

Program

{
public

s t a t i c

void

Main(string[]

strings)

{
Console.WriteLine("\пПередача
BankAccount")
BankAccount ba = new B a n k A c c o u n t ( ) ;

304

Часть IV. Объектно-ориентированное программирование

Testl(ba)

;

Console.WriteLine("\пПередача
SavingsAccount");
SavingsAccount
sa = new S a v i n g s A c c o u n t ( ) ;
Testl(sa) ;
Test2(sa) ;
Console.WriteLine("\пПередача
SpecialSaleAccount");
SpecialSaleAccount
s s a = new S p e c i a l S a l e A c c o u n t ( ) ;
Testl(ssa);
Test2(ssa) ;
Test3(ssa) ;
Console.WriteLine("\пПередача
S a l e S p e c i a l C u s t o m e r s s c = new
Testl(ssc);
Test2 (ssc)
Test3(ssc) ;
Test4(ssc) ;
//

Ожидаем

подтверждения

SaleSpecialCustomer");
SaleSpecialCustomer();

пользователя

Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();

public

s t a t i c

void

Testl(BankAccount

Console.WriteLine("\tB
account.Withdraw(100)

public

s t a t i c

void

account)

Test(BankAccount)");

;

Test2(SavingsAccount

Console .WriteLine ( " \ t B

account)

Test (SavingsAccount),") ;

account.Withdraw(100);

public

s t a t i c

void

Test3(SpecialSaleAccount

Console.WriteLine("\tB

account)

Test(SpecialSaleAccount)");

account.Withdraw(100);

public

s t a t i c

void

Test4(SaleSpecialCustomer

Console.WriteLine("\tB

account)

Test(SaleSpecialCustomer)");

account.Withdraw(10 0);

// B a n k A c c o u n t

-

// присваиваемым
public

class

моделирует
при

банковский

создании,

и

счет

с

номером,

балансом

BankAccount

taa 13. Полиморфизм

305

//
//
//

W i t h d r a w a l - вы м о ж е т е с н я т ь со с ч е т а любую сумму,
превышающую б а л а н с .
Возвращает реально снятую со
счета
сумму

v i r t u a l

public

void

Withdraw(decimal

не

dWithdraw)

{
C o n s o l e . W r i t e L i n e ( " \ Ь \ Ь в ы з ы в а е т "•+
11
" B a n k A c c o u n t . W i t h d r a w ()
) ;

}
}
//
//

SavingsAccount
процентов

public

class

-

банковский

SavingsAccount

счет

:

с

начислением

BankAccount

{
override

public

void

Withdraw(decimal

mWithdrawal)

{
Console .WriteLine ("\t\tBbi3biBaeT " +
"SavingsAccount.Withdraw()");

}
}
//
//

SpecialSaleAccount
продаж

public

class

-

счет

используется

SpecialSaleAccount

:

только

для

SavingsAccount

{
new

virtual

public

void

Withdraw(decimal

mWithdrawal)

{
Console .WriteLine ("\t\TBBI3BMAET " +
"SpecialSaleAccount.Withdraw()");

}
}
//

SaleSpecialCustomer

//

покупателей

public

class

-

счет

только

SaleSpecialCustomer

:

для

специальных

SpecialSaleAccount

{
override

public

void

Withdraw(decimal

mWithdrawal)

{

}

}

}

Console . WriteLine ("\t\TBBI3BIBAET " +
"SaleSpecialCustomer.Withdraw()");

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

S p e c i a l S a l e A c c o u n t . W i t h d r a w ()

помечен

как v i r t u a l ,

что

разрывай

ц е п ь н а с л е д о в а н и я в э т о й т о ч к е . П р и р а с с м о т р е н и и с т о ч к и з р е н и я B a n k A c c o u n t класс ы S p e c i a l S a l e A c c o u n t и S a l e S p e c i a l C u s t o m e r в ы г л я д я т в т о ч н о с т и как Savi n g s A c c o u n t . И только при рассмотрении с точки зрения S p e c i a l S a l e A c c o u n t
становятся доступны новые версии W i t h d r a w ( ) .
В с е э т о п о к а з а н о в п р и в е д е н н о й д е м о н с т р а ц и о н н о й п р о г р а м м е . Ф у н к ц и я Main()
в ы з ы в а е т р я д м е т о д о в T e s t ( ) , к а ж д ы й и з к о т о р ы х р а з р а б о т а н д л я с в о е г о подкласса,]

306

Часть IV. Объектно-ориентированное программировать

Каждая и з в е р с и й м е т о д а T e s t О

в ы з ы в а е т W i t h d r a w ()

с точки зрения различного

класса объекта.
Вывод п р о г р а м м ы и м е е т с л е д у ю щ и й в и д :
Передача
BankAccount
в Test(BankAccount)
вызывает
BankAccount.Withdraw()
1ередача
SavingsAccount
в
Test(BankAccount)
вызывает
SavingsAccount.Withdraw()
в
Test(SavingsAccount)
вызывает
SavingsAccount.Withdraw()
Передача
SpecialSaleAccount

в Test(BankAccount)
вызывает SavingsAccount.Withdraw()
в

Test(SavingsAccount)
вызывает

SavingsAccount.Withdraw()

в Test(SpecialSaleAccount)
вызывает SpecialSaleAccount.Withdraw()
Передача
SaleSpecialCustomer
в Test(BankAccount)
вызывает

SavingsAccount.Withdraw()

в Test(SavingsAccount)
вызывает SavingsAccount.Withdraw()
в Test(SpecialSaleAccount)
вызывает SaleSpecialCustomer.Withdraw()
в

Test(SaleSpecialCustomer)

Нажмите

вызывает
SaleSpecialCustomer.Withdraw()
для завершения программы...

Полужирным ш р и ф т о м в ы д е л е н ы с т р о к и , п р е д с т а в л я ю щ и е о с о б ы й и н т е р е с . К л а с с ы
BankAccount и S a v i n g s A c c o u n t р а б о т а ю т в т о ч н о с т и т а к , к а к и о ж и д а л о с ь . О д н а к о
при вызове T e s t ( S a v i n g s A c c o u n t )
Customer

передаются

как

и

SpecialSaleAccount,

SavingsAccount.

В

то

tomer при п е р е д а ч е в T e s t ( S p e c i a l S a l e A c c o u n t )

же

время

и

SaleSpecial­

SaleSpecialCus­

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

пасса, т.е. ф а к т и ч е с к и с о з д а е т с я н о в а я и е р а р х и я н а с л е д о в а н и я .

Создание новой иерархии
Зачем С# п о д д е р ж и в а е т с о з д а н и е н о в о й и е р а р х и и н а с л е д о в а н и я ? Н е у ж е л и о б ы ч н о г о
полиморфизма н е д о с т а т о ч н о ?
С# формировался к а к " с е т е в о й " я з ы к в т о м с м ы с л е , ч т о к л а с с ы п р и р а б о т е програм­
мы—и даже п о д к л а с с ы — м о г у т б ы т ь р а с п р е д е л е н ы п о с е т и И н т е р н е т . Т о е с т ь про­
грамма, к о т о р у ю в ы п и ш е т е , м о ж е т н е п о с р е д с т в е н н о и с п о л ь з о в а т ь к л а с с ы и з стан­
дартных х р а н и л и щ , р а с п о л о ж е н н ы х на д р у г и х к о м п ь ю т е р а х в И н т е р н е т е .
Вы можете р а с ш и р я т ь к л а с с , з а г р у ж е н н ы й из И н т е р н е т а . П е р е к р ы т и е м е т о д о в стан­
дартной, п р о т е с т и р о в а н н о й и е р а р х и и к л а с с о в м о ж е т п р и в е с т и к н е п р е д н а м е р е н н ы м
зффектам. С о з д а н и е н о в о й и е р а р х и и к л а с с о в п о з в о л я е т в а ш и м п р о г р а м м а м пользо­
ваться всеми п р е и м у щ е с т в а м и п о л и м о р ф и з м а б е з о п а с н о с т и р а з р у ш е н и я существую­
щего кода.

Глава 13. Полиморфизм

307

Вы м о ж е т е р е ш и т ь , ч т о п о с л е д у ю щ и е п о к о л е н и я п р о г р а м м и с т о в недостойны рая
р я т ь н а п и с а н н ы й в а м и класс. З а б л о к и р о в а т ь его от в о з м о ж н ы х р а с ш и р е н и й можно nocgj
с т в о м к л ю ч е в о г о с л о в а s e a l e d — т а к о й к л а с с не с м о ж е т в ы с т у п а т ь в качестве базовой
Рассмотрим следующий фрагмент исходного текста:
using

System;

public

class

BankAccount

{
//
//
//

W i t h d r a w a l - вы м о ж е т е с н я т ь со с ч е т а любую сумму,
превышающую б а л а н с . В о з в р а щ а е т р е а л ь н о с н я т у ю с о
с ч е т а сумму

v i r t u a l

public

void

Withdraw(decimal

не

mWithdraw)

{
Console.WriteLine("Вызов

BankAccount.Withdraw()");

}
}
public

sealed

class

SavingsAccount

:

BankAccount

{
override

public

void

Withdraw(decimal

mWithdrawal)

{
C o n s o l e . W r i t e L i n e ( "Вызов

S a v i n g s A c c o u n t . W i t h d r a w ()

11

) ;

}

}
public

class

SpecialSaleAccount

:

SavingsAccount

{
override

public

void

Withdraw(decimal

mWithdrawal)

{
Console.WriteLine("Вызов " +
11
" S p e c i a l S a l e A c c o u n t . W i t h d r a w ()
) ;

}

}
П р и к о м п и л я ц и и д а н н о г о и с х о д н о г о т е к с т а в ы п о л у ч и т е с л е д у ю щ е е сообщение

ошибке:
'SpecialSaleAccount'
'SavingsAccount'

:

cannot

inherit

from

sealed

class

К л ю ч е в о е с л о в о s e a l e d д а е т в о з м о ж н о с т ь з а щ и т и т ь к л а с с о т вмешательства м е т о
д о в н е к о т о р ы х п о д к л а с с о в . Н а п р и м е р , п о з в о л я я п р о г р а м м и с т у р а с ш и р я т ь класс, реал
з у ю щ и й с и с т е м у б е з о п а с н о с т и , в ы , п о с у т и , р а з р е ш а е т е с о з д а т ь ч е р н ы й х о д , минующий
эту систему.
О п е ч а т ы в а н и е к л а с с а з а щ и щ а е т д р у г и е п р о г р а м м ы , в о з м о ж н о , н а х о д я щ и е с я где-то
И н т е р н е т е , о т п р и м е н е н и я м о д и ф и ц и р о в а н н о й в е р с и и в а ш е г о к л а с с а . Удаленная п р о
г р а м м а м о ж е т и с п о л ь з о в а т ь к л а с с т а к и м , к а к о в о н е с т ь , и л и н е п р и м е н я т ь его вообщен о н е м о ж е т н а с л е д о в а т ь е г о с т е м , ч т о б ы и с п о л ь з о в а т ь т о л ь к о е г о ч а с т ь , перекрыв п р о
чие методы.

308

Часть IV. Объектно-ориентированное программировал

Часть V
За базовыми классами

В а ш и объекты до сих пор были п р о с т ы м и в е щ а м и наподобие целых
чисел или строк, в крайнем случае — счетов B a n k A c c o u n t . Но в С#
и м е ю т с я и д р у г и е о б ъ е к т ы . И з э т о й ч а с т и в ы у з н а е т е , к а к п и с а т ь соб­
ственные объекгы типов-значений (работающие подобно типам i n t
или f l o a t ) , и познакомитесь с интерфейсами, которые позволяют
с д е л а т ь в а ш и о б ъ е к т ы б о л е е о б о б щ е н н ы м и и г и б к и м и . В м е с т е с аб­
с т р а к т н ы м и к л а с с а м и , р а с с м о т р е н н ы м и в г л а в е 13, " П о л и м о р ф и з м " ,
и н т е р ф е й с ы п р е д о с т а в л я ю т к л ю ч к п е р е д о в ы м м е т о д а м проектиро­
вания программ. Так что читайте внимательно!
Однако интерфейсы — не единственный способ сделать обобщенный
и г и б к и й к о д . Н о в ы е в о з м о ж н о с т и С # п о з в о л я ю т с о з д а в а т ь обобщен­
ные

(generic)

объекты—

например,

контейнеры,

в

которых

могут

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

Глава 14

Интерфейсы и структуры
Отношение М О Ж Е Т _ И С П О Л Ь З О В А Т Ь С Я _ К А К
Определение и н т е р ф е й с а
Использование и н т е р ф е й с а д л я в ы п о л н е н и я р а с п р о с т р а н е н н ы х о п е р а ц и й
Определение с т р у к т у р ы
Использование с т р у к т у р ы д л я о б ъ е д и н е н и я к л а с с о в , и н т е р ф е й с о в и в с т р о е н н ы х т и п о в
в одну иерархию к л а с с о в

ласе может с о д е р ж а т ь с с ы л к у на д р у г о й к л а с с . Э т о — п р о с т о е о т н о ш е н и е СО­
ДЕРЖИТ. О д и н к л а с с м о ж е т р а с ш и р я т ь д р у г о й к л а с с с п о м о щ ь ю н а с л е д о в а н и я .
Ь—отношение Я В Л Я Е Т С Я . Интерфейсы С # р е а л и з у ю т е щ е о д н о ,

не менее важное

яношение — М О Ж Е Т _ И С П О Л Ь З О В А Т Ь С Я _ К А К .

Если вы хотите н а п и с а т ь п а м я т к у , вы м о ж е т е в з я т ь р у ч к у и о б р ы в о к б у м а г и , м о ж е т е
Епользоваться о р г а н а й з е р о м и л и с д е л а т ь э т о п о с р е д с т в о м с в о е г о к о м п ь ю т е р а . В с е э т и
ккты реализуют о п е р а ц и ю " н а п и с а т ь п а м я т к у " — T a k e A N o t e . И с п о л ь з у я м а г и ю нашедования, на я з ы к е С# э т о м о ж н о р е а л и з о в а т ь с л е д у ю щ и м о б р а з о м :
::stract class ThingsThatRecord
{
abstract

public

void

TakeANote ( s t r i n g

sNote) ;

}
public class Pen : ThingsThatRecord
{
override public void TakeANote (string sNote)
{
II . . . Написание заметки ручкой . . .
}
}
public class PDA : ThingsThatRecord
override
II
)

.

public
.

.

при

void

TakeANote ( s t r i n g

помощи

органайзера

.

sNote)
.

.

"

}

public

class

Laptop

:

ThingsThatRecord

{
override

public

void

TakeANote(string

sNote)

{
II... еще каким-то образом . . .
}
}
Если

ключевое

слово

abstract

вас

смущает—

обратитесь

к

главе

" П о л и м о р ф и з м " , з а п о я с н е н и я м и . Е с л и в а м н е п о н я т н о , ч т о т а к о е наследова
н и е — п е р е ч и т а й т е г л а в у 12, " Н а с л е д о в а н и е " .
Р е ш е н и е с и с п о л ь з о в а н и е м н а с л е д о в а н и я в ы г л я д и т н е п л о х о д о т е х п о р , пока и н т е р е с
в ы з ы в а е т т о л ь к о о п е р а ц и я T a k e A N o t e ( ) . Ф у н к ц и я н а п о д о б и е п о к а з а н н о й далее R e
c o r d T a s k ( ) м о ж е т и с п о л ь з о в а т ь м е т о д T a k e A N o t e ( ) д л я т о г о , ч т о б ы записать с п и
с о к н е о б х о д и м ы х п о к у п о к в з а в и с и м о с т и от т о г о , к а к о е с р е д с т в о е с т ь у в а с п о д рукой:
void

RecordTask(ThingsThatRecord

things)

{
// Этот абстрактный метод р е а л и з о в а н во
//
которые наследуют ThingsThatRecord
things.TakeANote("Список покупок"),// .
.
. и так далее .
.
.

всех

классах,

}
Однако это решение сталкивается с двумя большими проблемами.

Первая проблема — фундаментальная. Д е л о в т о м , ч т о р е а л ь н о связать ручку
о р г а н а й з е р и к о м п ь ю т е р с о о т н о ш е н и е м Я В Л Я Е Т С Я н е в о з м о ж н о . З н а н и е того, как
р а б о т а е т р у ч к а , н е д а е т н и к а к и х с в е д е н и й о т о м , к а к з а п и с ы в а ю т информацию
компьютер или органайзер.

Вторая проблема чисто техническая. Г о р а з д о л у ч ш е о п и с а т ь L a p t o p как под
к л а с с к л а с с а C o m p u t e r . Х о т я PDA т а к ж е м о ж н о н а с л е д о в а т ь о т т о г о ж е класса
C o m p u t e r , этого нельзя сказать о классе Р е п . Вы м о ж е т е охарактеризовать pyчку
как некоторый тип M e c h a n i c a l W r i t e D e v i c e (механическое пишущее y c т р о й
с т в о ) и л и D e v i c e T h a t S t a i n s Y o u r S h i r t ( у с т р о й с т в о , п а ч к а ю щ е е ваши шта­
н ы ) . О д н а к о в С # к л а с с н е м о ж е т б ы т ь н а с л е д о в а н о т д в у х р а з н ы х классов о д н о
временно — класс С# может быть в е щ ь ю только одного сорта.
В е р н е м с я к т р е м и с х о д н ы м к л а с с а м . Е д и н с т в е н н о е о б щ е е , ч т о у н и х есть —
то,

что

все

они

могут

использоваться

для

записи

чего-либо.

Отношение МО

Ж Е Т _ И С П О Л Ь З О В А Т Ь С Я _ К А К R e c o r d a b l e п о з в о л я е т с в я з а т ь и х пригодность
некоторой цели без применения наследования.

Описание

интерфейса выглядит очень

п о х о ж и м н а о п и с а н и е к л а с с а б е з членов-

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

312

Часть V. За базовыми классам

interface IRecordable
{
void

TakeANote(string

sNote)

{

Обратите в н и м а н и е н а к л ю ч е в о е с л о в о i n t e r f a c e т а м , г д е о б ы ч н о с т о и т к л ю ч е в о е
слово c l a s s . В ф и г у р н ы х с к о б к а х и н т е р ф е й с а п р и в е д е н с п и с о к а б с т р а к т н ы х м е т о д о в .
Интерфейсы не с о д е р ж а т о п р е д е л е н и я н и к а к и х ч л е н о в - д а н н ы х .
Метод T a k e A N o t e ( )

записан без реализации. Ключевые слова p u b l i c и v i r t u a l

ни a b s t r a c t не являются необходимыми. Все методы интерфейса открыты, а сам он
не включается ни в к а к о е о б ы ч н о е н а с л е д о в а н и е — э т о и н т е р ф е й с , а не к л а с с .
Класс, к о т о р ы й реализует и н т е р ф е й с , д о л ж е н п р е д о с т а в и т ь р е а л и з а ц и ю д л я к а ж д о г о
злемента и н т е р ф е й с а . М е т о д , р е а л и з у ю щ и й м е т о д и н т е р ф е й с а , н е и с п о л ь з у е т к л ю ч е в о е
слово o v e r r i d e — э т о н е п о х о ж е н а п е р е к р ы т и е в и р т у а л ь н о й ф у н к ц и и .
По соглашению имена интерфейсов начинаются с буквы I, Кроме того, для
них, как п р а в и л о , и с п о л ь з у ю т с я п р и л а г а т е л ь н ы е ( в т о в р е м я к а к д л я и м е н клас­
сов — с у щ е с т в и т е л ь н ы е ) . К а к о б ы ч н о , э т о т о л ь к о с о г л а ш е н и е — С # с о в е р ш е н ­
н о все р а в н о , к а к и м е н н о в ы н а з о в е т е в а ш и н т е р ф е й с .
Далее п р и в е д е н о

объявление,

указывающее,

что

класс

PDA р е а л и з у е т

интерфейс

IRecordable.
public

class

PDA

:

IRecordable

{
public

void

TakeANote ( s t r i n g

sNote)

{
II

.

.

Написание

.

памятки

.

.

.

Как видите, не существует о т л и ч и й м е ж д у с и н т а к с и с о м о б ъ я в л е н и я н а с л е д о в а н и я базового класса T h i n g s T h a t R e c o r d и о б ъ я в л е н и е м о р е а л и з а ц и и и н т е р ф е й с а I R e c o r d a b l e .
В этом и з а к л ю ч а е т с я о с н о в н а я п р и ч и н а с о г л а ш е н и я об и м е н о в а н и и и н т е р ф е й ­
сов — ч т о б ы с р а з у о т л и ч а т ь их от к л а с с о в .

Вывод из всего с к а з а н н о г о — и н т е р ф е й с о п и с ы в а е т в о з м о ж н о с т и и с в о й с т в а . К р о м е
иго, он представляет с о б о й контракт. Е с л и вы с о г л а с н ы р е а л и з о в а т ь в с е м е т о д ы , опре­
деленные в и н т е р ф е й с е , вы п о л у ч и т е в с е е г о в о з м о ж н о с т и .

Класс реализует и н т е р ф е й с , п р е д о с т а в л я я о п р е д е л е н и я в с е х м е т о д о в и н т е р ф е й с а , к а к
показано в п р и в е д е н н о м д а л е е ф р а г м е н т е и с х о д н о г о т е к с т а ,
public class Pen : IRecordable
{
public

void

TakeANote ( s t r i n g

.

Запись

sNote)

{
II

.

.

ручкой

. . .

}

Глава 14. Интерфейсы и структуры

313

}

public

class

PDA

.-

ElectronicDevice,

IRecordable

{
public

void

TakeANote(string

sNote)

{
//

.

.

Использование

.

органайзера

.

.

.

}
}
public

class

Laptop

:

Computer,

IRecordable

{
public
//
{

}

void

TakeANote(string
З а п и с ь п р и помощи

sNote)
компьютера

}

К а ж д ы й из этих т р е х классов наследует с в о й б а з о в ы й класс, но реализует один и тот)
и н т е р ф е й с I R e c o r d a b l e , у к а з ь ш а ю щ и й , ч т о к а ж д ы й и з т р е х классов м о ж е т использован
для н а п и с а н и я п а м я т к и с п р и м е н е н и е м м е т о д а T a k e A N o t e ( ) . Ч т о б ы понять, почему
м о ж е т оказаться п о л е з н ы м , р а с с м о т р и м с л е д у ю щ у ю ф у н к ц и ю R e c o r d S h o p p i n g L i s t ( ]
public

class

Program

{
s t a t i c

{

}

public

void

RecordShoppingList(IRecordable
recordingObj

ect)

// Создание списка покупок
string sList
= GenerateShoppingList();
// Запись списка
recordingObject.TakeANote(sList)
;

public

s t a t i c

void

Main(string []

args)

{
PDA p d a = n e w P D A ( ) ;
RecordShoppingList(pda);

}

}
Д а н н ы й ф р а г м е н т к о д а г л а с и т , ч т о ф у н к ц и я R e c o r d S h o p p i n g L i s t ( ) может п р и
нимать в качестве аргумента л ю б о й объект, р е а л и з у ю щ и й метод T a k e A N o t e () —гово
р я ч е л о в е ч е с к и м я з ы к о м , л ю б о й о б ъ е к т , к о т о р ы й в с о с т о я н и и з а п и с а т ь памятку. Ф у н к
ц и я R e c o r d S h o p p i n g L i s t () не д е л а е т н и к а к и х п р е д п о л о ж е н и й о т о м , к а к о й в точка
сти тип имеет r e c o r d i n g O b j e c t . Тот факт, что объект в действительности имеет
PDA и л и E l e c t r o n i c D e v i c e , с о в е р ш е н н о н е в а ж е н , п о с к о л ь к у о н в состоянии з а п и
сать памятку.
Э т о ч р е з в ы ч а й н о в а ж н о е с в о й с т в о , т а к к а к о н о о б е с п е ч и в а е т ф у н к ц и и Record
S h o p p i n g L i s t ()

в ы с о к у ю с т е п е н ь о б о б щ е н н о с т и , а с л е д о в а т е л ь н о , и повторно

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

314

Часть V. За базовыми класса» Глава 1

Рассматриваемая

далее

программа

SortInterface

демонстрирует

применение

описанных с в е д е н и й на п р а к т и к е .
Для лучшего п о н и м а н и я я д о л ж е н р а з б и т ь п р о г р а м м у на н е с к о л ь к о ч а с т е й . Т а к я с м о гу более четко п р о д е м о н с т р и р о в а т ь п р и м е н е н и е о т д е л ь н ы х п р и н ц и п о в . Е с л и у м е н я во­
обще есть п р и н ц и п ы . . . :) С е й ч а с г л а в н а я ц е л ь — о б е с п е ч и т ь п о н и м а н и е т о г о , к а к р а б о тает данная п р о г р а м м а .

Создание собственного интерфейса
Интерфейс I D i s p l a y a b l e у д о в л е т в о р я е т с я л ю б ы м к л а с с о м , к о т о р ы й с о д е р ж и т м е т о д G e t S t r i n g ( ) (и, с а м о с о б о й , о б ъ я в л я е т , ч т о о н р е а л и з у е т I D i s p l a y a b l e ) . G e t String ( )

возвращает объект типа s t r i n g , который может быть выведен на экран

(использованием W r i t e L i n e ( ) :
//IDisplayable объект,
interface
IDisplayable

реализующий

метод

GetStringO

{
// Возвращает с о б с т в е н н о е
string G e t S t r i n g O ;

описание

}
Приведенный д а л е е к л а с с S t u d e n t р е а л и з у е т и н т е р ф е й с I D i s p l a y a b l e :
class S t u d e n t
private
st
private d o
/ / Методы
public
str

:

IDisplayable

ring
sName;
uble dGrade = 0.0;
доступа только для
i n g Name

чтения

{
get

{

return

sName;

}

}
public

double

Grade

{
get

{

return

dGrade;

}

}
// G e t S t r i n g - в о з в р а щ а е т
// и н ф о р м а ц и и о с т у д е н т е

строковое

public

//

{

string

GetString()

представление

implements

the

interface

string
string

sPadName = N a m e . P a d R i g h t ( 9 ) ;
11
s = S t r i n g . F o r m a t ( {0} :
{l:N0}",
sPadName, G r a d e ) ;
return
s;

[да 14. Интерфейсы и структуры

315

В ы з о в P a d R i g h t ( ) г а р а н т и р у е т , ч т о п о л е и м е н и б у д е т и м е т ь ширину н е м е н е е
9 с и м в о л о в ( с п р а в а о т и м е н и п р и н е о б х о д и м о с т и б у д е т д о б а в л е н о необходимое к о л л ч е
с т в о п р о б е л о в ) . Э т о д е л а е т в ы в о д н а э к р а н б о л е е п р и в л е к а т е л ь н ы м ( д а н н ы й вопрос
р а с с м а т р и в а л с я в г л а в е 9 , " Р а б о т а с о с т р о к а м и в С # " ) . { l : N 0 } г л а с и т : выводит ч а с
с з а п я т ы м и ( и л и т о ч к а м и — в з а в и с и м о с т и от р е г и о н а л ь н ы х н а с т р о е к ) через каждые
3 цифры. О означает — округлить дробную часть.
С и с п о л ь з о в а н и е м п р и в е д е н н о г о о б ъ я в л е н и я м о ж н о н а п и са т ь следующий ф р а г м е н т а
исходного текста (полностью программа будет приведена позже):
// D i s p l a y A r r a y вывод массива объектов,
к о т о р ы е реализуют)
// интерфейс
IDisplayable
public
s t a t i c
void DisplayArray (IDisplayable []
displayables):
int
length =
displayables.Length;
f o r ( i n t i n d e x = 0;
index < length;

{

}

index++)

IDisplayable
displayable =
displayables[index];
Console.WriteLine("{0}",
displayable.GetString()

}
П р и в е д е н н ы й м е т о д D i s p l a y A r r a y ( ) м о ж е т в ы в е с т и и н ф о р м а ц и ю о массиве л ю

б о г о т и п а , л и ш ь б ы е г о э л е м е н т ы о п р е д е л я л и м е т о д G e t S t r i n g ( ) . В о т примере в ы в о д а
описанной функции:
Homer
Marge
Bart
Lisa
Maggie

:
:
:
:
:

0
85
50
100
30

Предопределенные интерфейсы
А н а л о г и ч н о м о ж н о и с п о л ь з о в а т ь и н т е р ф е й с ы из с т а н д а р т н о й б и б л и о т е к и С#. Напр
мер, С# определяет интерфейс I C o m p a r a b l e следующим образом:
interface IComparable
{
// Сравнивает текущий объект с
// е с л и текущий о б ъ е к т больше,
// противном случае
int
CompareTo(object
о);

объектом
- 1 , если

' о ' ; возвращает
меньше, и 0 в

1,

}
Класс реализует интерфейс I C o m p a r a b l e путем реализации метода СотрагеТои
Н а п р и м е р , S t r i n g р е а л и з у е т э т о т м е т о д п у т е м с р а в н е н и я д в у х с т р о к . Е с л и строки и м
т и ч н ы , м е т о д в о з в р а щ а е т 0. Е с л и с т р о к и р а з л и ч н ы , м е т о д в о з в р а щ а е т л и б о 1, либо а
в зависимости от того, какая из строк " б о л ь ш е " .
Как ни странно,

но

отношение

сравнения

можно

задать

и

для

объектов т а

S t u d e n t -— например, по их успеваемости.
Реализация метода C o m p a r e T o ()

п р и в о д и т к т о м у , ч т о о б ъ е к т ы м о г у т быть oral

т и р о в а н ы . Е с л и о д и н с т у д е н т " б о л ь ш е " д р у г о г о , и х м о ж н о у п о р я д о ч и т ь о т "меньше!
к " б о л ь ш е м у " . Н а с а м о м д е л е в к л а с с е A r r a y у ж е р е а л и з о в а н с о о т в е т с т в у ю щ и й метод:!
Array.Sort(IComparable[]

316

objects);

Часть V. За базовыми класса*

Этот метод с о р т и р у е т м а с с и в о б ъ е к т о в , к о т о р ы е р е а л и з у ю т и н т е р ф е й с I C o m p a r a ble. Не имеет з н а ч е н и я , к к а к о м у к л а с с у в д е й с т в и т е л ь н о с т и п р и н а д л е ж а т о б ъ е к т ы —
пример, это м о г у т б ы т ь о б ъ е к т ы S t u d e n t . К л а с с A r r a y м о ж е т с о р т и р о в а т ь с л е д у ю щую версию S t u d e n t :
//Student - о п и с а н и е с т у д е н т а
// успеваемости
class S t u d e n t :
IComparable
private

double

с

использованием

имени

и

dGrade;

/ / Методы д о с т у п а
public double Grade
{
get { return dGrade; }
}

только

для

чтения

// CompareTo - с р а в н е н и е д в у х с т у д е н т о в ;
// у с п е в а е м о с т ь ю " б о л ь ш е "
public i n t
CompareTo ( o b j e c t
rightObject)

студент

с

лучшей

{
Student l e f t S t u d e n t = t h i s ;
Student r i g h t S t u d e n t =
(Student) rightObject ;
// Возвращаем 16 -1 и л и 0 в з а в и с и м о с т и от в ы п о л н е н и я
// к р и т е р и я с о р т и р о в к и
if ( r i g h t S t u d e n t . G r a d e < l e f t S t u d e n t . G r a d e )
{

return

- 1 ;

}
if

(rightStudent.Grade

>

leftStudent.Grade)

{

return

1 ;

}
return

0;

Сортировка м а с с и в а о б ъ е к т о в S t u d e n t с в о д и т с я к е д и н с т в е н н о м у в ы з о в у :
id MyFunction ( S t u d e n t [] s t u d e n t s )
/ / Сортировка м а с с и в а
Array.Sort ( s t u d e n t s ) ;

объектов

IComparable

Ваше дело — о б е с п е ч и т ь к о м п а р а т о р ; A r r a y с д е л а е т в с е о с т а л ь н о е с а м .

Сборка воедино
И вот наступил д о л г о ж д а н н ы й м о м е н т : п о л н а я п р о г р а м м а S o r t I n t e r f a c e ,
использующая описанные ранее возможности.

[Sortlnterfасе - демонстрационная программа
илюстрирует к о н ц е п ц и ю и н т е р ф е й с а
using S y s t e m ;

ш 14. Интерфейсы и структуры

Sortlnterfасе

317

namespace

Sortlnterfасе

{
//

IDisplayable

//

информацию

interface

о

-

Объект,

себе

в

который

строковом

может

представить

формате

IDisplayable

{
// G e t S t r i n g - в о з в р а т
//
об объекте
string
G e t S t r i n g O ;

строки,

представляющей

информации

}
class

Program

{
public

s t a t i c

void

Main(string []

args)

{
// Сортировка студентов по успеваемости...
Console.WriteLine("Сортировка
списка
студентов");
// Получаем несортированный список студентов
Student[]
students = Student.CreateStudentList();
// Используем
//
массива
IComparable[]

интерфейс

IComparable

для

сортировки

comparableObjects =
(IComparable[])students;
Array.Sort(comparableObjects);
// Теперь интерфейс
I D i s p l a y a b l e выводит результат
I D i s p l a y a b l e []
displayableObjects
=
(IDisplayable[])students;
DisplayArray(displayableObj
ects);
// Теперь отсортируем массив птиц по имени
с
// и с п о л ь з о в а н и е м той же процедуры ,
хотя классы
// и S t u d e n t не имеют общего б а з о в о г о к л а с с а
Console.WriteLine("\пСортировка списка птиц");
Bird[]
birds = Bird.CreateBirdList();

Bird

// Обратите внимание на отсутствие необходимости
// явного преобразования типа о б ъ е к т о в . . .
Array.Sort(birds);
DisplayArray(birds);
/ / Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения программы.. . " ) ;
Console.Read();

}
// D i s p l a y A r r a y вывод массива
// интерфейс
IDisplayable
public
s t a t i c
void

318

объектов,

реализующих

Часть V. За базовыми класса

D i s p l a y A r r a y [ I D i s p l a y a b l e [] d i s p l a y a b l e s )

{

int
length =
displayables.Length;
f o r ( i n t i n d e x = 0;
index < length;

index++)

{
IDisplayable
displayable
Console.WriteLine("{0}",

=
displayables[index];
displayable.GetString());

//
S t u d e n t s - с о р т и р о в к а по у с п е в а е м о с т и
// S t u d e n t - о п и с а н и е с т у д е н т а с и с п о л ь з о в а н и е м и м е н и
// у с п е в а е м о с т и
class

Student

:

IComparable,

и

IDisplayable

(
private
private

string
double

sName;
dGrade

=

0.0;

// Конструктор - и н и ц и а л и з а ц и я н о в о г о о б ъ е к т а
public S t u d e n t ( s t r i n g sName,
double dGrade)
this.sName = sName;
this.dGrade = dGrade;

// C r e a t e S t u d e n t L i s t - для п р о с т о т ы
// фиксированный с п и с о к с т у д е н т о в
static s t r i n g []
sNames =
{"Homer",
static

doublet]

public

static

"Marge",
dGrades

"Bart",

=

S t u d e n t []

{0,

85,

просто

создаем

"Lisa",
50,

"Maggie"};

100,

30};

C r e a t e S t u d e n t L i s t ()

{
Student[]
s A r r a y = new S t u d e n t [ s N a m e s . L e n g t h ] ;
for ( i n t i = 0; i < s N a m e s . L e n g t h ; i + + )
{
sArrayti]

=

new

Student(sNames[i],

dGrades[i]);

}
return

sArray;

/ / Методы д о с т у п а т о л ь к о
public s t r i n g N a m e
get

{

public
get

return
double

{

return

sName;

для

чтения

}

Grade
dGrade;

}

//Реализация интерфейса I C o m p a r a b l e :
// CompareTo - с р а в н е н и е д в у х о б ъ е к т о в

ю 14. Интерфейсы и структуры



нашем

случае



319

//

объектов

//

должен

public

типа

идти

int

Student)

раньше

в

и

выяснение

того,

отсортированном

CompareTo(object

что

из

них

списке

rightObject)

{
// Сравнение текущего S t u d e n t
( н а з о в е м е г о левым) и
// другого
(назовем е г о правым)
и г е н е р а ц и я ошибки,
// если эти объекты — не S t u d e n t
Student leftStudent = t h i s ;
if
(!(rightObject
is
Student))

{
Console.WriteLine("Компаратору
r e t u r n 0;

передан

не

Student")

}
Student

rightStudent
0

=

//
//

Генерируем - 1 ,
сортировки

if

(rightStudent.Grade

(Student)rightObject;

или

1

на

основании

критерия

<

leftStudent.Grade)

>

leftStudent.Grade)

{
return

- 1 ;

}
if

{

(rightStudent.Grade
return

return

1;

0,

}
// Реализация интерфейса IDisplayable:
// G e t S t r i n g - возвращает строковое представление
// информации о с т у д е н т е
public
string
GetStringО

{
string

sPadName

string

s

return

}

=

=

Name. P a d R i g n t ( У ) ;

String.Format("{o}:

{l:N0}",

sPadName,

Grade)

s;

}

//

Birds

// Bird class Bird

-

сортировка

птиц

по

именам

j u s t an a r r a y of b i r d names
:
IComparable,
IDisplayable

{
private
//

string

Конструктор

public

sName;
-

инициализация

Bird(string

объекта

Bird

sName)

{
this.sName

=

sName;

}

320

//

CreateBirdList

//

используем

-

возвращает

фиксированный

список

птиц;

для

простота]

список

Часть V. За базовым ш

static
{

s t r i n g []

sBirdNames

"Oriole",

"Hawk",

"Bluejay",
public

s t a t i c

"Finch",
Bird[]

=

"Robin",

"Cardinal",

"Sparrow"};

CreateBirdList ()

{
Bird[]
'

birds

for(int

i

=

=
0;

new

Bird[sBirdNames.Length];

i

birds.Length;

<

i++)

{
birds[i]

=

new

Bird(sBirdNames[i]);

}
return

birds;

/ / Методы д о с т у п а т о л ь к о
p u b l i c s t r i n g Name
get

{

return

sName;

для

чтения

}

// Реализация интерфейса I C o m p a r a b l e :
// CompareTo - с р а в н е н и е имен п т и ц ; и с п о л ь з у е т с я
// встроенный метод сравнения класса S t r i n g
public
int
CompareTo(object
rightObject)

{
// Сравнение текущего B i r d
(назовем
// другого
(назовем е г о правым)
Bird l e f t B i r d =
t h i s ;
Bird r i g h t B i r d =
(Bird)rightObject;
return

String.Compare(leftBird.Name,

его

левым)

и

rightBird.Name);

// Реализация интерфейса I D i s p l a y a b l e :
// G e t S t r i n g - в о з в р а щ а е т с т р о к у с и м е н е м
public
string GetString()

птицы

{
return

Name,-

Класс S t u d e n t ( п р и м е р н о в с е р е д и н е л и с т и н г а ) р е а л и з у е т и н т е р ф е й с ы I C o m p a r a ­
ble и I D i s p l a y a b l e , к а к о п и с а н о р а н е е . М е т о д C o m p a r e T o О

сравнивает студентов

юуспеваемости, ч т о п р и в о д и т к с о о т в е т с т в у ю щ е й с о р т и р о в к е и х с п и с к а . М е т о д G e t ­
String () в о з в р а щ а е т и м я и у с п е в а е м о с т ь с т у д е н т а .
Прочие м е т о д ы к л а с с а S t u d e n t в к л ю ч а ю т с в о й с т в а т о л ь к о д л я ч т е н и я N a m e и G r a d e ,
простой конструктор и м е т о д C r e a t e S t u d e n t L i s t ( ) . П о с л е д н и й м е т о д п р о с т о возвра : для i n t , s t r i n g
в Student. В программе также продемонстрировано следующее:
безопасность типов, позволяющая избежать добавления данных неверного типа;
возможность применения для коллекции L i s t < T > цикла f o r e a c h , как и для лю­
бой другой коллекции;
добавление объектов, как по одному, так и сразу целым массивом;
сортировка списка (в предположении, что элементы реализуют интерфейс ICom­
parable);
вставка нового элемента между имеющимися;
получение количества элементов в списке;
проверка, содержится ли в списке конкретный объект;
удаление элемента из списка;
копирование элементов из списка в массив.
Это только небольшой пример использования методов L i s t < T > . У других обобщен­
ии коллекций имеются свои наборы методов, однако все они схожи в применении.
Главное улучшение заключается в том, что компилятор предупреждает добав­
ление в коллекцию данных типа, отличного от того, для которого она инстанцирована.

Помимо встроенных обобщенных классов коллекций, С# позволяет написать
собственные обобщенные классы — как коллекции, так и другие типы классов.
Главное, что вы имеете возможность создать обобщенные версии классов, которые
проектированы вами.
Определение обобщенного класса переполнено записями . Когда вы инстанцируете такой класс, вы указываете тип, который заменит Т так же, как и в случае
усмотренных обобщенных коллекций. Посмотрите, насколько схожи приведенные
иже объявления:
LinkedList a L i s t = new L i n k e d L i s t < i n t > () ;
MyClass a C l a s s = n e w M y C l a s s < i n t > () ;

toa 15. Обобщенное программирование

347

Оба являются инстанцированиями классов: одно — встроенного, второе — польва
тельского. Не каждый класс имеет смысл делать обобщенным, но далее в главе будет
рассмотрен пример класса, который следует сделать именно таковым.
Классы, которые логически могут делать одни и те же вещи с данными разных
типов — наилучшие кандидаты в обобщенные классы. Наиболее типичным
примером являются коллекции, способные хранить различные данные. Если
в какой-то момент у вас появляется мысль: "А ведь мне придется написать верс
сию этого класса еще и для объектов S t u d e n t " , — вероятно, ваш класс стоит
сделать обобщенным.
Чтобы показать, как пишутся собственные обобщенные классы, будет разработан
обобщенный класс для очереди специального вида, а именно очереди с приоритетами.

Очередь с приоритетами
Представим себе почтовую контору наподобие FedEx. В нее поступает постоянный
поток пакетов, которые надо доставить получателям. Однако пакеты не равны по возможно
сти: некоторые из них следует доставить немедленно (для них уже ведутся разработка
телепортаторов), другие можно доставить авиапочтой, а третьи могут быть доставлен
наземным транспортом.
Однако в контору пакеты приходят в произвольном порядке, так что при поступления
очередного пакета его нужно поставить в очередь на доставку. Слово прозвучало — необходима очередь, но очередь необычная. Вновь прибывшие пакеты становятся в оче­
редь на доставку, но часть из них имеет более высокий приоритет и должна ставиться ес­
ли и не в самое начало очереди, то уж точно не в ее конец.
Попробуем сформулировать правила такой очереди. Итак, имеются входящие пакеты
с высоким, средним и низким приоритетом. Ниже описан порядок их обработки.
Пакеты с высоким приоритетом помещаются в начало очереди, но после дру
гих пакетов с высоким приоритетом, уже присутствующих в ней.
Пакеты со средним приоритетом ставятся в начало очереди, но после пакетов!
высоким приоритетом и других пакетов со средним приоритетом, уже присутст
вующих в ней.
Пакеты с низким приоритетом ставятся в конец очереди.
С# предоставляет встроенный обобщенный класс очереди, но он не подходит да
создания очереди с приоритетами. Таким образом, нужно написать собственный класс
очереди, но как это сделать? Распространенный подход заключается в разработке клас
са-оболочки (wrapper class) для нескольких очередей:
class

Wrapper

//

Или

PriorityQueue!

{
Q u e u e q u e u e H i g h = new Queue ( ) ;
Q u e u e q u e u e M e d i u m = new Queue ( ) ;
Queue q u e u e L o w = new Queue ( ) ;
/ / Методы д л я р а б о т ы с э т и м и о ч е р е д я м и . . .
Оболочка инкапсулирует три обычные очереди (которые могут быть обобщенными)»
управляет внесением пакетов в эти очереди и получением их из очередей. Стандартный
интерфейс класса Q u e u e , реализованного в С#, содержит два ключевых метода:

348

Часть V. За базовыми классами

E n q u e u e () — для помещения объектов в конец очереди;
D e q u e u e () — для извлечения объектов из очереди.
Оболочки представляют собой классы (или функции), которые инкапсулируют
сложную работу. Оболочки могут иметь интерфейс, существенно отличающий­
ся от интерфейса(ов) использованных в нем элементов. Однако в данном слу­
чае интерфейс оболочки совпадает с интерфейсом обычной очереди. Класс
реализует метод E n q u e u e ( ) , который получает пакет и его приоритет, и на
основании приоритета принимает решение о том, в какую из внутренних оче­
редей его поместить. Он также реализует метод D e q u e u e ( ) , который находит
пакет с наивысшим приоритетом в своих внутренних очередях и извлекает его
из очереди. Дадим рассматриваемому классу-оболочке формальное имя P r i orityQueue.
Вот исходный текст этого класса:

// P r i o r i t y Q u e u e - д е м о н с т р а ц и я и с п о л ь з о в а н и я о б ъ е к т о в
// низкоуровневой о ч е р е д и д л я р е а л и з а ц и и в ы с о к о у р о в н е в о й
// обобщенной о ч е р е д и , в к о т о р о й о б ъ е к т ы х р а н я т с я с у ч е т о м
// их п р и о р и т е т а
using S y s t e m ;
using S y s t e m . C o l l e c t i o n s . G e n e r i c ;
namespace P r i o r i t y Q u e u e

(
class

Program

{^•

//Main - заполняем очередь с приоритетами пакетами,
// затем и з в л е к а е м из очереди их случайное к о л и ч е с т в о
static void M a i n ( s t r i n g [] args)
{
Console.WriteLine("Создание очереди с приоритетами:");
P r i o r i t y Q u e u e < P a c k a g e > pq =
new P r i o r i t y Q u e u e < P a c k a g e > ( ) ;
Console.WriteLine("Добавляем случайное количество" +
" (0 - 2 0 ) с л у ч а й н ы х п а к е т о в " +
" в очередь: " ) ;
Package p a c k ;
P a c k a g e F a c t o r y f a c t = new P a c k a g e F a c t o r y ( ) ;
// Нам нужно с л у ч а й н о е ч и с л о , м е н ь ш е е 2 0
Random r a n d = n e w R a n d o m ( ) ;
// Случайное число в д и а п а з о н е 0 - 2 0
int numToCreate = r a n d . N e x t ( 2 0 ) ;
Console.WriteLine("^Создание {о} пакетов:
",
numToCreate);
f o r ( i n t i = 0; i < n u m T o C r e a t e ; i + + )

{
Console.Write("\t\treHepam^ и добавление "
"случайного пакета { о } " , i ) ;
pack = f a c t . C r e a t e P a c k a g e ( ) ;
C o n s o l e . W r i t e L i n e ( " с приоритетом { о } " ,
pack.Priority);

15.

Обобщенное

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

+

349

pq.Enqueue(pack);

}
Console.WriteLine("Что
получилось:");
int nTotal = pq.Count;
Console.WriteLine("Получено пакетов: { о } " , nTotal);
Console.WriteLine("Извлекаем случайное количество" +
" п а к е т о в : 0 - 2 0 : ") ;
i n t numToRemove = r a n d . N e x t ( 2 0 ) ;
C o n s o l e . W r i t e L i n e ( " ^ И з в л е к а е м {0} п а к е т о в " ,
numToRemove);
f o r ( i n t i = 0; i < numToRemove; i + + )
{
pack = pq.Dequeue();
if
(pack != n u l l )

{

}

}

}

Console.WriteLine("\ъ\ЬДоставка пакета "
" с приоритетом { о } " ,
pack.Priority);

+

}

// Сколько пакетов " д о с т а в л е н о "
Console.WriteLine("Доставлено
{0} п а к е т о в " ,
nTotal - pq.Count);
/ / Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
" з а в е р ш е н и я п р о г р а м м ы . . . ") ;
Console.Read();

/ / P r i o r i t y - вместо числовых приоритетов наподобие
// 3,
. . . и с п о л ь з у е м приоритеты с именами
enum P r i o r i t y // Об enum мы п о г о в о р и м п о з ж е
{
Low, M e d i u m , H i g h

1,

2,

}
// I P r i o r i t i z a b l e - определяем пользовательский интерфейс:
// к л а с с ы , к о т о р ы е м о г у т быть д о б а в л е н ы в P r i o r i t y Q u e u e ,
/ / должны р е а л и з о в ы в а т ь э т о т и н т е р ф е й с
interface
IPrioritizable
{
// Пример с в о й с т в а в и н т е р ф е й с е
Priority Priority { get;
}

}
/ / P r i o r i t y Q u e u e - обобщенный к л а с с о ч е р е д и с п р и о р и т е т а м и ;
// типы данных, д о б а в л я е м ы х в о ч е р е д ь , о б я з а н ы
// реализовывать интерфейс I P r i o r i t i z a b l e
class PriorityQueue
w h e r e T : I P r i o r i t i z a b l e // ( ) ;
p r i v a t e Queue q u e u e M e d i u m = new Q u e u e < T > ( ) ;
p r i v a t e Queue queueLow
= new Q u e u e < T > ( ) ;

Часть V. За базовыми классы

//Enqueue - Добавляет
// приоритетом
public void Enqueue(Т

T

в

очередь

в

соответствии

с

item)

{
switch
(item.Priority)
// Требует реализации
{
//
IPrioritizable
case
Priority.High:
queueHigh.Enqueue(item);
break;
case
Priority.Low:
queueLow.Enqueue(item);
break;
case
Priority.Medium:
queueMedium.Enqueue(item);
break;
default:
t h r o w new
ArgumentOutOfRangeException(
item.Priority.ToString(),
"Неверный приоритет в P r i o r i t y Q u e u e . E n q u e u e " ) ;

}

1

/ / D e q u e u e - и з в л е ч е н и е T из о ч е р е д и с наивысшим
// приоритетом
public Т Dequeue()
{
// П р о с м а т р и в а е м о ч е р е д ь с наивысшим п р и о р и т е т о м
Queue q u e u e T o p = T o p Q u e u e ( ) ;
// Очередь не пуста
if ( q u e u e T o p != n u l l && q u e u e T o p . C o u n t > 0)
return

queueTop.Dequeue();

//
//

Возвращаем
элемент

первый

}
// Если все очереди пусты, возвращаем n u l l
(здесь
/ / можно с г е н е р и р о в а т ь и с к л ю ч е н и е )
r e t u r n d e f a u l t ( Т ) ; // Что э т о — мы р а с с м о т р и м позже
//TopQueue - н е п у с т а я очередь
p r i v a t e Queue T o p Q u e u e ( )

с

наивысшим

приоритетом

{
if

( q u e u e H i g h . C o u n t > 0)
r e t u r n queueHigh;
if ( q u e u e M e d i u m . C o u n t > 0)
r e t u r n queueMedium;
if ( q u e u e L o w . C o u n t > 0)
r e t u r n queueLow;
:
- r e t u r n queueLow;
//IsEmpty public bool

Проверка,
IsEmpty()

пуста

//
//
//
//
//
//
//
ли

Очередь с высоким
приоритетом пуста?
Очередь со средним
приоритетом пуста?
Очередь с низким
приоритетом пуста?
Все о ч е р е д и пусты
очередь

{ ,
// t r u e , если все очереди пусты
r e t u r n (queueHigh.Count
==0) &

Глава

15.

Обобщенное программирование

351

(queueMedium.Count = = 0 ) &
(queueLow.Count
== 0 ) ;

}

}

/ / C o u n t - Сколько в с е г о элементов во всех очередях?
p u b l i c i n t Count // Реализуем как свойство только
{
// для ч т е н и я
get { r e t u r n queueHigh.Count
+
queueMedium.Count +
queueLow.Count; }

}
}
/ / P a c k a g e - пример к л а с с а , к о т о р ы й может
// о ч е р е д и с п р и о р и т е т а м и
c l a s s Package : I P r i o r i t i z a b l e
{
private
Priority priority;
// Конструктор
public

Package(Priority

быть

размещен

в

priority)

{
this.priority

=

priority;

}
//Priority
// чтения
public
get

-

возвращает

Priority
{

приоритет

пакета;

только

для

Priority

return priority;

}

}
//А
/ / и

также методы
другие...

ToAddress,

FromAddress,

Insurance,

-

версию на

}
//
//

К л а с с P a c k a g e F a c t o r y опущен
прилагаемом компакт-диске

см.

полную

}
Демонстрационная программа P r i o r i t y Q u e u e несколько длиннее прочих демон­
страционных программ в книге, поэтому внимательно рассмотрите каждую ее часть.

Распаковка пакета
Класс P a c k a g e преднамеренно очень прост и написан исключительно для данной
демонстрационной программы. Основное в нем — часть с приоритетом, хотя реаль
ный класс P a c k a g e , несомненно, должен содержать массу других членов. Все, что
требуется классу P a c k a g e для участия в данном пакете — это член-данные для хра­
нения приоритета, конструктор для создания пакета с определенным приоритетом и
метод (реализованный здесь как свойство только для чтения) для возврата значения
приоритета.
Требуют пояснения два аспекта класса P a c k a g e : тип приоритета и интерфейс
I P r i o r i t i z a b l e , реализуемый данным классом.

О п р е д е л е н и е возможных приоритетов
Приоритеты представляют собой перечислимый тип (enum) под названием P r i o r ­
i t y . Он выглядит следующим образом:

352

Часть V. За базовыми классами

//Priority - в м е с т о числовых п р и о р и т е т о в н а п о д о б и е
// 3, . . . и с п о л ь з у е м п р и о р и т е т ы с и м е н а м и
e n u mP r i o r i t y

1,

2,

{
Low,

Medium,

High

{

Реализация интерфейса IPrioritizable
Любой объект, поступающий в P r i o r i t y Q u e u e , должен знать собственный при­
оритет (общий принцип объектно-ориентированного программирования гласит, что кажцый объект отвечает сам за себя).
Можно просто неформально убедиться, что класс P a c k a g e имеет член для
получения его приоритета, но лучше заставить компилятор проверять это
требование, т.е. то, что у любого объекта, помещаемого в P r i o r i t y Q u e u e ,
имеется этот член.
Один из способов обеспечить это состоит в требовании, чтобы все объекты реализовывали интерфейс I P r i o r i t i z a b l e :
// I P r i o r i t i z a b l e - о п р е д е л я е м п о л ь з о в а т е л ь с к и й и н т е р ф е й с :
// классы, к о т о р ы е м о г у т быть д о б а в л е н ы в P r i o r i t y Q u e u e ,
I I должны р е а л и з о в ы в а т ь э т о т и н т е р ф е й с
interface
IPrioritizable
Priority

Priority

{

get;

}

Запись { g e t ; } определяет, как должно быть описано свойство в объявлении
интерфейса. Обратите внимание, что тело функции доступа g e t отсутствует,
но интерфейс указывает, что свойство P r i o r i t y — только для чтения и воз­
вращает значение перечислимого типа P r i o r i t y .
Класс P a c k a g e реализует интерфейс путем предоставления реализации свойства
Priority:
public
get

Priority
{

Priority

return priority;

}

Функция Main()
Перед тем как приступить к исследованию класса P r i o r i t y Q u e u e , стоит посмот­
реть, как он применяется на практике. Вот исходный текст функции M a i n ( ) :
//Main - з а п о л н я е м о ч е р е д ь с п р и о р и т е т а м и п а к е т а м и ,
// затем и з в л е к а е м из очереди их случайное к о л и ч е с т в о
static v o i d Main ( s t r i n g [] a r g s )
{
Console.WriteLine("Создание очереди с приоритетами:");
P r i o r i t y Q u e u e < P a c k a g e > pq =
new P r i o r i t y Q u e u e < P a c k a g e > ( ) ;
Console.WriteLine("Добавляем случайное количество" +

Глава 15. Обобщенное программирование

353

" (0 - 2 0 ) с л у ч а й н ы х п а к е т о в "
*

очередь:");
Package pack;
P a c k a g e F a c t o r y f a c t = new P a c k a g e F a c t o r y ( ) ;
/ / Нам н у ж н о с л у ч а й н о е ч и с л о , м е н ь ш е е 2 0
R a n d o m r a n d = n e w R a n d o m () ;
// Случайное число в диапазоне 0 - 2 0
i n t numToCreate = r a n d . N e x t ( 2 0 ) ;
C o n s o l e . W r i t e L i n e ( " ^ С о з д а н и е {0} п а к е т о в :
",
numToCreate);
f o r ( i n t i = 0; i < n u m T o C r e a t e ; i + + )

+

{
Console.Write("\t\tTeHepauHH и добавление "
"случайного пакета { о } " , i ) ;
pack = f a c t . C r e a t e P a c k a g e ( ) ;
C o n s o l e . W r i t e L i n e ( " с приоритетом { о } " ,
pack.Priority);
pq.Enqueue(pack);

+

}
C o n s o l e . W r i t e L i n e ( " Ч т о п о л у ч и л о с ь : ") ;
int nTotal = pq.Count;
Console.WriteLine("Получено пакетов: { о } " , nTotal);
C o n s o l e . W r i t e L i n e ( " И з в л е к а е м случайное количество" +
" п а к е т о в : 0-2 0 : " ) ;
i n t numToRemove = r a n d . N e x t ( 2 0 ) ;
C o n s o l e . W r i t e L i n e ( " ^ И з в л е к а е м {0} п а к е т о в " ,
numToRemove);
f o r ( i n t i = 0; i < numToRemove; i + + )
{
pack = pq.Dequeue();
if
(pack != n u l l )
{
Console.WriteL
i n e ( " \ t \ t Д о с т а в к а пакета
"с приоритетом {О}",
pack.Priority);

}
}
// Сколько пакетов " д о с т а в л е н о "
Console.WriteLine("Доставлено {о} пакетов",
nTotal - pq.Count);
/ / Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения п р о г р а м м ы . . . " ) ;
Console.Read();
Итак, что же происходит в функции M a i n () ?
1 . Инстанцируется объект P r i o r i t y Q u e u e для типа P a c k a g e .
2. Создается объект P a c k a g e F a c t o r y , работа которого состоит в формировании
новых пакетов со случайно выбранными приоритетами. ( Ф а б р и к а — это класс
или метод, который создает для вас объекты.)

354

Часть V. За базовыми классами

3. Для генерации случайного числа используется класс R a n d o m из библиотеки
.NET, а затем вызывается P a c k a g e F a c t o r y для создания соответствующего
количества новых объектов P a c k a g e со случайными приоритетами.
4. Выполняется добавление созданных пакетов в P r i o r i t y Q u e u e с помощью вы­
зова p g . E n q u e u e ( p a c k ) .
5. Выводится число созданных пакетов, после чего некоторое случайное их количе­
ство извлекается из P r i o r i t y Q u e u e .
6. Функция завершается выводом количества извлеченных из P r i o r i t y Q u e u e па­
кетов.

Написание обобщенного кода
Как же написать собственный обобщенный класс со всеми этими ? Выглядит это,
конечно, устрашающе, но все не так уж и страшно.
Простейший путь написания обобщенного класса состоит в создании сначала
его необобщенной версии, а затем расстановки в ней всех этих . Так, на­
пример, вы можете написать класс P r i o r i t y Q u e u e для объектов P a c k a g e ,
протестировать его, а затем "обобщить".
Вот небольшая часть необобщенного класса P r i o r i t y Q u e u e для иллюстрации ска­
занного:
public

class

PriorityQueue

{

//Queues - три в н у т р е н н и е (обобщенные!) о ч е р е д и
1
private Queue queueHigh
= new Q u e u e < P a c k a g e > ( ) ;
p r i v a t e Q u e u e < P a c k a g e > q u e u e M e d i u m = new Q u e u e < P a c k a g e > ( ) ;
p r i v a t e Queue queueLow
= new Q u e u e < P a c k a g e > ( ) ;
//Enqueue - на основании приоритета Package добавляем е г о
// в с о о т в е т с т в у ю щ у ю о ч е р е д ь
public void Enqueue(Package item)

{
switch(item.Priority)

//

Package

имеет

это

свойство

{
case
Priority.High:
queueHigh.Enqueue(item);
break;
case
Priority.Low:
queueLow.Enqueue(item);
break;
case Priority.Medium:
queueMedium.Enqueue(item);
break;

}
}
// и так далее . . .
Написание необобщенного класса упрощает тестирование его логики. Затем, после тести­
рования и исправления всех ошибок, вы можете сделать контекстную замену P a c k a g e на
(конечно, все не так прямолинейно, но и не очень отличается от сказанного).

355

Обобщенная очередь с приоритетами
Теперь пришло время разобраться с основным классом, из-за которого все и затева
лось — с обобщенным классом P r i o r i t y Q u e u e .

Внутренние очереди
Класс P r i o r i t y Q u e u e — оболочка, за которой скрываются три обычных объекта
Q u e u e < T > , по одному для каждого уровня приоритета. Вот первая часть исходного тек
ста P r i o r i t y Q u e u e , в которой показаны эти три внутренние очереди:
/ / P r i o r i t y Q u e u e - обобщенный к л а с с о ч е р е д и с п р и о р и т е т а м и ;
// типы данных, д о б а в л я е м ы х в о ч е р е д ь , о б я з а н ы
// реализовывать интерфейс I P r i o r i t i z a b l e
class
PriorityQueue
where T : I P r i o r i t i z a b l e

{
//Queues - три внутренние (обобщенные!) очереди
p r i v a t e Queue q u e u e H i g h
= new Q u e u e < T > ( ) ;
p r i v a t e Queue q u e u e M e d i u m = new Q u e u e < T > ( ) ;
p r i v a t e Queue queueLow
= new Q u e u e < T > ( ) ;
/ / Все о с т а л ь н о е м ы в о т - в о т р а с с м о т р и м . . .
В данных строках объявляются три закрытых члена-данных типа Q u e u e < Т > , ини­
циализируемые путем создания соответствующих объектов Q u e u e < T > .

М е т о д Enqueue()
E n q u e u e О добавляет элемент типа Т в P r i o r i t y Q u e u e . Работа состоит в том,
чтобы выяснить приоритет элемента и поместить его в соответствующую приоритету
очередь. В первой строке метод получает приоритет элемента и использует конструкцию
s w i t c h для определения целевой очереди исходя из полученного значения. -Например,
получив элемент с приоритетом P r i o r i t y . H i g h , метод E n q u e u e () помещает его
в очередь q u e u e H i g h . Вот исходный текст метода P r i o r i t y Q u e u e . E n q u e u e ( ) :
//
//

Добавляет элемент
приоритета

public

void

Enqueue(Т

Т

в

очередь

на

основании

значения

его

item)

{
switch (item.Priority)
// Требует реализации
{
//
IPrioritizable
case
Priority.High:
queueHigh.Enqueue(item);
break;
case
Priority.Low:
queueLow.Enqueue(item);
break;
case
Priority.Medium:
queueMedium.Enqueue(item);
break;
default:
t h r o w new
ArgumentOutOfRangeException(
item.Priority.ToString(),

356

Часть V. За базовыми классам

"Неверный приоритет

в

PriorityQueue.Enqueue");

Метод Dequeue()
Работа метода Dequeue () немного более хитрая. Он должен найти непустую оче­
редь элементов с наивысшим приоритетом и выбрать из нее первый элемент. Первую
часть своей работы — поиск непустой очереди элементов с наивысшим приоритетом —
Dequeue () делегирует закрытому методу T o p Q u e u e ( ) , который будет описан ниже.
Затем метод D e q u e u e () вызывает метод D e q u e u e () найденной очереди для извлече­
ния из нее объекта, который и возвращает. Вот исходный текст метода D e q u e u e ( ) :
//Dequeue - и з в л е ч е н и е
// п р и о р и т е т о м
public Т D e q u e u e ()

Т из

очереди с

наивысшим

// П р о с м а т р и в а е м о ч е р е д ь с наивысшим п р и о р и т е т о м
Queue q u e u e T o p = T o p Q u e u e ( ) ;
// Очередь не п у с т а
if

(queueTop

!= n u l l

&& q u e u e T o p . C o u n t

>

0)

{
return queueTop.Dequeue();

//
//

Возвращаем
элемент

первый

/ / Если в с е о ч е р е д и п у с т ы , в о з в р а щ а е м n u l l
// можно с г е н е р и р о в а т ь и с к л ю ч е н и е )
return d e f a u l t ( Т ) ;

(здесь

}

}
Единственная сложность состоит в том, как поступить, если все внутренние очереди
пусты, т.е. по сути пуста очередь P r i o r i t y Q u e u e в целом? Что следует вернуть в этом
случае? Представленный метод D e q u e u e () в этом случае возвращает значение n u l l .
Таким образом, клиент — код, вызывающий P r i o r i t y Q u e u e . D e q u e u e () — должен
проверять, не вернул ли метод D e q u e u e О значение n u l l . Где именно возвращается
значение n u l l ? В d e f a u l t ( Т ) , в конце исходного текста метода. О выражении d e ­
fault (Т) речь пойдет чуть позже.

Вспомогательный м е т о д TopQueue()
Метод D e q u e u e () использует вспомогательный метод T o p Q u e u e () для того,
чтобы найти непустую внутреннюю очередь с наивысшим приоритетом. Метод
TopQueue () начинает с очереди q u e u e H i g h и проверяет ее свойство C o u n t . Если оно
больше 0, очередь содержит элементы, так что метод T o p Q u e u e () возвращает ссылку
на эту внутреннюю очередь (тип возвращаемого значения метода T o p Q u e u e () —
Queue). Если же очередь q u e u e H i g h пуста, метод T o p Q u e u e () повторяет свои
действия с очередями q u e u e M e d i u m и q u e u e L o w .
Что происходит, если все внутренние очереди пусты? В этом случае метод
TopQueue () мог бы вернуть значение n u l l , но более полезным будет возврат од­
ной из пустых очередей. Когда после этого метод D e q u e u e () вызовет метод De­
queue () возвращенной очереди, тот вернет значение n u l l . Вот как выглядит ис­
ходный текст метода T o p Q u e u e ( ) :

Глава 15. Обобщенное программирование

357

/ / T o p Q u e u e - н е п у с т а я о ч е р е д ь с наивысшим п р и о р и т е т о м
p r i v a t e Queue T o p Q u e u e ( )
{
// О ч е р е д ь с в ы с о к и м
if ( q u e u e H i g h . C o u n t > 0)
// п р и о р и т е т о м п у с т а ?
r e t u r n queueHigh;
i f ( q u e u e M e d i u m . C o u n t > 0 ) // О ч е р е д ь с о с р е д н и м
// п р и о р и т е т о м п у с т а ?
r e t u r n queueMedium;
// О ч е р е д ь с н и з к и м
if ( q u e u e L o w . C o u n t > 0)
// п р и о р и т е т о м п у с т а ?
r e t u r n queueLow;
// В с е о ч е р е д и п у с т ы
r e t u r n queueLow;

}
Остальные члены PriorityQueue
Полезно знать, пуста ли очередь P r i o r i t y Q u e u e или нет, и если нет, то сколько
элементов в ней содержится (каждый объект отвечает сам за себя!). Вернитесь к листингу
демонстрационной программы и рассмотрите исходный текст метода Is E m p t y () и свой
ства C o u n t класса P r i o r i t y Q u e u e .

Незавершенные дела
P r i o r i t y Q u e u e все еще нуждается в небольшой доработке.
Сам по себе класс P r i o r i t y Q u e u e не защищен от попыток инстанцирования для типов, например, i n t , s t r i n g или S t u d e n t , т.е. типов, не
имеющих приоритетов, Вы должны наложить ограничения на класс с тем,
чтобы он мог быть инстанцирован только для типов, реализующих интер­
фейс I P r i o r i t i z a b l e . Попытки инстанцировать P r i o r i t y Q u e u e да
классов, не реализующих I P r i o r i t i z a b l e , должны приводить к ошибк
времени компиляции.
Метод D e q u e u e () класса P r i o r i t y Q u e u e возвращает значение null
вместо реального объекта. Однако обобщенные типы наподобие не
имеют естественного значения n u l l по умолчанию, как, например, int
или s t r i n g . Эта часть метода D e q u e u e () также требует обобщения.

Добавление ограничений
Класс P r i o r i t y Q u e u e должен быть способен запросить у помещаемого в очередь
объекта о его приоритете. Для этого все классы, объекты которых могут быть размеще­
ны в P r i o r i t y Q u e u e , должны реализовывать интерфейс I P r i o r i t i z a b l e , как это
делает класс P a c k a g e . Класс P a c k a g e указывает интерфейс I P r i o r i t i z a b l e в за­
головке своего объявления:
class

Package

:

IPrioritizable

после чего реализует свойство P r i o r i t y интерфейса I P r i o r i t i z a b l e .
Компилятор в любом случае сообщит об ошибке, если один из методов обобщенно­
го класса вызовет метод, отсутствующий у типа, для которого инстанцируется
обобщенный класс. Однако лучше использовать явные ограничения. Поскольку вы
можете инстанцировать обобщенный класс буквально для любого типа, должен
быть способ указать компилятору, какие типы допустимы, а какие — нет.

358

Часть V. За базовыми классами

Вы добавляете ограничение путем указания интерфейса I P r i o r i t i z a b l e
в заголовке P r i o r i t y Q u e u e :
c l a s s P r i o r i t y Q u e u e < T > where T :

IPrioritizable

Обратите внимание на выделенную полужирным шрифтом конструкцию, начинаю­
щуюся со слова w h e r e . Это принудителъ (enforcer), который указывает, что тип Т обя­
зан реализовывать интерфейс I P r i o r i t i z a b l e , т.е. как бы говорит компилятору:
"Убедись, подставляя конкретный тип вместо Т, что он реализует интерфейс I P r i o r i ­
t i z a b l e , а иначе просто сообщи об ошибке".
Вы указываете ограничения, перечисляя в конструкции w h e r e одно или не­
сколько имен:
имя базового класса, от которого должен быть порожден класс Т (или
должен быть этим классом);
имя интерфейса, который должен быть реализован классом Т, как было
показано в предыдущем примере.
Дополнительные варианты ограничений включают ключевые слова s t r u c t , c l a s s
inew(). С new () вы встретитесь чуть позже в этой главе, а об ограничениях s t r u c t
и class можно прочесть в разделе "Generics, constraints" справочной системы.
Эти варианты ограничений повышают гибкость в описании поведения обобщенного
иасса. Вот пример гипотетического обобщенного класса, объявленного с несколькими
ограничениями на Т:
class MyClass

}

:

where

Т:

class,

IPrioritizable,

new ()

Здесь тип T должен быть классом, а не типом-значением; он должен реализовать
интерфейс I P r i o r i t i z a b l e и содержать конструктор без параметров. Достаточно

А если у вас есть два обобщенных параметра и оба должны иметь ограниче­
ния? (Да, да — вы можете использовать несколько обобщенных параметров
одновременно!) Вот как можно записать две конструкции w h e r e :
c l a s s MyClass

: where T:

IPrioritizable,

where U:

new()

Определение значения null д л я т и п а T
Как уже упоминалось ранее, у каждого типа есть свое значение по умолчанию, озна­
чающее "ничто" для данного типа. Для i n t и других типов чисел это 0 (или 0.0). Для
s t r i n g — пустая строка " " . Для b o o l это f a l s e , а для всех ссылочных типов, таких
как Package, это n u l l .
Однако поскольку обобщенный класс наподобие P r i o r i t y Q u e u e может быть инпанцирован практически для любого типа данных, С# не в состоянии предсказать, каким
должно быть правильное значение n u l l в исходном тексте обобщенного класса. Напри­
мер, в методе D e q u e u e () класса P r i o r i t y Q u e u e вы можете оказаться именно в такой
ситуации: вы вызываете D e q u e u e ( ) , но очередь пуста и пакетов нет. Что вы должны вер­
нуть, что бы могло означать "ничего"? Поскольку P a c k a g e — класс, следует вернуть зна­
чение n u l l .

Это сообщит вызывающей функции, что ничего вернуть не удалось

(вызывающая функция, само собой, должна проверять, не вернулось ли значение n u l l ) .

Глава 15. Обобщенное программирование

359

Компилятор не может придать смысл ключевому слову null в исходном тек
сте обобщенного класса, поскольку обобщенный класс может быть инстанци
рован для любых типов данных. Вот почему в исходном тексте метода De­
q u e u e () используется следующая конструкция:
return

default(Т);

//

Значение

null

для

типа

Т

Эта строка указывает компилятору, что нужно посмотреть, что собой представляет
тип Т и вернуть верное значение n u l l для этого типа. В случае P a c k a g e , который в ка
честве класса представляет собой ссылочный тип, верным возвращаемым значением у
дет n u l l . Однако для некоторых других Т это значение может быть иным, и компилятор
сможет верно определить, что именно следует вернуть.
Если вы думаете, что обобщенный класс P r i o r i t y Q u e u e достаточно ги
бок, то на прилагаемом компакт-диске вы можете найти еще более гибкую
версию обобщенного класса P r i o r i t y Q u e u e и познакомиться с некоторыми принципами объектно-ориентированного дизайна, обратившись к де-|
монстрационной программе P r o g r a m m i n g T o A n l n t e r f а с е .

Часто методы в обобщенных классах также должны быть обобщенными. Вы уже ви­
дели это в примере в предыдущем разделе. Метод D e q u e u e () в классе Priori­
t y Q u e u e имеет возвращаемый тип Т. В этом разделе рассказывается, как можно ис­
пользовать обобщенные методы как в обобщенных, так и в необобщенных классах.
Обобщенными могут быть даже методы в обычных необобщенных классах. Напри­
мер, приведенный далее код показывает обобщенный метод Swap ( ) , разработанный дм
обмена двух своих аргументов. Первый аргумент получает значение второго аргумента
и наоборот (такой обмен проиллюстрирован на рис. 6.2 в главе 6, "Объединение дан­
н ы х — классы и массивы"). Чтобы увидеть, что он работает, объявите два параметра
Swap () с применением ключевого слова r e f , чтобы аргументы типов-значений также
можно было передавать по ссылке, и посмотрите на результат работы метода (об исполь­
зовании ключевого слова r e f рассказывается в главе 7, "Функции функций").
Перед вами исходный текст демонстрационной программы, в которой объ­
является и используется метод Swap ( ) .

// G e n e r i c M e t h o d - м е т о д ,
// разных типов
using System;
namespace GenericMethod

который может

работать

с данными

{
class

Program

{
// Main - п р о в е р к а двух в е р с и й обобщенного м е т о д а ; один
// р а б о т а е т с к л а с с о м на том же у р о в н е , ч т о и функция
// M a i n ( ) ; в т о р о й н а х о д и т с я в обобщенном к л а с с е
s t a t i c void Main(string[] args)
{

360

Часть V. За базовыми классам»

// Проверка обобщенного метода из необобщенного класса
C o n s o l e . W r i t e L i n e ( " О б о б щ е н н ы й м е т о д из " +
"необобщенного к л а с с а : \ п " ) ;
C o n s o l e . W r i t e L i n e ( " \ t n p o B e p K a для аргументов типа i n t " ) ;
i n t nOne = 1;
i n t nTwo = 2 ;
C o n s o l e . W r i t e L i n e ( " \ t \ t n e p e f l : n O n e = { 0 } , nTwo = {1} " ,
nOne, nTwo);
// Инстанцирование для i n t

Swap(ref nOne,

ref n T w o ) ;

C o n s o l e . W r i t e L i n e ( " \ ь \ Ъ П о с л е : n O n e = { 0 } , nTwo = { l } " ,
. nOne, nTwo);
Console . W r i t e L i n e ( " \ t П p o в e p K a для аргументов " +
"типа s t r i n g " ) ;
s t r i n g sOne = " o n e " ;
s t r i n g sTwo = " t w o " ;
C o n s o l e . W r i t e L i n e ( " \ t \ t f l o : s O n e = { o } , sTwo = { l } " ,
sOne,
sTwo);
// Инстанцирование для s t r i n g

Swap(ref sOne,

ref s T w o ) ;

C o n s o l e . W r i t e L i n e ( " \ t \ t r i o a n e : s O n e = { o } , sTwo =
sOne,
sTwo);
Console.WriteLine("\пОбобщенный метод в " +
"обобщенном к л а с с е " ) ;
Console . W r i t e L i n e ( " ^ П р о в е р к а для аргументов типа
Console.WriteLine("\t GenericClass.Swap с " +
"аргументами типа i n t " ) ;
n O n e = 1;
nTwo = 2;

{l}",

int");

GenericClass intClass = new GenericClass();
C o n s o l e . W r i t e L i n e ( " \ t \ t f l o : nOne
nOne, nTwo);

intClass.Swap(ref nOne,

=

{o},

nTwo =

{1}",

ref n T w o ) ;

C o n s o l e . W r i t e L i n e ( " \ t \ t n o ^ e : n O n e = { o } , nTwo =
nOne, nTwo);
C o n s o l e . W r i t e L i n e ( " \ t n p o B e p K a для аргументов " +
"типа s t r i n g " ) ;
Console.WriteLine("\t GenericClass.Swap с " +
"аргументами типа s t r i n g " ) ;
sOne = " o n e " ;
sTwo = " t w o " ;

{l}",

GenericClass strClass =
new GenericClass();
C o n s o l e . W r i t e L i n e ( " Д о : sOne = { o } ,
sOne,
sTwo);

strClass.Swap(ref sOne,

sTwo

=

{l}",

ref sTwo);

C o n s o l e . W r i t e L i n e ( " П о с л е : s O n e = { o } , sTwo =
sOne,
sTwo);
/ / Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
C o n s o l e . W r i t e L i n e ( " Н а ж м и т е для " +
"завершения п р о г р а м м ы . . . " ) ;
Console.Read();

(лава

15.

Обобщенное программирование

{l}",

361

} // e n d Main
/ / s t a t i c Swap - о б о б щ е н н ы й м е т о д в н е о б о б щ е н н о м
p u b l i c s t a t i c v o i d Swap(ref T l e f t s i d e ,
ref T rightSide)

классе

{
T temp;
temp = l e f t s i d e ;
leftside = rightSide;
r i g h t S i d e = temp;

}

} // e n d P r o g r a m
/ / G e n e r i c C l a s s - обобщенный
/ / Swap
class

класс

с

собственным методом

GenericClass

{
//Swap '- метод обобщенный, п о с к о л ь к у принимает параметры
/ / т и п а Т ; о б р а т и т е в н и м а н и е , ч т о м ы н е можем
/ / и с п о л ь з о в а т ь Swap, и н а ч е получим п р е д у п р е ж д е н и е
// компилятора о дублировании параметра к л а с с а
p u b l i c void Swap(ref T l e f t s i d e ,
ref T rightSide)

{
T temp;
temp = l e f t s i d e ;
l e f t s i d e = rightSide,r i g h t S i d e = temp;

Первая версия Swap () в предыдущем примере (всего в нем рассмотрены две вер­
сии) — статическая функция класса P r o g r a m , объявленная следующим образом:
public

static

void

Swap(ref

T

leftside,

ref

T

rightSide)

Объявление обобщенного метода схоже с объявлением обобщенного к л а с с а — за
именем метода следуют обобщенные параметры наподобие . После этого можно ис­
пользовать Т в методе в качестве произвольного типа, включая параметры метода и воз­
вращаемый тип.
В примере функция M a i n () дважды вызывает статическую функцию Swap ( ) , спер­
ва инстанцируя ее для i n t , а затем для s t r i n g (в листинге эти места выделены полу­
жирным шрифтом). Вот как выглядят вызовы этих методов (все верно, инстанцирование
выполняется при вызове метода):
Swap(ref
nOne,
r e f nTwo);
// Инстанцирование для i nt
Swap(ref
ref

362

sOne,
sTwo);

//

Инстанцирование

для

string

Часть V. За базовыми классами

При инстанцировании Swap () для i n t вы можете использовать i n t в качестве типа
^гументов в вызове. Аналогично, при инстанцировании Swap () для s t r i n g можно
применять аргументы типа s t r i n g . .
Обобщенный метод Swap () в обобщенном классе несколько отличается от описан­
ного метода. Эти отличия рассматриваются в следующем подразделе.

Обобщенные методы в обобщенных классах
В предыдущем примере имеется обобщенный класс G e n e r i c C l a s s , в котором со­
держится обобщенный метод Swap ( ) , объявленный следующим образом (здесь показан
и заголовок класса, чтобы было понятно, откуда берется тип Т):
class G e n e r i c C l a s s < T >

public

void

//
//

Swap(ref

T

П а р а м е т р и с п о л ь з у е т с я
функции Swap()
leftside,

ref

T

rightSide)

ниже

в

...

Основное отличие между методом Swap () и его статическим аналогом в классе
Program заключается в том, что обобщенную параметризацию предоставляет класс
G e n e r i c C l a s s , так что метод Swap () в ней не нуждается (и более того, не может ее
использовать). В этой версии Swap () вы не найдете выражения , а сам Т применя­
ется только в самом методе Swap ( ) .
Кроме этого отличия, версии Swap () практически идентичны. Вот несколько вызометода Swap () в функции M a i n ( ) :
GenericClass i n t C l a s s =
new G e n e r i c C l a s s < i n t > ( ) ;

//
//

Создание
для i n t

//

Вызов

GenericClass s t r C l a s s =
new G e n e r i c C l a s s < s t r i n g > ( ) ;

//
//

Создание объекта
для s t r i n g

S t r C l a s s . Swap ( r e f

//

Вызов

i n t C l a s s . Swap ( r e f

nOne,

sOne,

ref

ref

nTwo) ;

sTwo) ;

его

его

объекта

SwapO

SwapO

В данном случае для типа i n t или s t r i n g инстанцируется сам класс. Тип Т
может использоваться в методе S w a p () точно так же, как и в его версии в необоб­
щенном классе.

Ограничения д л я обобщенного метода
Вам могут понадобиться ограничения для обобщенного метода, с тем чтобы он мог
принимать только определенные виды типов, отвечающих некоторым требованиям —
^какэто было сделано для класса P r i o r i t y Q u e u e ранее в настоящей главе. В этом слу­
чае вы должны объявить метод примерно таким образом:
static void Sort(T[]
tArray) where T:
IComparable
( ... }
Например, если методу необходимо сравнение параметров типа Т, то лучше потребо­
вать от Т реализации интерфейса I C o m p a r a b l e и указать это ограничение в описании
обобщенного метода.

Шва

15.

Обобщенное

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

363

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

Обобщенные и необобщенные интерфейсы
Давайте сравним обобщенные и необобщенные интерфейсы.
// Необобщенный
interface IDisplayable

/ / Обобщенный
interface
ICertifiable

{

{
void Display();

}

void

Certify(T

criteria);

}

Вот как выглядит шаблон использования интерфейса. Сначала его надо объявить, и
показано в приведенном фрагменте. Затем он должен быть реализован в вашем исходно»
тексте некоторым классом следующим образом:
// Необобщенный
c l a s s MyClass :
/ / Обобщенный
c l a s s MyClass

:

IDisplayable

...

ICertifiable

...

После этого следует завершить реализацию интерфейса в вашем классе:
//

Необобщенный

c l a s s MyClass

:

IDisplayable

{
public

void

Display()

{
/ / Обобщенный
c l a s s MyClass
:

ICertifiable

//

Инстанцирование

{
public

void

Certify(MyCriteria

criteria)

{
Обратите внимание, что когда вы реализуете обобщенный интерфейс в классе,]
вы инстанцируете его обобщенную часть с использованием имени реального]
типа, такого как M y C r i t e r i a .
Теперь вы можете видеть, почему интерфейс является обобщенным: он требует заме­
ны , используемого в качестве типа параметра или возвращаемого типа в одном или
нескольких методах интерфейса. Другими словами, как и в случае обобщенного класса,
вы указываете замещаемый тип для применения в обобщенных методах. Возможно, эти
методы нужны для работы с различными типами данных, как и в случае класса коллек­
ции L i s t < T > или метода Swap ( Т i t e m l , Т i t e m 2 ) .

Часть V. За базовыми классами

Обобщенные интерфейсы — новинка, и я еще не рассматривал здесь все способы их
_ использования. Основное их применение — в обобщенных коллекциях. Использование
I обобщенной версии распространенного интерфейса С#, такого как I C o m p a r a b l e < T > ,
поможет избежать упаковки/распаковки типов-значений. Обобщенные интерфейсы мо­
гут также помочь реализовать такие вещи, как функции сортировки для применения
коллекциями. (О том, что такое упаковка/распаковка, было рассказано в главе 14,
| "Интерфейсы и структуры".)
Приведенный далее пример достаточно абстрактный, поэтому он будет создаваться
постепенно.

Использование (необобщенной) фабрики классов
Ранее в главе уже использовалась фабрика классов — хотя само назва­
ние " ф а б р и к а " вводится только сейчас — для генерации бесконечного
потока объектов P a c k a g e со случайными приоритетами. Вот как вы­
глядит этот класс:
// P a c k a g e F a c t o r y я в л я е т с я частью д е м о н с т р а ц и о н н о й программы
// P r i o r i t y Q u e u e , н а х о д я щ е й с я на п р и л а г а е м о м к о м п а к т - д и с к е
// P a c k a g e F a c t o r y - нам нужен к л а с с , к о т о р ы й з н а е т , к а к
// создаются новые п а к е т ы нужного нам т и п а по т р е б о в а н и ю ;
/ / такой к л а с с н а з ы в а е т с я ф а б р и к о й
class P a c k a g e F a c t o r y
Random r a n d = n e w R a n d o m ( ) ; / / Г е н е р а т о р с л у ч а й н ы х ч и с е л
//CreatePackage - этот метод фабрики выбирает случайный
// п р и о р и т е т , а з а т е м с о з д а е т п а к е т с этим п р и о р и т е т о м
public Package C r e a t e P a c k a g e ( )
{
/ / В о з в р а щ а е т с л у ч а й н о в ы б р а н н ы й п р и о р и т е т п а к е т а . Нам
// нужны з н а ч е н и я 0, 1 и л и 2 ( м е н ь ш и е 3)
i n t nRand = r a n d . N e x t ( 3 ) ;
// Используется для генерации нового пакета
// Приведение к перечислению н е с к о л ь к о г р о м о з д к о , но
// з а т о п е р е ч и с л е н и я удобны при и с п о л ь з о в а н и и
// конструкции s w i t c h
r e t u r n new P a c k a g e ( ( P r i o r i t y ) n R a n d ) ;

Класс P a c k a g e F a c t o r y имеет один член-данные и один метод (фабрику легко
реализовать и не как класс, а как метод — например, метод класса P r o g r a m ) .
Когда вы инстанцируете объект P a c k a g e F a c t o r y , он создает объект класса
Random и сохраняет его в члене r a n d . R a n d o m — библиотечный класс С#,
предназначенный для генерации случайных чисел. (Взгляните также на демонст­
рационную программу P a c k a g e F a c t o r y W i t h l t e r a t o r на прилагаемом
компакт-диске.)

Использование PackageFactory
Для генерации объектов P a c k a g e со случайными приоритетами вызывается метод объ­
ЕКТА

фабрики C r e a t e P a c k a g e ( ) , как показано в следующем фрагменте исходного текста:

Гшэ 15. Обобщенное программирование

365

PackageFactory fact
I P r i o r i t i z a b l e pack

=
=

new P a c k a g e F a c t o r y ( ) ;
f a c t . C r e a t e P a c k a g e ( ) ; / / Обратите
// внимание на интерфейс

C r e a t e P a c k a g e () использует генератор случайных чисел для генерации случая
го числа от 0 до 2 включительно, и применяет сгенерированное число в качестве приори
тета нового объекта P a c k a g e , возвращаемого этим методом (и сохраняемого в пере
менной типа P a c k a g e , а еще лучше — I P r i o r i t i z a b l e ) .

Е щ е немного о фабриках
Фабрики очень удобны для генерации большого количества тестовых дани!
(фабрика не обязательно использует генератор случайных чисел — он потребо
вался для конкретной демонстрационной программы P r i o r i t y Q u e u e ) .
Фабрики усовершенствуют программу, изолируя создание объектов. Каш
раз при упоминании имени определенного класса в вашем исходном тексте!
создаете зависимость (dependency) от этого класса. Чем больше таких завися
мостей, тем больше степень связности классов, тем "теснее" они связаны дм
с другом. Программистам давно известно, что следует избегать тесного CBHL
вания. (Один из многих методов развязки (decoupling) заключается в примеви
нии фабрик посредством интерфейсов, как, например, I P r i o r i t i z a b l e !
а не с использованием конкретных классов наподобие P a c k a g e . ) Программ
сты постоянно создают объекты непосредственно, с применением операм
new, и это нормальная практика. Однако использование фабрик может сдем|
код менее тесно связанным, а следовательно, более гибким.

Построение обобщенной фабрики
А если бы у вас был класс, который мог бы создавать любые необходима!
вам объекты? Эту интересную концепцию достаточно легко спрограммирсвать, как видно из демонстрационной программы G e n e r i c l n t e r f a c e a
прилагаемом компакт-диске.
// G e n e r i c l n t e r f а с е - использование обобщенного
// для р е а л и з а ц и и обобщенной фабрики
u s i n g System;
using
System.Collections.Generic;
namespace G e n e r i c l n t e r f a c e

интерфейса

{
class

Program

{
static

void

Main(string[]

args)

{
C o n s o l e . W r i t e L i n e ( " С о з д а н и е фабрики для " +
"производства Blob без параметров");
GenericFactory blobFact =
new G e n e r i c F a c t o r y < B l o b > ( ) ;
C o n s o l e . W r i t e L i n e ( " С о з д а н и е фабрики для " +
"производства Students, " +
"параметризованных с т р о к о й " ) ;
GenericFactoryl stuFact =
new G e n e r i c F a c t o r y l < S t u d e n t ,
string>();
366

Часть V. За базовыми классам.

/ / См. д о п о л н и т е л ь н ы е к л а с с ы н а п р и л а г а е м о м
// компакт-диске
// Готовим место для хранения объектов
L i s t < B l o b > b L i s t = new L i s t < B l o b > ( ) ;
Student[]
s t u d e n t s = new S t u d e n t [ 1 0 ] ;
Console.WriteLine("Создание и сохранение о б ъ е к т о в : " ) ;
f o r ( i n t i = 0; i < 10; i++)

{
C o n s o l e . W r i t e L i n e ( " ^ С о з д а н и е Blob - " +
"вызов конструктора без " +
"параметров.");
Blob b = b l o b F a c t . C r e a t e ( ) ;
b.name = "blob" + i . T o S t r i n g ( ) ;
bList.Add(b);
Console .WriteLine ("^Создание Student с " +
" у с т а н о в к о й имени - " +
" в ы з о в к о н с т р у к т о р а с одним " +
"параметром.");
s t r i n g sName = " s t u d e n t " + i . T o S t r i n g ( ) ;
students[i] = stuFact.Create(sName);
/ / . . . см. полный т е к с т н а п р и л а г а е м о м к о м п а к т - д и с к е

}
/ / Вывод р е з у л ь т а т о в .
foreach(Blob b in b L i s t )

{
Console.WriteLine(b.ToString());

}
foreach(Student

s

in

students)

{ Console.WriteLine(s.ToString()
II Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
C o n s o l e . W r i t e L i n e ( " Н а ж м и т е < E n t e r > для '
"завершения программы.
Console.Read();

}
// Классы данных: S t u d e n t , B l o b (см. также к о м п а к т - д и с к )
// B l o b - п р о с т о й к л а с с с к о н с т р у к т о р о м по умолчанию ( б е з
/ / п а р а м е т р о в , п р е д о с т а в л я е м ы м С # ) . Для э к о н о м и и м е с т а к о д
// опущен ( с м . к о м п а к т - д и с к )
// S t u d e n t - к л а с с с к о н с т р у к т о р о м по умолчанию и
// к о н с т р у к т о р о м с одним п а р а м е т р о м
c l a s s S t u d e n t : ISettable / / Обобщенный интерфейс

{
// Часть кода опущена, см. к о м п а к т - д и с к
public Student()
{}
/ / В ы должны о б е с п е ч и т ь
// этого конструктора
p u b l i c S t u d e n t ( s t r i n g name)
// конструктор с
{
// п а р а м е т р о м
this.name
name;

наличие
одним

}
// Реализация ISettable
public void SetParameter(string name)
классами

367

this.name = name;
//

ToString()

-

на

компакт-диске

}
//

См.

также

исходный

текст

на

компакт-диске

// Интерфейсы ISettable, используемые фабриками
interface ISettable
{
void SetParameter(U u ) ;

}
interface ISettable2
{ void SetParameter(U u, V v ) ;

}
// Фабрика для объектов с конструктором без параметров не
// требует реализации ISettable
class GenericFactory where T : new()
{
public T Create()
{
return new T ( ) ;

}
}
// Фабрика для создания объектов с конструктором с одним
// параметром
class GenericFactoryl where T : ISettable, new()
{
// Создает новый объект типа Т с параметром U и
// возвращает Т
public
Т Create(U и)
{
Т t = new Т ( ) ;
t.SetParameter(и); // Т должен реализовать ISettable,
// так что у него есть метод SetParameter()
return t;
}
}
//
//
//

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

}
Демонстрационная

программа

G e n e r i c l n t e r fасе

на

самом

деле

создает два

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

368

Часть V. За базовыми классами

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

Обобщенное создание объектов
Создание объектов, конструктор которых не имеет параметров, очень простое, так
как при этом отсутствуют аргументы, о которых вам пришлось бы беспокоиться (за ис­
ключением — параметра типа):
/ / Метод C r e a t e ( )
public Т C r e a t e ()

класса GenericFactory

{
return new Т(); // Вызов конструктора без параметров

}

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

должен быть к л а с с о м MyBase и л и е г о п о д к л а с с о м
M y C l a s s < T > : w h e r e Т: MyBase
должен р е а л и з о в ы в а т ь I M y l n t e r f a c e
MyClass: where T: I M y l n t e r f a c e
может б ы т ь т о л ь к о с с ы л о ч н ы м т и п о м
MyClass: where Т: c l a s s
может быть т о л ь к о т и п о м - з н а ч е н и е м
MyClass: where Т: s t r u c t
должен и м е т ь к о н с т р у к т о р б е з п а р а м е т р о в
M y C l a s s < T > : w h e r e Т.- n e w ( )

Именно это последнее ограничение и устанавливает пределы вашим возможностям
по написанию мощных обобщенных фабрик. Оно требует, чтобы тип Т имел конструктор
по умолчанию, т.е. конструктор, у которого нет параметров. Тип Т может иметь и другие
конструкторы, но один из них обязательно должен быть конструктором по умолчанию —
напишете ли вы его сами или он будет сгенерирован С#.
Ограничение n e w () представляет собой требование для каждого обобщенного
класса или метода, которые хотят создавать объекты типа Т. Однако у этого
ограничения нет версий наподобие n e w (U) или n e w (U, V) для конструкторов
с параметрами.
Обобщенный интерфейс также может иметь ограничения:
interface

ISettable

:

where

Т:

new()

...

Поиск обобщенного решения
Вопрос в том, как поступить в случае, когда конструктору надо передавать парамет­
ры. Фабрика объектов, конструктор которых требует одного параметра, должна исполь­
зовать в методе C r e a t e () код наподобие приведенного:
public Т C r e a t e ( U и )

/ / и — а р г у м е н т , который мы хотим
// передать конструктору

| {

Т t = new Т ( ) ; / / Н о new Т ( ) не может
...
/ / И что т е п е р ь ? . . .

Глава

15.

Обобщенное программирование

принимать

аргументы

369

В результате следующий подход оказывается неработоспособным:
Т t = n e w T ( u ) ; // Не р а б о т а е т
/ / или
Т t = n e w T ( U ) ; // Не р а б о т а е т
Итак, как же поступить, чтобы передать аргумент и новому объекту?

О б х о д конструктора по умолчанию
Только что описанная проблема решается путем использования обобщенного интер
фейса. Чтобы позволить фабрике передавать параметры новым объектам, необходимо
наложить определенные ограничения на производимые объекты: они должны реализовывать интерфейс наподобие I S e t t a b l e < T > :
interface
ISettable
{
void SetParameter(U u ) ;

}
Тогда вы можете объявить фабрику для объектов с одним параметром следующим
образом:
// Т — с о з д а в а е м ы й т и п ; U — т и п п а р а м е т р а к о н с т р у к т о р а
c l a s s G e n e r i c F a c t o r y l < T , U> w h e r e T: I S e t t a b l e < U > , new()

'{

}

Любой тип T, производимый такой фабрикой, должен реализовывать интерфейс
I S e t t a b l e < U > c U замененным реальным типом, таким как s t r i n g .

Использование найденного решения
Если вы хотите производить объекты S t u d e n t посредством такой фабрики, а конст
руктор S t u d e n t требует один параметр, скажем, s t r i n g , для передачи имени студен­
та, т о класс S t u d e n t должен реализовать интерфейс I S e t t a b l e < s t r i n g > :
// Инстанцирование интерфейса для
class Student:
ISettable

типа

string

{
private
s t r i n g name;
p u b l i c S t u d e n t ( ) {}
p u b l i c S t u d e n t ( s t r i n g name)
{
SetParameter(name);

//
//
//
//

Требуется наличие
к о н с т р у к т о р а по умолчанию
К о н с т р у к т о р с одним
параметром

}
public void
{
this.name

SetParameter(string

name)

//
II

Реализация
ISettable

= name;

}
//

Прочие

методы

и

члены-данные

класса

Student

При этом вы можете создавать класс S t u d e n t и так, как это делали ранее, с
помощью оператора new:
students[0]

370

=

new

Student("Juan

Valdez");

Часть V. За базовыми классами

Однако для использования фабрики вам нужен объект G e n e r i c F a c t o r y l < T , U>:
// И н с т а н ц и р у е м с Т = S t u d e n t , U = s t r i n g
GenericFactoryl factl =
new G e n e r i c F a c t o r y l < S t u d e n t ,
string>();
Вам также необходим метод C r e a t e () этой фабрики для получения объектов с ус­
тановленным именем:
// И с п о л ь з о в а н и е с т р о к о в о г о а р г у м е н т а
s t u d e n t s [1]
= factl. Create ( "Richard Corey");
Вот что происходит внутри метода G e n e r i c F a c t o r y l < T , U> . C r e a t e ( ) :
public T C r e a t e (U u)
{
T t = new T(); // Как и ранее, параметры конструктору
/ / н е
передаются
t . S e t P a r a m e t e r ( u ) ; // Используется метод, предоставленный
/ / при р е а л и з а ц и и I S e t t a b l e
return t;
}
Поскольку C r e a t e () может создавать только объекты без параметров, вы исполь­
зуете метод S e t P a r a m e t e r () для передачи параметра и объекту t. После этого можно
вернуть объект S t u d e n t с установленным членом и м е н и — такой же, как если бы для
его создания был вызван конструктор с одним параметром. Вы знаете, что тип Т имеет
метод S e t P a r a m e t e r () из-за ограничений, накладываемых на класс S t u d e n t :
: I S e t t a b l e < s t r i n g > . Этот интерфейс гарантирует, что класс S t u d e n t имеет метод
SetParameter ( ) .

Обсуждение решения
Насколько хорошее решение получено? Да, оно не является лучшим из тех, кото­
рые вы видели в своей практике. По сути, это обходной путь, но он приводит нас туда,
куда нужно!
Но что делать, если конструктор объекта требует двух параметров? трех или че­
тырех? Увы, I S e t t a b l e < U > годится только для конструктора с одним параметром.
В случае двух параметров вы должны добавить интерфейс I S e t t a b l e 2 < U , V>. Для
трех — интерфейс I S e t t a b l e 3 < U , V, W> и т.д. Кроме того, для каждого из этих типов
потребуется своя фабрика. Хорошая новость только в том, что крайне редко встречаются
конструкторы более чем с пятью-шестью параметрами. Это и определяет, сколько ин­
терфейсов I S e t t a b l e < U , V , . . . > и фабрик вам понадобится.
Конечно, при желании класс может реализовывать как интерфейс I S e t t a b l e < U > ,
так и I S e t t a b l e 2 < U , V > . В этом случае вам потребуется реализовать как метод
S e t P a r a m e t e r (U и ) , так и метод S e t P a r a m e t e r (U u , V v ) . ( Э т о — пере­
грузка, поскольку два метода S e t P a r a m e t e r () имеют разные сигнатуры.)

Глава

15.

Обобщенное программирование

371

Часть VI

Великолепные десятки

Какая книга из серии Для чайников была бы полна без этой части?
С# отлично умеет искать ошибки в ваших программах — вы, навер­
ное, и сами это заметили. Однако сообщения об ошибках часто на­
поминают военные шифровки — это тоже наверняка бросилось вам
в глаза. В главе 16, "Десять наиболее распространенных ошибок
компиляции", будут рассмотрены десять наиболее часто встречаю­
щихся ошибок в программах С#, а также будег рассказано, что они
означают и как с ними бороться.
Многие читатели интересуются местом С# в семье объектноориентированных языков программирования и его связью с наиболее
распространенным объектно-ориентированным языком — С++. В гла­
ве 17, "Десять основных отличий С# и С++", вкратце описаны отличия
этих двух языков, включая различия между обобщенными классами С#
и шаблонами С++.

Глава 16

Десять наиболее распространенных
ошибок компиляции
> The name 'memberName' does not exist in the class or namespace 'className'
> Cannot implicitly convert type 'x' into 'y'
> 'className.memberName' is inaccessible due to its protection level
> Use of unassigned local variable 'n'
} Unable to copy the file 'programName.exe' to 'programName.exe.' The process c a n n o t . . .
> 'subclassName.methodName' hides inherited member 'baseclassName.methodName'. Use
the new keyword if hiding was intended.
> 'subclassName' : cannot inherit from sealed class 'baseclassName'
> 'className' does not implement interface member 'methodName'
> 'methodName' : not all code paths return a value
> j expected

# очень строго подходит к программам и буквально с лупой выискивает в них
мельчайшие ошибки. В этой главе будут рассмотрены 10 наиболее распростра­
ненных сообщений об ошибках. Но перед тем как приступить к этому, следует сделать
несколько замечаний. С# достаточно многословен, и при работе над книгой мне попада­
лись ошибки, сообщения о которых не помещались на одной странице, так что я обрезал
некоторые сообщения до одной-двух первых строк. Кроме того, в сообщениях об ошибке
С# часто вставляет имена переменных, методов или классов, с которыми эти ошибки
связаны. Вместо конкретных имен здесь я использую такие имена, как v a r i a b l e N a m e ,
memberName или c l a s s N a m e . Наконец, С# не просто выводит имя к л а с с а — он пред­
почитает выводить его полностью, с указанием пространства имен, что тоже никак не
приводит к сокращению сообщения.

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

for(index =

0;

index

<

10;

index++)

{
//

.

.

.

Какие-то действия

.

.

.

}
Переменная i n d e x нигде не определена (см. главу 3, "Объявление переменныхзначений", о том, как правильно объявлять переменные). Приведенный исходный теш
должен быть переписан следующим образом:
for(int

index

=

0;

index

<

10;

index++)

{
II

.

.

.

Какие-то действия

.

.

.

}
To же применимо и к членам класса (см. главу 6, "Объединение данных — классы и
массивы").
Не менее вероятно неверное написание имени переменной. Приведенный далее
фрагмент исходного текста — хорошая иллюстрация такой ошибки,
class

Student

{
public
public

s t r i n g sStudentName;
i n t nID;

}
class

MyClass

{
static

public

void

MyFunction(Student

s)

{
Console.WriteLine("Имя =
Console.WriteLine("Id
=

"
"

+ s.sStudentName);
+ s.nld);

}

}
Здесь проблема заключается в том, что M y F u n c t i o n O обращается к члену nld,
в то время как настоящее имя члена — n I D . Хотя это очень похожие имена, С# не
считает их одинаковыми. Программист написал n l d , но никакого n l d не существу­
ет, и С# честно пытается об этом сообщить. (В данном случае сообщение об ошибке
немного отличается: ' c l a s s . m e m b e r N a m e '
d o e s n o t c o n t a i n a defini­
1
t i o n for
v a r i a b l e N a m e ' . Более подробно о б этом вопросе рассказывается
в главе 3, "Объявление переменных-значений".)
Менее распространена, но все же попадает в десятку самых распространенных,
ошибка, связанная с объявлением переменной в другой области видимости,
c l a s s MyClass
{
static

public

void

AverageInput()

{

376

i n t nSum = 0 ;
i n t nCount = 0;
while(true)
{
// Считываем число
string s = Console.ReadLine();
int n = Int32.Parse(s);
/ / В ы х о д , е с л и в в е д е н о о т р и ц а т е л ь Часть
н о е ч VI.
и с л оВеликолепные

десятки

if

(n

<

0)

{
break;

}
// Накопление
nSum + = n ;
nCount++;

вводимых

чисел

}
/ / Вывод р е з у л ь т а т а
Console.WriteLine("Сумма равна" + nSum);
C o n s o l e . W r i t e L i n e ( " С р е д н е е р а в н о " + nSum / < n C o u n t ) ;
// З д е с ь г е н е р и р у е т с я сообщение об ошибке
Console.WriteLine("Завершающее значение — " + s ) ;

Последняя строка этой функции некорректна. Дело в том, что переменная s ограни­
чена областью видимости, в которой определена, и вне цикла w h i l e она не видна (см.
главу 5, "Управление потоком выполнения").

Эта ошибка обычно указывает на попытку использования двух переменных различно­
го типа в одном выражении, например:
int nAge = 1 0 ;
// Г е н е р а ц и я с о о б щ е н и я об о ш и б к е
int n F a c t o r e d A g e = 2 . 0 * n A g e ;
Проблема заключается в том, что 2 . 0 — переменная типа d o u b l e . Целое значение
нАде умножается на число с плавающей точкой 2.0, что в результате дает значение типа
double. С# не в состоянии автоматически сохранить значение типа d o u b l e в перемен­
яй n F a c t o r e d A g e типа i n t , потому что при этом может оказаться потерянной ин­
формация — скорее всего, дробная часть числа с плавающей точкой.
Некоторые преобразования не настолько очевидны, как в следующем примере:
class M y C l a s s

{

static

public

float

FloatTimes2(float

f)

{
// Г е н е р и р у е т с я
float f R e s u l t =
return
fResult;

сообщение
2 . 0 * f;

об

ошибке

Вы можете решить, что здесь все в порядке, так как везде используется тип f l o a t .
Яо дело в том, что 2.0 имеет тип не f l o a t , a d o u b l e , d o u b l e , умноженный на f l o a t ,
нет d o u b l e . С# не может сохранить значение типа d o u b l e в переменной типа f l o a t
в-за возможной потери информации, в данном случае — количества знаков результата,
но приводит к снижению точности (см. главу 3, "Объявление переменных-значений").
Неявное преобразование легко запутывает неискушенного читателя. В приведенном да|кфрагменте исходного текста функция F l o a t T i m e s 2 () работает вполне корректно:

та

16. Десять

наиболее распространенных ошибок компиляции

377

class

MyClass

{
static

public

float

FloatTimes2(float

f)

{
/ / Все о т л и ч н о р а б о т а е т
float fResult =2
*
f;
return
fResult;

}

}
Константа 2 имеет тип i n t . i n t , умноженный н а f l o a t , дает f l o a t , к о т о р ы й

вполне может быть сохранен в переменной f R e s u l t типа f l o a t .
Такое же сообщение об ошибке может возникать и при выполнении операций не
"неестественными" типами. Например, нельзя сложить две переменные типа char, не
С# может при необходимости конвертировать переменную c h a r в значение int.
приводит к следующему:
class

MyClass

{
static

public

void

SomeFunction()

{

} }

char cl = ' a ' ;
c h a r c2 = ' b ' ;
// Я не знаю, ч т о э т о должно о з н а ч а т ь ,
// н е в е р н о — х о т я и не по т о й п р и ч и н е ,
// думаете
c h a r сЗ = cl + с 2 ;

н о в с е р а в н о это
о к о т о р о й вы

Сложение двух символов не имеет смысла, но С# все равно попытается это сделать
Поскольку сложение для типа c h a r не определено, он преобразует cl и с2 в значение
типа i n t и выполнит их сложение (технически c h a r представляет собой интегральный
тип). К сожалению, результат этого сложения преобразовать обратно в c h a r не удастся
без помощи со стороны программиста (см. главу 3, "Объявление переменных
значений").
Большинство (хотя и не все) преобразований без проблем выполняется при их явном
указании. Так, следующая функция компилируется без каких-либо нареканий:
c l a s s MyClass
{
static

public

float

FloatTimes2(float

f)

{
// Здесь использовано явное преобразование
float fResult =
(float)(2.0 * f);
return
fResult;

}
}
Результат умножения 2 . 0 * f имеет тип d o u b l e , как и ранее, однако программист'
явно указал, что он хочет преобразовать полученное значение к типу f l o a t , даже если
это приведет к потере информации (см. главу 3, "Объявление переменных-значений").
Второй подход к проблеме может состоять в том, чтобы явно указать необходимы!
тип константы:

378

Часть VI. Великолепные

десятки

Глав

class

MyClass

{
static

public

float

FloatTimes2(float

f)

{
// Здесь 2.OF — к о н с т а н т а
f l o a t f R e s u l t = 2 . O F * f;
return
fResult;

типа

float

}
В этой версии функции использована константа 2.0 типа f l o a t , а не d o u b l e , как
принято п о умолчанию, f l o a t , умноженный н а f l o a t , дает f l o a t .

Данная ошибка указывает на попытку функции обратиться к члену, на обращение
к которому она не имеет прав. Например, метод в одном классе может пытаться обра­
титься к закрытому члену другого класса (см. главу 11, "Классы"), как показано в приве­
денном фрагменте исходного текста,
public c l a s s M y C l a s s
{
public void SomeFunction()

{
Y o u r C l a s s uc = new Y o u r C l a s s ( ) ;
// MyClass не имеет д о с т у п а к закрытому
uc.nPrivateMember = 1;
}
}
public

class

"{

члену

YourClass

private int nPrivateMember = 0;
}
Обычно такая ошибка не столь очевидна. Зачастую оказывается просто забытым де­
скриптор члена или всего класса, а по умолчанию член класса является закрытым. Так,
n P r i v a t e M e m b e r остается закрытым в следующем фрагменте исходного текста:
class M y C l a s s

//

Доступ

к

классу

по

умолчанию

-

internal

(
public

void

SomeFunction()

{
Y o u r C l a s s u c = new Y o u r C l a s s ( ) ;
// MyClass не имеет д о с т у п а к закрытому
uc . n P r i v a t e M e m b e r = 1;

члену

}

'public

class

YourClass

(
int n P r i v a t e M e m b e r

Глава

16.

Десять

= 0 ;

//

Этот

член

-

закрытый!

наиболее распространенных ошибок компиляции

379

Кроме того, несмотря на то что функция S o m e F u n c t i o n () объявлена как public
к ней нельзя обратиться из классов других модулей, поскольку класс M y C l a s s сам
себе является внутренним.
Мораль этой истории — всегда указывайте уровень защиты ваших классов и их чле
нов. Кроме того, не объявляйте открытые члены во внутренних классах — это том
сбивает с толку.

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

class

MyClass

{
public

void

SomeFunction()

{
i n t n;
// Все в п о р я д к е , т а к к а к С# т о л ь к о в о з в р а щ а е т значение
// в п; в функцию з н а ч е н и е э т о й п е р е м е н н о й не п е р е д а е т с я
SomeOtherFunction(out n ) ;

}
public

void

SomeOtherFunction(out

int

n)

{
n =

1;

}
В данном случае переменной п в функции S o m e F u n c t i o n () не присваивается ни
какого значения, поскольку это выполняется в функции S o m e O t h e r F u n c t i o n d
Функция S o m e O t h e r F u n c t i o n () игнорирует значение out-аргумента, как если 6ы|
его не существовало вовсе. (В главе 3, "Объявление переменных-значений", рассказыва
ется о переменных, а ключевое слово o u t рассмотрено в главе 7, "Функции функций".)

Обычно это сообщение повторяется несколько раз. И почти всегда означает, что вы
забыли завершить выполнение программы перед ее пересборкой. Другими словами, вы
сделали следующее.
1. Успешно собрали программу.
2. Когда вы запустили программу с помощью команды D e b u g ^ S t a r t without
Debugging, то получили сообщение Н а ж м и т е < E n t e r > д л я завершения
п р о г р а м м ы . . ., но по какой-то причине не сделали этого (таким образом, вы-

380

Часть VI. Великолепные десяти

полнение программы продолжается), а вы переключились в Visual Studio 2005
и продолжили редактирование файла.
Примечание: если вы запустили программу посредством команды Debug^Start
Debugging и забыли ее завершить, то Visual Studio 2005 запросит вас о том, не
следует ли завершить выполнение программы.
3. Вы попытались собрать программу заново, с новыми обновлениями. В этот мо­
мент вы и получаете рассматриваемое сообщение об ошибке в окне Error List.
Выполнимый (. EXE) файл блокируется Windows до тех пор, пока программа не прекратит работу. Visual Studio не в состоянии перезаписать заблокированный старый
.ЕХЕ-файл новой версией без полного завершения программы.
Чтобы исправить ситуацию, переключитесь на ваше активное приложение и заверши­
те его работу. Если это одно из консольных приложений из данной книги, просто нажми­
те клавишу . Вы также можете завершить программу из Visual Studio 2005 по­
средством команды меню Debug^Stop Debugging. После того как старое приложение
прекратит работу, соберите приложение заново.
Если вы не можете избавиться от ошибки таким способом, выйдите из Visual Studio
2005 и перегрузите компьютер. Если не сработает и это — ну, тогда не знаю, чем вам
можно помочь...

Посредством этого сообщения С# пытается информировать о том, что вы перегру­
жаете метод базового класса без его перекрытия (см. детальное описание в главе 13,
Полиморфизм"). Давайте рассмотрим следующий пример:
public

class

I

BaseClass
v

public v o i d F u n c t i o n ()

public

class

public

void

Subclass

:

Function()

BaseClass
//

here's

the

overload

{
}
public
public

{

class
void

MyClass
Test()

S u b c l a s s s b = new
sb.Function();

Subclass();

}
16. Десять наиболее распространенных ошибок компиляции

381

Функция T e s t () не может обратиться к методу B a s e C l a s s . F u n c t i o n () из on
екта подкласса s b , поскольку он скрыт методом S u b c l a s s . F u n c t i o n ( ) . Вы намер
вались сделать одно из двух перечисленных действий.
Хотели скрыть метод базового класса. В этом случае добавьте ключевое слово
n e w в определение S u b c l a s s , как показано в следующем фрагменте исход
ного текста:
public

{

new

class
public

Subclass
void

:

BaseClass

Function()

{

}

}

Намеревались полиморфно наследовать базовый класс. В этом случае вы долщ|
объявить два класса следующим образом:
public

class

BaseClass

{
public

virtual

void

Function()

{

}

}

public

class

Subclass

:

BaseClass

{
public

}

{
}

override

void

Function()

См. детальное описание проблемы в главе 13, "Полиморфизм".
Это не ошибка, а всего лишь предупреждение.

Это сообщение говорит о том, что класс, который вы хотите наследовать, опечатан,
поэтому вы не можете ни наследовать его, ни изменить любое из его свойств. Обычно
опечатываются только библиотечные классы. Обойти опечатывание невозможно, но вы
можете попытаться воспользоваться классом с помощью отношения СОДЕРЖИТ (см.
главу 13, "Полиморфизм").

382

Часть

VI.

Великолепные

десятки

Реализация интерфейса представляет собой обещание предоставить определение для
всех его методов. Данное сообщение свидетельствует о том, что вы нарушили это обе­
щание и не реализовали указанный метод. Такое могло произойти по ряду причин.
Ваша собака просто сожрала вашу работу. Конечно, это шутка — вы просто забыли о
необходимости реализовать указанный метод. В следующий раз будьте внимательнее.
Вы ошиблись при написании имени метода или дали ему неверные аргументы.
Рассмотрим следующий пример:
interface
void

Me

aFunction(float

public c l a s s M y C l a s s
public

void

:

f);
Me

aFunction(double

d)

Класс M y C l a s s не реализует функцию интерфейса a F u n c t i o n ( f l o a t ) . Функция
aFunction ( d o u b l e ) не играет роли, поскольку аргументы этих двух функций не сов­
падают.
Вернитесь к исходному тексту программы и продолжите реализацию методов, пока
интерфейс не будет реализован полностью (см. главу 14, "Интерфейсы и структуры").
Неполная реализация интерфейса — практически то же самое, что и попытка
создать конкретный класс из абстрактного, не перекрывая при этом все его аб­
страктные методы.

Этим сообщением С# ставит вас в известность, что ваш метод, который объявлен как
извращающий значение, на одном или нескольких путях выполнения не возвращает ни­
кто. Это может случиться по двум причинам.
У вас имеется конструкция i f , которая содержит выход без возврата значения.
Более вероятно, что вы вычислили значение, но не возвращаете его.
Обе возможности иллюстрируются следующим исходным текстом:
public

class

MyClass

л
'ава

16. Десять наиболее распространенных ошибок компиляции

383

public

string

ConvertToString(int

n)

{
// Конвертируем i n t n в s t r i n g
string s = n.ToString();

s

}
public

string

ConvertPositiveNumbers(int

n)

{
//
if

Преобразуем
( n > 0)

только

положительные

числа

{
string s = n.ToString();
return s;
Console.WriteLine("Аргумент
{о}
/ / Т р е б у е т с я еще о д и н о п е р а т о р

некорректен",
return

n);

>

}
Функция C o n v e r t T o S t r i n g () вычисляет значение типа s t r i n g , но не возвраща­
ет его. Все, что нужно для исправления этой ошибки — добавить в конце функции re­
t u r n S;.
Функция C o n v e r t P o s i t i v e N u m b e r s () возвращает s t r i n g - в е р с и ю аргументап
типа i n t , если п положительно. Кроме того, функция совершенно корректно генерируй
сообщение об ошибке, если п не положительно. Однако в этом случае функция ничего не
возвращает. Здесь вы должны вернуть либо n u l l , либо пустую строку "" — что болыв
подходит для нужд вашего приложения (см. главу 7, "Функции функций").

Эта ошибка указывает, что С# ожидал закрывающую скобку в месте завершения ис­
ходного текста программы. Где-то по дороге вы забыли закрыть определение класса,
функцию или блок. Вернитесь и внимательно просмотрите исходный текст программы,
пока не найдете это место.
Такое сообщение об ошибке зачастую оказывается последним в серии практически
бессмысленных сообщений. Не беспокойтесь о них до тех пор, пока не найдете место,
где была потеряна закрывающая фигурная скобка. В поиске пар открывающихся и соот­
ветствующих закрывающихся скобок вам может помочь Visual Studio 2005.

384

Часть VI. Великолепные десятки

Глава 17

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

зык С# в большей или меньшей степени основан на языке программирования
С++. Это и не удивительно, если вспомнить, что Microsoft создала Visual С++,
1ру из наиболее успешных сред программирования для Windows. Подавляющее боль­
шинство программ, с которыми приходится иметь дело, написаны на Visual С++.
I Однако С # — не просто новые одежды на старом языке: в нем имеется масса улучЬний, представляющих собой как новые возможности, так и замену старых возможно•4 С++ более мощными. В этой главе будет рассказано о десятке самых важных усоиршенствований. Само собой, их гораздо больше, так что, в принципе, можно было бы
рписать и о двадцати (и более) главных улучшениях С# по сравнению с С++.
Вы могли прийти к С# разными путями — например, из Java или Visual Basic.
С# даже более схож с Java, чем с С++ что также не удивительно, поскольку
язык Java тоже появился как усовершенствование С++, ориентированное на ис­
пользовании в Интернете. Между С# и Java много синтаксических различий, но
все равно они выглядят как близнецы. Если вы можете читать программы на
одном из них — то сможете это делать и на другом.
Что касается Visual Basic — т.е. Visual Basic .NET, а не старого Visual Basic 6.0 — то
и синтаксис совершенно иной, однако Visual Basic .NET основан на той же инфраструктуре .NET Framework, что и С#, так что он дает практически одинаковый с С# код
CIL (Common Intermediate Language, общий промежуточный язык) и способен к взаимодействию с С#. Класс С# может наследовать класс Visual Basic и наоборот, а ваша
про- грамма может представлять комбинацию модулей С# и Visual Basic.

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

C/C++ позволяет выделять память несколькими способами, каждый из которых има
свои недостатки.
Глобальные объекты существуют на протяжении

в с е г о времени жим

программы. Программа может легко создать множество указателей на один и
тот же глобальный объект. При изменении посредством одного из них изменения отражаются во всех. Указатель представляет собой переменную, содержащую адрес некоторого удаленного блока памяти. Технически ссылки С# явля
ются указателями.
Стековые объекты уникальны для отдельных функций (и это хорошо),но
они освобождаются при выходе из функции. Любой указатель на освобожден­
ный объект становится недействительным. Это было бы нормально, если бы никто
не работал с этими указателями; однако ничто не мешает использовать указатель,
даже если объекта, на который он указывает, уже нет. Стек С++ — это область
памяти, отличная от кучи, и это действительно стек.
Объекты в куче создаются по мере необходимости. Эти объекты уникальны
в рамках одной нити выполнения программы.
Проблема заключается в том, что очень легко забыть, на какой тип памяти ссы­
лается данный указатель. Объекты в куче следует освободить после того, как вы по­
работали с ними. Если забыть об этом, в программе образуется "утечка памяти", ко-'
торая может привести к исчерпанию последней и неработоспособности программы,
С другой стороны, если вы освободите блок в куче дважды или "освободите" блок
глобальной или стековой памяти, ваша программа может вызвать проблемы, с кото
р ы м и справятся только три веселые клавиши , и , и то не пооди­
ночке, а все в м е с т е . . .
С# решает эту проблему путем выделения памяти для всех объектов вне кучи. Кроме
того, С# использует сборку мусора для ее возврата в кучу. При работе с С# вы можете
забыть о синем экране смерти из-за пересылки неверного блока памяти в куче.

386

Часть VI. Великолепные

десятки

Введение указателей в С обеспечило успех этого языка программирования. Работа
с указателями— очень мощная возможность. Старые программисты на машинных языи были несказанно рады, получив в свои руки такой инструмент. В С++ возможности
работы с указателями унаследованы без изменений от С.
К сожалению, ни программист, ни программа не в состоянии отличить хороший ука­
затель от плохого. Прочтите память по неинициализированному у к а з а т е л ю — и ваша
программа аварийно завершится, если, конечно, вам повезет... Если не п о в е з е т — она
продолжит работу, сочтя случайный блок памяти корректным объектом...
Проблемы, связанные с указателями, трудно локализовать. Зачастую программа с не­
корректным указателем при каждом новом запуске ведет себя по-новому.
С# решает проблемы, связанные с указателями, очень просто — он попросту устраня­
ет их из языка. Используемые вместо них ссылки безопасны с точкизрения типов и не
могут быть применены так, чтобы это приводило к краху программы.

Если вы сравните новые обобщенные возможности С# (см. главу 15, "Обобщенное
программирование") с шаблонами С++, то обнаружите высокую степень схожести их
(интаксиса. Однако хотя оба средства имеют общее предназначение, такое сходство яв­
ляется чисто внешним.
Как обобщенные классы, так и шаблоны безопасны с точки зрения типов, но
реализованы они совершенно по-разному. Шаблоны инстанцируются в про­
цессе компиляции, в то время как инстанцирование обобщенных классов
происходит во время выполнения программы. Это означает, что один и тот
же шаблон в разных модулях дает в результате два различных типа, инстанцированных во время компиляции. Но один и тот же обобщенный класс
в разных модулях дает только один тип, инстанцируемый во время выполне­
ния. Это приводит к меньшему "раздутию" кода для обобщенных классов по
сравнению с шаблонами.
Наибольшее различие между обобщенными классами и шаблонами состоит
пом, что обобщенные классы работают с несколькими языками, включая Visual
Basic, С++ и другие языки .NET, в том числе С#. Шаблоны же используются только
(рамках С++.
Что же лучше? Шаблоны более мощны — и более сложны, как и множество других
идей в С++, но и больше подвержены ошибкам. Обобщенные классы проще в испольювании и реже приводят к ошибкам в программах.
Конечно, это всего лишь некоторые тезисы бурной дискуссии. Существенно
большую информацию можно найти в блоге Брендона Брея (Brandon Bray) по адресу
weblogs . a s p . n e t / b r a n b r a y / a r c h i v e / 2 0 0 3 / 1 1 / 1 9 / 5 1 0 2 3 . a s p x .

Глава 17, Десять основных отличий С # и С++

387

С++ обеспечивает строгую проверку типов — и это хорошо. Он выполняет эот
заставляя объявлять функции и классы в так называемых включаемых файлах, кото
рые затем используются модулями. Однако правильное перечисление в правили
порядке всех включаемых файлов для компиляции вашего модуля — задача не из
простых.
С# избегает бессмысленной работы. Он ищет и находит определения всех классов
Если вы вызываете класс S t u d e n t , С# находит определение этого класса, чтобы убе
диться, что вы используете его корректно.

Сначала казалось, что конструкторы приносят большую пользу. Наличие специальн
ной функции, гарантирующей корректную настройку всех членов-данных... Отличная
идея! Единственная проблема в том, что приходится добавлять в каждый написанный
класс тривиальный конструктор по умолчанию. Рассмотрим следующий пример:
p u b l i c c l a s s Account
{
p r i v a t e double balance;
private
i n t numChecksProcessed;
p r i v a t e CheckBook checkBook;
public Account()
{
balance = 0.0;
n u m C h e c k s P r o c e s s e d = 0;
c h e c k B o o k = new C h e c k B o o k ( ) ;

}
}
Почему же нельзя инициализировать члены-данные непосредственно и позволить
языку программирования самому сгенерировать конструктор? С++ отвечает, почему; C#
отвечает — почему нет? С# позволяет избавиться от ненужных конструкторов с помощью непосредственной инициализации:
public
c l a s s Account
{
p r i v a t e double balance = 0.0;
p r i v a t e i n t numChecksProcessed = 0;
p r i v a t e CheckBook c h e c k B o o k = new C h e c k B o o k ( ) ;
// Больше э т о не надо д е л а т ь в к о н с т р у к т о р е

}
Более того, если все, что нужно — это соответствующая версия нуля для определенного типа, как в случае первых двух членов, С# примет необходимые меры автоматически, как минимум для членов-данных классов. Если вы хотите нечто, отличное от нуля,
добавьте вашу собственную инициализацию к объявлению членов-данных. (Однако сле­
дует всегда инициализировать локальные переменные в функциях.)

388

Часть VI. Великолепные

десятки

С++ очень политкорректен. Он и шагу не ступит ни на одном компьютере без того,
чтобы определить требования к диапазону значений и размеру конкретных типов. Он
указывает, что i n t имеет такой-то размер, a l o n g — больший. Все это приводит к по­
явлению ошибок при переносе программ с одного типа процессора на другой.
С# не заботится о таких мелочах. Он прямо говорит — i n t имеет 32 бит, a l o n g —
64 бит, и так должно быть.

С++ позволяет одному классу наследовать более чем один базовый класс. Например,
класс S l e e p e r S o f а (диван-кровать) может наследовать классы B e d (кровать) и S o f a
(диван). Наследование от двух классов звучит неплохо, и это и в самом деле бывает
очень полезно. Проблема только в том, что множественное наследование может приво­
дить к некоторым трудно обнаружимым ошибкам.
С# не рискует и снижает количество возможных ошибок, запрещая множественное
наследование. Однако в С# имеется возможность, которая в ряде ситуаций может заме­
нить множественное наследование, а именно — интерфейсы.

Когда программисты продираются сквозь кошмар множественного наследования и 90%
времени проводят в отладчике, зачастую выясняется, что второй базовый класс нужен
только для того, чтобы описать подкласс. Например, обычный класс может наследовать
абстрактный класс P e r s i s t a b l e с абстрактными методами r e a d () и w r i t e ( ) . Это за­
ставляет подкласс реализовать методы r e a d () и w r i t e () и объявить всему миру, что эти
методы доступны для использования.
После этого программисты осознают, что того же можно добиться существенно более
легкими средствами — посредством интерфейса. Класс, который реализует интерфейс
наподобие приведенного ниже, тоже обещает предоставить методы r e a d () и w r i t e ( ) :
interface

{

IPersistable

void r e a d ( ) ;
void write () ;

}
Так вы избегаете опасностей множественного наследования С++ и получаете желае­
мый результат.

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

Глава 17. Десять основных

ОТЛИЧИЙ

С#

И

С++

389

ческим представлениям о реальном мире. Единственная проблема заключается в том, что
любой язык программирования должен обеспечить еще и простейшие типы переменных
для хранения, например, целых чисел или чисел с плавающей точкой. Это определяет
необходимость системы преобразования типов. Объекты классов и переменные типов
значений находятся по разные стороны баррикад, хотя и участвуют вместе в одних и тех
же программах. Программист вынужден все время помнить о том, что это разные вещей
по своей природе.
С# разрушает эту баррикаду, отделяющую типы-значения от классов. Для каждом
типа-значения имеется соответствующий класс, именуемый структурой (вы можете так
же писать и собственные структуры; см. главу 14, "Интерфейсы и структуры"). Эти
структуры могут легко объединяться с классами, позволяя программисту писать исход­
ный текст наподобие следующего:
M y C l a s s m y O b j e c t = n e w M y C l a s s О,/ / Вывод " m y O b j e c t " в с т р о к о в о м ф о р м а т е
Console.WriteLine(myObj e c t . T o S t r i n g ( ) ) ;
i n t i = 5;
/ / Вывод i n t в с т р о к о в о м ф о р м а т е
Console.WriteLine(i.ToString());
/ / Вывод к о н с т а н т ы 5 в с т р о к о в о м ф о р м а т е
Console.WriteLine(5.ToString());
Можно вызвать один и тот же метод не только для переменной i n t и объекта класса
M y C l a s s , но даже для константы наподобие 5. Такое вавилонское смешение типоводна из мощных возможностей С#.

390

Часть VI. Великолепные десятки

Часть VII

Дополнительные главы

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

Глава 18

Эти исключительные исключения
> Обработка ошибок с помощью кодов ошибки
У Использование механизма исключений вместо кодов ошибки
} Создание собственного класса исключения
} Перекрытие ключевых методов в классе исключения

не сомнения, трудно смириться с тем, что иногда метод (или функция) не делает
то, для чего он предназначался. Это раздражает программистов ничуть не
меньше, чем пользователей их программ, тоже часто являющихся источником недоразу­
мений. В книге встречалась программа, в которой пользователь должен был вводить це­
лое число как строку. Такой метод можно написать так, что он будет просто игнориро­
вать введенный пользователем мусор вместо реального числа, но хороший программист
напишет функцию таким образом, чтобы она распознавала неверный ввод пользователя
и докладывала об ошибке.
Здесь говорится об ошибках времени выполнения, а не времени компиляции, с кото­
рыми С# разберется сам при сборке вашей программы.
Механизм исключений представляет собой средство для сообщения о таких ошибках
способом, который вызывающая функция может лучше понять и использовать для реше­
ния возникшей проблемы.

Промолчать о том, что произошла ошибка времени выполнения — это всегда наи­
худшее решение, применимое только в том случае, если вы не намерены отлаживать
программу и вас не интересует результат ее работы...
В приведенной далее демонстрационной программе FactorialWithErr o r показано, что может произойти, если не выявить ошибку. Эта програм­
ма вычисляет и выводит значение факториала для ряда значений.
Факториал числа N равен N*(N-l)*(N-2)*... *1. Например, факториал 4 равен
4*3*2*1 = 24. Функция вычисления факториала работает только для поло­
жительных целых чисел. Это банальный программистский пример для ил­
люстрации ситуации, когда требуется обработка ошибок.

// FactorialWithError - пример функции вычисления
// факториала, в которой отсутствует проверка ошибок

using System;
namespace FactorialWithError
{
// MyMathFunctions - набор созданных мною математических
// функций
public class MyMathFunctions
{
// Factorial - возвращает факториал переданного
// аргумента
public static double Factorial(double dValue)
{
// Начинаем со значения аккумулятора, равного 1
double dFactorial = 1.0;
// Цикл со счетчиком nValue, уменьшающимся до 1, с
// умножением на каждой итерации значения аккумулятора
// на величину счетчика
do
{
dFactorial *= dValue;
dValue -= 1.0;
} while(dValue > 1 ) ;
// Возвращаем вычисленное значение
return dFactorial;

}
}
public class Program
{
public static void Main(string[] args)
{
// Вызов функции вычисления факториала в
// цикле от 6 до -6
for (int i = 6; i > -6; I--)
{
// Вывод результата на каждой итерации
Console.WriteLine("i = { о } , факториал = {l}",
i, MyMathFunctions.Factorial(i));

}

}

}

}

// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
"завершения программы.. . ") ;
Console.Read();

Функция Factorial () начинается с инициализации переменной-аккумулятора значе­
нием 1. Затем функция входит в цикл, в котором на каждой итерации выполняется умноже­
ние на последовательно уменьшающееся значение счетчика nValue, пока nValue не дос­
тигнет 1. Накопленное в аккумуляторе значение возвращается вызывающей функции.
Алгоритм Factorial () выглядит к о р р е к т н о — пока вы не начнете вызывать эту
функцию. Функция Main () также содержит цикл, в котором вычисляются значения фак­
ториала для ряда убывающих значений. Однако вместо того чтобы остановиться на значе­
нии
значений — до -6.
394 1, функция Main () продолжает вычисления для отрицательных
Часть VII. Дополнительные
главы

В результате на экране получается следующее:

i = 6, факториал = 72 0
i = 5, факториал = 12 0
i = 4, факториал = 24
i = 3 , факториал = 6
i = 2, факториал = 2
1 = 1 , факториал = 1
| = 0 , факториал = 0
i = -1, факториал = -1
i = -2, факториал = -2
i = -3, факториал = -3
i = -4, факториал = -4
i - -5, факториал = -5
Нажмите для завершения программы...
Как видите, часть результатов не имеет смысла. Во-первых, значение факториала не
может быть отрицательным. Во-вторых, обратите внимание, что отрицательные значения
растут совсем не так, как положительные. Понятно, что здесь присутствует ошибка.
Если попытаться изменить цикл внутри Factorial () и записать его как

do{ . . . }while (dValue ! =0), то программу при передаче отрицательного
значения просто ждет крах. Поэтому никогда не пишите такой оператор срав­
нений — while (dValue ! =0), поскольку ошибка приближения может в лю­
бом случае привести к неверному результату проверки на равенство 0.
В особенности при работе с числами с плавающей точкой избегайте условий
наподобие dValue ! =0, в которых требуется точное сравнение для выхода из
цикла. Используйте менее строгое условие, как, например, dValue>l. Не­
большая ошибка п р и б л и ж е н и я — такая как dValue = 0.00001— может
привести к бесконечному циклу. Об ошибках приближения рассказывается
в главе 3, "Объявление переменных-значений".

Возврат индикатора ошибки
Несмотря на свою простоту, функция Factorial О требует проверки ошибочной
ситуации: факториал отрицательного числа не определен. Функция Factorial ()
должна включать проверку этого условия.
Но что должна делать функция Factorial ( ) , столкнувшись с ошибкой? Лучшее,
что она может сделать в такой ситуации — это сообщить об ошибке вызывающей
функции в надежде на то, что источник ошибки знает, почему она произошла и как с
ней справиться.
Классический способ указать на происшедшую ошибку в функции — это возвратить
значение, которое функция не в состоянии вернуть при безошибочной работе. Например,
значение факториала не может быть отрицательным. Таким образом, факториал может
возвращать значение - 1 , если ему передается отрицательный аргумент, -2 для нецелого
аргумента и так далее — для каждой ошибки некоторое соответствующее ей число. Та­
кие числа называются кодами ошибки. Вызывающая функция может проверить, не вер­
нула ли вызываемая функция отрицательное значение, и если д а — то вызывающая
функция будет знать о том, что произошла ошибка. Значение возвращаемого кода ошиб­
ки позволяет определить ее природу.

Глава

18.

Эти исключительные исключения

395

jjjdWfeK

Указанные изменения внесены в код демонстрационной программы Facto­

// FactorialErrorReturn - создание функции вычисления
// факториала, которая возвращает код ошибки, если что-то
// идет не так
using System;
namespace FactorialErrorReturn
{
// MyMathFunctions - набор созданных мною математических
// функций
public class MyMathFunctions
{
// Следующие коды ошибок представляют некорректные
// значения
public const int NEGATIVE_NUMBER
= -1;
public const int NON_INTEGER_VALUE = -2;
// Factorial - возвращает факториал переданного
// аргумента
public static double Factorial(double dValue)
{
// Проверка: отрицательные значения запрещены
if (dValue < 0)

{
return NEGATIVE NUMBER;

}
// Проверка: передано ли целое значение аргумента
int nValue = (int)dValue;
if (nValue != dValue)
{
return NON INTEGER VALUE;

}

// Тесты пройдены, начинаем со значения аккумулятора,
// равного 1
double dFactorial = 1.0;
// Цикл со счетчиком nValue, уменьшающимся до 1, с
// умножением на каждой итерации значения аккумулятора
// на величину счетчика
do

{
dFactorial *= dValue;
dValue -= 1.0;
} while(dValue > 1 ) ;
// Возвращаем вычисленное значение
return dFactorial;

}
}
public class Program
{
public static void Main(stririg[] args)
396 {
Часть VII. Дополнительные главы
// Вызов функции вычисления факториала в
// цикле от 6 до -6

for (int i = 6; i > -6; i--)
{
double dFactorial = MyMathFunctions.Factorial(i);
if (dFactorial == MyMathFunctions.NEGATIVE NUMBER)
{
Console.WriteLine
("Factorial() получила отрицательный параметр");
break;

}
if (dFactorial == MyMathFunctions.NON INTEGER VALUE)
{

'

"

Console.WriteLine
("Factorial() получила нецелый параметр");
break;

}

// Вывод результата на каждой итерации
Console.WriteLine("i = { о } , факториал = {l}",
i, MyMathFunctions.Factorial(i));

}

// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read ();

Теперь перед началом вычислений функция Factorial () выполняет ряд проверок.
Первая проверка — не отрицателен ли переданный функции аргумент. Обратите внима­
7
ние, что значение 0 разрешено, поскольку приводит к разумному результату . Если про­
верка не пройдена, функция тут же возвращает код ошибки. Затем выполняется второй
тест, проверяющий, равен ли переданный аргумент своей целочисленной версии. Если
да — дробная часть аргумента равна 0.
Функция Main () проверяет результат, возвращаемый функцией Factorial ( ) , на
предмет обнаружения ошибок. Однако значения наподобие -1 и -2 мало информативны
для программиста, так что класс MyMathFunctions определяет пару целочисленных

констант. Константа NEGATIVE_NUMBER равна - 1 , a NON_INTEGER_VALUE

2. Это

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

Обращение к этим константам выполняется посредством имени класса, как

MyMathClass. NEGATIVE_NUMBER. Константные переменные автоматиче­
ски являются статическими, что делает их свойствами класса, разделяемыми
всеми объектами. Другие варианты работы с константами описаны во врезке
"Немного о константах".

7

Этот "разумный" результат некорректен, так как в математике принято, что факториал 0 ра­

вен 1. — Примеч. ред.

Глава

18.

Эти исключительные исключения

397

Немного о константах
Предпочтительный способ записи констант почти всегда использует следую»
щий, более гибкий по сравнению с применением const, подход.

public static readonly int NEGATIVE_NUMBER = -1;
Значение const вычисляется во время компиляции и может быть инициализировано
только числом или строкой. Статическая переменная только для чтения вычисляется во
время выполнения программы и может быть инициализирована объектом любого вида,
Используйте const только там, где производительность программы сверхкритична.
Еще один способ определения констант — в данном случае группы связанных конс т а н т — п о с р е д с т в о м ключевого слова enum, как описано в главе 15, "Обобщенное
программирование". Типы ошибок для MyMathClass могут быть определены еледующим образом:

enum MathErrors
NegativeNumber,
NonlntegerValue
Функция Factorial () может возвращать значение MathErrors, и вы можете проверить его в своей программе следующим образом (как можно часто увидеть в классах
.NET Framework):

MathErrors meResult = MyMathFunctions.Factorial(6);
if(meResult == MathErrors.NegativeNumber) ...
Теперь функция Factorial () сообщает об ошибках функции Main ( ) , которая выводит соответствующее сообщение на экран и завершает на этом свою работу:

i = 6, факториал = 720
i = 5, факториал = 12 0
i = 4, факториал = 24
i = 3, факториал = 6
i = 2, факториал = 2
1 = 1 , факториал = 1
i = 0, факториал = 0
Factorial() получила отрицательный параметр
Нажмите для завершения программы...
(Здесь я предпочел прекращать работу при обнаружении ошибки.) Указание о про­
исшедшей ошибке посредством возвращаемого функцией значения повсеместно исполь­
зуется еще со времен FORTRAN. Зачем же менять этот механизм?

Чем плохи коды ошибок
Что же не так с кодами ошибок? Они были достаточно хороши даже для FORTRAN!
Да, но в те времена компьютеры были ламповыми. Увы, но коды ошибок приводят к ря­
ду проблем.
Этот метод основан на том факте, что у функции имеются значения, которые она
не может вернуть при корректной работе. Однако существуют функции, у которых

398

Часть

VII. Дополнительные главы

любые возвращаемые значения корректны. Не всегда везет поработать
с функцией, которая должна возвращать только положительные значения.
Например, вы не можете получить логарифм отрицательного числа, но само
значение логарифма может быть как положительным, так и отрицательным.
Вы можете предложить справиться с этой проблемой, возвращая код
ошибки как значение функции, а необходимые данные — посредством ар­
гумента, описанного как out, но такое решение менее интуитивно и теря­
ет выразительную силу функции. Сперва познакомьтесь с исключениями,
и вы убедитесь, что они предоставляют гораздо более красивый путь ре­
шения проблемы.
В целом числе не удается разместить большое количество информации. Так,
рассматриваемая функция Factorial О возвращает -1, если ее аргумент от­
рицателен. Локализовать ошибку было бы проще, если бы был известен сам
аргумент, но в возвращаемом функцией типе для него просто нет места.
Обработка ошибок является необязательной. Вы не получите никаких пре­
имуществ от проверок в функции Factorial ( ) , если вызывающая функ­
ция не будет в свою очередь проверять возвращаемое значение. Конечно,
руководитель группы может просто сказать: "Парни, или вы проверяете ко­
ды ошибок, или занимаете очередь на бирже труда", но совсем иное дело,
когда проверку заставляет выполнить сам язык программирования.
Зачастую при проверке кода ошибки, возвращаемого функцией Factorial () или лю­
бой другой функцией, практически вся вызывающая функция оказывается заполненной
проверками всех возможных кодов ошибок от всех вызванных функций, при этом просто
не остается ни сил, ни места сделать в функции хоть что-то полезное. Судите сами:

// Вызов SomeFuncO, проверка кода ошибки, его обработка и
// возврат из функции
errRtn = SomeFunc () ;
if (errRtn == SF_ERRORl)
(

Console .WriteLine ("Ошибка типа 1 при вызове SomeFuncO");
return MY ERROR 1;

if (errRtn == SF ERROR2)
Console .WriteLine ("Ошибка типа 2 при вызове SomeFuncO");
return My_ERROR 2;
}

// Вызов другой функции, проверка кода ошибки и так далее..
.errRtn = SomeOtherFuncО;
if (errRtn == SOF_ERRORl)
{
Console.WriteLine("Ошибка типа 1 при вызове " +
"SomeOtherFunc () ") ;
return MY ERROR 3;
)

if (errRtn == SOF ERROR2)
{
Console.WriteLine("Ошибка типа 2 при вызове " +
Глава 18. Эти исключительные исключения

399

"SomeOtherFunc()");
return MY_ERR0R_4;

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

В С# для перехвата и обработки ошибок используется совершенно иной механизм,
называемый исключениями. Он основан на ключевых словах try, catch, throw и fi­
nally. Набросать схему его работы можно следующим образом. Функция пытается
(try) пробраться через кусок кода. Если в нем обнаружена проблема, она бросает
(throw) индикатор ошибки, который функции могут поймать (catch), и независимо от
того, что именно произошло, в конце (finally) выполнить специальный блок кода, как
показано в следующем наброске исходного текста:

public class MyClass
{
public void SomeFunction()
{
// Настройка для перехвата ошибки
try

{

// Вызов функции или выполнение каких-то иных
// действий, которые могут генерировать исключение
SomeOtherFunction();
// . . . Какие-то иные действия . . .

}
catch(Exception е)
{
// Сюда управление передается в случае, когда в блоке
// try сгенерировано исключение — в самом ли блоке, в
// функции, которая в нем вызывается, в функции,
// которая вызывается функцией, вызванной в try-блоке
// и так далее — словом, где угодно. Объект Exception
// описывает ошибку
' Далее будет использоваться выражение "генерирует исключение". — Примеч. ред.

400

Часть VII. Дополнительные главы

}

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

}
}
public void SomeOtherFunction()
{
I I . . . Ошибка произошла где-то в теле функции . . .
11 . . . Ж "пузырек" исключения "всплывает" вверх по
// всей цепочке вызовов, пока не будет перехвачен в
// блоке catch
throw new Exception("Описание ошибки");
I I . . . Продолжение функции . . .
Функция Some Function () помещает некоторую часть кода в блок, помеченный
ключевым словом try. Любая функция, вызываемая в этом блоке (или функция, вызы­
ваемая функцией, вызываемой в этом блоке — и так далее...), рассматривается как вы­
званная в данном try-блоке.
Непосредственно за блоком try следует ключевое слово catch с блоком, которому
передается управление в случае, если где-то в try-блоке произошла ошибка. Аргумент

catch-блока — объект класса Exception (или некоторого подкласса Exception).
Однако catch-блок не обязан иметь аргументы: пустой catch перехватывает все

исключения, как и catch (Exception):
catch

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

catch (MyException)
{
// Действия, которые не требуют обращения к объекту
// исключения
}
Блок finally— если таковой имеется в вашем исходном т е к с т е — выполняется
даже в случае перехвата исключения, не говоря уже о том, что он выполняется при нор­
мальной работе. Обычно он предназначается для "уборки" — закрытия открытых фай­
лов, освобождения ресурсов и т.п.
В отличие от исключений С++, в которых аргументом catch может быть объ­
ект произвольного типа, исключения С# требуют, чтобы он был объектом клас­
са Exception или производного от него.
Итак, где-то в дебрях кода на неизвестно каком уровне вызовов в функции SomeOt h e r F u n c t i o n ( ) случилась ошибка... Функция сообщает об этом, генерируя исклю-

Глава

18.

Эти исключительные исключения

401

чение в виде объекта Exception и передает его с помощью оператора throw вверх
цепочке вызовов в первый же блок, который в состоянии перехватить его и обработан!
Иногда обработчик try/catch располагается в той же функции, в которой
нерировано исключение. Первая же функция, владеющая достаточным количес
вом контекстуальной информации для выполнения действий по его обработке
может перехватить и обработать его; если данная функция не в состоянии этого
сделать, исключение передается дальше вверх по цепочке вызовов.

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

использованием

исключений

using System;
namespace FactorialException
{
// MyMathFunctions - набор созданных мною математических
// функций
public class MyMathFunctions
{
// Factorial - возвращает факториал переданного
// аргумента
public static double Factorial(int nValue)
{
// Проверка: отрицательные значения запрещены
if (nValue < 0)

{
// Сообщение об отрицательном аргументе
string s = String.Format(
"Отрицательный аргумент в вызове Factorial { о } " ,
nValue);
throw new Exception(s); // Генерация исключения...

}

// начинаем со значения аккумулятора,
// равного 1
double dFactorial = 1.0;
// Цикл со счетчиком nValue, уменьшающимся до 1, с
// умножением на каждой итерации значения аккумулятора
// на величину счетчика
do

{
dFactorial *= nValue,} while(--nValue > 1 ) ;
// Возвращаем вычисленное значение
return dFactorial;
402

Часть VII. Дополнительные главн

public class Program
{
public static void Main(string[]

args)

{

try // Исключения от функции Factorial()
// этого блока

"всплывут" до

{
// Вызов функции вычисления факториала в
// цикле от 6 до -6
for (int i = 6; i > -6; i--)

{
// Вычисление факториала
double dFactorial = MyMathFunctions.Factorial(i);
// Вывод результата на каждой итерации
Console.WriteLine("i = { о } , факториал = {l}",
i, MyMathFunctions.Factorial(i));
catch(Exception e) // ... перехват исключения
{
Console.WriteLine("Ошибка:");
Console.WriteLine(e.ToString());
}
// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();

Эта "исключительная" версия функции Main () практически полностью находится
в try-блоке.
Всегда помещайте содержимое функции Main () в try-блок, поскольку функ­
ция Main () — начальная и конечная точка программы. Любое исключение, не
перехваченное где-то в другом месте, будет передано функции Main ( ) . Это
последняя возможность перехватить исключение перед тем, как оно попадет
прямо в Windows, где это сообщение об ошибке будет гораздо сложнее интер­
претировать.
Блок catch в конце функции Main () перехватывает объект Exception и использует его метод ToString () для вывода информации об ошибке, содержащейся в этом
объекте в виде строки.
Более консервативное свойство Exception.Message возвращает более удо­
бочитаемую, но менее информативную информацию по сравнению с предос­
тавляемой методом е . ToString ().
Эта версия функции Factorial () включает ту же проверку на отрицательность пе­
реданного аргумента, что и предыдущая (для экономии места в ней опущена проверка
того, что аргумент — целое число). Если аргумент отрицателен, функция Factorial ()

403

форматирует сообщение об ошибке с описанием ситуации, включая само отрицательное
значение, вызвавшее ошибку. Затем функция Factorial () вносит информацию в
вновь создаваемый объект типа Exception, который передается с помощью механизма
исключений вызывающей функции.
Вывод этой программы выглядит следующим образом:
i = 6, факториал
i = 5, факториал
i = 4, факториал
i = 3, факториал
i = 2, факториал
i = 1, факториал
i = 0, факториал
Ошибка:

=
=
=
=
=
=
=

720
120
24
6
2
1
0

System.Exception: Отрицательный аргумент в вызове Factorial -1
at Factorial(Int32 nValue) in
с:\c#programs\Factorial\Program.cs:line 21
at FactorialException.Program.Main(String[] args) in
с:\c#programs\Factorial\Program.cs:line 49
Нажмите для завершения программы...
В первых нескольких строках выводятся корректно вычисленные факториалы число
9

от 6 до 0 . Попытка вычислить факториал -1 приводит к генерации исключения.
В первой строке сообщения об ошибке выводится информация, сгенерированная
в функции Factorial ( ) . Эта строка описывает природу ошибки, включая вызвавшее
неприятности значение аргумента

1.

В оставшейся части вывода выполняется трассировка стека. В первой строке указы
вается, в какой функции сгенерировано исключение. В данном случае это было сделано
в функции Factorial (int) — а именно в 21 строке исходного файла Program.cs
Функция Factorial () была вызвана из функции Main (string [] ) в строке 49 того
же файла. На этом трассировка файла прекращается, поскольку функция Main () содер­
жит блок, перехвативший и обработавший указанное исключение.
Трассировка стека доступна в одном из окон отладчика Visual Studio.

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

9

404

Еще раз напомним читателю, что в математике принято считать, что 0! = 1. — Примеч. ред.

Часть VII. Дополнительные главы

Стандартный класс исключения Exception, предоставляемый библиотекой С#,
в состоянии предоставить вам достаточное количество информации. Вы можете запро­
сить объект исключения о том, где он был сгенерирован, какая строка была передана ему
генерирующей функцией. Однако в ряде случаев стандартного класса Exception быва­
ет недостаточно. У вас может оказаться слишком много информации, чтобы разместить
ее в одной строке. Например, функция приложения может захотеть передать вызвавший
проблемы объект для последующего анализа. Изучение этого объекта может быть полез­
ным вплоть до полного восстановления после происшедшей ошибки.
Локально определенный класс может наследовать класс Exception так же, как
и любой другой класс. Однако пользовательский класс исключения должен наследовать
не непосредственно класс Exception, а класс ApplicationException, являющийся
подклассом Exception, как показано в следующем фрагменте исходного текста:
// CustomException - добавление ссылки на MyClass к
// стандартному классу исключения
public class CustomException : ApplicationException
{
private MyClass myobject;

// Хранит ссылку на вызвавший

// проблемы объект
CustomException(string sMsg, MyClass mo) : base(sMsg)
{
myobject = m o ;
' }

// Предоставляет доступ к объекту,
// исключения
public MyClass MyCustomObject{ get

сохраненному в объекте
{return myobject;}}

}
Класс CustomException представляет собой самодельный класс для сообщения об
ошибке в любой программе, работающей с классом MyClass. Этот подкласс класса Ap­
plicationException содержит такую же строку, как и исходный класс, но добавляет
к ней ссылку на объект MyClass, вызвавший проблемы. Это позволяет произвести де­
тальное исследование случившегося.
В приведенном далее примере выполняется перехват исключения CustomExcep­
tion и используется информация об объекте MyClass:
public class Program
{
public void SomeFunction()
{
try

{
// ... действия перед вызовом демонстрационной функции
SomeOtherFunctionO ;
// ... продолжение работы ...

}
catch(MyException me)

[лава{ 18. ЭТИ исключительные исключения

405

// Здесь у вас имеется доступ к методам Exception и
// ApplicationException
string s = m e . T o S t r i n g О ;
// Но у вас есть еще и доступ к методам, уникальным
// для вашего класса исключения
MyClass mo = me.MyCustomObject;
// Например, вы можете запросить у объекта MyClass его
// собственное описание
string s = mo.GetDescription();

}

}

public

void

SomeOtherFunctionO

{
// Создание myobject
MyClass myobject = new M y C l a s s 0 ;
// ... сообщение об ошибке с участием myobject ..
throw new MyException("Ошибка в объекте MyClass",
myobj ect) ;
// ... Остальная часть функции ...

}

}
В этом фрагменте кода функция SomeFunction () вызывает функцию SomeO-|
therFunction () из охватывающего блока try. Функция SomeOtherFunction()
создает и использует объект myobject. Где-то в функции SomeOtherFunction()
программа проверки ошибок подготавливает исключение к генерации для сообщения
о происшедшей ошибке. Вместо создания простого объекта типа Exception или Ap­
plicationException, функция SomeFunction О применяет разработанный ваш
тип MyExcept ion, пригодный не только для передачи текстового сообщения об ошиб­
ке, но и ссылки на вызвавший ее объект.
Блок catch в функции Main () указывает, что он предназначен для перехвата объ­
ектов MyExcept ion. После того как такой объект перехвачен, код приложения в со­
стоянии применить все методы Exception, как, например, метод ToString ( ) . Одна­
ко в этом catch-блоке может использоваться и другая информация, к примеру, вызов
методов объекта MyClass, ссылка на который передана в объекте исключения.

Фрагмент кода в предыдущем разделе продемонстрировал генерацию и перехват ло­
кально определенного объекта исключения MyExcept ion. Рассмотрим еще раз конст­
рукцию catch из этого примера:
public

void SomeFunction()

{
try

{
SomeOtherFunctionO ;

}
catch(MyException me)

{
}

}
406

Часть VII. Дополнительные главы

Главе

А если функция SomeOtherFunction () сгенерирует простое исключение Excep­
tion или исключение еще какого-то типа, отличного от MyException? Это будет на­
поминать ситуацию, когда футбольный вратарь ловит баскетбольный мяч — мяч, ловить
который он не научен. К счастью, С# позволяет программе определить несколько блоков
catch, каждый из которых предназначен для различного типа исключений.
Блоки catch должны в этом случае следовать один за одним, без разрывов, в порядке
от наиболее специализированных классов ко все более общим. С# проверяет каждый
catch-блок, последовательно сравнивая сгенерированный объект с аргументами catchолоков, как показано в следующем фрагменте исходного текста:
public void SomeFunction ()
(
try
SomeOtherFunctionO ;
catch(MyException me) // Наиболее специализированный тип
{
// исключения
// Здесь перехватываются все объекты MyException
} // Между этими catch-блоками могут находиться блоки с
// другими типами исключений
catch(Exception е)
// Наиболее общий тип исключения
// Все остальные неперехваченные исключения
// перехватываются в этом блоке

Если функция SomeOtherFunction () сгенерирует объект Exception, он минует
блок catch (MyException), поскольку Exception не является типом MyExcep­
tion. Он будет перехвачен в следующем блоке — catch (Exception).
Любой класс, наследующий MyException, ЯВЛЯЕТСЯ MyException:
class MySpecialException : MyException
{
// ... что-то там ...

}
В этом случае блок для MyException перехватит и объект MySpecialException.
(Наследование всех пользовательских исключений от одного базового пользовательского
исключения — неплохая мысль. Само базовое исключение наследуйте от ApplicationException.)
Всегда располагайте catch-блоки от наиболее специализированного к наибо­
лее общему. Никогда не размещайте более общий блок первым, как это сделано
в приведенном фрагменте исходного текста:
public void SomeFunction ()

(
try

{

SomeOtherFunction();

}
Глава 18. Эти исключительные исключения

407

catch(Exception me) // Самый общий блок - это неверно!
{
// Все объекты MyException будут перехвачены здесь

}
catch(MyException е)
{
// Сюда не доберется ни один объект - все они будут
// перехвачены более общим блоком

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

Как исключения протекают сквозь пальцы
Что, если С#, пройдя все catch-блоки, так и не найдет подходящего? Или в вы»
вающей функции вообще нет catch-блока? Что будет тогда?
Рассмотрим следующую простую цепочку вызовов функций:

// MyException - демонстрация того, как можно создать новый
// класс исключения и как функция может перехватывать только
// те исключения, которые может обработать
using System;
namespace MyException

{
// Вводим некоторый тип MyClass
public class MyClass{}
// MyException - - добавляем ссылку на MyClass к
// стандартному классу исключения
public class MyException : ApplicationException
private MyClass myobject;
public MyException(string sMsg, MyClass mo)

: base(sMsg)

myobject = mo,-

}

// Дает внешним классам доступ к объекту
public MyClass MyCustomObject{ get {return myobject;}}
public class Program
{
// fl - - перехватывает обобщенный объект исключения
public void f1(bool bExceptionType)
try
{
f2(bExceptionType);
catch(Exception e)
{

408

Часть VII. Дополнительные главы

Console.WriteLine("Перехват обобщенного " +
"исключения в fl О ") ;
Console.WriteLine(е.Message);

}
}

// f2 - - готов к перехвату MyException
public void f2(bool bExceptionType)
{
try
{
f3(bExceptionType);

}

catch(MyException
me)
{
Console.WriteLine("Перехват MyException в f 2 ( ) " ) ;
Console.WriteLine(me.Message);

}

}

// f3 - - He перехватывает никаких исключений
public void f3(bool bExceptionType)
{ f4(bExceptionType);

}

// f4 - - генерация одного из двух типов исключений
public void f4(bool bExceptionType)
{

// Работаем с некоторым локальным объектом
MyClass mc = new MyClass О;
if(bExceptionType)
{
// Произошла ошибка — генерируем объект исключения с
// объектом
throw new MyException("Генерация MyException " +
"в f 4 ( ) " , mc) ;
}
throw new Exception("Обобщенное исключение в f4 () " );

}

public
static void Main(string[] args)
{
// Сначала генерируем обобщенное исключение...
Console.WriteLine("Сначала генерируем " +
"обобщенное исключение");
new P r o g r a m O . fl (false) ;
// ... а теперь наше исключение
Console.WriteLine("\пГенерируем исключение " +
"MyException");
new P r o g r a m O .fl(true) ;
// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();

Глава 18. Эти исключительные исключения

409

Функция Main () создает объект Program и тут же использует его для вызова мето
да f 1 ( ) , который, в свою очередь, вызывает метод f 2 ( ) , который вызывает метод
f 3 ( ) , вызывающий метод f 4 ( ) . Функция f 4 () выполняет сложную проверку ошибки
которая выливается в генерацию либо исключения MyException, либо обобщенного
исключения Exception, в зависимости от аргумента типа bool. Вначале сгенериро
ванное исключение Exception передается в функцию f3 ( ) . Здесь С# не находит
catch-блока, и управление передается вверх по цепочке в функцию f 2 ( ) , которая пе
рехватывает исключения MyException и его наследников. Этот тип исключения не со
ответствует обобщенному исключению Exception, и управление передается еще
дальше вверх. Наконец, в функции f 1 () находится catch-блок, соответствующий сге
нерированному исключению.
Второй вызов в функции Main () заставляет функцию f 4 () сгенерировать объект MyEx
ception, который перехватывается в функции £2 ( ) . Это исключение не пересылается
функцию f 1 ( ) , поскольку оно перехватывается и обрабатывается функцией f 2 ().
(Может ли функция Main () в действительности создать объект класса, содержащий
объект класса, в котором содержится Main () — т.е. класса Program? Конечно, почему
бы и нет? См. последний раздел главы 14, "Интерфейсы и структуры".)
Вывод программы выглядит следующим образом:
Сначала генерируем обобщенное исключение
Перехват обобщенного исключения в fl()
Обобщенное исключение в f4()
'Генерируем исключение MyException
Перехват MyException в f2()
Генерация MyException в f4()
Нажмите для завершения программы...
Функция наподобие f3 ( ) , не содержащая ни одного catch-блока, вовсе не ред
кость. Можно сказать даже больше — такие функции встречаются гораздо чаще, чем
функции с catch-блоками. Функция не должна перехватывать исключения, если она
не готова их обработать. Должна ли некоторая математическая функция ComputeX()
в которой вызывается функция Factorial () как часть вычислений, перехватывать
исключение, которое может быть сгенерировано функцией Factorial () ? Функция
ComputeXO

может совершенно не представлять, откуда взялись неверные входные

данные для функции Factorial О и что теперь делать. В этом случае функция ComputeX ( ) , конечно же, не должна содержать catch-блока и перехватывать генери­
руемые исключения.
Функция наподобие f 2 () перехватывает только один тип исключений. Она ожида­
ет только один определенный тип ошибки, который умеет обрабатывать. Например
MyException может быть исключением, определенным для выдающейся библиотеки
классов гениального автора, написанной, понятное дело, мной, и так и называющей­
с я — BrilliantLibrary. Функции, составляющие BrilliantLibrary, генери­
руют и перехватывают только исключения MyException.
Однако

функции

BrilliantLibrary

могут также

вызывать

функции

обычной

стандартной библиотеки System. Функции BrilliantLibrary могут не знать, как
следует обрабатывать обобщенные исключения System, в особенности если они вызва­
ны некорректными входными данными.

410

Часть VII. Дополнительные

главы

Если вы не знаете, что делать с исключением — лучше не делайте ничего: по­
звольте разобраться с ним вызывающей функции. Но будьте честны сами с со­
бой: не позволяйте исключению уйти только потому, что вы слишком заняты,
чтобы писать обработчик.

Регенерация исключения
В ряде случаев метод не в состоянии полностью обработать ошибку, но не хочет пе­
редавать исключение вызывающей функции, не вложив свои "пять копеек" в его обра­
ботку. В таком случае catch-блок может частично выполнить обработку исключения,
а затем передать его дальше (вообще-то, не слишком привлекательная картина).
Рассмотрим, например, метод F (), который открываетфайл при входе в метод
с намерением закрыть его при выходе из метода. Где-то в середине работы F () вызы­
вается G (). Исключение, сгенерированное в G (), может привести к тому, что у F ()
не будет шансов закрыть этот файл, который так и останется открытым до полного за­
вершения программы. Идеальным решением было бы включение в F () catch-блока
(или блока finally), который бы закрывал все открытые файлы. F() может пере­
дать исключение дальше после того, как закроет все необходимые файлы и выполнит
прочие требуемые действия.
"Регенерировать" исключение можно двумя способами. Один из них состоит в гене­
рации второго исключения с дополнительной (или, как минимум, той же) информацией
следующим образом:
public void f 1 ()
{
try

{
f 2 () ;
}
// Перехват исключения...
catch(MyException me)

{

// ... Частичная обработка исключения ...
Console .WriteLine ("Перехват MyException в fl ()")'';
// ... Генерация нового исключения для передачи его
// вверх по цепочке вызовов
throw new Exception(
"Исключение, сгенерированное в f l ( ) " ) ;

Генерация нового объекта исключения позволяет классу переформулировать
сообщение об ошибке, добавив в него дополнительную информацию. Генера­
ция обобщенного объекта Exception вместо специализированного MyEx­
ception обеспечивает гарантированный перехват этого исключения на уров­
нях выше f 1 ().
Генерация нового исключения имеет тот недостаток, что трассировка стека при
этом начинается заново, с точки генерации нового исключения. Источник исходной
ошибки оказывается потерян, если только fit) не предпримет специальных мер для
его сохранения.

(пава 18. Эти исключительные исключения

411

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

void

fl()

{
try
{
f 2 () ;

}
// Перехват исключения...
catch(Exception e)
{
// ... Частичная обработка исключения ...
Console.WriteLine("Перехват исключения в f l ( ) " ) ;
// ... исходное исключение продолжает свой путь по
// цепочке вызовов
throw;

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

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

412

Часть VII. Дополнительные главы

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

Следующий пользовательский класс может сохранить дополнительную информацию,
что невозможно в процессе применения стандартных объектов Exception или Appli­
cationException:
// MyException - к стандартному классу исключения добавлена
// ссылка на MyClass
public class MyException : ApplicationException
{

private MyClass myobject;
MyException(string sMsg, MyClass mo)

: base(sMsg)

{
myobject = m o ;
}
// Позволяет внешним классам обращаться к сохраненному
// в исключении классу
public MyClass MyObject{ get {return myobject;}}
Вернемся вновь к библиотеке функций BrilliantLibrary. Эти функции знают,
как заполнять и считывать новые члены класса MyException, тем самым предоставляя
информацию, необходимую для отслеживания каждой ошибки. Проблема при таком
подходе заключается в том, что только функции BrilliantLibrary могут получить
все преимущества от использования новых членов MyException.
Перекрытие методов, имеющихся у классов Exception или Applica­
tionException, может предоставить функциям вне BrilliantLi­
brary доступ к новым данным. Рассмотрим класс исключения из следую­
щей демонстрационной программы CustomException.
// CustomException - создание пользовательского исключения,
// которое выводит информацию в более дружественном формате
using System;
namespace CustomException
{
public class CustomException : ApplicationException
{
private MathClass mathobject;
private string sMessage;

Глава 18. Эти исключительные исключения

413

public CustomException(string sMsg,
{
mathobject = m o ;
sMessage = sMsg;

MathClass mo)

}
override public string Message
{

get{return

String.Format("Сообщение < { o } > ,
Объект {l}",
sMessage,
mathobj ect.ToString());}

override public string ToString()
{

string s = Message;
s += "\пИсключение сгенерировано в ";
s += TargetSite.ToString(); // Информация о методе,
// сгенерировавшем исключение
return s;

}
}
// MathClass - набор созданных мною математических функций
public class MathClass

{
private int nValueOfObject;
private string sObjectDescription;
public MathClass(string sDescription,

int nValue)

{
nValueOfObject = nValue;
sObjectDescription = sDescription;
public int Value {get {return nValueOfObject;}}
// Message - вывод сообщения со значением
// присоединенного объекта MathClass
public string Message

{
get

{
return String.Format("({0} = { l } ) " ,
sObj ectDescription,
nValueOfObject);

}
}
// ToString - расширение нашего пользовательского
// свойства Message с использованием Message из базового
// класса исключения
override public string ToString()
{

string s = Message + "\n";
s +- base.ToString();
return s;

}
// Вычисление обратного значения 1/x
public double Inverse()

414

Часть VII. Дополнительные глава

{
if
{

(nValueOfObject == 0)
throw new CustomException("Нельзя делить на 0",
this);

}
return 1.0 /

(double)nValueOfObject;

public class Program
{
public static void Main(string[]

args)

try
{
// take the inverse of 0
MathClass mathObject = new MathClass("Value", 0 ) ;
Console.WriteLine("Обратное к d.Value равно { О } " ,
mathObj ect.Inverse() ) ;
catch(Exception e)

{

}

Console.WriteLine(
"\пНеизвестная фатальная ошибка:\n{0}",
e.ToString());

// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
"завершения программы.. . ") ;
Console.Read();
}
Класс CustomException несложен. Он хранит сообщение и объект, как это делал
класс MyException ранее. Однако вместо предоставления новых методов для обраще­
ния к этим элементам данных он перекрывает существующее свойство Message, кото­
рое возвращает сообщение об ошибке, содержащееся в исключении, и метод
ToString ( ) , возвращающий сообщение и трассировку стека.
Перекрытие этих функций означает, что даже функции, разработанные для перехвата
обобщенного класса Exception, получают ограниченный доступ к новым членамданным. Новый класс лучше обеспечить собственными методами для их данных и оста­
вить нетронутыми методы базового класса.
Функция Main () демонстрационной программы начинает с создания объекта MathClass со значением 0, а затем пытается вычислить обратную к нему величину. Не знаю,
как вам, а мне не приходилось видеть разумные результаты деления на 0, так что если
моя функция вдруг сделает это, я отнесусь к происшедшему с явным недоверием.
На самом деле процессоры Intel возвращают значение 1.0/0.0: бесконечность.
Имеется ряд специальных значений с плавающей точкой, используемых вместо
генерации исключений в языках, которые не поддерживают их. Эти специаль­
ные значения включают положительную и отрицательную бесконечности и по­
ложительное и отрицательное NaN (Not_a_Number, не число).

(пава 18. Эти исключительные исключения

415

В нормальных условиях метод Inverse () возвращает корректное значение. При
передаче ему нуля он генерирует исключение CustomException, передавая ему стро­
ку пояснения вместе с вызвавшим исключение объектом.
Функция Main () перехватывает исключение и выводит короткое сообщение, поясняющее суть происшедшего. "Неизвестная фатальная ошибка", вероятно, означает, та
программа "закрывает лавочку и уходит на отдых". Но функция Main () дает исключе­
нию шанс пояснить, что же все-таки произошло, вызывая его метод ToString ().

Визитка класса: метод T o S t r i n g ()
Все классы наследуют один общий базовый класс с именем Object. Об этом уже го­
ворилось в главе 14, "Интерфейсы и структуры". Здесь, однако, стоит упомянуть о ме­
тоде ToString () в составе этого класса. Метод предназначен для преобразования
содержимого класса в строку. Идея заключается в том, что каждый класс должен пе­
рекрывать метод ToString ( ) , чтобы осуществить вывод значащей информации.
В первых главах был использован метод GetString ( ) , чтобы не касаться в них во­
проса наследования; однако принцип остается тем же. Например, корректный метод
Student. ToString () может выводить имя и идентификатор студента.
Большинство ф у н к ц и й — даже встроенных в библиотеку С # — применяют метод
ToString () для вывода объектов. Таким образом, перекрытие ToString () имеет
очень полезное побочное действие, заключающееся в том, что каждый объект выво­
дится в своем собственном формате, безотносительно к тому, кем именно он выведен.
Поскольку объект исключения в этом случае на самом деле принадлежит типу Cus­
tomException, управление передается CustomException. ToString ().
Метод Message () представляет собой виртуальный метод класса Excep­
tion, так что его можно перекрывать, но пользовательское исключение долж­
но наследовать его без перекрытия.
Метод Message О позволяет объекту MathClass выводить информацию о самом
себе с использованием метода ToString ( ) . Метод MathClass.ToString() воз­
вращает строку, в которой содержится описание и значение объекта.
Не следует брать на себя больше того, что имеете. Используйте метод объекта
ToString () для создания строковой версии объекта, не пытаясь влезть в сам
объект и получить его значения. В общем случае нужно полагаться на откры­
тый интерфейс — открытые члены, — а не на знания о внутреннем устройстве
объекта. Оно инкапсулировано (по крайней мере должно быть инкапсулирова­
но) и может измениться в новых версиях.
Вывод демонстрационной программы CustomException имеет следующий вид:
Неизвестная фатальная ошибка:
Сообщение , Объект (Value = 0)
CustomException.MathClass
Исключение сгенерировано в Double Inverse()
Нажмите для завершения программы...

416

Часть VII. Дополнительные глава

И последнее: сообщение "Неизвестная фатальная ошибка:" поступает от
Main ( ) . Строка "Сообщение , Объект < ~ ~ > " посту­
пает от CustomException. Часть Value = 0 предоставляет объект MathClass.
Последняя строка, Исключение сгенерировано в Double Inverse ( ) , при­
надлежит CustomException. Это нельзя назвать иначе, как исключительным со­
трудничеством.

Глава 18. Эти исключительные исключения

417

Глава 19

Работа с файлами и библиотеками
> Работа с несколькими исходными файлами в одной программе
У Сборки и пространства имен
> Библиотеки классов
> Чтение и запись файлов данных

оступ к файлам в С# может иметь два различных значения. Наиболее очевид­
ное — это хранение и считывание данных с диска. О том, как осуществляется
ввод-вывод данных с диска, вы узнаете из этой главы. Второе значение связано
с тем, каким образом исходный текст С# группируется в исходные файлы.
Функции позволяют разделить длинную строку исходного текста на отдельные моду­
ли, которые можно легче сопровождать и поддерживать. Классы дают возможность
группировать данные и функции для дальнейшего снижения сложности программы —
программы достаточно сложны, а людям свойственно ошибаться, так что нужно пользо­
ваться любой возможностью упрощения, которая может помочь избежать ошибок.
С# обеспечивает еще один уровень группировки: он позволяет сгруппировать подоб­
ные классы в отдельную библиотеку. Помимо написания собственных библиотек, вы
можете использовать в ваших программах и чужие библиотеки. Такие программы со­
держат множество модулей, называемых сборками (assemblies). О них также будет рас­
сказано в данной главе. Кроме того, описанное в главе 11, "Классы", управление досту­
пом на самом деле несколько сложнее в связи с применением пространств имен — еще
одного способа группирования похожих классов, которое заодно позволяет избежать
дублирования имен в двух частях программы. В этой главе речь пойдет и о них.

Программы в настоящей книге носят исключительно демонстрационный характер. Ка­
ждая из них длиной не более нескольких десятков строк и содержит не более пары классов.
Программы же промышленного уровня со всеми "рюшечками" и "финтифлюшечками" мо­
гут состоять из сотен тысяч строк кода с сотнями классов.
Рассмотрим систему продажи авиабилетов. У вас должен быть один интерфейс для
заказа билетов по телефону, д р у г о й — для тех, кто заказывает билет по Интернету,
должна быть часть программы, отвечающая за управление базой данных билетов, дабы
не продавать один и тот же билет несколько раз, еще одна часть должна следить за стой-

мостью билетов с учетом всех налогов и скидок, и так далее и тому подобное... Такая
программа будет иметь огромный размер.
Размещение всех составляющих программу классов в одном исходном файле Pro­
gram, с s быстро становится непрактичным. Оно даже более неприемлемо, чем разде
имущества, которого добилась моя бывшая жена, по следующим причинам.
У вас возникнут проблемы при поддержке классов. Единый исходный фа
очень трудно поддается пониманию. Гораздо проще разбить его на отдельные мо­
дули, например ResAgentlnterf асе . cs, GateAgentlnterf асе . cs, Res-j

Agent.cs, GateAgent.cs, Fare.cs и Aircraft.cs.
Работа над большими программами обычно ведется группами программи­
стов. Два программиста не в состоянии редактировать одновременно один и та
же файл — каждому требуется его собственный исходный файл (или файлы).;
У вас может быть 20 или 30 программистов, одновременно работающих над од­
ним большим проектом. Один файл ограничит работу каждого из 24 программи­
стов над проектом всего одним часом в сутки, но стоит разбить программу на 24
файла, как становится возможным (хотя и сложным) заставить всех программи­
стов трудиться круглые сутки. Разбейте программу так, чтобы каждый класс со­
держался в отдельном файле, и ваша группа заработает как слаженный оркестр.
Компиляция больших файлов занимает слишком много времени. В результа­
те босс начнет нервничать и выяснять, почему это вы так долго пьете кофе вместо
того, чтобы стучать по клавишам?
Какой смысл перестраивать всю программу, когда кто-то из программистов изме­
нил пару строк кода? Visual Studio 2005 может перекомпилировать только изме­
ненный файл и собрать программу из уже готовых объектных файлов.
По всем этим причинам программисты на С# предпочитают разделять программу на
отдельные исходные файлы . CS, которые компилируются и собираются вместе в единый выполнимый . Е Х Е - ф а й л .
Файл проекта содержит инструкции о том, какие файлы входят в проект и как
они должны быть скомбинированы друг с другом.

Можно объединить файлы проектов для генерации комбинаций программ, которые
зависят от одних и тех же пользовательских классов. Например, вы можете захотеть объ­
единить программу записи с соответствующей программой чтения. Тогда, если изменя­
ется одна из них, вторая перестраивается автоматически. Один проект может описывать
программу записи, второй — программу чтения. Набор файлов проектов известен под
названием решение (solution). (Далее в главе будут рассматриваться две такие програм­
м ы — FileRead и FileWrite, которые можно было бы объединить в одно решение,
но это так и не было сделано.)
Программисты на Visual С# используют Visual Studio Solution Explorer для объ­
единения нескольких исходных файлов С# в проекты в среде Visual Studio
2005. Solution Explorer будет описан в главе 21, "Использование интерфейса
Visual Studio".

420

Часть VII. Дополнительные главы

В Visual Studio, а также в C#, Visual Basic .NET и прочих языках .NET один проект
соответствует одному скомпилированному модулю — в .NET он носит имя сборка.
С# может создавать два основных типа сборок — выполнимые файлы (с расширени­
ем .ЕХЕ) и библиотеки классов (.DLL). Выполнимые файлы представляют собой про­
граммы сами по себе и используют код поддержки из библиотек. Во всей этой книге созда­
вались исключительно выполнимые файлы. Что касается библиотек классов, то опять же
все программы в книге их используют. Например, пространство имен S y s t e m — место
размещения таких классов, как String, Console, Exception, Math и O b j e c t — су­
ществует как набор библиотечных сборок. Каждой программе требуются классы System.
Библиотеки не являются самостоятельными выполнимыми программами.

Библиотека классов состоит из одного или нескольких классов, обычно работающих
вместе тем или иным способом. Зачастую классы в библиотеках находятся в своем соб­
ственном пространстве имен (namespace). (О них речь пойдет в следующем разделе.)
Вы можете построить библиотеку математических подпрограмм, библиотеку для работы
со строками, библиотеку классов-фабрик и т.д.
Небольшие программы обычно состоят из одной сборки programName.ехе. Одна­
ко часто создаются решения, состоящие из нескольких отдельных (но связанных) проек­
тов, как упоминалось в предыдущем разделе. Каждый из них компилируется в отдельную
сборку. В решении вы можете объединять и . ЕХЕ-, и . DLL-файлы, что является обычной практикой ДЛЯ больших программ. Когда ВЫ строите многопроектное решение,
сборки работают совместно, обеспечивая функциональность приложения в целом.
Если решение содержит более одного . ЕХЕ-проекта, вы должны указать Visual
Studio, какой проект является начальным (startup project). Именно он будет запус­
каться при выборе команды меню Debug^Start Debugging (F5) or Debugs
Start Without Debugging (). Для указания начального проекта щелкни­
те на нем правой кнопкой мыши в окне Solution Explorer и выберите в раскры­
вающемся меню команду Set as Startup Project. Имя начального проекта в окне
Solution Explorer выделяется полужирным шрифтом. О Solution Explorer речь
пойдет в главе 21, "Использование интерфейса Visual Studio".
Большие программы обычно разделяют свои компоненты на один выполнимый файл
и несколько библиотек. Например, весь код, связанный с заказом билетов в рассматривав­
шемся ранее приложении, может находиться в одной библиотеке, работа с Интернетом —
в другой, а управление базами данных — в третьей. Когда такая программа устанавливает­
ся на компьютер пользователя, процесс инсталляции включает копирование ряда файлов в
соответствующие места на диске компьютера, причем многие из них являются .DLLфайлами, или просто "DLL" на сленге программистов (DLL означает dynamic link library
(динамически компонуемые библиотеки) — код, который загружается в память тогда, ко­
гда в нем возникает необходимость при запуске используемой программы).

taa 19. Работа с файлами и библиотеками

421

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

Пространства имен существуют для того, чтобы можно было поместить связанны
классы в "одну корзину", и для снижения коллизий между именами, используемые
в разных местах. Например, вы можете собрать все классы, связанные с математически­
ми вычислениям, в одно пространство имен MathRoutines.
Можно (но вряд ли будет сделано на практике) разделить на несколько пространств
имен один исходный файл. Гораздо более распространена ситуация, когда несколько
файлов группируются в одно пространство имен. Например, файл Point.cs может со=
держать класс Point, а файл T h r e e D S p a c e . c s — класс ThreeDSpace, описываю­
щий свойства Евклидова пространства. Вы можете объединить Point.cs, ThreeD­
Space . cs и другие исходные файлы С# в пространство имен MathRoutines (и, веро­
ятно, в библиотечную сборку MathRoutines). Каждый файл будет помещать свой код
в одно и то же пространство имен. (В действительности пространство имен составляют
классы в этих исходных файлах, а не файлы сами по себе.)
Пространства имен служат для следующих целей.
Пространства имен помещают груши к грушам, а не к яблокам. Как при
кладной программист, вы можете не без оснований предполагать, что все классы,
составляющие пространство имен MathRoutines, имеют отношение к математи­
ческим вычислениям. Так что поиск некоторой математической функции следует
начать с просмотра классов, составляющих пространство имен MathRoutines.
Пространства имен позволяют избежать конфликта имен. Например, библио
тека для работы с файлами может содержать класс Convert, который преобразу­
ет представление файла одного типа к другому. В то же время библиотека перево­
да может содержать класс с точно таким же именем. Назначая этим двум множе­
ствам классов пространства имен FilelO и TranslationLibrary, вы
устраняете проблему: класс FilelO. Convert, очевидно, отличается от класса
TranslationLibrary.Convert.

Объявление пространств имен
Пространства имен объявляются с использованием ключевого слова namespace, за
которым следует имя и блок в фигурных скобках. Классы в этом блоке являются частью
пространства имён,
namespace

MyStuff

{

422

Часть VII. Дополнительные главы

class MyClass {}
class UrClass {}
}
В этом примере классы MyClass и UrClass являются частью пространства имен
MyStuff.
Кроме классов, пространства имен могут содержать другие типы, такие как структуры и
интерфейсы. Одно пространство имен может также содержать вложенные пространства имен
с любой глубиной вложенности. У вас может быть пространство имен Namespace2, вло­
женное в Namespacel, как показано в следующем фрагменте исходного текста:
namespace Namespacel
{
// Классы в Namespacel
namespace Namespace2

{
// Классы в Namespace2
public class Class2
public void AMethod() { }
}
}
}

Для вызова метода из Class2 в Namespace2 откуда-то извне пространства имен
Namespacel применяется следующая запись:
Namespacel.Namespace2.Class2.AMethod();
Пространства имен неявно открыты, так что для них нельзя использовать специфика­
торы доступа, даже спецификатор public.
Удобно добавлять к пространствам имен в ваших программах название вашей
фирмы: MyCompany .MathRoutines. (Конечно, если вы работаете на фирме.
Вы можете также использовать свое собственное имя. Я бы мог применять для
своих программ что-то наподобие CMSCo.MathRoutines или Sphar.MathRoutines.) Добавление названия фирмы предупреждает коллизии имен в вашем
коде при использовании двух библиотек сторонних производителей, у которых ока­
зывается одно и то же базовое имя пространства имен, например, MathRoutines.
Такие "имена с точками" выглядят как вложенные пространства имен, но на самом
деле это одно пространство имен, так что System. Data — это полное имя Пространст­
ва имен, а не имя пространства имен Data, вложенного в пространство имен System.
Такое соглашение позволяет проще создавать несколько связанных пространств имен,
таких как System. 10, System. Data и System. Text.
Visual Studio Application Wizard помещает каждый формируемый им класс
в пространство имен, имеющее такое же имя, как и создаваемый им каталог.
Взгляните на любую программу в этой книге, созданную Application Wizard.
Например, программа AlignOutput размещается в папке AlignOutput.
Имя исходного ф а й л а — Program.cs, соответствующее имени класса по
умолчанию. Имя пространства имен в Program.cs то же, что и имя папки:
AlignOutput. (Можно изменить любое из этих имен, только делайте это ос­
торожно и аккуратно. Изменение имени общего пространства имен проекта
выполняется в окне Properties проекта.)

Глава 19. Работа с файлами и библиотеками

423

Если вы не помещаете ваши классы в пространство имен, С# сам поместит их в глобаль­
ное пространство имен. Однако лучше использовать собственные пространства имен.

Важность пространств имен
Самое важное в пространствах имен с практической точки зрения то, что от
расширяют описание управления доступом, о котором говорилось в главе 11,
"Классы" (где были введены такие ключевые слова, как public, private
protected, internal и protected internal). Пространства имен рас­
ширяют управление доступом с помощью дальнейшего ограничения на доступ
к членам класса.
Реально пространства имен влияют не на доступность, а на видимость. По умолчанию.
классы и методы в пространстве имен NamespaceA невидимы классам в пространстве
имен NamespaceB, независимо от их спецификаторов доступа. Но можно сделать мас­
сы и методы из пространства имен NamespaceB видимыми для пространства имен
NamespaceA. Обращаться вы можете только к тому, что видимо для вас.

Видимы ли вам необходимые классы и методы?
Для того чтобы определить, может ли класс Classl в пространстве имен NamespaceA
вызывать NamespaceB. Class2 . AMethod ( ) , рассмотрим следующие два элемента.
1. Видим ли класс Class2 из пространства имен NamespaceB вызывающему
классу Classl? Это вопрос видимости пространства имен, который будет вскоре
рассмотрен.
2. Если ответ на первый вопрос — "да", то "достаточно ли открыты" Class2 и его
метод AMethod () классу Classl для доступа? "Достаточная открытость" опре­
деляется как наличие спецификаторов доступа нужной степени строгости с точки
зрения вызывающего класса Classl. Это вопрос управления доступом, рассмат­
ривающийся в главе 11, "Классы".
Если Class2 находится в сборке, отличной от сборки Classl, он должен быть
открыт (publ ic) для Classl для доступа к его членам. Если же это одна и та же
сборка, Class2 должен быть объявлен как минимум как internal. Классы мо­
гут быть только public, protected, internal или private.
Аналогично, метод
уровень доступа в
internal в список
зывается в главе 11,

класса Class2 должен иметь как минимум определенный
каждой из этих ситуаций. Методы добавляют protected
спецификаторов доступа класса. Детальнее об этом расска­
"Классы".

Вы должны получить ответ "да" на оба поставленных вопроса, чтобы класс Classl
мог вызывать методы Class2.
Остальной приведенный здесь материал посвящен вопросам использования и види­
мости пространств имен.

Как сделать видимыми классы и методы в другом пространстве имен
С# предоставляет два пути сделать элементы в пространстве имен NamespaceB ви­
димыми в пространстве имен NamespaceA.

424

Насть VII. Дополнительные главы

Применяя полностью квалифицированные имена из пространства имен Name­
spaceB при использовании их в пространстве имен NamespaceA. Это приводит
к коду наподобие приведенного, начинающемуся с имени пространства имен,
к которому добавляется имя класса и имя метода:
System.Console.WriteLine("my s t r i n g " ) ;
Устраняя необходимость в полностью квалифицированных именах в пространстве
имен

NamespaceA

посредством

директивы

using

для

пространства

имен

NamespaceB:
using NamespaceB;
Программы в этой книге используют последний способ — директиву using. Полно­
стью квалифицированные имена и директивы using будут рассмотрены в двух следую­
щих разделах.

Доступ к классам с использованием полностью
квалифицированных имен
Пространство имен класса является составной частью его расширенного имени, что
приводит к первому способу обеспечения видимости класса из одного пространства
имен в другом. Рассмотрим следующий пример, в котором не имеется ни одной директи­
вы using для упрощения обращения к классам в других пространствах имен:
namespace MathRoutines
{
class Sort
{

public void SomeFunction(){}
namespace

Paint

{
public class

{

PaintColor

public PaintColor(int nRed, int nGreen,
public void Paint() {}
public static- void StaticPaint () {}

int nBlue)

{}

}
namespace MathRoutines
{
public class Test
{
static public void Main(string[]

args)

// Создание объекта типа Sort из того же пространства
// имен, в котором мы находимся, и вызов некоторой
// функции
Sort obj = new S o r t ( ) ;
obj.SomeFunction();
// Создание объекта в другом пространстве имен —
// обратите внимание, что пространство имен должно
// быть явно включено в каждую ссылку на класс

Глава 19. Работа с файлами и библиотеками

425

Paint.PaintColor black = new Paint.PaintColor(0, 0, 0);
black.Paint();
Paint.PaintColor.StaticPaint();

}

}

В этом случае классы Sort и Test содержатся внутри одного и того же пространст­
ва имен MathRoutines, хотя и объявляются в разных местах файла. Это пространство
имен разбито на две части (в данном случае в одном и том же файле).
В обычной ситуации Sort и Test оказались бы в различных исходных файлах
С#, которые вы бы собрали в одну программу.
Функция Test. Main () может обращаться к классу Sort без указания его про­
странства имен, так как оба эти класса находятся в одном и том же пространстве имен,
Однако Main () должна указывать пространство имен Paint при обращении к
PaintColor, как это сделано в вызове Paint, PaintColor. StaticPaint ( ) . Здесь
использовано полностью квалифицированное имя.
Обратите внимание, что вам не требуется принимать специальных мер при обраще­
нии к black. Paint ( ) , поскольку класс и пространство имен объекта black известны

Директива using
Обращение к классу с использованием полностью квалифицированного имени быст
ро становится раздражающим. С# позволяет избежать излишнего раздражения с помо­
щ ь ю ключевого слова using. Директива using добавляет указанное пространство имен
в список пространств имен по умолчанию, в которых С# выполняет поиск при разреше­
нии имени класса. Следующий пример компилируется без каких-либо замечаний:
namespace Paint
{
public class PaintColor

{
public PaintColor(int nRed, int nGreen, int nBlue)
public void Paint() {}
public static void StaticPaint() {}

{}

namespace MathRoutines

{
// Добавляем Paint к пространствам имен, в которых
// выполняется автоматический поиск
using Paint;
public class Test
{
static public void Main(string[] args)
{
// Создаем объект в другом пространстве имен —
// название пространства имен не требуется включать в
// имя, поскольку само пространство имен было включено
// полностью с использованием директивы "using"
PaintColor black = new PaintColor(0, 0, 0 ) ;

426

Часть VII. Дополнительные главы

black.Paint();
PaintColor.StaticPaint();

Директива using говорит компилятору: "Если ты не в состоянии найти определен­
ный класс в текущем пространстве имен, посмотри еще и в этом пространстве имен, мо­
жет, ты найдешь его там". Можно указать любое количество пространств имен, но все
они должны быть указаны в самом начале программы (либо внутри, либо снаружи блока
пространства имен), как описано ранее в разделе "Объявление пространств имен".
Все программы включают директиву using System,-. Эта команда дает
программе автоматический доступ к функциям, включенным в системную
библиотеку, таким как WriteLine ().

Использование полностью квалифицированных имен
Приведенная далее демонстрационная программа NamespaceUse иллюст­
рирует влияние пространств имен на видимость и использование директивы
using и полностью квалифицированных имен, чтобы обеспечить види­
мость элемента.
// NamespaceUse - демонстрирует доступ к объектам с одним и
// тем же именем в разных пространствах имен
using System; // Все пространства имен нуждаются в этой
// директиве для доступа к классам типа String
// и Console
namespace MainCode
{
using LibraryCodel; // Эта директива упрощает MainCode
public class Classl

{
public void AMethod()
{
Console.WriteLine("MainCode.Classl.AMethod() ") ;

}
// Функция M a i n ( ) :
static void Main(string[] args)
// Создание экземпляра класса, содержащего функцию
// Main, в данном пространстве имен
Classl cl = new C l a s s l О ; // MainCode.Classl
cl.AMethod();
// Никогда не вызывайте
// Main() самостоятельно!
// Создание экземпляра LibraryCodel.Classl
// Приведенный далее код создает объект
// MainCode.Classl, а не тот, что вы хотели, так как
// нет ни директивы using, ни полностью
// квалифицированного имени
Classl с2 = new Classl(),•
с2.AMethod();
// Однако полностью квалифицированное имя создает
// объект требуемого класса. Имя следует

Глава 19. Работа с файлами и библиотеками

427

// квалифицировать даже при использовании директивы
// using, поскольку оба пространства имен содержат
// класс Classl
LibraryCodel.Classl сЗ = new LibraryCodel.Classl();
c3 .AMethod () ,// В то же время создание LibraryCodel.Class2 не
// требует полностью квалифицированного имени,
// поскольку имеется директива using при отсутствии
// коллизии имен; С# может без труда найти Class2
Class2 с4 = new C l a s s 2 ( ) ;
с4.AMethod();
// Создание экземпляра LibraryCode2.Classl требует
// полностью квалифицированного имени, как из-за
// отсутствия директивы using для LibraryCode2, так и
// потому, что оба пространства имен имеют Classl
// Примечание: этот способ работает даже несмотря на
// то, что LibraryCode2.Classl объявлен как internal,
// а не public, поскольку оба класса находятся в одной
// компилируемой сборке
LibraryCode2.Classl с5 = new LibraryCode2.Classl();
с5.AMethod();
// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();
}
namespace LibraryCodel
{
public class Classl
// Имя дублирует Classl в другом
{
// пространстве имен
public void AMethod() // Имя дублировано в другом
{
// пространстве имен
Console.WriteLine("LibraryCodel.Classl.AMethod()");
}

}
public class Class2
// Имя уникально, его нет в другом
{
// пространстве имен
public void AMethod()

{

Console.WriteLine("LibraryCodel.Class2.AMethod()");

namespace LibraryCode2
{
class Classl
{
public void AMethod()

{

// Нет ключевых слов, описывающих
// доступ: по умолчанию доступ —
// internal

Console.WriteLine("LibraryCode2.Classl.AMethod()");

}
428

Часть VII. Дополнительные главы

Данная демонстрационная программа включает три пространства имен: MainCode,
в которое входит один класс Classl, содержащий функцию Main() и один дополни­
тельный метод AMethod ( ) . Пространство имен LibraryCodel содержит два класса:
Classl дублирует имя Classl из пространства имен MainCode, класс Class2 уни­
кален. Пространство имен LibraryCode2 имеет один класс, также названный Classl,
имя которого создавало бы коллизию с именем Classl в другом пространстве имен, ес­
ли бы эти имена не были разделены и размещены каждое в своем пространстве имен.
Каждый их этих классов имеет метод AMethod ().
Функция Main () из MainCode . Classl пытается создать и использовать MainCode. Classl (класс-владелец M a i n O ) , LibraryCodel. Classl, Library­
Codel .Class2 и LibraryCode2 .Classl. После создания объектов функция вызы­
вает метод AMethod () каждого из них. Каждый метод идентифицирует свое местопо­
ложение. Вывод демонстрационной программы на экран выглядит следующим образом:
MainCode.Classl.AMethod()
MainCode.Classl.AMethod()
LibraryCodel.Classl.AMethod()
LibraryCodel.Class2.AMethod()
LibraryCode2.Classl.AMethod()
Нажмите для завершения

программы...

Без разделения на различные пространства имен компилятор не может позволить
дублирования имен классов в M a i n O . При применении пространств имен исходный
текст компилируется, но вы должны использовать либо директиву using, либо полно­
стью квалифицированные имена для обращения к объектам в разных пространствах
имен. Пространство имен MainCode содержит директиву using для пространства имен
LibraryCodel, так что функция M a i n O из MainCode. Classl может обратиться
к LibraryCodel. Class2 без полностью квалифицированного имени благодаря нали­
чию упомянутой директивы.
Попытка создать LibraryCodel. Classl без использования полностью квалифи­
цированного имени приводит ко второй строке в выводе на экран в рассмотренном при­
мере. Как видите, оказался создан объект класса MainCode. Classl, а не класса из
пространства имен LibraryCodel. Без применения полностью квалифицированного
имени компилятор находит Classl в пространстве имен MainCode. Остальные вызовы
работают так, как от них и ожидалось.
Однако использование директивы using для пространства имен LibraryCodel
сослужит функции Main () плохую службу при ее желании обратиться к Library­
Codel . Classl, так как имя этого класса дублировано в двух пространствах имен. Не­
смотря на наличие директивы using, единственным решением является применение
полностью квалифицированного имени для доступа к LibraryCodel. Classl.
Последний вызов в Main () для создания и использования объекта Library2 .
Classl был бы неверен, если бы пространство имен Library2 находилось
в сборке, отличной от сборки MainCode. Причина заключается в том, что спе­
цификаторы доступа у класса Library2 .Classl не указаны, так что вместо
того чтобы быть public, он является internal. Это поведение по умолча­
нию для классов без спецификаторов доступа (для методов по умолчанию ис­
пользуется private). Повторяясь еще р а з — всегда явно указывайте уровень
доступа к каждому классу и методу.

Глава 19. Работа с файлами и библиотеками

429

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

В главе 21, "Использование интерфейса Visual Studio", вы познакомитесь с тем, как
создавать проект с несколькими . СS-файлами. Даже при разбиении проекта на несколь­
ко файлов (обычно по одному классу в файле) один проект равен одной скомпилированной сборке. Библиотеки классов располагаются в файлах с расширением . DLL и не яв­
ляются выполнимыми программами сами по себе. Они служат для поддержки других
программ, предоставляя им полезные классы и методы.
Простейшее определение проекта библиотеки классов — это классы, не содер­
жащие функции Main ( ) , что отличает библиотеку классов от выполнимой
программы.
Visual Studio 2005 позволяет построить либо . ЕХЕ, либо . DLL (либо решение, со­
держащее их оба). В следующем разделе поясняются основы создания ваших собствен­
ных библиотек классов. Не беспокойтесь: рабочая лошадка С# вытянет за вас и этот груз.
Менее дорогие версии Visual Studio 2005, такие как Visual С# Express, не позволяют
собирать библиотеки классов. Однако вам будет показан один трюк, позволяющий обой­
ти данное ограничение.

Создание проекта библиотеки классов
Для формирования нового проекта библиотеки в полной версии Visual Studio 2005
выберите тип проекта Class Library в диалоговом окне New Project. Затем пропустите
следующий раздел главы и переходите к созданию классов, которые будут составлять
библиотеку.
Если вы используете Visual С# Express, процесс создания нового проекта биб­
лиотеки будет немного более сложным. Несмотря на снижение функциональ­
ности, которому Microsoft подвергла Visual С# Express, если выполнить опи­
санные далее действия, все равно можно сформировать библиотеку классов.

1. При создании нового проекта создавайте Console Application (или Win­
dows Application).
Если вы не уверены в том, как это делается, вернитесь к главам 1, "Создание вашей
первой Windows-программы на С#", и 2, "Создание консольного приложения на C#",
Обычно библиотека классов формируется под видом консольного приложения.
2. В Solution Explorer удалите файл Program.cs, закройте и сохраните ре­
шение.
Поскольку библиотека классов не может содержать функцию Main ( ) , вы просто
удаляете файл с ней.
3. Запустите Блокнот Windows или другой обычный текстовый редактор и от­
кройте в нем файл ProjectName.csproj из вновь созданного вами проекта.

430

Часть VII. Дополнительные главы

Он выглядит страшновато, но это всего лишь масса информации о программе, за­
писанная с использованием языка XML.
4. Воспользуйтесь командой меню Edit"=>Find для того, чтобы найти строку:
Exe
Если вы создали проект приложения Windows, а не консольного приложения,
найдите строку
WinExe
Это примерно восьмая строка в файле.
5. Замените в найденной строке Ехе (или WinExe) на Library:
Library
Вот и все!
6. Сохраните файл и заново откройте проект в Visual С# Express.
7. Добавляйте в проект новые классы и работайте. Только убедитесь, что вы не раз­
местили где-то в проекте функцию Main ().
Когда вы соберете новый проект, то получите . DLL-файл, а не . ЕХЕ-файл.

Создание классов для библиотеки
После того как вы сформировали проект библиотеки классов, вы создаете
классы, составляющие эту библиотеку. Приведенный далее пример
ClassLibrary демонстрирует простую библиотеку классов, которую вы
сможете увидеть в действии (в примере показан как исходный текст библио­
теки, так и описываемого далее драйвера).
// ClassLibrary - простая библиотека классов и ее
// программа-драйвер
// Файл ClassLibrary.cs
using System;
namespace ClassLibrary

{
public

{

class MyLibrary

public

void

LibraryFunctionl()

{
Console.WriteLine("Это

LibraryFunctionl()");

}
public

int LibraryFunction2(int

input)

{
Console.Write("Это LibraryFunction2(), " +
"возвращает { о } , i n p u t ) ;
return input; // Возвращает аргумент

}
}
// Драйвер — в отдельном проекте
// Файл Program.cs
using System;

Глава 19. Работа с файлами и библиотеками

431

using ClassLibrary;
namespace
{
class

// Вам надо использовать эту библиотеку
/ / в программе
ClassLibraryDriver

Program

{
static void Main(string[]

args)

{

}

}

}

// Создание объекта библиотеки и использование его
// методов
MyLibrary ml = new M y L i b r a r y ( ) ;
ml.LibraryFunctionl();
// Вызов статической функции посредством класса
int nResult = MyLibrary.LibraryFunction2(27);
Console . WriteLine (nResu-lt. ToString () ) ;
// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
"завершения программы.. . ") ;
Console.Read();

Вывод тестовой программы-драйвера, описанной в следующем разделе, выглядит
следующим образом:
Это LibraryFunctionl()
Это LibraryFunction2(), возвращает 27
27
Нажмите для завершения программы...
Библиотеки зачастую предоставляют только статические функции. В этом случае не
нужно инстанцировать библиотечный объект — можно просто вызвать функцию по­
средством класса.

Создание проекта драйвера
Сама по себе библиотека классов не делает ничего, так что вам нужна программадрайвер, небольшаявыполнимая программа, которая тестирует библиотеку в процессе
разработки путем вызова ее методов. Для создания драйвера для проекта ClassLi­
brary выполните следующие действия.
1. Щелкните правой кнопкой мыши на имени решения в окне Solution Ex­
plorer проекта ClassLibrary и выберите Add : New Project.
Тем самым вы добавите проект в то же решение, в котором находится тестируе­
мая библиотека классов.
2. В диалоговом окне New Project выберите C o n s o l e Application и назовите
его ClassLibraryDriver или как вам заблагорассудится.
3. Находясь в диалоговом окне New Project, укажите местоположение про­
екта, щелкнув на кнопке B r o w s e рядом с полем L o c a t i o n . Перейдите в
папку, в которой вы хотите хранить проект драйвера, и щелкните на
кнопке O p e n .
432

Часть VII. Дополнительные главы

Где именно вы расположите проект, зависит от того, как вы хотите организовать
ваше решение. Вы можете поместить папку ClassLibraryDriver в той же
общей папке, что и папку ClassLibrary, или вложить ее в папку ClassLi­
brary. Демонстрационная программа ClassLibrary в этом разделе придер­
живается первого подхода.
Выбор местоположения не зависит от того, что вы добавляете новый проект не­
посредственно в решение ClassLibrary. Папки этих двух проектов могут на­
ходиться в совершенно разных местах.
4. Щелкните на кнопке ОК для того, чтобы закрыть диалоговое окно New
Project и создать проект.
Мастер Visual Studio АррWizard создаст папку проекта вместе с файлами проекта.
В окне Solution Explorer вы увидите два проекта: ClassLibrary и ClassLi­
braryDriver.
5. Щелкните правой кнопкой мыши на проекте ClassLibraryDriver и вы­
берите в меню Set as Startup Project.
Тем самым вы указываете С#, где находится функция Main () для данного реше­
ния. Она должна находиться в сборке драйвера, но не в сборке библиотеки классов.
6. В файле Program, cs проекта ClassLibraryDriver добавьте в функцию
Main () исходный текст наподобие приведенного:
MyLibrary myLib = new M y L i b r a r y ( ) ;

// Или что именно вы
// хотите вызывать

// вызов библиотечной функции
myLib.LibraryFunctionl();
// Вызов статической функции
int result = MyLibrary.LibraryFunction2();
Другими словами, напишите программу, которая использует классы и методы из
библиотеки. Вы уже сталкивались с этим ранее как в данной главе, так и в других
главах книги — например, когда вызывали метод WriteLine () класса Con­
sole из библиотеки .NET Framework. (Console находится в пространстве имен
System в файле mscorlib . dll.)
Код примера библиотеки и драйвера приведен выше — см. листинг демонстраци­
онной программы ClassLibrary.
7. Выберите команду меню P r o j e c t s Add Reference.
8. В диалоговом окне Add Reference щелкните на вкладке Projects. Выберите
ваш проект ClassLibrary и щелкните на кнопке ОК.
Вы можете также добавить директиву using для пространства имен ClassLi­
brary в файл Program, cs проекта ClassLibraryDriver, чтобы сэконо­
мить на набираемом тексте.
В любой программе, которую вы напишете в будущем, достаточно включить дирек­
тиву using для пространства имен вашей библиотеки классов и добавить ссылку на
.DLL-файл, содержащий библиотеку, чтобы иметь возможность использовать библио­
течные классы в своей программе. Именно так программы в данной книге применяют
классы из библиотеки .NET Framework.

Глава 19. Работа с файлами и библиотеками

433

Хранение данных в файлах
Консольные программы в настоящей книге в большинстве случаев получают входные
данные с консоли и выводят результат работы на консоль. Однако стоит заметить, что
вероятность встретить в реальном мире программу, не работающую с файлами, сопоста
вима с вероятностью встретить в академическом институте рекламу казино в Ницце.
Классы для работы с файлами определены в пространстве имен System.IO. Базо
вым классом для файлового ввода-вывода является класс FileStream. Для работы с файлом
программист должен его открыть. Команда open подготавливает файл
к работе и возвращает его дескриптор. Обычно дескриптор — это просто число, которое
используется всякий раз при чтении из файла или записи в него.

Асинхронный ввод-вывод: есть ли что-то хуже ожидания?
Обычно программа ожидает завершения ее запроса на ввод-вывод и только
затем продолжает выполнение. Вызовите метод read ( ) , и в общем случае
вы не получите управление назад до тех пор, пока данные из файла не будут считаны
Такой способ работы называется синхронным вводом-выводом.
Классы С# System.IO поддерживают также и асинхронный ввод-вывод. При ис­
пользовании асинхронного ввода-вывода вызов read () тут же вернет управление
программе, позволяя ей заниматься чем-то еще, пока ее запрос на чтение данных из
файла выполняется в фоновом режиме. Программа может проверить флаг выполнения
запроса, чтобы узнать, завершено ли его выполнение.
Это чем-то напоминает варианты приготовления гамбургеров. При синхронном изго­
товлении вы нарезаете мясо и жарите его, после чего нарезаете лук и выполняете все
остальные действия по приготовлению гамбургера. При асинхронном приготовлении
вы начинаете жарить мясо, и, поглядывая на него, тут же, не дожидаясь готовности
мяса, начинаете резать лук и делать все остальное.
Асинхронный ввод-вывод может существенно повысить производительность про­
граммы, но при этом вносит дополнительный уровень сложности.
С# использует более интуитивный подход. Он связывает каждый файл с объектом
класса FileStream. Конструктор класса FileStream открывает файл, а методы
FileStream выполняют операции ввода-вывода.
F i l e S t r e a m — не единственный класс, который может осуществлять файловый
ввод-вывод. Однако он предоставляет хорошую основу для работы с файлами, выполняя
90% всех ваших нужд по работе с ними. Это корневой класс, описываемый в данном
разделе. Он достаточно хорош для С# и для вас.
FileStream— фундаментальный класс. Весь набор его действий — это открытие фай­
ла, чтение и запись блока байтов. К счастью, пространство имен System.І0 содержит, по­
мимо прочего, следующий набор классов, которые обернуты вокруг FileStream и предос­
тавляют более простые и богатые возможности.
BinaryReader/BinaryWriter — пара потоковых классов, которые содержат
методы для чтения и записи каждого из типов-значений: ReadChar (), Write-

434

Часть VII. Дополнительные главы

Char (), ReadByte (), WriteByte () и так далее. Эти классы полезны для чте­
ния и записи объекта в бинарном (не читаемом человеком) формате, в противопо­
ложность текстовому формату. Для работы с бинарными данными можно исполь­
зовать массив или коллекцию элементов типа Byte.
TextReader/TextWriter— пара классов для чтения символов (текста).
классы

предоставляются

в

двух

видах

(наборах

подклассов):

Эти

Strin-

gReader/StringWriter и StreamReader/StreamWriter.
StringReader/StringWriter— простые потоковые классы, которые огра­
ничены чтением и записью строк. Они позволяют рассматривать строку как файл,
предоставляя альтернативу доступу к символам строк с помощью записи с ис­
пользованием квадратных скобок ( [ ] ) , цикла foreach или методов класса
String наподобие Split (), Concatenate () и IndexOf ( ) . Вы считываете
и записываете строки почти так же, как и файлы. Этот метод полезен для длинных
файлов с сотнями или тысячами символов, которые вы хотите обработать вместе.
Методы в этих классах аналогичны методам классов StreamReader и StreamWriter, описываемым далее.
StreamReader/StreamWriter— более интеллектуальные классы чтения
и записи текста. Например, класс StreamWriter имеет метод WriteLine (),
очень похожий на метод класса Console. StreamReader имеет соответствую­
щий метод ReadLine () и очень удобный метод ReadToEnd ( ) , собирающий
весь текстовый файл в одну группу и возвращающий считанные символы как
строку string (которую вы можете затем использовать с классом StringReader, циклом foreach и тому подобным).
TextReader/TextWriter применяются как сами по себе, так и их более удобные
подклассы, такие как StreamReader/StreamWriter.
В следующем разделе будут рассмотрены демонстрационные программы FileWrite и FileRead, которые иллюстрируют способы использования классов для тек­
стового ввода-вывода.

Использование Stream Writer
Программы генерируют два вида вывода.
Некоторые программы пишут блоки данных в виде байтов в чисто бинар­
ном формате. Этот тип вывода полезен для эффективного сохранения объ­
ектов (например, файл объектов Student, которые сохраняются между
запусками программы в файле на диске).
Большинство программ читает и записывает информацию в виде текста,
который может читать человек. Классы StreamWriter и StreamReader являются наиболее гибкими классами для работы с данными
в таком виде.
Данные в удобном для чтения человеком виде ранее назывались ASCIIстроками, а сейчас — ANSI-строками. Эти два термина указывают названия
организаций по стандартизации, которые определяют соответствующие
стандарты. Однако кодировка ANSI работает только с латинским алфавитом

Глава 19. Работа с файлами и библиотеками

435

и не имеет кириллических символов, символов иврита, арабского языка или хины
не говоря уж о такой экзотике, как корейские, японские или китайские иероглифы
Гораздо более гибким является стандарт Unicode, который включает ANSI-символ
как свою начальную часть, а кроме них — массу других алфавитов, включая все пе
речисленные выше. Unicode имеет несколько форматов, именуемых кодировками
форматом по умолчанию для С# является UTF8.
Приведенная далее демонстрационная программа FileWrite считывает
строки данных с консоли и записывает их в выбранный пользователем файл
// FileWrite - запись ввода с консоли в текстовый файл
using System;
using System.10; // Требуется для работы с файлами
namespace FileWrite

{
- public class Program
{
public static void Main(string []

args)

// Создание объекта для имени файла — цикл while
// позволяет пользователю продолжать попытки до тех
// пор, пока файл не будет успешно открыт
StreamWriter sw = null;
string sFileName = "";
while(true)

{

try

{
// Ввод имени файла для вывода (просто Enter для
// завершения программы)
Console.Write("Введите имя файла "
+ "(пустое имя для завершения):");
sFileName = Console.ReadLine();
if (sFileName.Length == 0)
{
// Имени файла нет — выходим из цикла
break;

}
// Открываем файл для записи; если файл уже
// существует, генерируем исключение:
// FileMode.CreateNew - для создания файла, если
// он еще не существует и генерации исключения при
// наличии такого файла; FileMode.Append для
// создания нового файла или добавления данных к
// существующему файлу; FileMode.Create для
// создания нового файла или урезания уже
// имеющегося до нулевого размера. Возможные
// варианты FileAccess: FileAccess.Read,
// FileAccess.Write, FileAccess.ReadWrite
FileStream fs = File.Open(sFileName,
FileMode.CreateNew,
FileAccess.Write);
// Генерируем файловый поток с UTF8-символами (по

436

Часть VII. Дополнительные главы

// умолчанию второй параметр дает UTF8, так что он
// может быть опущен)
sw = new StreamWriter(fs,
System.Text.Encoding.UTF8);
// Считываем по одной строке, выводя каждую из них
// в FileStream для записи
Console.WriteLine("Введите текст " +
"(пустую строку для в ы х о д а ) " ) ;
while(true)
{
// Считываем очередную строку с консоли; если
// строка пуста, завершаем цикл
string slnput = Console.ReadLine();
if (slnput.Length == 0)

{
break;

}
// Записываем считанную строку в файл вывода
sw.WriteLine(slnput);
}
// Закрываем созданный файл
sw.Close();
sw = null; // Желательно обнуление ссылочных
// переменных после использования

}
catch(IOException fe)
{
// Произошла ошибка при работе с файлом — о ней
// надо сообщить пользователю вместе с полным
// именем файла
string sDir = Directory.GetCurrentDirectory();
string s = Path.Combine(sDir, sFileName);
Console.WriteLine("Ошибка с файлом {О}", s ) ;
// Теперь выводим сообщение об ошибке из
// исключения
Console.WriteLine(fe.Message);
// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();

Демонстрационная программа FileWrite использует пространства имен Sys­
tem. 10 и System. Пространство имен System. 10 содержит классы, предназначенные
дня работы с файлами.
Обратитесь к разделу справочной системы, посвященному пространству имен Sys tern.Text. Одним из его более полезных классов является StringBuilder, ко­
торый предоставляет эффективный подход для работы со сложными строками, со­
стоящими из нескольких частей. Это гораздо более эффективный способ работы,
чем использование оператора + для конкатенации большого количества строк.

Глава 19. Работа с файлами и библиотеками

437

Программа начинает работу с функции Main ( ) , которая включает цикл while со
держащий try-блок. В этом нет ничего необычного для программ, работающих с фай
лами. (В следующем разделе, где описывается работа с классом StreamReader,ис
пользуется немного другой подход, дающий те же результаты.)
Все функции ввода-вывода вставлены в try-блок с перехватом, в котором гене
рируется соответствующее сообщение об ошибке. В этом надо быть очень акку
ратным, так как файловый ввод-вывод является источником множества ошибок
таких как отсутствующие файлы и каталоги, неверные пути и тому подобное.
Цикл while служит двум следующим целям.
Он позволяет программе вернуться и повторить попытку в случае, если произошла
ошибка ввода-вывода. Например, если демонстрационная программа не может
найти файл, который планирует читать пользователь, она может запросить у него
имя файла еще раз, а не просто оставить его с сообщением об ошибке.
Команда break в программе переносит вас за try-блок, тем самым предоставляет
удобный механизм для выхода из функции или программы. Не забывайте о том;
что break работает только в пределах цикла, в котором вызвана эта команда,
Демонстрационная программа FileWrite считывает имя создаваемого файла с ком
соли. Программа прекращает работу путем выхода из цикла с помощью команды break,
если пользователь вводит пустое имя файла. Ключ к программе заключается в следую­
щих строках:
FileStream fs = File.Open(sFileName, FileMode.CreateNew,
FileAccess.Write);
/ / ...
sw = new StreamWriter(fs, System.Text.Encoding.UTF8);
В первой строке программа создает объект FileStream, который представляет
файл, записываемый на диск. Конструктор FileStream использует следующие три ар­
гумента.
Имя файла: это просто имя файла, который следует открыть. Простое имя файла
наподобие filename. txt предполагает, что файл находится в текущем каталоге
(для демонстрационной программы FileWrite это подкаталог \bin\Debugn
каталоге проекта; словом, это каталог, в котором находится сам . ЕХЕ-файл). Имя
файла,

начинающееся

с

обратной

косой

черты

наподобие

\directory\

filename.txt, рассматривается как полный путь на локальной машине. Имя
файла,

начинающееся с двух обратных косых черт (например,

\\machine\

directory\filename.txt), указывает файл, расположенный на другой ма­
шине в вашей сети. Кодировка файла — существенно более сложный вопрос, вы­
ходящий за рамки данной книги.
Режим работы с файлом: этот аргумент определяет, что вы намерены делать
с файлом. Основными режимами работы с файлом для записи являются создание
(CreateNew), добавление к файлу (Append) и перезапись (Create). Creat­
eNew создает новый файл, но генерирует исключение IOException, если такой
файл уже существует. Простой режим Create создает файл, если он отсутствует,
но если он есть, то просто перезаписывает его. И наконец, Append создает файл,

438

Часть VII. Дополнительные главы

если он не существует, но если он имеется, открывает его для дописывания ин­
формации в конец файла.
Тип доступа: файл может быть открыт для чтения, записи или для обеих операций.
Класс FileStream имеет ряд конструкторов, у каждого из которых один или
оба аргумента, отвечающие за режим открытия и тип доступа, имеют значения
по умолчанию. Однако, по моему скромному мнению, вы должны указывать
эти аргументы явно, поскольку это существенно повышает понятность про­
граммы. Поверьте, это хороший с о в е т — значения по умолчанию могут быть
удобны для программиста, но не для того, кто будет читать его код.
В следующей строке программа "оборачивает" вновь открытый файловый объект
FileStream в объект StreamWriter. Класс StreamWriter служит оберткой для
объекта FileStream, которая предоставляет набор методов для работы с текстом.
Такой вид "оборачивания" одного класса вокруг другого представляет собой полез­
ный программный шаблон проектирования— StreamWriter "оборачивается"
(содержит ссылку) вокруг другого класса FileStream и расширяет интерфейс
FileStream, добавляя некоторые мелкие удобства. Методы StreamWriter
вызывают методы внутреннего объекта FileStream. Это — рассматривавшееся
в главе 12, "Наследование", отношение СОДЕРЖИТ.
Первый аргумент конструктора StreamWriter— объект FileStream. Второй аргу­
мент указывает используемую кодировку. Кодировка по умолчанию — UTF8.
Вы не должны указывать кодировку при чтении файла. Дело в том, что StreamWriter
записывает тип применяемой кодировки в первых трех байтах файла. StreamReader счи­
тывает эти три байта при открытии файла и определяет тип используемой кодировки. Сокры­
тие такого рода деталей представляет собой одно из преимуществ хорошей библиотеки.
Затем программа FileWrite начинает чтение строк, вводимых с консоли. Программа
завершает работу при считывании пустой строки, но до этого она собирает все считанные
строки и записывает их, используя метод WriteLine () класса StreamWriter.
Подобие

StreamWriter.WriteLine()

и

Console.WriteLine()—

больше, чем простое совпадение.

И наконец, файл закрывается с помощью вызова sw. Close ().
Обратите внимание, что программа обнуляет ссылку sw по закрытии файла.
Файловый объект становится бесполезен после того, как файл закрыт. Правила
хорошего тона требуют обнулять ссылки после того, как они становятся недей­
ствительны, так, чтобы обращений к ним больше не было (если вы попытаетесь
это сделать, то будет сгенерировано исключение).
Блок catch напоминает футбольного вратаря: он стоит здесь для того, чтобы ловить
все исключения, которые могут быть сгенерированы в программе. Он выводит сообще­
ние об ошибке, включая имя вызвавшего ее файла. Однако выводится не просто имя
файла, а его полное имя, включая путь к нему. Это делается посредством класса Direc­
tory, который позволяет получить текущий каталог и добавить его перед введенным
именем файла с использованием метода Path. Combine () (Path — класс, разработан-

Глава 19. Работа с файлами и библиотеками

439

ный для работы с информацией о путях, a Directory предоставляет свойства и метода
для работы с каталогами).
Путь — это полное имя каталога. Например, если имя файла — с: \user\directory\!
text. txt, то его путь — с : \user\directory.
Метод Combine () достаточно интеллектуален, чтобы разобраться, что для
файла наподобие c:\test.txt Path О не является текущим каталогом
Path. Combine () представляет также наиболее безопасный путь, гарантирую­
щий корректное объединение двух частей пути, включая символ-разделитель (\)
между ними. (В Windows символ-разделитель п у т и — \. Вы можете получить
корректный разделитель для операционной системы, под управлением которой
запущена программа, с помощью Path.DirectorySeparatorChar. Библио­
тека .NET Framework изобилует такого рода возможностями, существенно облег­
чая программистам на С# написание программ, которые должны работать под
управлением нескольких операционных систем.)
Достигнув конца цикла w h i l e — либо после выполнения try-блока, либо после
блока catch, — программа возвращается к началу цикла и позволяет пользователю за­
писать другой файл.
Вот как выглядит пример выполнения демонстрационной программы (пользовательский
ввод выделен полужирным шрифтом).
Введите имя файла (пустое имя для завершения):TestFilel.txt
Введите текст (пустую,строку для выхода)
Это какой-то текст
И еще
И еще р а з . . .
Введите имя файла (пустое имя для завершения):TestFilel.txt
Ошибка с файлом С:\C#Programs\FileWrite\bin\Debug\TestFilel.txt
The file already exists.
Введите имя файла (пустое имя для завершения):TestFile2.txt
Введите текст (пустую строку для выхода)
Я ошибся - мне надо было ввести
имя файла TestFile2.
Введите имя файла (пустое имя для з а в е р ш е н и я ) :
Нажмите для завершения программы...
Все отлично работает, пока некоторый текст вводится в файл TestFilel. txt. Но
при попытке открыть файл TestFilel.txt заново программа выводит сообщение
The

file

already exists (файл уже существует). Обратите внимание на полный

путь к файлу, выводимый вместе с сообщением об ошибке. Если исправить ошибку
и ввести имя TestFile2 . txt, все продолжает отлично работать.

Повышение скорости чтения с использованием StreamReader
Запись файла — дело стоящее, но совершенно бесполезное, если вы не мо­
жете позже прочесть записанное. Приведенная далее демонстрационная про­
грамма считывает текстовый файл, например, созданный демонстрационной
программой FileWrite или с помощью Блокнота.

440

Часть VII. Дополнительные главы

Глава :

// FileRead - читает текстовый файл и выводит на консоль его
// содержимое
using System;
using System. 10;
namespace FileRead

{

public class Program
public static void Main(string [] args)

{

// Нам нужен объект для чтения файла
StreamReader sr;
string sFileName = " ";
// Пытаемся получить корректное имя файла до тех пор,
// пока наконец его не получим (для выхода из
// программы надо использовать комбинацию клавиш
// )
while(true)

{
try

{
// Ввод имени файла
Console.Write("Введите имя текстового ф а й л а : " ) ;
sFileName = Console.ReadLine();
// Если пользователь ничего не ввел, генерируем
// исключение для указания, что такой ввод
// неприемлем
if (sFileName.Length == 0)

{
throw new
IOException("Введено пустое имя ф а й л а " ) ;

}
// Открываем файловый поток для чтения; если файл
// не существует - не создаем его
FileStream fs = File.Open(sFileName,
F i1eMode.Open,
FileAccess.Read);
// Преобразуем в StreamReader - этот класс
// использует первые три байта файла для
// определения использованной кодировки (но не для
// языка)
sr = new StreamReader(fs, t r u e ) ;
break;

}
// Сообщение об ошибке с указанием имени файла
catch(IOException fe)

{
Console.WriteLine("{0}\n\n", fe.Message);

}
}
// Чтение содержимого файла
Console.WriteLine("\пСодержимое файла:") ;
try

Глава 19. Работа с файлами и библиотеками

441

{
// Чтение по одной строке
while(true)

{
// Считывание строки
string slnput = sr.ReadLine() ;
// Выход, когда больше считать
if (slnput == null)

строку не удается

{
break;

}
// Вывод считанного на консоль
Console.WriteLine(slnput);

}
catch(IOException

fe)

// перехватывает все ошибки и сообщает о них
Console.Write(fe.Message);
// Закрываем файл
try

(игнорируя возможные

ошибки)

sr.Close();
catch {}
sr = null;
// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
"завершения программы.. . ") ;
Console.Read();

}
Вспомним, что текущим каталогом, использовавшимся демонстрационной програм­
мой FileRead, был подкаталог \bin\Debug в каталоге проекта FileRead (но не ка­
талог \bin\Debug в каталоге проекта FileWrite). Перед тем как вы запустите про­
грамму FileRead, поместите текстовый файл в подкаталог \bin\Debug каталогам
проекта и запомните имя этого файла, чтобы впоследствии вы могли открыть его. Дли
этого вполне подойдет копия файла TestFilel.txt, созданного демонстрационно!
программой FileWrite.
Демонстрационная программа FileRead применяет другой подход к именам фай­
лов. В ней пользователь считывает один и только один файл. Пользователь должен вве­
сти корректное имя файла, который будет считан программой. После того как программа
прочтет файл, она завершает свою работу. Если пользователь хочет прочесть второй
файл, он должен заново запустить программу.
Одно из ограничений подхода, использованного в демонстрационной программе Fil­
eRead, заключается в том, что попытки получить имя файла от пользователя продолжают­
ся до бесконечности. Если пользователь ошибается, он должен продолжать свои попытки.
Единственный выход из программы без ввода корректного имени — воспользоваться ком­
бинацией клавиш либо щелкнуть на кнопке закрытия консольного окна.

442

Часть VII. Дополнительные глава

Программа начинается с цикла while, как и демонстрационная программа FileWrite. В цикле программа получает имя файла для чтения от пользователя. Если имя
файла пустое, программа генерирует свое собственное сообщение об ошибке: Введено
пустое и м я файла. Если имя файла не пустое, оно используется для открытия объек­
та FileStream в режиме для чтения. Вызов File.Open() работает так же, как и
в демонстрационной программе FileWrite.
Первый аргумент — это имя файла.
Второй а р г у м е н т — режим открытия файла. Режим FileMode. Open гласит:
"Открыть файл, если он существует, и сгенерировать исключение, если его нет".
Другой в а р и а н т — OpenNew, который создает файл нулевой длины в случае от­
сутствия последнего. Лично я никогда не использовал этот режим (кому надо чи­
тать из пустого файла?), но мало ли — может, имеются люди, умеющие напиться
из пустого стакана?
Последний аргумент указывает на желание читать из объекта FileStream. Дру­
гие возможные варианты — Write и ReadWrite. (Кажется странным открывать
файл в программе FileRead с использованием режима Write, не правда ли?)
Полученный в результате объект f s класса FileStream оборачивается в объект sr
класса StreamReader — для предоставления удобного доступа к текстовому файлу.
Весь раздел открытия файла размещен в try-блоке в цикле while. Этот try-блок
предназначен исключительно для открытия файла. Если в процессе открытия файла про­
исходит ошибка, генерируется и перехватывается исключение, выводится сообщение об
ошибке, и программа возвращается к запросу имени файла. Однако если процесс завер­
шается благополучно, то команда break передает управление за пределы цикла в раздел
чтения файла.
Демонстрационные программы FileRead и FileWrite представляют два
разных способа обработки исключений. Вы можете поместить всю программу
в один try-блок, как в демонстрационной программе FileWrite, либо по­
местить раздел открытия файла в собственный t ry-блок. Обычно использовать
отдельные try-блоки проще, а кроме того, это позволяет генерировать более
точные сообщения об ошибках.
Когда процесс открытия файла завершен, программа FileRead считывает строку
текста из файла с помощью вызова ReadLine ( ) . Программа выводит эту строку на
консоль посредством хорошо знакомого вызова Console .WriteLine О , после чего
возвращается к считыванию очередной строки текста. Когда программа достигает конца
файла, вызов ReadLine () возвращает значение null. Когда это происходит, програм­
ма прекращает цикл чтения, закрывает объект и завершает работу.
Обратите внимание, как вызов Close () обернут в свой собственный небольшой tryблок. Блок catch без аргументов перехватывает все классы исключений (что эквивалентно
catch (Exception)). Любая ошибка, сгенерированная в Close О, перехватывается
и игнорируется. Этот блок catch предназначен для того, чтобы предотвратить распро­
странение исключения и завершение программы. Ошибка игнорируется, поскольку про­
грамма ничего не может поделать со сбоем в вызове Close ( ) , тем более что через пару
строк программа все равно завершается. Вы могли бы поместить вызов Close () в блок

Глава 19. Работа с файлами и библиотеками

443

finally после блока catch для того, чтобы гарантировать его выполнение в любом слу
чае, но в данной ситуации это излишне.
Пустой catch включен исключительно в демонстрационных целях. Пред»
ставление вызову собственного try-блока с перехватом всего, что можно,пре
дотвращает завершение программы из-за несущественных ошибок. Оли
этот метод можно использовать только если ошибка действительно некритгга
и не вредит работе программы.
Вот как выглядит пример вывода программы:
Введите имя текстового файла:TestFilex.txt
Could not find file "C:\C#Programs\FileRead\TestFilex.txt".
Введите имя текстового файла:TestFilel.txt
Содержимое файла:
Это какой-то текст
И еще
И еще раз...
Нажмите для завершения программы...
Как видите, это тот же текст, который был записан в файл TestFilel. txt, созданный в демонстрационной программе FileWrite (ведь вы не забыли скопировать его!
каталог \FileRead\bin\debug?).

444

Часть VII. Дополнительные главы

Глава 20

Работа с коллекциями
В этой главе...
У Каталог как коллекция
> Реализация коллекции LinkedLi st
> Итеративный обход коллекции LinkedList
> Реализация индексирования для упрощения доступа к объектам коллекций
> Упрощение циклического обхода коллекции с помощью нового блока итератора С#

айл представляет собой один из типов коллекций данных, но существуют и дру­
гие коллекции. Например, каталог можно рассматривать как коллекцию файлов.
Кроме того, С# предоставляет множество типов контейнеров в оперативной памяти.
Эта глава построена на основе главы 15, "Обобщенное программирование", и мате­
риале, посвященном файлам, из главы 19, "Работа с файлами и библиотеками". Основ­
ной вопрос, рассматриваемый в данной главе — проход (итерирование) по коллекциям
разного вида, от каталогов до массивов и списков всех видов. Вы также узнаете, как на­
писать собственный класс коллекции (более фундаментальный, чем рассматривавшийся
в главе 15, "Обобщенное программирование", пример очереди с п р и о р и т е т а м и ) — свя­
занный список.

Чтение и з а п и с ь — вот основное, что необходимо знать для работы с файлами.
Иименно этим и занимались демонстрационные программы FileRead и FileWrite
из главы 19, "Работа с файлами и библиотеками". Однако в ряде случаев вам просто
нужно просканировать каталог файлов в поисках чего-то.
Приведенная далее демонстрационная программа LoopThroughFiles
просматривает все файлы в данном каталоге, считывая каждый файл и вы­
водя его содержимое на консоль в шестнадцатеричном формате. (Это де­
монстрирует вам, что файл можно выводить не только в виде строк. Что -та­
кое шестнадцатеричный формат — вы узнаете немного позже.)
Если запустить эту программу в каталоге с большим количеством файлов, то
вывод шестнадцатеричного дампа может занять длительное время. Много
времени требует и вывод дампа большого файла. Либо испытайте програм­
му на каталоге покороче, либо, когда вам надоест, просто нажмите клавиши
. Эта команда должна прервать выполнение программы в любом
консольном окне.

// LoopThroughFiles - проход по всем файлам, содержащимся в
// каталоге. Здесь выполняется вывод шестнадцатеричного
// дампа файла на экран, но могут выполняться и любые иные
// действия
using System;
using System. 10,namespace LoopThroughFiles
{
public class Program

{
public static void Main(string [] args)
{
// Если каталог не указан...
string sDirectoryName;
if (args.Length == 0)
{
// ... получаем имя текущего каталога...
sDirectoryName = Directory.GetCurrentDirectory();
}
else
{
// ...в противном случае считаем, что первый
// переданный программе аргумент и есть имя
// используемого каталога
sDirectoryName = a r g s [ 0 ] ;
}
Console.WriteLine(sDirectoryName);
// Получение списка всех файлов каталога
FileInfo[] files = GetFileList(sDirectoryName);
// Проход по всем файлам списка с выводом
// шестнадцатеричного дампа каждого файла
foreach(Filelnfо file in files)
{
// Вывод имени файла
Console.WriteLine("\п\пДамп файла
{о}:",
file.FullName);
// Вывод содержимого файла
DumpHex(file);
// Ожидание подтверждения пользователя
Console.WriteLine("\пНажмите для вывода " +
"следующего ф а й л а " ) ;
Console.ReadLine() ;
}
// Файлы закончились!
Console.WriteLine("\пБольше файлов н е т " ) ;
// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();

}
// GetFileList - получение списка всех файлов в
// указанном каталоге
public static FileInfo[]
GetFileList(string sDirectoryName)
446
Часть VII. Дополнительные главы

{
// Начинаем с пустого списка
Filelnfot] files = new Filelnfо [Obtry

{
// Получаем информацию о каталоге
Directorylnfо di =
new Directorylnfо(sDirectoryName);
// В ней имеется список файлов
files = di.GetFiles();

}
catch(Exception e)
{
Console .WriteLine ( "Каталог \"" + sDirectoryName +
" \" неверен") ,Console.WriteLine(e.Message);

}

return files;
}
// DumpHex - для заданного файла выводит его содержимое
// на консоль
public static void DumpHex(Filelnfо file)
{
// Открываем файл
FileStream fs;
try
{
string sFileName = file.FullName;
fs = new FileStream(sFileName, FileMode.Open,
FileAccess.Read);
// В действительности Filelnfо предоставляет метод
// file.OpenRead(), который открывает FileStream за
// вас, если вы слишком ленивы

}
catch(Exception е)
{
Console.WriteLine("\пНе могу читать \"" +
file.FullName + " \ " " ) ;
Console.WriteLine(е.Message);
return;

}
// Построчный проход по содержимому файла
for(int nLine = 1; true; nLine++)

{
// Считываем очередные 10 байтов (это все, что можно
// разместить в одной с т р о к е ) ; выходим, когда все
// байты считаны
byte[] buffer = new byte [10] ;
int numBytes = fs.Read(buffer, 0, buffer.Length);
if (numBytes == 0)

{

return;

}
Выводим
считанные только что данные, предваряя их 447
Глава 20. //
Работа
с коллекциями

// номером строки
Console.Write("{0:D3} - ", n L i n e ) ;
DumpBuffer(buffer, numBytes),// После каждых 2 0 строк останавливаемся, так как
// прокрутка консольного окна отсутствует
if ((nLine % 20) == 0)
{
Console.WriteLine("Нажмите для вывода " +
"очередных 20 с т р о к " ) ;
Console.ReadLine();

}
}
// DumpBuffer - вывод буфера символов в виде единой
// строки в шестнадцатеричном формате
public static void DumpBuffer(byte[] buffer,
int numBytes)
for(int index = 0;

index < numBytes;

index++)

byte b = buffer[index] ,•
Console.Write("{0:X2}, ", b) ,Console.WriteLine();
}
}
}
В командной строке пользователь указывает каталог, применяемый в качестве apryl
мента программы. Приведенная далее команда выведет шестнадцатеричный дамп каждо- [
го файла из временного каталога (как текстовых, так и бинарных файлов):
loopthroughfiles
c:\randy\temp
Если не ввести имя файла, программа по умолчанию использует текущий каталог.!
(Шестнадцатеричный дамп выводит все числа в шестнадцатеричной системе счисле-j
ния — см. врезку "Шестнадцатеричные числа".)

Шестнадцатеричные числа
Как и бинарные числа (0 и 1), шестнадцатеричные числа также очень важны в компью­
терном программировании. В шестнадцатеричной системе счисления цифрами явля­
ются обычные десятичные цифры 0-9 и буквы А, В, С, D, Е, F — где А = 10, В = 11,
F = 15. Для иллюстрации (префикс Ох указывает на шестнадцатеричность выводи­
мого числа): OxD = 13. 0x10 = 16: 1*16 + 0 * 1 . 0х2А = 42: 2*16 + А*1 (здесь А*1 =
10*1). Буквы могут быть как строчными, так и прописными: F означает то же, что ит.
Эти числа выглядят причудливо, но они очень полезны, в особенности при отладке
или работе с аппаратной частью или содержимым памяти.
Демонстрационные программы FileRead и FileWrite считывали имена
файлов с консоли, в то время как в этой программе имя файла передается в ко­
мандной строке. Поверьте, вас никто не пытается запутать, а всего лишь пред­
лагаются различные варианты решения одной и той же задачи.

448

Часть VII. Дополнительные главы

Первая строка
личие аргумента в
вен 0), программа
запущена из Visual
ваться подкаталог

демонстрационной программы LoopThroughFiles определяет на­
командной строке. Если список аргументов пуст (args. Length ра­
вызывает Directory. GetCurrentDirectory ( ) . Если программа
Studio, а не из командной строки, то по умолчанию будет использо­
bin\Debug в каталоге проекта LoopThroughFiles.

Класс Directory предоставляет пользователю набор методов для работы
с каталогами, а класс Filelnf о— методы для перемещения, копирования
и удаления файлов.
Затем программа получает список всех файлов в указанном каталоге посредством вы­
зова GetFileList ( ) . Эта функция возвращает массив объектов Filelnf о. Каждый
объект Filelnf о содержит информацию о ф а й л е — например, имя файла (как полное
имя с путем, FullName, так и без пути — Name), дату его создания и время последнего
изменения. Функция Main () проходит по всему списку файлов с помощью цикла
foreach. Она выводит имя каждого файла и передает его функции DumpHex () для вы­
вода содержимого на консоль.
Пауза в конце каждого цикла позволяет программисту просмотреть выведенную
DumpHex () информацию.
Функция GetFileList () начинает работу с создания пустого списка Filelnf о,
который будет возвращен в случае ошибки.
Этот прием стоит запомнить и использовать при работе с функциями Get...List ():
если происходит ошибка, вывести сообщение о ней и вернуть пустой список.
Будьте внимательны при возврате ссылок. Например, не возвращайте ссылки
ни на одну из внутренних очередей в классе PriorityQueue в главе 15,
"Обобщенное программирование", если не хотите намеренно пригласить поль­
зователей мешать нормальной работе класса (работой не посредством методов
класса, а напрямую с очередями). Но GetFileList (} не дает вам доступа
к внутренностям одного из ваших классов, так что в данном случае все в порядке.
Затем функция GetFileList () создает объект Directorylnf о. Как и гласит его
имя, объект Directorylnf о содержит тот же вид информации о каталоге, что и объект
Filelnf о о файле. Однако у объекта Directorylnf о есть доступ к одной вещи, к ко­
торой нет доступа у объекта Filelnf о, а именно к списку файлов каталога в виде мас­
сива Filelnfо.
Как обычно, функция GetFileList () помещает код, работающий с файлами и ка­
талогами, в большой try-блок. Конструкция catch в конце функции перехватывает все
генерируемые ошибки и выводит имя каталога (которое, вероятно, введено неверно, т.е.
такого каталога не существует).
Функция DumpHex () несколько сложнее из-за трудностей в форматировании вывода.
Функция DumpHex () начинает работу с открытия файла. Объект Filelnf о содер­
жит информацию о файле, но не открывает его. Функция DumpHex () получает полное
имя файла, включая путь. Затем она открывает FileStream в режиме только для чте­
ния с использованием этого имени. Блок catch перехватывает исключение, если
FileStream не в состоянии прочесть файл по той или иной причине.
Затем DumpHex () считывает файл по 10 байт за раз и выводит их в одну строку в
шестнадцатеричном формате. После вывода каждых 20 строк программа приостанавли­
вает работу в ожидании нажатия пользователем клавиши .

Глава 20. Работа с коллекциями

449

По вертикали консольное окно по умолчанию имеет 25 строк (правда, пользо­
ватель может изменить эту настройку, добавив или убрав строки). Это означа­
ет, что вы должны делать паузу после вывода каждых 20 строк или около того,
В противном случае данные будут быстро выведены на экран и пользователь не
сможет их прочесть.
Операция деления по модулю (%) возвращает остаток после деления. То есть выраже­
ние (nLine%20) = = 0 истинно при значениях nLine, равных 20, 40, 60, 80.... Словом,
идея понятна. Это важный метод, применимый для всех видов циклов, когда нужно вы­
полнять некоторую операцию только с определенной частотой.
Функция DumpBuf fer () выводит каждый член массива байтов с использованием
управляющего элемента форматирования Х2. Х2 хотя и звучит как название какого-то
секретного военного эксперимента, означает всего лишь "вывести число в виде двух шестнадцатеричных цифр" (см. главу 9, "Работа со строками в С # " ) .
Диапазон значений byte — от 0 до 255, или O x F F — т.е. двух шестнадцатеричных
цифр для вывода одного байта достаточно.
Вот как выглядят первые 20 строк при выводе содержимого файла output.txt.
Даже его собственная мать не узнала бы его в таком виде...

Дамп файла С:\C#ProgramsVi\holdtank\Test2\bin\output.txt:
001 - 53, 74, 72, 65, 61, 6D, 20, 28, 70, 72,
002 - 6F, 74, 65, 63, 74, 65, 64, 29, 0D, OA,
003 - 20, 20, 46, 69, 6C, 65, 53, 74, 72, 65,
004 - 61, 6D, 28, 73, 74, 72, 69, 6E, 67, 2C,
005 - 20, 46, 69, 6C, 65, 4D, 6F, 64, 65, 2C,
006 - 20, 46, 69, 6C, 65, 41, 63, 63, 65, 73,
007 - 73, 29, OD, OA, 20, 20, 4D, 65, 6D, 6F,
008 - 72, 79, 53, 74, 72, 65, 61, 6D, 28, 29,
009 - 3B, OD, OA, 20, 20, 4E, 65, 74, 77, 6F,
010 - 72, 6B, 53, 74, 72, 65, 61, 6D, OD, OA,
011 - 20, 20, 42, 75, 66, 66, 65, 72, 53, 74,
012 - 72, 65, 61, 6D, 20, 2D, 20, 62, 75, 66,
013 - 66, 65, 72, 73, 20, 61, 6E, 20, 65, 78,
014 - 69, 73, 74, 69, 6E, 67, 20, 73, 74, 72,
015 - 65, 61, 6D, 20, 6F, 62, 6A, 65, 63, 74,
016 - OD, OA, OD, OA, 42, 69, 6E, 61, 72, 79,
017 - 52, 65, 61, 64, 65, 72, 20, 2D, 20, 72,
018 - 65, 61, 64, 20, 69, 6E, 20, 76, 61, 72,
019 - 69, 6F, 75, 73, 20, 74, 79, 70, 65, 73,
020 - 20, 28, 43, 68, 61, 72, 2C, 20, 49, 6E,
Нажмите для вывода очередных 2 0 строк
Можно восстановить файл в виде строк из вывода в шестнадцатеричном фор­
мате. 0x61 — числовой эквивалент символа а. Буквы, расположены в алфавит­
ном порядке, так что 0x65 должно быть символом е. 0x20 — пробел. Приве­
денная здесь первая строка выглядит при обычной записи в виде строк как
" S t r e a m ( р г " . Интригующе, не правда ли? Полностью коды букв вы можете
найти в разделе "ASCII, table of codes" справочной системы.
Эти коды корректны и при использовании набора символов Unicode, который приме­
няется С# по умолчанию (побольше о Unicode вы можете узнать, прогулявшись в Интер­
нете в поисках "Unicode characters").

450

Часть VII. Дополнительные главы

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

Каталог "х" неверен
Could not find a part of the path
"C:\C#Programs\LoopThroughFiles\bin\Debug\x".
Больше файлов нет
Нажмите для завершения программы...
Не впечатляет?...

Написание собственного класса
коллекции: связанный список
Я из тех учителей, которые по старинке считают, что сначала следует освоить табли­
цу умножения, а уж потом давать ученику калькулятор. Так что сейчас вы пройдете
сквозь дебри создания собственной коллекции, перед тем как познакомиться со встроен­
ными коллекциями, о которых упоминалось в главе 15, "Обобщенное программирова­
ние". Здесь будут рассмотрены все "болты и гайки", из которых состоит класс коллек­
ции, и как все они объединяются в одно целое.
Одним из наиболее распространенных видов контейнеров после массива является
связанный список, каждый объект которого указывает на предыдущий и последующий
элементы списка, т.е. объекты, составляющие список, оказываются соединены в цепочку.
Вы используете ссылки на объекты для объединения отдельных узлов в цепь. В каждом та­
ком узле содержатся дополнительные данные, указывающие на следующий узел в цепи.
Отдельная переменная, обычно называющаяся ссылкой на голову списка, указывает на
первый объект в списке, в то время как хвост списка указывает на его последний элемент.
Односвязные списки содержат узлы, связанные только с узлами, следующими
за ними. По такому списку можно пройти только в одном направлении, следуя
связям между узлами. Дважды связанный список содержит узлы, которые ука­
зывают как на последующий, так и на предыдущий узлы. По таким спискам
можно проходить в обоих направлениях.
Связанный список по сравнению с массивом обладает рядом преимуществ и недос­
татков.
Можно легко вставить элемент в средину списка. Для выполнения вставки про­
грамма должна изменить только значения четырех ссылок (в дважды связанном
списке), но это простые, быстро вносимые изменения.
Точно так же можно легко удалить элемент из связанного списка.
Связанный список при необходимости может расти или уменьшаться. Программа
начинает работу с пустым связанным списком, а затем по мере необходимости до­
бавляет и удаляет элементы.
Доступ к элементу, располагающемуся следующим, быстр и прост, однако эле­
менты связанного списка не индексированы. Таким образом, обращение к опреде­
ленному элементу списка может потребовать проход по всему списку, что весьма
неэффективно.

Глава 20. Работа с коллекциями

451

Связанные списки идеально подходят для хранения последовательностей данных,
особенно если программа не знает заранее их точное количество (тем не менее следует
серьезно подумать о возможном примененииобобщенного класса List, который
был описан в главе 15, "Обобщенное программирование". Если вам нужен именно свя­
занный список, можно воспользоваться встроенным связанным списком из С# 2.0, а не
тем, который разрабатывается в данном разделе. Обратитесь к справочной системе за
информацией о пространстве имен System. Collections .Generic).
Другие пространства имен коллекций, которыми вы можете захотеть воспользовать­
с я — System. Collections и System. Collections . Specialized. Поищите
информацию о них в справочной системе, но в первую очередь следует искать подходя­
щую коллекцию именно в пространстве имен System. Collections . Generic.

Пример связанного списка
Приведенная далее демонстрационная программа иллюстрирует создание
и использование связанного списка.
// LinkedListContainer - демонстрация "самодельного"
// связанного списка. Этот контейнер реализует интерфейс
// IEnumerable для поддержки таких операторов, как foreach.
// Этот пример включает также итератор, который реализует
// интерфейс IEnumerator
using System;
using System.Collections;
namespace LinkedListContainer
{
// LLNode - каждый LLNode образует узел списка. Каждый
// узел LLNode содержит ссылку на целевые данные,
// встроенные в список
public class LLNode

{
// Это данные, которые хранятся в узле списка
internal object linkedData = null;
// Указатели на следующий и предыдущий узлы в списке
internal LLNode forward = null; // Следующий узел
internal LLNode backward = null; // Предыдущий узел
internal LLNode(object linkedData)
{
this.linkedData = linkedData;
}
// Получение данных, хранящихся в узле
public object Data
{
get
{
return linkedData;
}
}

452

Часть VII. Дополнительные главы

}

II LinkedList - реализация дважды связанного списка
public class LinkedList : IEnumerable
{
// Концы связанного списка. Спецификатор internal
// позволяет итераторам обращаться к ним непосредственно
internal LLNode head = null;
// Начало списка
internal LLNode tail = null;
// Конец списка
public IEnumerator GetEnumerator()
return new LinkedListlterator(this);
// AddObject - добавление объекта в конец списка
public LLNode AddObject(object objectToAdd)

{

return AddObject(tail, objectToAdd);

}
// AddObject- добавление объекта в список
public LLNode AddObject(LLNode previousNode,
object objectToAdd)
{

// Создание нового узла с добавляемым объектом
LLNode newNode = new LLNode(objectToAdd);
// Начнем с простейшего случая — пустого списка.
if (head == null && tail == null)

{
// ...теперь в нем один элемент
head = newNode;
tail = newNode;
return newNode;

}
// Добавляем ли мы новый узел в средину списка?
if (previousNode != null &&
previousNode.forward != null)

{

// Просто изменяем указатели
LLNode nextNode = previousNode.forward;
// Указатель на следующий узел
newNode.forward = nextNode;
previousNode.forward = newNode;
// Указатель на предыдущий узел
nextNode.backward = newNode;
newNode.backward = previousNode;
return newNode;

}
// Добавление в начало списка?
if (previousNode == null)

Глава 20. Работа с коллекциями

453

{
// Делаем его головой списка
LLNode nextNode = head;
newNode.forward = nextNode;
nextNode.backward = newNode;
head = newNode;
return newNode;
}
// Добавление в конец списка
newNode.backward = previousNode;
previousNode.forward = newNode;
tail = newNode;
return newNode;

}
// RemoveObject - удаление объекта из списка
public void RemoveObject(LLNode currentNode)
{
// Получаем соседей удаляемого узла
LLNode previousNode = currentNode.backward;
LLNode nextNode
= currentNode.forward;
// Обнуляем указатели удаляемого объекта
currentNode.forward = currentNode.backward = null;
// Был ли это последний элемент списка?
if (head == currentNode && tail == currentNode)
head = tail = null;
return;
// Это узел в средине списка?
if (head != currentNode && tail

!= currentNode)

previousNode.forward = nextNode;
nextNode.backward = previousNode;
return;
// Это узел в начале списка?
if (head •== currentNode && tail

!= currentNode)

head = nextNode;
nextNode.backward = null;
return;
// Это узел в конце списка...
tail = previousNode;
previousNode.forward = null;

// LinkedListlterator - дает приложению доступ к спискам

454

Часть VII. Дополнительные главы

// LinkedList
public class LinkedListlterator : IEnumerator
{

// Итерируемый связанный список
private LinkedList linkedList;
// "Текущий" и "следующий" элементы связанного списка.
// Объявлены как private для предотвращения
// непосредственного обращения извне
private LLNode currentNode = null;
private LLNode nextNode = null;
//LinkedListlterator - конструктор
public LinkedListlterator(LinkedList linkedList)

{
this.linkedList = linkedList;
Reset ( ) ;
}
// Current- возвращаем объект данных в текущей позиции
public object. Current

{
get
{
if
{

(currentNode == null)

return null;
}
return currentNode.1inkedData;
// Reset - перемещение итератора назад, в позицию,
// непосредственно предшествующую первому узлу списка
public void Reset()
{
currentNode = null;
nextNode = linkedList.head;
}
// MoveNext - переход к следующему элементу списка, пока
// не будет достигнут его конец
public bool MoveNext()
{
currentNode = nextNode;
if (currentNode == null)
{
return false;
}
nextNode = nextNode.forward;
return true;

public class Program

Глава 20. Работа с коллекциями

455

public static void Main (string [] args)
{
// Создаем контейнер и добавляем в него три элемента
LinkedList 11с = new LinkedList();
LLNode first = 11c.AddObject("Первая с т р о к а " ) ;
LLNode second = 11c.AddObject("Вторая с т р о к а " ) ;
LLNode third = 11c.AddObject("Последняя с т р о к а " ) ;
// Добавляем элементы в начале и средине списка
LLNode newfirst
= 11с.AddObject(null,"Перед первой с т р о к о й " ) ;
LLNode newmiddle
= 11с.AddObject(second, "Между второй и " +
"третьей с т р о к о й " ) ;
// Итератором можно управлять "вручную"
Console.WriteLine("Проход по контейнеру в р у ч н у ю " ) ;
LinkedListlterator H i
= (LinkedListlterator)11с.GetEnumerator();
H i . Reset () ;
while ( H i .MoveNext () )
{
string s = (string) Hi . Current ;
Console.WriteLine(s);

}
// Либо использовать цикл foreach
Console.WriteLine("\пОбход с использованием foreach");
foreach(string s in 11c)

{

Console.WriteLine(s);

}
// Ожидаем подтверждения пользователя
Console.WriteLine("Нажмите для " +
"завершения п р о г р а м м ы . . . " ) ;
Console.Read();

}
}
Классы LinkedList и LLNode образуют фундамент приведенной демонстрацион­
ной программы. На рис. 20.1 показан связанный список с тремя узлами, каждый из кото­
рых указывает на (или "содержит") отдельную строку, так что LinkedList вполне за­
служивает названия "контейнер". Лично я, впрочем, как и большинство в мире .NET,
предпочитаю термин коллекция.
Узлы в связанном списке представлены объектами LLNode. Для каждого данно­
го узла член forward указывает на следующий узел в списке, а член backward —
на предыдущий. Класс LinkedList представляет сам список. Член head указыва­
ет на первый узел списка, член tail — на последний. По сути, это все, что есть
в данном классе.

456

Часть VII. Дополнительные главы

Рис. 20.1. Классы

L i n k e d L i s t

и

LLNode

совместно создают связанный список

Добавление объекта в связанный список
Основные сложности содержатся в методах AddObject О
Сначала рассмотрим метод AddOb j ect ().

и RemoveObject ().

Для того чтобы добавить объект в список, вы должны знать, куда именно его нужно
поместить. По существу, имеется четыре ситуации.
1. Вы добавляете новый объект в пустой список. Это простейшая из всех ситуаций;
она показана на рис. 20.2. Указатель head равен null, как и указатель tail.
Следует просто заставить указывать их на добавляемый элемент, и на этом все —
вы получите список с одним узлом.

Рис. 20.2. Добавление нового узла в пустой
связанный список выполняется в два счета
2. Наиболее сложная ситуация — это добавление элемента в середину списка.
В этом случае предыдущий и следующий узлы не равны null. На рис. 20.3 пока­
зана такая ситуация. Чтобы вставить объект в середину списка, следует настроить
указатели forward и backward как вставляемого объекта, так и объектов, рас-

Глава 20. Работа с коллекциями

457

положенных по сторонам от места, куда он будет вставлен. На рис. 20.4 пред
ставлен результат (показанные шаги очень важно делать в определенном порядке,
чтобы не "потерять" часть списка из-за того, что у вас не останется ссылки на нее
Вы можете следовать порядку, показанному в приведенном исходном тексте де­
монстрационной программы).

Рис. 20.3. Перед вставкой в список объект не связан ни с чем

Рис. 20.4. После вставки объекта в список он становится частью команды

458

Часть VII. Дополнительные главы

Рис. 20.5. Добавление объекта в голову списка делает его первым
в списке
3. Объект может быть добавлен и в голову списка. Это гибрид первых двух случаев.
Указатель на голову списка после вставки должен указывать на новый объект,
а старый первый объект списка после вставки должен указывать на вставленный
узел, как на предыдущий в списке. Все это продемонстрировано на рис. 20.5.
4. И наконец, новый объект может быть вставлен в конец списка. Это ситуация, об­
ратная случаю 3.

Удаление объекта из связанного списка
Метод R e m o v e O b j e c t () рассматривает те же четыре ситуации, но в обратном на­
правлении.
Единственный способ следовать A d d O b j e c t () и R e m o v e Ob j e c t () — нари­
совать рисунки наподобие рис. 20.2-20.5 и аккуратно пройти каждый шаг. Не
стесняйтесь — не родился еще программист, который в подобной ситуации ни
разу не рисовал бы рисунка для того, чтобы разобраться во всем. Сложные ве­
щи остаются сложными независимо от того, кто с ними работает.

Реализация перечислителя связанного списка
Обратите внимание, что демонстрационная программа L i n k e d L i s t C o n t a i n e r
в действительности содержит три класса: L L N o d e , L i n k e d L i s t и L i n k e d L i s t l t e r a t o r . Класс L i n k e d L i s t l t e r a t o r — сопутствующий связанному списку класс с о
специальными привилегиями. Он в деталях знаком с внутренним устройством связанно­
го списка, которое недоступно никому во внешнем мире. Внешние клиенты используют
его для итерирования связанного списка.
Класс L i n k e d L i s t l t e r a t o r работает путем отслеживания текущего и следующе­
го за ним узла. Изначально c u r r e n t N o d e равен n u l l , a n e x t N o d e — первому эле­
менту связанного списка. После каждого вызова M o v e N e x t () c u r r e n t N o d e указывает
на то, что перед этим было следующим узлом, a n e x t N o d e перемещается к следующему
за ним узлу. После того как M o v e N e x t () достигает конца списка, c u r r e n t N o d e равно

Глава 20. Работа с коллекциями

459

n u l l , и функция отвергает все дальнейшие вызовы, возвращая для каждого из них зна­
чение f a l s e . Лучше не оказываться за концом списка.
Функция M a i n () демонстрирует использование связанного списка, сначала добавляя
в него три строки s t r i n g . Затем одна строка добавляется в начало списка, и еще од­
на — в его середину.
Первый цикл вывода создает итератор для связанного списка. Программа проходит
по всему списку путем вызова M o v e N e x t () до тех пор, пока функция не вернет значе­
ние f a l s e , указывающее, что достигнут конец списка. Для каждого узла программа по­
лучает его объект и преобразует его обратно в строку, из которой он был создан.
Затем функция M a i n () делает все то же, но с использованием цикла f o r e a c h .
Цикл f o r e a c h точно так же проходит по всем узлам связанного списка, как функция
M a i n () делала это вручную.
Предпочтительно использовать цикл f o r e a c h . Он дает лучший код, прост
и меньше подвержен ошибкам. Однако он имеет два ограничения: в нем нет
счетчика i n t i, но можно объявить собственный счетчик перед циклом и са­
мостоятельно увеличивать его в теле цикла. Также нельзя удалять элементы из
коллекции внутри цикла f o r e a c h . В этом случае вам нужна коллекция re­
m o v e d l t e m s , в которой вы сохраняете индексы или ссылки на элементы, най­
денные в цикле f o r e a c h и которые должны быть удалены из исходной кол­
лекции. Затем используйте цикл f o r e a c h еще раз для прохода по коллекции
r e m o v e d l t e m s и удалите указанные в нем элементы из исходной коллекции,
Лучше один раз увидеть, чем сто — услышать, так что вот как это выглядит
в исходном тексте:
List

removedltems

=

new

List();

// Цикл no o r i g i n a l C o l l e c t i o n
foreach(string s
in o r i g i n a l C o l l e c t i o n )

{
// Если s т р е б у е т с я у д а л и т ь ,
//
removedltems
removedltems.Add(s);

сохраняем

ссылку

или

индекс

в

}
foreach(string

s

in

removedltems)

//

Цикл

no

removedltems

{
originalCollection.Remove(s);

}
Вывод программы L i n k e d L i s t C o n t a i n e r выглядит следующим образом:
Проход по контейнеру вручную
Перед первой строкой
Первая строка
Вторая строка
Между в т о р о й и т р е т ь е й с т р о к о й
Последняя строка
Обход с и с п о л ь з о в а н и е м
Перед первой строкой
Первая строка

460

foreach

Часть VII. Дополнительные главы

Глав

Вторая
строка
Между в т о р о й и т р е т ь е й с т р о к о й
Последняя с т р о к а
Нажмите < E n t e r > д л я з а в е р ш е н и я

программы...

Обобщенную версию этого связанного списка можно найти в демонстраци­
онной программе G e n e r i c L i n k e d L i s t C o n t a i n e r на прилагаемом ком­
пакт-диске. Обратите внимание, что G e n e r i c L i n k e d L i s t C o n t a i n e r
продолжает использовать интерфейс I E n u m e r a t o r , который будет рас­
смотрен немного позже, но в некоторых ситуациях следует применять вме­
сто него новую обобщенную версию I E n u m e r a t o r < T > . Однако пока не
стоит начинать анализировать обобщенные классы. Кроме того, обратитесь
к встроенному обобщенному классу L i n k e d L i s t , который, несомненно,
превосходит написанный здесь.

Зачем нужен связанный список
Связанный список может показаться пустыми хлопотами. Его основное преимущест­
во заключается в большой скорости вставки и удаления узлов. "Ну хорошо, — можете
сказать вы. — Но ведь добавление s t r i n g в массив из четырех или пяти строк не слож­
нее перемещения нескольких ссылок для освобождения места." А что вы скажете, если
этот массив будет содержать несколько сотен тысяч строк, и вы должны выполнять мас­
су вставок и удалений из него?
Второе преимущество связанного списка в том, что он может расти и уменьшаться.
Если вы думаете, что вам будут нужны 1000 объектов, то вы должны создать массив на
1000 элементов, независимо от того, будете вы их использовать или нет. Что еще хуже,
если вы в действительности создадите 2000 объектов, то можете считать, что в этот раз
вам крупно не повезло. (Да, конечно, можно создать второй массив с большей емкостью
и скопировать в него содержимое первого, но что при этом можно сказать об эффектив­
ности и затратах памяти?)
На этом принципе основаны многие распространенные вирусы. Например, неко­
торый исполненный благих намерений программист решает, что 256 символов
будет достаточно для любого имени файла, и объявляет массив c h a r [ 2 5 6 ] . Ес­
ли программист забудет убедиться, что имя на самом деле не длиннее, чем ожи­
дается, то у хакера появляется шанс сломать программу, передав ей неправдопо­
добно длинное имя, и переписать тем самым часть кода за массивом (это называ­
ется переполнением буфера). Впрочем, это проблема в первую очередь C/C++:
С# автоматически проверяет выход за границы массива.

В оставшейся части главы будут проанализированы три разных подхода к общей за­
даче итерирования коллекции. В этом разделе будет продолжено обсуждение наиболее
традиционного (как минимум, для программистов на С#) подхода с использованием ите­
раторов, которые реализуют интерфейс I E n u m e r a t o r . В качестве примера рассматри­
вается итератор для связанного списка из предыдущего раздела.

Глава 20. Работа с коллекциями

461

Термины итератор (iterator) и перечислитель (enumerator) являются синони­
мами. Термин итератор более распространен, несмотря на имя реализуемого
им интерфейса. От обоих терминов можно произвести глагольную форму —вы
можете итерировать контейнер, а можете перечислять. Другими подходами
к решению этой же задачи являются индексаторы и новые блоки итераторов.

Доступ к коллекции: общая задача
Различные типы коллекций могут иметь разные схемы доступа. Не все виды коллекции
могут быть эффективно доступны с использованием индексов наподобие массивов — та
ков, например, связанный список. Различия между типами коллекций делают невозможным
написание без специальных средств функции наподобие приведенной далее:
// п е р е д а е т с я коллекция любого вида
void myClearFunction(Collection aColl,
i n t index)
{
aColl[index]
= 0; // Индексирование р а б о т а е т не
// типов коллекций
//
... продолжение...

для

всех

}
Коллекции каждого типа могут сами определять свои методы доступа (и делают это).
Например, связанный список может предоставить метод G e t N e x t () для выборки сле­
дующего элемента из цепочки объектов; стек может предложить методы P u s h ( )
и P o p () для добавления и удаления объектов и т.д.
Более общий подход состоит в предоставлении для каждого класса коллекции от­
дельного так называемого класса итератора, который знает, как работать с конкретной
коллекцией. Каждая коллекция X определяет свой собственный класс I t e r a t o r X . В от­
личие от X, I t e r a t o r X представляет общий интерфейс I E n u m e r a t o r , золотой стан­
дарт итерирования. Этот метод использует второй объект, именуемый итератором, в ка­
честве указателя внутрь коллекции.
Итератор (перечислитель) обладают следующими преимуществами.
Каждый класс коллекции может определить свой собственный класс итератора.
Поскольку итератор реализует стандартный интерфейс I E n u m e r a t o r , с ним
обычно легко работать.
Прикладной код не должен знать о внутреннем устройстве коллекций. Пока про­
граммист работает с итератором, тот берет на себя все заботы о деталях. Это —
хорошая инкапсуляция.
Прикладной код может создать много независимых объектов-итераторов для од­
ной и той же коллекции. Поскольку итератор содержит информацию о своем соб­
ственном состоянии (знает, где он находится в процессе итерирования), каждый
итератор может независимо проходить по коллекции. Вы можете одновременно
выполнять несколько итераций, причем в один и тот же момент все они могут на­
ходиться в разных позициях.
Чтобы сделать возможным наличие цикла f o r e a c h , интерфейс I E n u m e r a t o r должен
поддерживать различные типы коллекций — от массивов до связанных списков. Следова­
тельно, его методы должны быть максимально обобщенными, насколько это возможно.
Например, нельзя использовать итератор для произвольного доступа к элементам коллек­
ции, поскольку большинство коллекций не обеспечивают подобного доступа.

462

Часть VII. Дополнительные главы

I E n u m e r a t o r предоставляет три следующих метода.
R e s e t () — устанавливает итератор таким образом, чтобы он указывал на начало
коллекции. Примечание: обобщенная версия I E n u m e r a t o r , I E n u m e r a t o r < T > ,
н е предоставляет метод R e s e t ( ) . В случае обобщенного L i n k e d L i s t просто
начинайте работу с вызова M o v e N e x t ( ) .
M o v e N e x t () — перемещает итератор от текущего объекта в контейнере к сле­
дующему.
C u r r e n t — свойство (не метод), которое дает объект данных, хранящийся в те­
кущей позиции итератора.
Описанный принцип продемонстрирован приведенной далее функцией. Про­
граммист класса M y C o l l e c t i o n (не показанного здесь) создает соответствующий
класс и т е р а т о р а — скажем, I t e r a t o r M y C o n t a i n e r (применяя соглашение о б
именах I t e r a t o r X , упоминавшееся ранее). Прикладной программист ранее сохра­
нил ряд объектов C o n t a i n e d D a t a O b j e c t s в коллекции M y C o l l e c t i o n . Приве­
денный далее фрагмент исходного текста использует три стандартных метода
I E n u m e r a t o r для чтения этих объектов:
// Класс M y C o l l e c t i o n хранит
// C o n t a i n e d D a t a O b j e c t d a t a
void M y F u n c t i o n ( M y C o l l e c t i o n

объекты

типа

myColl)

{
// Программист,
создавший класс MyCollection,
создал также
// и к л а с с и т е р а т о р а I t e r a t o r M y C o l l e c t i o n ; прикладной
// программист создает объект итератора для прохода по
// объекту myColl
I E n u m e r a t o r i t e r a t o r = new I t e r a t o r M y C o l l e c t i o n ( m y C o l l ) ;
// перемещаем и т е р а т о р в "следующую п о з и ц и ю " в н у т р и
// к о л л е к ц и и
while(iterator.MoveNext

())

{
// Получаем ссылку на о б ъ е к т данных в текущей позиции
// к о л л е к ц и и
ContainedDataObject containedData;
// data
contained =
(ContainedDataObject)iterator.Current;
//
. . . и с п о л ь з у е м объект данных c o n t a i n e d . . .

Функция M y F u n c t i o n ( )

принимает в

качестве аргумента коллекцию

Con­

t a i n e d D a t a O b j e c t s . Она начинается с создания итератора типа I t e r a t o r M y ­
C o l l e c t i o n . Функция начинает цикл с вызова M o v e N e x t ( ) . При первом вызове
M o v e N e x t () перемещает итератор к первому элементу коллекции. При каждом по­
следующем вызове M o v e N e x t () перемещает указатель " н а одну п о з и ц и ю . " Функ­
ция M o v e N e x t () возвращает f a l s e , когда коллекция исчерпана и итератор боль­
ше нельзя передвинуть.
Свойство C u r r e n t возвращает ссылку на объект данных в текущей позиции итера­
тора. Программа преобразует возвращаемый объект в C o n t a i n e d D a t a O b j e c t перед
тем, как присвоить его переменной c o n t a i n e d . Вызов C u r r e n t некорректен, если
предшествующий вызов метода M o v e N e x t ( ) н е вернул t r u e .

Глава 20. Работа с коллекциями

463

Использование foreach
Методы I E n u m e r a t o r достаточно стандартны для того, чтобы С# использовали их
автоматически для реализации конструкции f o r e a c h .
Цикл

f o r e a c h может обращаться к любому классу, реализующему интерфейс

I E n u m e r a b l e , как показано в приведенной обобщенной функции, которая может работать с любым классом — от массивов и связанных списков до стеков и очередей:
void

MyFunction(IEnumerable

containerOfStrings)

{
foreach(string

s

in

containerOfStrings)

{
Console.WriteLine("Следующая

строка

-

{о}",

s);

}
}
Класс реализует I E n u m e r a b l e путем определения метода G e t E n u m e r a t o r ( ) ,
который возвращает экземпляр I E n u m e r a t o r . Скрыто от посторонних глаз
f o r e a c h вызывает метод G e t E n u m e r a t o r () для получения итератора. Цикл ис­
пользует этот итератор для обхода контейнера. Каждый выбираемый им элемент
приводится к соответствующему типу перед тем, как продолжить выполнение тела
цикла. Обратите внимание, что I E n u m e r a b l e и I E n u m e r a t o r различные, но связанные интерфейсы. С# 2.0 предоставляет обобщенную версию обоих интерфей­
с о в — см. информацию о пространстве имен S y s t e m . C o l l e c t i o n s . G e n e r i c
в справочной системе.
Итак, цикл f o r e a c h можно записать таким образом:
foreach(int

nValue

in

myContainer)

{

// ...

}
Это эквивалентно следующему циклу f o r :
for(IEnumerator i =
myContainer.GetEnumerator();
i.MoveNext();
)

{

int

//

nValue

=

(int)i.Current

//
//
//

Инициализация
Условие
Пустой инкремент

//
//

Получение
элемента

текущего

}
Раздел инициализации цикла f o r получает итератор. Раздел условия использует
M o v e N e x t () для определения конца контейнера. M o v e N e x t () сам увеличивает указа­
тель, т.е. раздел инкремента цикла пуст. (Тем не менее, вам все равно следует указать
точку с запятой после раздела условия, после которой не следует никакой код. В приве­
денном фрагменте исходного текста это точка с запятой после i . M o v e N e x t ( ) . ) В пер­
вой строке цикла выбирается очередной объект и преобразуется к i n t ( C u r r e n t всегда
возвращает тип O b j e c t ) . Если возвращенный объект не является i n t , С# генерирует
исключение неверного преобразования типа.

464

Часть VII. Дополнительные главы

Обращение к элементам массива очень простое и понятное: команда c o n t a i n e r [п]
обеспечивает обращение к и-му элементу массива c o n t a i n e r . Было бы хорошо, если
бы так же просто можно было обращаться и к другим типам коллекций.
С# позволяет написать вам свою собственную реализацию операции индексирова­
ния. Вы можете обеспечить возможностью обращения через индекс коллекции, кото­
рые таким свойством изначально не обладают. Кроме того, вы можете индексировать
с использованием в качестве индексов не только типа i n t , но и других типов, напри­
мер, s t r i n g .

Формат индексатора
Индексатор выглядит очень похоже на свойство, за тем исключением, что в нем вме­
сто имени свойства появляются ключевое слово t h i s и оператор индекса [ ] :
class

MyArray

{
public
{
get

string this[int

index]

// Обратите внимание на
// ключевое с л о в о " t h i s "

{
return

array[index];

}
set
{
array[index]

=

value;

}
За сценой выражение s = m y A r r a y [ i ] ; вызывает функцию доступа g e t , переда­
вая е й значение индекса i . Выражение m y A r r a y [ i ] = " с т р о к а " ; приводит к вызову
функции доступа s e t , которой передаются индекс i и строка " с т р о к а " .

Пример программы с использованием индексатора
Индексы не ограничены типом i n t . Например, вы можете использовать для индек­
сирования коллекции домов имена их владельцев или адреса. Кроме того, свойство ин­
дексатора может быть перегружено для различных типов индекса.
Приведенная далее демонстрационная программа I n d e x e r генерирует
класс виртуального массива K e y e d A r r a y , который выглядит и функциони­
рует точно так же, как и обычный массив, с тем исключением, что в качестве
его индексов применяется значение типа s t r i n g .
// I n d e x e r - д а н н а я д е м о н с т р а ц и о н н а я программа иллюстрирует
// и с п о л ь з о в а н и е о п е р а т о р а и н д е к с а для о б е с п е ч е н и я доступа к
// м а с с и в у с и с п о л ь з о в а н и е м с т р о к в к а ч е с т в е и н д е к с о в

Глава 20. Работа с коллекциями

465

using

System;

namespace

Indexer

{
public

class

KeyedArray

// Следующая с т р о к а о б е с п е ч и в а е т
"ключ" к массиву
/ / с т р о к а ,
которая идентифицирует элемент
private
string[]
sKeys;
//
//

object представляет
с ключом

private

object []

KeyedArray
размера

II
//

public

-

собой

реальные

данные,



это

связанные

oArrayElements;
создание

KeyedArray(int

KeyedArray

фиксированного

nSize)

sKeys = new s t r i n g [ n S i z e ] ;
oArrayElements
= new o b j e c t [ n S i z e ] ;

// Find - поиск индекса записи,
соответствующей строке
// sTargetKey
(если запись не найдена,
возвращает
-1)
private
int
Find(string
sTargetKey)
for (int
if

i

=

0,-

i

<

sKeys.Length;

(String.Compare(sKeys[i],
return

i++)

sTargetKey)

==

0)

i,-

}
}
return

- 1 ;

}
// F i n d E m p t y - поиск с в о б о д н о г о
// новой записи
private
int
FindEmpty()
for

(int

if

i

=

(sKeys[i]

0;

i
==

<

места

sKeys.Length;

в

массиве

для

i++)

null)

{
return i;
}
}
throw

new

Exception("Массив

/ / Ищем с о д е р ж и м о е
//
индексатор
public object this[string sKey]
{

по

заполнен");

указанной

строке

-

это

и

есть

set
{

466

Часть VII. Дополнительные главы

//

Проверяем,

int
if

нет

лит

уже

index

=

Find(sKey);

(index

<

0)

такой

строки

{
// Е с л и н е т - ищем н о в о е
index = FindEmpty();
sKeys[index]
= sKey;

место

}
// Сохраняем объект в
oArrayElements[index]

соответствующей
= value;

позиции

}
get
{
int index
if
(index

=
<

Find(sKey);
0)

{
return

null;

}
return

}

oArrayElements[index];

}

}
public

class

Program

{
public

s t a t i c

void

Main(string[]

args)

{
// Создаем массив с достаточным количеством
K e y e d A r r a y ma = new K e y e d A r r a y ( 1 0 0 ) ;
// Сохраняем возраст
ma [ " B a r t " ] = 8;
та["Lisa"]
= 10;
та["Maggie"]
= 2;

членов

семьи

элементов

Симпсонов

/ / Ищем в о з р а с т L i s a
Console.WriteLine("Ищем
возраст
Lisa");
i n t age =
(int)ma["Lisa"];
Console.WriteLine("Возраст Lisa {o}",

age);

// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();

Класс K e y e d A r r a y в к л ю ч а е т д в а о б ы ч н ы х м а с с и в а . М а с с и в o A r r a y E l e m e n t s со­
держит р е а л ь н ы е д а н н ы е K e y e d A r r a y . С т р о к и , к о т о р ы е х р а н я т с я в м а с с и в е s K e y s , ра­
ботают в к а ч е с т в е и д е н т и ф и к а т о р о в м а с с и в а о б ъ е к т о в , г'-ый э л е м е н т s K e y s соответству­
е т /-ой з а п и с и o A r r a y E l e m e n t s . Э т о п о з в о л я е т п р и к л а д н о й п р о г р а м м е и н д е к с и р о в а т ь
K e y e d A r r a y с помощью индексов типа s t r i n g .

Глава 20. Работа с коллекциями

467

Индексы, не являющиеся целыми числами, известны как ключи (key). Кстати, можно реализовать K e y e d A r r a y с использованием L i s t < T > (см. г л а
ву 15, " О б о б щ е н н о е п р о г р а м м и р о в а н и е " ) вместо массива фиксированного
размера. L i s t < T > индексируется, как и массив, так как они оба реализуют
интерфейс I L i s t (или I L i s t < T > ) . Это позволит сделать K e y e d A r r a y
обобщенным классом и получить большую гибкость, чем при использова­
нии внутреннего массива.
Индексатор s e t [ s t r i n g ] начинает с проверки, не имеется ли уже данного индекса
в массиве, для чего он применяет функцию F i n d ( ) . Если она возвращает индекс,
s e t []

сохраняет новый объект данных в соответствующем элементе o A r r a y E l e ­

m e n t s . Ели F i n d ( ) н е может найти ключ, s e t [ ] вызывает F i n d E m p t y ( ) для возвра­
та пустого элемента, где и будет сохранен переданный объект.
Функция g e t [] работает с индексом с применением аналогичной логики. Сначала она
ищет определенный ключ с использованием метода F i n d О . Если F i n d () возвращает не­
отрицательный индекс, g e t [] возвращает соответствующий член o A r r a y E l e m e n t s , где
хранятся запрошенные данные. Если же F i n d О возвращает - 1 , то метод g e t [] возвра­
щает значение n u l l , указывающее, что переданный ключ в списке отсутствует.
Метод F i n d () циклически проходит по всем элементам массива s K e y s в поисках
элемента с тем же значением, что и переданное значение типа s t r i n g . Метод F i n d ( )
возвращает индекс найденного элемента (или значение - 1 , если элемент не найден).
Функция F i n d E m p t y () возвращает индекс первого элемента, который не имеет связан­
ного ключевого элемента.
При написании методов F i n d ( ) и F i n d E m p t y () не ставилась цель повысить их
эффективность, так что имеется множество возможностей сделать их быстрее, но все они
не имеют никакого отношения к индексаторам.
Правда, было бы здорово добавить возможность индексирования к классу связанного
списка L i n k e d L i s t ? Да, это можно сделать. Но вспомните, что даже в классе K e y e d A r ­
r a y требуется проход по массиву s K e y s для поиска определенного ключа, а значит,
и функций F i n d () и F i n d E m p t y ( ) , которые этим занимаются. Точно так же при реа­
лизации индексатора для L i n k e d L i s t вам придется осуществлять проход по всему свя­
занному списку, и единственный способ сделать это — пройти по всему списку с исполь­
зованием итератора L i n k e d L i s t l t e r a t o r , следуя по ссылкам f o r w a r d от узла к уз­
лу. Индексатор окажется удобным, но очень медлительным.
Заметьте, что вы не можете удалять элементы посредством ключа n u l l . Как же реа­
лизовать удаление? Как часто говорится в учебниках — "данная задача остается читате­
лю в качестве домашнего упражнения".
Функция M a i n () демонстрирует применение индексатора. Сначала программа соз­
дает объект та типа K e y e d A r r a y длины 100 (т.е. со 100 свободными элементами). Да­
лее в этом объекте сохраняется возраст детей семьи Симпсонов с использованием имен
в качестве индексов. И наконец, программа получает возраст Лизы с применением вы­
ражения ma [" L i s a " ] и выводит его на экран.
Обратите внимание, что программа должна выполнить преобразование типа для зна­
чения, возвращенного из та [ ] , так как K e y e d A r r a y написан таким образом, что может
хранить объекты любого типа. Без такого преобразования типов можно обойтись, если
индексатор написан так, что может работать только со значениями типа i n t , или если
K e y e d A r r a y — обобщенный класс (см. главу 15, "Обобщенное программирование").

468

Часть VII. Дополнительные главы

Вывод п р о г р а м м ы п р о с т и э л е г а н т е н :
Ищем

возраст

Lisa

Возраст

Lisa

Нажмите


В

-

10
для

завершения

программы...

качестве о т с т у п л е н и я — интерфейс

ляющий

целый

индексатор

в

форме

IList
object

описывает класс,
t h i s [int].

В

предостав­
С# имеется

также и н т е р ф е й с I L i s t < T > , к о т о р ы й позволяет заменить o b j e c t выбран­
ным вами типом Т. Это устраняет необходимость преобразования типов из
предыдущего примера.

Помните код функции M a i n ( ) , написанный при демонстрации самостоятельно раз­
работанного класса L i n k e d L i s t ? (Его можно найти в демонстрационной программе
L i n k e d L i s t C o n t a i n e r на прилагаемом компакт-диске.) Вот его фрагмент:
public

s t a t i c

void

Main ( s t r i n g []

args)

{
// Создаем к о н т е й н е р и д о б а в л я е м в
L i n k e d L i s t 11с = new L i n k e d L i s t ( ) ;
// Добавляем о б ъ е к т ы . . .
Console.WriteLine("Проход
LinkedListlterator
H i
=

по

него

контейнеру

три

элемента

вручную");

(LinkedListlterator)11с.GetEnumerator();

Hi . R e s e t ()

;

while ( H i .MoveNext () )
{
string s
=
( s t r i n g ) Hi . C u r r e n t ;
Console.WriteLine(s);

}
Данный код получает L i n k e d L i s t l t e r a t o r и использует его метод M o v e N e x t ( )
и свойство C u r r e n t для обхода связанного списка. Однако С# 2.0 может упростить вам
этот обход так, что вы получите перечисленные преимущества.
Вам не придется вызывать G e t E n u m e r a t o r () (и выполнять преобразование ти­
па результатов).
Вам н е понадобится вызывать M o v e N e x t ( ) .
Вам не придется вызывать C u r r e n t и выполнять преобразование типа, возвра­
щаемого значения.
Вы сможете просто использовать f o r e a c h для обхода коллекции (С# сделает все
остальное за вас).
Если быть честным, то f o r e a c h работает и для класса L i n k e d L i s t из этой гла­
вы. Это связано с наличием метода G e t E n u m e r a t o r ( ) . Но я все еще должен са­
мостоятельно писать класс L i n k e d L i s t l t e r a t o r . Новизна состоит в том, что
вы можете пропустить при обходе часть вашего класса.

Глава 20. Работа с коллекциями

469

В создаваемых классах коллекций можно предоставить блок итератора (iterate
block) вместо написания собственного класса итератора для поддержки коллекции.
Можно использовать блок итератора и для других рутинных работ.
Этот новый подход применяет блоки итераторов. Когда вы пишете класс коллекции,
такой как K e y e d L i s t или P r i o r i t y Q u e u e — вы реализуете блок итератора вместо
реализации интерфейса I E n u m e r a t o r . Затем пользователи этого класса могут просто
итерировать коллекцию с помощью цикла f o r e a c h . Приготовьтесь расстаться с частью
вашего драгоценного времени, чтобы ознакомиться с несколькими вариантами блоков
итераторов.
Все примеры в этом разделе являются частью демонстрационной програм­
м ы I t e r a t o r B l o c k s н а прилагаемом компакт-диске.

// I t e r a t o r B l o c k s - демонстрация применения блоков
// итераторов для написания итераторов коллекций
using
System;
namespace
IteratorBlocks

{
class

IteratorBlocks

{
//Main - демонстрация
//
итераторов
s t a t i c

void

пяти

Main(string[]

различных

приложений

блоков

args)

{
// Итерирование месяцев года,
вывод
// каждом из них
M o n t h D a y s md = new M o n t h D a y s ( ) ;
//
Итерируем

количества

дней

в

Console.WriteLine("Месяцы:\n");
foreach

(string

sMonth

in

md)

{
Console.WriteLine(sMonth);

}
// Итерируем коллекцию строк
S t r i n g C h u n k s sc = new S t r i n g C h u n k s ( ) ;
// Итерируем - выводим т е к с т ,
помещая
// в с в о е й с о б с т в е н н о й с т р о к е
Console.WriteLine("\Строки:\п");
foreach
( s t r i n g sChunk in sc)

каждый

фрагмент

{
Console.WriteLine(sChunk);

}
/ / А теперь выводим их в одну строку
Console.WriteLine("\пВывод в
одну с т р о к у : \ п " ) ;
foreach
( s t r i n g sChunk in sc)

{
Console.Write(sChunk);

}
Console.WriteLine();
// Итерируем простые

470

числа

до

13

Часть VII. Дополнительные главы

Y i e l d B r e a k E x yb = new Y i e l d B r e a k E x ( ) ;
// Итерируем,
останавливаясь после 13
Console.WriteLine("\пПростые
числа:\n");
foreach
(int nPrime
i n yb)
Console.WriteLine(nPrime);

}
// И т е р и р у е м ч е т н ы е ч и с л а в убывающем п о р я д к е
EvenNumbers en = new E v e n N u m b e r s ( ) ;
// Вывод четных ч и с е л от 10 до 4
Console.WriteLine("\пЧетные
числа:\п");
foreach
(int nEven in en.DescendingEvens(11,
3))
Console.WriteLine(nEven);

}
// Итерируем числа типа double
P r o p e r t y l t e r a t o r p r o p = new P r o p e r t y l t e r a t o r ( ) ;
Console.WriteLine("\пЧисла
double:\n");
foreach
(double db in prop.DoubleProp)
Console.WriteLine(db);

}

}

}

// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения программы...");
Console.Read();

//
Классы I t e r a t o r B l o c k s
// MonthDays - определяем и т е р а т о р ,
который возвращает
// м е с я ц ы и к о л и ч е с т в о д н е й в них
class
MonthDays
{
string[]
months =
{
"January 31",
"February 28",
"March 3 1 " ,
"April 30",
"May 3 1 " ,
"June 30",
"July 31",
"August 3 1 " ,
"September 30",
"October 31",
"November 30",
"December 31"
};
//GetEnumerator - это и есть итератор
public
System.Collections.IEnumerator GetEnumerator()
{
foreach

(string

sMonth

in

months)

{

}

}

}

// Возвращаем по одному
y i e l d r e t u r n sMonth;
//

//StringChunks
//

фрагменты

class

-

определение

месяцу в каждой
Новый с и н т а к с и с

итератора,

итерации

возвращающего

текста

StringChunks

{
//GetEnumerator - итератор. Обратите
//
(дважды)
вызывается в Main

Глава 20. Работа с коллекциями

внимание,

как

он

471

public

System.Collections.IEnumerator

GetEnumerator()

{
//
yie
yie
yie
yie
yie

Возврат разных фрагментов
ld return
"Using i t e r a t o r
ld return
"blocks
";
ld return
" i s n ' t
all
";
ld return
"that
hard";
ld
return
".";

текста
";

на

каждой

итерации

}
}
//YieldBreakEx
//

yield

class

-

пример

использования

ключевого

слова

break

YieldBreakEx

{
i n t [ ] p r i m e s = { 2, 3, 5, 7,
11, 13,
17, 19, 23 };
/ / G e t E n u m e r a t o r - возврат последовательности простых
// чисел с демонстрацией применения конструкции y i e l d
//
break
public
System.Collections.IEnumerator GetEnumerator()
{
foreach

(int

nPrime

in

primes)

{

}

}

}

if
(nPrime >
yield
return

//EvenNumbers
//

числа

class

между

-

13)
yield
nPrime;

числовой
граничными

break;

итератор,

//Новый

синтаксис

возвращающий

значениями

в

четные

убывающем

порядке

EvenNumbers

{
//DescendingEvens - "именованный" итератор,
который
// также демонстрирует использование конструкции y i e l d
//
break
public
System.Collections.IEnumerable
{
DescendingEvens(int nTop,
i n t nStop)
// Начинаем с ближайшего к nTop меньшего ч е т н о г о числа
if
(nTop % 2
!= 0)
nTop -= 1;
// Итерируем до ближайшего к n S t o p четного
// превышающего э т о з н а ч е н и е
f o r ( i n t i = n T o p ; i >= n S t o p ; i -= 2)

числа,

{
if

(i < n S t o p )
yield
break;
// Возвращаем на
//
число
yield
return
i;

}

}

итерации

очередное

четное

доступа

свойства

}

//Propertylterator // класса в качестве

472

каждой

реализация функции
блока итератора

Часть VII. Дополнительные главы

class

Propertylterator

{
doublet]
doubles =
{
1.0,
2.0,
3.5,
4.67
};
// D o u b l e P r o p - с в о й с т в о
"get" с блоком итератора
public
System.Collections.IEnumerable
DoubleProp
get
{
foreach

(double

db

in

doubles)

{
yield

return

db;

}

Итерация месяцев
Следующий фрагмент из демонстрационной программы

I t e r a t o r B l o c k s предо­

ставляет и т е р а т о р , к о т о р ы й п р о х о д и т п о м е с я ц а м г о д а :
//

MonthDays

//

месяцы

class

и

-

определяем

количество

итератор,

дней

в

который

возвращает

них

MonthDays

{
string[]
{

months

"January

"April
"August

30",

31",

30",

"December
-

это

28",

"June

"September

//GetEnumerator
public

"February

"May

31",

"November

=

31",

и

"March

30",

30",

"July

"October

31"

31",
31",
31",

};

есть

итератор

System.Collections.IEnumerator

GetEnumerator()

{
foreach(string

sMonth

in

months)

{
// Возвращаем по одному
y i e l d r e t u r n sMonth;
//

}

месяцу в каждой
Новый с и н т а к с и с

итерации

}
А вот часть функции M a i n ( ) , где выполняется итерирование этой коллекции с при­

менением цикла f o r e a c h :
//
//

Итерирование месяцев
каждом из них

M o n t h D a y s md
// Итерируем

=

года,

вывод

количества

дней

в

new M o n t h D a y s ( ) ;

Console.WriteLine("Месяцы:\n");
foreach

(string

sMonth

in

md)

{
Console.WriteLine(sMonth);

Глава 20. Работа с коллекциями

473

Это исключительно простой "класс коллекции", основанный на массиве, как и клак
K e y e d A r r a y . Класс содержит массив, элементы которого имеют тип s t r i n g . Ков
клиент итерирует данную коллекцию, ее блок итератора выдает ему эти строки по одной
Каждая строка содержит имя месяца с количеством дней в нем. Тут нет ничего сложной,
Класс определяет собственный блок итератора, в данном случае как метод GetEnumera­
t o r ( ) . Метод G e t E n u m e r a t o r ( ) возвращает объект типа S y s t e m . C o l l e c t i o n s ,
I E n u m e r a t o r . Да, вы должны были писать такой метод и ранее, но вы должны были
писать не только его, но и собственный класс-перечислитель для поддержки вашего
класса-коллекции. Теперь же вы пишете только простой метод, возвращающий

пере.

числитель с использованием новых ключевых слов y i e l d r e t u r n . Все остальное C#
делает за вас: создает класс-перечислитель и применяет его метод M o v e N e x t () для
итерирования. У вас уменьшается количество работы и размер исходного текста.
Ваш класс, содержащий метод G e t E n u m e r a t o r ( ) , больше не должен реализовы
вать интерфейс I E n u m e r a t o r . В следующих разделах вам будет показано несколько
вариаций блоков итераторов:
обычные итераторы;
именованные итераторы;
свойства классов, реализованные как итераторы.

Что такое коллекция
Остановимся

на минутку и

сравним

эту небольшую

коллекцию

с

коллекцией

L i n k e d L i s t , рассматривавшейся ранее в главе. В то время как L i n k e d L i s t имеет
сложную структуру узлов, связанных посредством указателей, приведенная простейшая
коллекция месяцев основана на простом массиве с фиксированным содержимым. Но все
же следует расширить понятие коллекции.
(Ваш класс коллекции не обязан иметь фиксированное содержимое — большинство
коллекций разработаны для хранения объектов путем добавления их в коллекции, на­
пример, с помощью метода A d d () или чего-то в этом роде. Класс K e y e d A r r a y , к при­
меру, использует для добавления элементов в коллекцию индексатор. Ваша коллекция
также должна обеспечивать метод A d d ( ) , как и блок итератора, чтобы вы могли рабо­
тать с ней с помощью f o r e a c h . )
Цель коллекции, в наиболее общем смысле, заключается в хранении множества объ­
ектов и обеспечении возможности их обхода, последовательно выбирая их по одномухотя иногда может использоваться и произвольная выборка, как в демонстрационной
программе I n d e x e r . (Конечно, массив и так в состоянии справиться с этим, без допол­
нительных "наворотов" наподобие класса M o n t h D a y s , но итераторы вполне могут при­
меняться и за пределами примера M o n t h D a y s . )
Говоря более обобщенно, независимо от того, что именно происходит за сценой, ите­
рируемая коллекция генерирует "поток" значений, который можно получитьс помощью
foreach.
Для лучшего понимания данной концепции ознакомьтесь с еще одним примером про­
стого класса из демонстрационной программы I t e r a t o r B l o c k s , который иллюстри­
рует чистую идею коллекции:
//StringChunks - определение
// фрагменты текста

474

итератора,

возвращающего

Часть VII. Дополнительные главы

class

StringChunks

/ / G e t E n u m e r a t o r - и т е р а т о р . Обратите внимание, как он
//
(дважды)
вызывается в Main
public
System.Collections.IEnumerator GetEnumerator()

{

//
yie
yie
yie
yie
yie

l
l
l
l
l

Возврат разных фрагментов текста
d return
" U s i n g i t e r a t o r и /.
d return
"blocks
";
d return
"isn't all
";
d return
"that hard";
d
return
".";

на

каждой

итерации

}
Коллекция S t r i n g C h u n k s , как ни странно, ничего не хранит в обычном смысле
этого слова. В ней нет даже массива. Так где же тут коллекция? Она — в последователь­
ности вызовов y i e l d r e t u r n , использующих специальный новый синтаксис для воз­
врата элементов один за другим, пока все они не будут возвращены вызывающей функ­
ции. Эта коллекция "содержит" пять объектов, каждый из которых представляет собой
простую строку, как и в рассмотренном только что примере M o n t h D a y s . Извне класса,
в функции M a i n ( ) , вы можете итерировать эти объекты посредством простого цикла
f o r e a c h , поскольку конструкция y i e l d r e t u r n возвращает п о одной строке з а раз.
Вот часть функции M a i n ( ) , в которой выполняется итерирование "коллекции"
StringChunks:
// Итерируем коллекцию с т р о к
StringChunks sc = new S t r i n g C h u n k s ( ) ;
// И т е р и р у е м - выводим т е к с т , помещая
// в с в о е й с о б с т в е н н о й с т р о к е
Console.WriteLine("\Строки:\п");
foreach
( s t r i n g sChunk in sc)
{

каждый

фрагмент

Console.WriteLine(sChunk);

Синтаксис итератора
В C# 2.0 вводятся два новых варианта синтаксиса итераторов. Конструкция
y i e l d r e t u r n больше всего напоминает старую комбинацию M o v e N e x t ( ) и C u r ­
r e n t для получения очередного элемента коллекции. Конструкция y i e l d b r e a k по­
хожа на оператор b r e a k , который позволяет прекратить работу цикла или конструкции
switch.

yield return
Синтаксис y i e l d r e t u r n работает следующим образом.
1. При первом вызове он возвращает первое значение коллекции.
2. При следующем вызове возвращается второе значение.

»

3. И так далее...

Глава 20. Работа с коллекциями

475

Это очень похоже на старый метод итератора M o v e N e x t ( ) , использовавшийся в ко|
де L i n k e d L i s t . Каждый вызов M o v e N e x t () предоставляет новый элемент коллекции
Однако в данном случае вызов M o v e N e x t () не требуется.
Что же подразумевается под очередным вызовом? Давайте еще раз посмотрим на
цикл f o r e a c h , использующийся для итерирования коллекции S t r i n g C h u n k s :
foreach

(string

sChunk

in

sc)

{
Console.WriteLine(sChunk);

}
Каждый раз, когда цикл получает новый элемент посредством итератора, последний
сохраняет достигнутую им позицию в коллекции. При очередной итерации цикла
f o r e a c h итератор возвращает следующий элемент коллекции.

yield break
Следует упомянуть еще об одном синтаксисе. Можно остановить работу итератора
в определенный момент, использовав в нем конструкцию y i e l d b r e a k . Например
достигнут некоторый порог при тестировании определенного условия в блоке итератор
класса коллекции, и вы хотите на этом прекратить итерации. Вот краткий пример блока
итератора, использующего y i e l d b r e a k именно таким образом:
//YieldBreakEx - пример
// yield break
class
YieldBreakEx

использования

ключевого

слова

{
i n t [ ] p r i m e s = { 2, 3, 5, 7, 1 1 , 1 3 , 1 7 , 1 9 , 23 } ,/ / G e t E n u m e r a t o r - возврат последовательности простых
// чисел с демонстрацией применения конструкции y i e l d
//
break
public

System.Collections.IEnumerator

GetEnumerator()

{
foreach

(int

nPrime

in

primes)

{
if (nPrime >
yield return

}

}

13) y i e l d
nPrime;

break;

//

Новый

синтаксис

}

В рассмотренном случае блок итератора содержит оператор i f , который проверяет
простые числа, возвращаемые итератором (кстати, с применением еще одного цикла
f o r e a c h внутри итератора). Если простое число превышает 13, в блоке выполняется инструкция y i e l d b r e a k , которая прекращает возврат простых чисел итератором. В противном
случае работа итератора продолжалась бы, и каждая инструкция y i e l d r e t u r n давала оче­
редное простое число, пока коллекция полностью не исчерпалась бы.

Блоки итераторов произвольного вида и размера
До этого момента блоки итераторов выглядели примерно следующим образом:
public

System.Collections.IEnumerator

GetEnumerator()

{

476

Часть VII. Дополнительные глава

yield

return

something;

}
Однако

они

могут также

принимать

и другие

ф о р м ы — именованных

итераторов

и свойств классов.

Именованные итераторы
Вместо т о г о ч т о б ы писать блок итератора в виде м е т о д а с и м е н е м G e t E n u m e r a t o r ( ) ,
можно н а п и с а т ь именованный итератор— ф у н к ц и ю , в о з в р а щ а ю щ у ю и н т е р ф е й с S y s t e m .
Collections. IEnumerable

вместо

IEnumerator,

которая

не

обязана

иметь

имя

G e t E n u m e r a t o r ( ) — м о ж е т е назвать е е хоть M y F u n c t i o n ( ) .
Вот, н а п р и м е р , п р о с т а я ф у н к ц и я , к о т о р а я м о ж е т и с п о л ь з о в а т ь с я д л я и т е р и р о в а н и я
четных ч и с е л от н е к о т о р о г о з н а ч е н и я в п о р я д к е убывания до н е к о т о р о г о к о н е ч н о г о зна­
чения — да, да, и м е н н о в п о р я д к е у б ы в а н и я : д л я и т е р а т о р о в э т о с у щ и е п у с т я к и !
//EvenNumbers

-

//

возвращает

четные

//

порядке

class

определяет
числа

именованный
в

итератор,

определенном

который

диапазоне

в

убывания

EvenNumbers

{
//DescendingEvens - это "именованный итератор",
в котором
// используется ключевое
слово y i e l d break.
Обратите
// внимание на е г о и с п о л ь з о в а н и е в цикле f o r e a c h в функции
// Main()
public
System.Collections.IEnumerable
DescendingEvens(int
nTop,
int
nStop)

{

//
//
if

Начинаем с ближайшего к nTop четного числа,
не
превосходящего
его
(nTop % 2 != 0)
// Если n T o p н е ч е т н о
n T o p -= 1;
// И т е р а ц и и от nTop в п о р я д к е уменьшения до ближайшего
// nStop четного числа,
превосходящего его
f o r ( i n t i = n T o p ; i >= n S t o p ; i -= 2)
if
//
//
yie

}

(i < nStop)
yield break;
Возвращаем очередное четное
итерации
ld
return
i;

число

на

к

каждой

}

Метод D e s c e n d i n g E v e n s () получает два аргумента (удобная возможность), опре­
деляющих верхнюю и нижнюю границы выводимых четных чисел. Первое четное число
равно первому аргументу или, если он нечетен, на 1 меньше него. Последнее генерируе­
мое четное число равно значению второго аргумента n S t o p (или, если n S t o p нечетно,
на 1 больше него). Эта функция возвращает не значение типа i n t , а интерфейс I E n u ­
m e r a b l e . Н о в ней все равно имеется инструкция y i e l d r e t u r n , которая возвращает
четное число и затем ожидает очередного вызова из цикла f o r e a c h .
Примечание:

это еще один пример

"коллекции",

в основе которой нет никакой

"настоящей" коллекции, наподобие уже рассматривавшегося ранее класса s t r i n g -

цельные главы

Глава 20. Работа с коллекциями

477

C h u n k s . Заметим также, что эта коллекция вычисляется— на этот раз возвращаемые
значения не жестко закодированы, а вычисляются по мере необходимости. Это еще один
способ получить коллекцию без коллекции. (Вы можете получать элементы коллекции
откуда угодно — например, из базы данных или от Web-сервиса). И наконец, этот при­
мер демонстрирует, что вы можете итерировать так, как вам заблагорассудится — на­
пример с шагом -2, а не стандартным единичным.
Вот как можно вызвать D e s c e n d i n g E v e n s () из цикла f o r e a c h в функции
M a i n () (заодно здесь показано, что произойдет, если передать нечетные гранична
значения — еще одно применение оператора %):
// Инстанцирование класса "коллекции" EvenNumbers
EvenNumbers en = new E v e n N u m b e r s ( ) ;
// И т е р и р о в а н и е : выводим четные ч и с л а от 10 до 4
C o n s o l e . W r i t e L i n e ( " \ п П о т о к убывающих ч е т н ы х ч и с е л : " ) ;
f o r e a c h ( i n t even in en.DescendingEvens(11,
3))

{
Console.WriteLine(even);

}
Этот вызов дает список четных чисел от 10 до 4. Обратите также внимание, как ис­
пользуется цикл f o r e a c h . Вы должны инстанцировать объект E v e n N u m b e r s (класс
коллекции). Затем в инструкции f o r e a c h вызывается метод именованного итератора:
EvenNumbers
foreach(int

en = new E v e n N u m b e r s ( ) ;
even in en.DescendingEvens(nTop,

nStop))

...

Если бы D e s c e n d i n g E v e n s () был статической функцией, можно было бы
обойтись без экземпляра класса. В этом случае ее можно было бы вызвать
с использованием имени класса, как обычно:
foreach(int even
(nTop,nStop))...

in

EvenNumbers.DescendingEvens

Поток идей для потоков объектов
Теперь, когда вы можете сгенерировать "поток" четных чисел таким образом, поду­
майте о массе других полезных вещей, потоки которых вы можете получить с помощью
аналогичных "коллекций" специального назначения: потоки степеней двойки, членов
арифметических или геометрических прогрессий, простых чисел или чисел Фибонач­
чи — да что угодно. Как вам идея потока случайных чисел (чем, собственно, и занимает­
ся класс R a n d o m ) или сгенерированных случайным образом объектов?
Если вы помните демонстрационную программу P r i o r i t y Q u e u e из гла­
вы 15, "Обобщённое программирование", то можете взглянуть на другую де­
монстрационную п р о г р а м м у — P a c k a g e F a c t o r y W i t h l t e r a t o r — на
прилагаемом компакт-диске. В ней проиллюстрировано использование блока
итератора для создания потока сгенерированных случайным образом объек­
тов, представляющих пакеты. Для этого применяется та же функция, что
и в классе P a c k a g e F a c t o r y в демонстрационной программе P r i o r i ­
t y Q u e u e , но содержащая блок итератора.

478

Часть VII. Дополнительные главы I

Гл

Итерируемые свойства
Можно также реализовать блок итератора в виде свойства класса.— конкретнее,
в функции доступа g e t ( )

свойства. Вот простой класс с о свойством D o u b l e P r o p .

Функция доступа g e t () этого класса работает как блок итератора, возвращающий поток
значений типа d o u b l e :
// P r o p e r t y l t e r a t o r - д е м о н с т р и р у е т р е а л и з а ц и ю функции
// доступа get свойства класса как блока итератора
class
Propertylterator
{
doublet]
doubles = {
1.0,
2.0,
3.5,
4.67
};
// D o u b l e P r o p - с в о й с т в о " g e t " с блоком и т е р а т о р а
public
System.Collections.IEnumerable
DoubleProp
get
{
foreach(double

db

in

doubles)

{
}

}

yield

return

db;

}
}
Заголовок D o u b l e P r o p пишется так же, как и заголовок метода D e s c e n d ­
i n g E v e n s ( ) в примере именованного итератора. О н возвращает интерфейс I E n u ­
m e r a b l e , но в виде свойства, не использует скобок после имени свойства и имеет
только функцию доступа g e t ( ) , н о н е s e t ( ) . Функция доступа g e t ( ) реализова­
на как цикл f o r e a c h , который итерирует коллекцию и применяет стандартную ин­
струкцию y i e l d

r e t u r n для поочередного возврата элементов и з коллекции чи­

сел типа d o u b l e .
Вот как это свойство можно использовать в функции M a i n ( ) :
// Инстанцируем класс "коллекции" P r o p e r t y l t e r a t o r
P r o p e r t y l t e r a t o r p r o p = new P r o p e r t y l t e r a t o r ( ) ;
// Итерируем е е :
генерируем значения типа double по
foreach
(double db in prop.DoubleProp)

одному

{

Console.WriteLine(db);

Вы можете использовать обобщенные итераторы.

Подробнее о

них можно

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

Где надо размещать итераторы
В н е б о л ь ш и х к л а с с а х и т е р а т о р о в с п е ц и а л ь н о г о н а з н а ч е н и я в д е м о н с т р а ц и о н н о й про­
грамме I t e r a t o r B l o c k s к о л л е к ц и и р а з м е щ а л и с ь внутри класса итератора, к а к , на­
пример, в M o n t h D a y s . В н е к о т о р ы х с л у ч а я х э т о в п о л н е к о р р е к т н о , н а п р и м е р , к о г д а
коллекция п о х о ж а н а к л а с с S e n t e n c e C h u n k s , в о з в р а щ а ю щ и й ч а с т и т е к с т а , и л и D e ­
s c e n d i n g E v e n s , который возвращает вычисляемые значения. Но что, если вы хотите

Глава 20. Работа с коллекциями

479

п р е д о с т а в и т ь и т е р а т о р , о с н о в а н н ы й н а б л о к е и т е р а т о р а д л я р е а л ь н о г о к л а с с а коллекции,
например такого, как L i n k e d L i s t ?
Эта задача решается в демонстрационной программе L i n k e d L i s t W i t h l t e r a t o r B l o c k на п р и л а г а е м о м к о м п а к т - д и с к е . В н е й класс L i n k e d L i s t пе­
р е п и с а н и и с п о л ь з у е т м е т о д G e t E n u m e r a t o r ( ) , р е а л и з о в а н н ы й как блок
итератора.

О н п о л н о с т ь ю з а м е н я е т с т а р ы й класс

LinkedListlterator.

В п р и в е д е н н о м д а л е е л и с т и н г е п р е д с т а в л е н а т о л ь к о н о в а я в е р с и я GetEnu­
m e r a t o r ( ) . П о л н о с т ь ю д е м о н с т р а ц и о н н у ю п р о г р а м м у м о ж н о найти н а при­
лагаемом компакт-диске.
// L i n k e d L i s t W i t h l t e r a t o r B l o c k
- реализует итератор для
// связанного списка в виде блока итератора
class LinkedList
//
":
I E n u m e r a t o r " больше не т р е б у е т с я

{
...

Остальная

//

часть

GetEnumerator

public

класса

-

реализован

IEnumerator

как

блок

итератора

GetEnumerator()

{
//

Проверяем

//

null,

//

установить

так,

//

связанного

списка

он

действительность
еще

if(currentNode

не

==

текущего

использовался,
чтобы

он

так

указывал

узла.
что

на

Если

его

он

надо

голову

null)

{
currentNode

=

head;

}
//

Здесь

//

возвращаемого

выполняются

итерации

методом

while(currentNode

!=

перечислителя,

GetEnumerator()

null)

{
yield return currentNode.Data;
currentNode = currentNode.forward;

} }

}

Такой базовый вид блока итератора уже встречался ранее в этой главе:
public

System.Collections.IEnumerator

GetEnumerator()

{}

Это выглядит точно так же, как и объект I E n u m e r a t o r , который метод G e t E n u ­
m e r a t o r О возвращает в исходном классе L i n k e d L i s t . Однако реализация метода
G e t E n u m e r a t o r ( ) теперь работает совершенно иначе.
Когда вы пишете блок итератора, С# создает для вас скрытый класс
L i n k e d L i s t l t e r a t o r . Вы не пишете этот класс и не видите его код.
О н н е является частью демонстрационной программы L i n k e d L i s t WithlteratorBlock.
В методе G e t E n u m e r a t o r ()

вы просто используете цикл для обхода

всех узлов связанного списка и возврата с помощью y i e l d r e t u r n эле­
ментов данных, хранящихся в каждом узле. Этот код приведен в преды­
дущем листинге.

480

Часть VII. Дополнительные главы

Вам больше не нужно определять ваш класс коллекции, как реализующий I E n u ­
m e r a t o r , что видно из приведенного в листинге заголовка класса.

Все не так просто
При этом нельзя забывать о некоторых неизбежных вещах.
Вы должны убедиться, что начинаете обход с начала списка.
Для этого в новый класс L i n k e d L i s t добавлен член-данные c u r r e n t N o d e , по­
средством которого отслеживается перемещение итератора по списку. Изначально
член c u r r e n t N o d e равен n u l l , так что итератор должен проверять это условие.
Если это так, он устанавливает c u r r e n t N o d e таким образом, чтобы тот указывал
на голову связанного списка.
Если только h e a d н е равен n u l l (связанный список н е пуст), т о c u r r e n t N o d e
становится ненулевым до конца итераций. Когда же он достигает конца списка,
итератор должен вернуть n u l l , что послужит сигналом о прекращении работы
для цикла f o r e a c h .
При каждом шаге по списку необходимо осуществлять все действия, которые вы­
полнялись ранее функцией M o v e N e x t () по перемещению к следующему узлу:
// Действия, выполнявшиеся
while(currentNode
!= null)

{

ранее

MoveNextО

// То, что делало с в о й с т в о C u r r e n t
y i e l d r e t u r n c u r r e n t N o d e . . . ; // Часть
currentNode = currentNode.forward;

кода

опущена

}
Большинство реализаций блоков итераторов используют цикл для прохода по
к о л л е к ц и и — а иногда даже внутренний цикл f o r e a c h (но пример S t r i n g C h u n k s показывает, что это не единственно возможный путь).
Когда вы проходите по списку и начинаете возврат данных с помощью
y i e l d r e t u r n , в ы должны "выковырять" хранящиеся данные и з объекта
L L N o d e . Узел связанного списка — это всего лишь корзина для хранения
s t r i n g , i n t , S t u d e n t и т.п. объектов. Поэтому в ы должны вернуть н е c u r ­
r e n t N o d e , а сделать следующее:
y i e l d r e t u r n c u r r e n t N o d e . D a t a ; / / Вот
currentNode = currentNode.forward;

теперь

верно

To же, но за сценой, делает и исходный перечислитель. Свойство D a t a класса
L L N o d e возвращает данные в узле как O b j e c t . Исходный необобщенный свя­
занный список преднамеренно спроектирован обобщенным настолько, насколько
это возможно, поэтому он и хранит объекты класса O b j e c t .
Теперь цикл w h i l e с инструкцией y i e l d b r e a k выполняет то, что ранее в ы должны
были делать с огромным количеством работы. В результате метод G e t E n u m e r a t o r () ра­
ботает в цикле f o r e a c h в функции M a i n ( ) , как и ранее.
Если вы немного поразмышляете над этим, то поймете, что такая реализация просто пе­
ремещает функциональность старого класса итератора L i n k e d L i s t l t e r a t o r в класс
LinkedList.

Глава 20. Работа с коллекциями

481

За сценой цикл f o r e a c h выполняет за вас необходимые приведения. Так что если
вы храните в списке строки s t r i n g , ваш цикл f o r e a c h ищет именно их:
f o r e a c h ( s t r i n g s in 11c)
{
Console.WriteLine(s);

//
//

foreach выполняет
приведение типов

}
Обобщенная версия связанного списка из этой главы с блоком итератора ис
пользована в демонстрационной программе G e n e r i c L i n k e d L i s t C o n ­
t a i n e r на прилагаемом компакт-диске. В этой демонстрационной про­
грамме инстанцируется обобщенный класс L i n k e d L i s t для объектов типа
s t r i n g , а затем — типа i n t . Чтобы лучше понять, как все это работая,
стоит пошагово пройти цикл f o r e a c h в отладчике. Для сравнения вы мо
жете познакомиться с новым встроенным классом L i n k e d L i s t < T > в про
странстве имен S y s t e m . C o l l e c t i o n s . G e n e r i c .
Советую вам при работе полностью забыть о мире необобщенных коллек­
ций — за исключением старого доброго массива, который может оказаться
полезен и при этом безопасен с точки зрения типов. Воспользуйтесь лучше
обобщенными коллекциями. Правда, лично я собираюсь не следовать этому^
совету уже в следующем разделе...

Осталось еще немного...
Реализация исходного итератора в L i n k e d L i s t реализует итератор как отдельный
класс, спроектированный для работы с классом L i n k e d L i s t . У такого решения есть
одна привлекательная возможность, которая отсутствует у решения с использованием
блока итератора. Вы можете легко создать несколько экземпляров итераторов и приме­
нять каждый из них независимо от других. Так что i t e r a t o r l может пройти полпути,
когда i t e r a t o r 2 только начинает обход.
Последняя демонстрационная программа решает этот вопрос (хотя и для более
лее простой коллекции, не L i n k e d L i s t ) . I t e r a t o r B l o c k l t e r a t o r ис­
пользует объект итератора с доступом к внутреннему устройству коллекции,
но сам этот объект реализован с применением блока итератора.
// I t e r a t o r B l o c k l t e r a t o r - реализует отдельный объект
// и т е р а т о р а для работы с классом коллекции, типа
// L i n k e d L i s t , но сам и т е р а т о р р е а л и з у е т с я с использованием
// блока итератора
using System;
using
System.Collections;
namespace
IteratorBlocklterator

{
class

Program

{
//
//
//
sta
{
s

482

Создание коллекции и использование двух
итератора для независимого итерирования
использует блок итератора)
t i c void Main(string[]
args)
t r i n g []

strs

=

new

объектов
(каждый

s t r i n g []

Часть VII. Дополнительные главы

{
"Joe",
"Bob",
"Tony",
"Fred"
};
M y C o l l e c t i o n mc = new M y C o l l e c t i o n ( s t r s ) ;
// Создание первого итератора и начало итераций
MyCollectionlterator mcil = mc.GetEnumerator();
foreach
(string si in mcil)
/ / Первый и т е р а т о р
// Какая-то работа со
Console.WriteLine(si);
/ / Ищем б о с с а Т о н и
i f ( s i == "Tony")

строками

{
/ / В средине э т о й и т е р а ц и и начинаем новую с
// использованием второго итератора
M y C o l l e c t i o n l t e r a t o r mci2 = m c . G e t E n u m e r a t o r ( ) ;
foreach
(string s2
in mci2)
// Второй итератор
//

Работа

if(s2

==

со

строками

"Bob")

{
Console.WriteLine("\t{0}

}

}

-

босс

{l}",

s2,

s i ) ;

}

}

/ / Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения программы.. . " ) ;
Console.Read();

}

}
//

Простая

public

коллекция

class

строк

MyCollection

{
// Реализация коллекции с использованием A r r a y L i s t
// i n t e r n a l
- так что объекты итераторов могут
// о б р а щ а т ь с я к с т р о к а м
i n t e r n a l A r r a y L i s t l i s t = new A r r a y L i s t ( ) ;
public
MyCollection(string[]
strs)
foreach(string

s

in

strs)

{
list.Add(s);

}
}
//
//

GetEnumerator - как и в
из объектов итераторов

public

MyCollectionlterator

LinkedList,

возвращает

один

GetEnumerator()

{
return

new

MyCollectionlterator(this);

}
}
// M y C o l l e c t i o n l t e r a t o r - класс итератора
public
class
MyCollectionlterator

Глава 20. Работа с коллекциями

для

MyCollection

483

// Храним ссылку на коллекцию
p r i v a t e M y C o l l e c t i o n mc;
public

MyCollectionlterator(MyCollection

mc)

{
this.mc

=

mc,-

}
// G e t E n u m e r a t o r - блок итератора,
который выполняет
// реальные итерации для объекта итератора
public
System.Collections.IEnumerator GetEnumerator()

{
//

Итерируем

список

связанной

//

доступен,

потому

что

foreach

(string

s

in

коллекции,

объявлен

как

который

internal

mc.list)

{
yield

}

return

s;

//

Сердце

блока

итератора

}

} }

Коллекция в

I t e r a t o r B l o c k l t e r a t o r представляет собой простой класс-

оболочку вокруг класса A r r a y L i s t . Его метод G e t E n u m e r a t o r ( ) просто возвращает
новый экземпляр сопутствующего класса итератора, такого же, как для L i n k e d L i s t .
//
//

GetEnumerator - как и в
из объектов итераторов

public

LinkedList,

MyCollectionlterator

возвращает

один

GetEnumerator()

{
return

new

MyCollectionlterator(this);

}
Однако внутри самого класса итератора все гораздо интереснее. Он также содержит
метод G e t E n u m e r a t o r ( ) . Реализованный с применением блока итератора, он выпол­
няет всю работу по итерированию. Вот этот метод:
// G e t E n u m e r a t o r - блок итератора,
который выполняет
// реальные итерации для объекта итератора
public
System.Collections.IEnumerator GetEnumerator()

{
//

Итерируем

список

связанной

//

доступен,

потому

что

foreach

(string

s

in

коллекции,

объявлен

как

который

internal

mc.list)

{
yield

return

s;

//

Сердце

блока

итератора

}
}
Данный метод имеет доступ к A r r a y L i s t из сопутствующей'коллекции, так что его
инструкция y i e l d r e t u r n может поочередно возвращать хранимые в коллекции строки
Выигрыш от этих сложностей концентрируется в функции M a i n ( ) , где создаются две
копии объекта итератора. Цикл f o r e a c h для второго итератора вложен в цикл f o r e a c h
для первого, что позволяет получить вывод программы наподобие приведенного:
Joe
Bob
Tony

484

Часть VII. Дополнительные главы

Bob

-

босс

Tony-

Fred
//• - •
Строка с отступом выводится вложенной итерацией.
Вот как выглядят эти вложенные циклы в функции M a i n ( ) :
MyCollectionlterator
foreach

(string

si

in

mcil

=

mcil)

// Какая-то работа со
Console.WriteLine(si);
/ / Ищем б о с с а Т о н и
i f ( s i == "Tony")

mc.GetEnumerator();
//

Первый

итератор

строками

{
/ / В средине э т о й и т е р а ц и и начинаем новую с
// использованием второго итератора
M y C o l l e c t i o n l t e r a t o r mci2 = m c . G e t E n u m e r a t o r ( ) ;
foreach
( s t r i n g s2 in mci2)
// Второй итератор
// Работа со строками
i f ( s 2 == "Bob")

{
Console.WriteLine("\t{o}

}

-

босс

{l}",

s2,

s i ) ;

}

}
Впрочем, исходный итератор, с M o v e N e x t () и C u r r e n t , все равно остается более
гибким и простым...

Глава 20. Работа с коллекциями

485

Глава 21

Использование интерфейса Visual Studio
>

Использование инструментария Visual Studio

>

Настройка рабочего места

>

Отладка программ

стественно, для работы необходимо знание языка. Программист на С#, не знающий
С# — нонсенс. Однако важно также знать и используемый инструментарий — в ча­
стности, пользовательский интерфейс пакета Visual Studio, который вы, вероятно,
применяете в работе. В этой главе речь пойдет о том, как работать с Visual Studio.
Материал настоящей главы применим ко всем редакциям Visual Studio 2005, включая
Visual С# Express. Большая часть времени при работе над консольными приложениями
из этой книги была потрачена на работу со следующими четырьмя окнами Visual Studio:

Solution Explorer и Class V i e w ;
Editor;
Help;
Debugger.
Эти окна перечислены в "хронологическом" порядке, а не в порядке важности. Когда
вы пишете большую программу, вы работаете с окном Solution Explorer, а затем вводите
исходный текст С# в окне редактора, все время пользуясь окном справки, а также окнами
Solution E x p l o r e r или C l a s s V i e w . После того как вы ввели программу, вы ищете ошиб­
ки в ней в окне отладчика. Пару раз в месяц вы обращаетесь еще к одному окну, за кото­
рым сидит кассир, но это окно не относится к Visual Studio.
Перед тем как приступить к работе, обычно настраивается расположение окон так,
чтобы это было удобно программисту.
Разработка графических приложений Windows включает ряд дополнительных

окон: F o r m Designer, T o o l b o x и Properties. Вкратце о них было рассказано
в главе 1, "Создание вашей первой Windows-программы на С # " .

Visual Studio организует свои различные инструменты в окна для лучшего использо­
вания наиболее ограниченного компьютерного ресурса — экрана монитора.

Состояния окон
Окно может находиться в одном из четырех состояний.
Закрытое (Closed)
Свободное (Floating)
Закрепленное (Docked)
Свернутое (Tabbed)
Эти состояния описаны в следующих разделах.

Закрытое окно
Закрытое окно — это окно, убранное с экрана. Единственный способ увидеть его
в н о в ь — воспользоваться подменю V i e w , как показано на рис. 21.1. Наиболее часто ис­
пользуемые окна перечислены в середине меню V i e w ; менее распространенные в подменю V i e w ^ O t h e r W i n d o w s . Некоторые отладочные окна доступны только из ме­
ню D e b u g в режиме отладки.

Свободные окна
Свободное окно выглядит парящим над рабочим столом Visual Studio, как показано
на рис. 21.2. Свободное окно не является совершенно независимым. Например, его нель­
зя минимизировать или разместить за главным окном, но зато можно поместить "за пре­
делами" окна Visual Studio, эффективно расширяя рабочий стол последнего.
Каждый режим Visual Studio имеет собственные настройки. У вас может быть
одно расположение окон в режиме разработки программы, когда вы редакти­
руете ее исходный текст и компилируете ее, и другое в режиме отладки, как бу­
дет описано позже в этой главе.

Закрепленное окно
Вы можете закрепить практически любое окно, выбрав его и воспользовавшись ко­
мандой W i n d o w ^ D o c k a b l e . Закрепленное окно "хватается" за другое окно или рамку
главного окна Visual Studio. Это состояние сохраняется и при изменении размеров окна
Visual Studio — закрепленные окна цепко держатся за границы основного окна и изме­
няют свои размеры вместе с ним.
На рис. 21.3, например, окно Output закреплено в верхнем правом углу, а окно Error
List — в нижней части окна. Перемещая границу между двумя окнами, вы автоматически
изменяете размеры каждого из окон.

Свернутое окно
Скрытые закрепленные окна, или, говоря более точно, минимизированные, выглядят
как тонкие закладки, закрепленные на внешних границах окна Visual Studio. На рис. 21,3
показаны окна Solution E x p l o r e r и C l a s s V i e w , свернутые в закладки у правой границы
окна Visual Studio.
Разместите курсор мыши над такой закладкой, чтобы развернуть окно. Чтобы оно ос­
талось в таком состоянии, воспользуйтесь кнопкой в правой верхней части окна с изо­
бражением канцелярской кнопки или оставьте его в автоматически скрываемом состоя­
нии, что бывает удобным, но, правда, далеко не всегда.

488

Часть VII. Дополнительные глав

Рис. 21.1. Подменю View позволяет открыть все необходимые окна

Puc. 21.2. Свободное окно выглядит независящим от Visual Studio

Шва 21. Использование интерфейса Visual Studio

489

Рис. 27.5. Основные окна Visual Studio
Щелчок правой кнопкой мыши на заголовке открытого окна позволяет изме­
нить его с о с т о я н и е — сделать его свободным, закрепленным, свернутым или
скрытым.

Скрытие окна
Независимо от установок, большинство открытых окон имеют маленькую кнопку
с изображением канцелярской кнопки (чтобы не говорить "кнопка" дважды, далее речь
пойдет просто о канцелярской кнопке) рядом с кнопкой закрытия окна в его полосе заго­
ловка. Такую канцелярскую кнопку вы можете увидеть у окна Output на рис. 21.3, где
показаны несколько основных окон Visual Studio. Щелкните на этой канцелярской кноп­
ке, и окно будет скрываться с ваших глаз, когда будет становиться ненужным. Это свой­
ство называется автоскрытием (auto-hide).
Поднятое состояние канцелярской кнопки означает закрепленное и заблокированное
окно. Опущенная канцелярская кнопка указывает, что окно не заблокировано и будет ав­
томатически скрываться, когда вы покидаете его.
Скрытое окно остается открытым (и его можно видеть в виде закладки ). Все на­
стройки, которые действовали, пока окно находилось в открытом состоянии, продолжа­
ют действовать и в скрытом состоянии.

Перестановка окон
Вы можете разместить окна так, как вам кажется более удобным. Возьмите окно
за полосу заголовка и переместите в другое место. При перетаскивании появится се­
рое изображение окна, указывающее, где окно будет закреплено, если вы перенесете
его в это место. На рис. 21.4 показано то же окно Visual Studio, что и на рис. 21.3,
после того как окно O u t p u t было перемещено для закрепления в верхней части окна
Visual Studio.

490

Часть VII. Дополнительные главы

При перемещении окна можно использовать "направляющий ромб" в центре с че­
тырьмя стрелками, направленными в разные стороны от центра.

Рис. 21.4. Закрепленное окно можно перезакрепить в новом месте
Для того чтобы перетащить окно, его нужно взять за полосу заголовка, перенести
к рамке, за которую вы хотите его закрепить, переместить указатель мыши на направ­
ляющую стрелку для этой стороны и отпустить его. Окно будет закреплено в данной по­
зиции, если вы отпустите кнопку мыши над направляющей стрелкой (одной из централь­
ного ромба или ее дубля у края о к н а — на рис. 21.4 указатель мыши находится как раз
над таким дублем).
Расстановка окон — увлекательное занятие, чем-то похожее на игру (можно
при этом вспомнить знаменитый кубик Рубика). Вам может потребоваться
подправить несколько окон, чтобы достичь желаемого эффекта. Например,
начав с конфигурации, показанной на рис. 21.3, вы можете перенести окно
O u t p u t к левой границе, а окно E r r o r List сместить в нижний правый угол,
как показано на рис. 21.5. Чтобы окно E r r o r List было закреплено у всей
нижней границы окна Visual Studio, закрепите его за нижнюю рамку (на
рис. 21.6 показана данная конфигурация). Экспериментируйте, пока не полу­
чите устраивающий вас результат.

Наложение окон
Перетаскивание и отпускание окна на центральном квадрате направляющего ромба
позволяет складывать окна в "стопку" (центральная пиктограмма играет роль своеобраз­
ного клея). Каждое окно в такой стопке доступно при щелчке на вкладке, которая может
быть вверху или внизу окна. На рис. 21.7 показана стопка окон редактирования, состоя­
щая и з трех о к о н — для файлов U n i v e r s i t y . c s , S t u d e n t , c s и P r o g r a m , c s .
Двойной щелчок на имени файла в Solution E x p l o r e r (о нем чуть позже) откроет окно с
этим файлом так, что оно окажется верхним в стопке.

Глава 21. Использование интерфейса Visual Studio

491

Рис. 21.5. Чтобы получить данную конфигурацию окон из конфигурации на
рис. 21.3, требуется два шага. Еще один шаг— и вы получите конфигурацию,
показанную на рис. 21.6

Рис. 21.6. Последовательное закрепление окон позволяет достичь желаемой
конфигурации

492

Часть VII. Дополнительные главы

Рис. 21.7. Стопки окон помогают эффективно использовать рабочее про­
странство окна Visual Studio

Модные штучки
Щелкните правой кнопкой мыши в окне Solution E x p l o r e r и выберите пункт
V i e w C l a s s D i a g r a m . Visual Studio сгенерирует в проекте новый файл C l a s s D i a g r a m l . c d . Вы можете открыть его и воспользоваться C l a s s D e s i g n e r для
визуализации и работы со связями в вашей программе с использованием сим­
волики в стиле UML (подробнее об этом можно узнать из раздела "class dia­
gram, presentation and documentation" справочной системы). Это не полнофунк­
циональное средство для работы с UML-диаграммами, но оно может помочь
визуализировать вашу программу и быть полезным при работе с кодом.
Чтобы познакомиться с другими модными штучками в Visual Studio 2005, об­

ратите внимание на новое меню Refactor и команду C o d e Snippets Manager
в меню T o o l s , а кроме того, обратитесь к разделу "What's New" справочной
системы. Запомните эти комбинации клавиш: , а потом — .

Программа может состоять из любого количества исходных файлов С# — ну, скажем,
из любого разумного количества. Несколько тысяч может оказаться слишком большим
числом, хотя, вероятно, Visual Studio приходилось сталкиваться с подобным количест­
вом при создании продуктов Microsoft.
"Ну и зачем создавать все эти файлы?" — спросите вы. Реальные программы могут
быть очень большими, как уже говорилось в главе 19, "Работа с файлами и библиотека­
ми". В этой главе рассматривалась система продажи авиабилетов, состоящая из многих

Глава 21. Использование интерфейса Visual Studio

493

частей: интерфейса для заказа билетов по телефону, для работы через Интернет, часть
для работы с ценами и налогами и так далее. Такие программы становятся огромным
задолго до их завершения.
Такие сложные системы могут состоять из множества отдельных классов, по одному
для каждого описанного интерфейса. В главе 19, "Работа с файлами и библиотеками", бьш
предложено не размещать все эти классы в одном большом файле P r o g r a m , с s , посколь­
ку это затруднит поиск классов, с которыми вы работаете, работу команды — так как про­
граммисты не могут работать одновременно с одним файлом, да и компиляция такого фай­
ла станет занимать слишком большое время. По этим причинам были даны рекомендации
размещать классы в файлах по одному, давая файлам имена классов. В этой главе вы по­
знакомитесь с примером такого подхода.
Конечно, чем больше файлов, тем больше работы с ними. К счастью, Solution Ex­
plorer может помочь в решении практически всех задач при разделении большого проек­
та на множество исходных файлов. На рис. 21.8 показано окно Solution Explorer с от­
крытым проектом С#.

Упрощение жизни с помощью проектов и решений
Файл проекта с расширением . C S P R O J содержит инструкции о том, какие файлы
входят в проект и как именно они должны быть скомбинированы. Именно с этим файлом
вы и работаете посредством окна Solution Explorer.
Проекты могут объединять программы, которые зависят от одних и тех же пользова­
тельских классов, как правило, сложные программы разделяются на несколько проектов,
в совокупности составляющих одно решение. Пара стандартных сценариев организации
проектов уже была описана в главе 19, "Работа с файлами и библиотеками": объединение
программы записи файлов с программой чтения, или программа, которая разделена на
код в выполнимом файле, и одна или несколько библиотек классов. В этих сценариях
при изменениях в одном проекте остальные перекомпилировались автоматически. Про­
грамма записи файла описывалась одним проектом, программа чтения — другим. Ана­
логично, у вас был один проект для выполнимого файла, и другой — для библиотеки.
Набор проектов называется в Visual Studio решением (файлы решений имеют расшире­
ния . S L N ) .
Проект описывает не только исходные файлы, которые должны быть собраны вместе
в одну программу. Файл проекта включает такие свойства, как, например, имя програм­
мы и аргументы, передаваемые ей при запуске из Visual Studio.
Каждая программа, независимо от ее размера, описывается решением Visual
Studio, содержащим как минимум один проект. Чтобы увидеть пример много­
проектного решения, обратитесь к решению демонстрационной программы
C l a s s L i b r a r y на прилагаемом компакт-диске. Это решение содержит два
проекта, один — для небольшой тестовой программы, или "драйвера", и вто­
рой — для простой библиотеки классов. Эта программа также рассматривалась
в главе 19, "Работа с файлами и библиотеками".
В мире имеются миллионы программ. В следующем разделе будет рассмотрена толь­
к о одна демонстрационная программа V S I n t e r f a c e , определяющая класс U n i v e r ­
s i t y и класс S t u d e n t . Каждый класс находится в своем собственном файле. Програм­
ма добавляет несколько объектов S t u d e n t в U n i v e r s i t y , а затем выводит результат.

494

Часть УН. Дополнительные главы

Рис. 21.8. Проект no умолчанию содержит шаблонный класс Pro­
gram, cs, выделенный в окне Solution Explorer

Отображение проекта
Перечисленные далее шаги создают схему приложения по умолчанию для программы
VSInterface.
1. Выберите команду меню File 1 ^New^Project.
2. Выберите пиктограмму Console Application.
3. Введите имя VSInterface и щелкните на кнопке О К .
Выберите команду меню View"=>Solution E x p l o r e r для того, чтобы увидеть файл про­
екта V S I n t e r f a c e , как показано на рис. 21.8. Таким образом, создано решение V S I n ­
t e r f a c e , содержащее один проект с тем ж е именем V S I n t e r f a c e .

Изучение Solution Explorer
Окно Solution Explorer показывает две начальные подпапки, P r o p e r t i e s и R e f ­
e r e n c e s . P r o p e r t i e s содержит файл A s s e m b l y I n f o . c s , а также некоторые файлы
"ресурсов" и "настроек". О настройках речь пойдет чугь позже, а что касается ресурсов, то
тут достаточно будет сказать, что они содержат такие вещи, как изображения, пиктограм­
мы, строки, входящие в пользовательский интерфейс (такие как сообщения в диалоговых
окнах) и тому подобное. Подробнее о ресурсах можно узнать из справочной системы..
Подпапка R e f e r e n c e s содержит все ссылки на внешние пространства имен, указанные
с помощью команды меню P r o j e c t s A d d Reference. Ссылки, добавленные в ваш проект, со­
ответствуют вашим директивам u s i n g (включая некоторые "предположения" по умолчанию,
которые могут не оправдаться для вашей программы; их можно удалить, если они вам не
нужны). В окне также перечислены: исходный файл по умолчанию P r o g r a m , cs и прочие
исходные файлы, которые вы добавляете в проект в процессе работы. (Эти файлы немного
отличаются для графических программ Windows, как вы могли видеть при создании про­
граммы, описанной в главе 1, "Создание вашей первой Windows-программы на С#".)

Глава 21. Использование интерфейса Visual Studio

495

Двойной щелчок на файле в окне Solution E x p l o r e r приводит к открытию окна дм
его редактирования (если редактирование файла возможно). Файл P r o g r a m , cs содер­
жит трамплин программы на С# — функцию M a i n ( ) . Конечно, большинство програш
в этой книге написаны непосредственно в одном этом файле. В предыдущих версия
Visual Studio файл P r o g r a m , cs назывался C l a s s l . c s , что заставляло всякий раз его
переименовывать. P r o g r a m , с s — вполне приличное название, в переименовании не
нуждающееся.
В больших проектах файлы могут быть размещены в большем количестве подка­
талогов. Щелкните правой кнопкой мыши в окне Solution Explorer и выберите
в меню команду A d d ^ N e w Folder. Перетащите файлы во вновь созданную пап­
ку. Visual Studio сам разберется, что к чему, и где лежат нужные файлы.
В ы можете редактировать файл A s s e m b l y l n f о . c s , снабжая вашу программу
такими свойствами, как ее название и название компании, торговая марка и номер
версии. Не стоит тратить время на эти свойства, следует только сказать, что это все­
го л и ш ь простой текстовый файл. Эти свойства появляются на вкладке V e r s i o n in­
f o r m a t i o n ( В е р с и я ) окна свойств . ЕХЕ-файла, открывающегося при щелчке правой
кнопкой м ы ш и на файле в Проводнике Windows и выборе из раскрывающегося ме­

ню команды P r o p e r t i e s .
Вы можете добавить в проект файлы любого вида. Обычно я храню здесь электрон­
ную таблицу или текстовый файл для собственных примечаний. Щелкните правой кноп­
кой мыши на проекте в окне Solution E x p l o r e r и выберите команду меню Add^Existing
Item. Выберите добавляемый файл.

Свойства проекта
Щелкните правой кнопкой мыши на имени проекта в окне Solution Explorer
и выберите команду Properties, чтобы открыть диалоговое окно свойств про­
екта. Здесь можно обновить ряд настроек проекта. Многие категории в окне
Properties имеют имена, которые вполне проясняют их предназначение, но вы
всегда можете обратиться к справочному материалу: поищите раздел "project
properties" в справочной системе. Одна из наиболее полезных вкладок окна
Properties — вкладка

Settings.

Определите

ваши

собственные

настройки:

только для чтения — Application и для чтения и записи — U s e r — и вы сможе­
те обратиться к ним из своей программы следующим образом:
// Эту с т р о к у надо д о б а в и т ь в р а з д е л u s i n g
u s i n g MyAppName. P r o p e r t i e s ,//
. . . затем где угодно в вашей программе напишите
//
вроде:
// для
string

что-то

получения значения настройки только для чтения:
myString
=
Settings.Default.MyAppSetting;

// для установки значения настройки для чтения и записи:
Settings.Default.MyUserSetting = myUserString;
// Доступ к р е с у р с а м о с у щ е с т в л я е т с я п р а к т и ч е с к и т а к же
Таким образом, оказывается очень просто сохранить пользовательские настройки без
необходимости управления ими самостоятельно с помощью классов S y s t e m . 10, опи­
санных в главе 19, "Работа с файлами и библиотеками". В завершенной программе, ко-

496

Часть VII. Дополнительные главы

торую вы распространяете, пользовательские настройки хранятся в персональных облас­
тях Application Data каждого пользователя. (Эта папка является частью персонального
профиля каждого пользователя и обычно хранится в папке С : \ D o c u m e n t s a n d S e t t i n g s \ < u s e r n a m e > . ) Чтобы сохранить измененные настройки, вызовите в вашей про­
грамме S e t t i n g s . D e f a u l t . S a v e ( ) (например, в конце функции M a i n ( ) или когда
вы закрываете W i n d o w s F o r m ) . Сохраненные настройки загружаются автоматически
при новом запуске приложения.

Навигация по исходному тексту
В больших программах достаточно трудно переходить от работы над методом
А класса В к работе над методом С класса D. Вы можете найти определенный
класс или метод путем открытия файла для редактирования ипросмотра его
содержимого или воспользоваться возможностью C l a s s V i e w , что значительно
быстрее — особенно в больших проектах.
В Solution E x p l o r e r вы должны дважды щелкнуть на файле для того, чтобы открыть
его, а затем прокрутить его или воспользоваться средствами поиска для того, чтобы най­
ти искомый метод. Вы можете также применить два раскрывающихся списка поверх ка­
ждого окна редактора для поиска класса и его методов в текущем файле. Левый раскры­
вающийся список содержит классы текущего файла, а правый — члены выбранного
класса. Я часто пользуюсь этими списками, но все же лично мне больше нравится ис­

пользовать C l a s s View.
C l a s s V i e w рассматривает программу не как множество файлов, а как множество
классов и их членов, что позволяет данному средству быть незаменимым помощником
при навигации по проекту. Щелкните на классе в верхней панели для того, чтобы уви­
деть список его членов (с сигнатурами параметров) в нижней панели. Это очень полез­
но — иметь возможность быстро вспомнить об аргументах и типе возвращаемого значе­
ния метода. Вернитесь к рис. 21.7, на котором показан результат двойного щелчка на
классе U n i v e r s i t y в окне C l a s s V i e w — открыт соответствующий исходный файл;
двойной щелчок на методе E n r o l l () показывает этот член в открытом исходном фай­
ле. (Посредством правого щелчка на классе в окне C l a s s V i e w отображается раскры­
вающееся меню с некоторыми интересными возможностями.)

Держите окна Solution Explorer и C l a s s V i e w в закрепленном наложенном со­
стоянии, чтобы быстро переключаться между ними с помощью одного щелчка
на вкладке.

Добавление класса
Размещать каждый класс в отдельном файле, да еще так, чтобы имя файла совпадало
с именем класса — хорошая программистская привычка. Подклассы могут находиться
либо в своих собственных файлах, либо в файле базового класса, в зависимости от того,
насколько тесно они связаны друг с другом.
Классы S c h o o l и S t u d e n t определенно следует разместить в разных файлах. Точно
так же нужно разделить классы H i g h S c h o o l , U n i v e r s i t y и S c h o o l , поскольку кон­
цептуально они достаточно далеки друг от друга. Вместе в одном файле лучше размес­
тить классы наподобие L e s s e r C a n a d i a n G o o s e и G r e a t e r C a n a d i a n G o o s e .

Глава 21. Использование интерфейса Visual Studio

497

Рис. 21.9. Добавление нового класса в проект с использованием окна
Add New Item
Для того чтобы добавить класс U n i v e r s i t y в программу V S I n t e r f а с е , выполни­
те следующие шаги.
1. Щелкните правой кнопкой м ы ш и на имени проекта V S I n t e r f а с е в окне
Solution Explorer, а затем выберите команду меню Add

Add New Item.

В появившемся окне вам будет предложена масса шаблонов объектов на выбор.
Их слишком много, чтобы поместиться на одном рисунке!
2. Выберите Class, введите University. cs в поле Name в нижней части ок­
на и затем щелкните на кнопке O p e n .
На рис. 21.9 показано окно Add N e w Item с выбранным шаблоном C l a s s .
Содержимое нового исходного файла U n i v e r s i t y . cs выглядит очень похоже
на содержимое файла P r o g r a m , c s , который строится по умолчанию при созда­
нии новой программы.
3. Повторите процесс для класса Student. После этого проект будет содер­
жать файлы Student.cs и University.cs наряду с Program.cs.
Вернитесь к рис. 21.8, на котором показан результат выполнения указанных ша­
гов. Три исходных файла представлены вкладками в окне редактирования.

Завершение демонстрационной программы
Данная версия класса S t u d e n t создает и выводит, информацию о студенте,
состоящую из идентификатора и имени.
// V S I n t e r f а с е - файл S t u d e n t . c s
// S t u d e n t - м о д е л и р о в а н и е с т у д е н т а ,
// н а п и с а т ь с в о е имя
using
System;
namespace
VSInterface

498

который

в

состоянии

сам

///

/// S t u d e n t - учащийся школы
///

public
class
Student
{
private
string
sStudentName;
private int nID;
public

Student(string

sStudentName,

int

nID)

{
this.sStudentName
this.nID = nID;

=

sStudentName;

}
///

/// Name - имя у ч а щ е г о с я
///

p u b l i c s t r i n g Name { g e t {
///

/// T o S t r i n g - в о з в р а щ а е т
///

public

override

string

return
имя

и

sStudentName;}

}

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

ToString()

{
return

String.Format("{0}

({l})",

sStudentName,

nID);

}

}
Конструктор класса S t u d e n t получает имя и идентификатор студента. Метод
T o S t r i n g ( ) перекрывает версию базового класса п о умолчанию O b j e c t .
Эта пользовательская версия возвращает имя студента и его идентификатор в
скобках. В главе 18, "Эти исключительные исключения", более подробно рас­
сказано о перекрытии T o S t r i n g ( ) .
Класс S t u d e n t включает комментарии документирования, помеченные как / / / . Та­
кое документирование делает код более понятным, в особенности если классы распреде­
лены по нескольким файлам, и может оказаться полезным для других программистов (да
даже и для самого автора исходного текста через некоторое время). Об использовании
таких документирующих комментариев и генерации справочных файлов для ваших про­
грамм уже рассказывалось в главе 8, "Методы класса".
Вы можете перекомпилировать вашу программу даже до того, как введете
исходный текст файла S t u d e n t . cs — после того, как введете исходный
текст файлов U n i v e r s i t y . c s и P r o g r a m , c s . Это неплохая и д е я — инкрементная разработка программ. Перекомпилируйте и исправляйте вашу
программу до тех пор, пока компилятор не перестанет выводить сообщения
об ошибках или предупреждения. Поступайте так для каждого класса или
даже метода, пока не избавитесь от всех ошибок при компиляции.
Исходный текст файла U n i v e r s i t y . cs столь же прост, как и его предше­
ственник:
//
//

V S I n t e r f а с е - файл U n i v e r s i t y . c s
U n i v e r s i t y - простейший контейнер

Глава 21. Использование интерфейса Visual Studio

для

студентов

499

using

System;

using

System.Collections;

namespace

VSInterface

{
///



///

University

///

-

учебное

заведение



public

class

University

{
private

string

private

SortedList

public

sName;
students;

University(string

//

Словарь

sNarae)

{
t h i s . s N a m e = sName;
s t u d e n t s = new S o r t e d L i s t ( ) ;

}
///
///
///


Enroll
- добавить


public

void

студента

Enroll(Student

в

университет

student)

{
students.Add(student.Name,

student);

}
public

override

string

ToStringO

{
s t r i n g s = sNarae + " \ n " ;
s += " С п и с о к с т у д е н т о в : " + " \ n " ;
// Итерация по всем студентам у н и в е р с и т е т а с
// использованием обычного перечислителя
IEnumerator i t e r = students.GetEnumeratorО;
while(iter.MoveNext())

{
object о = iter.Current;
/ / Следующий п о д х о д н е р а б о т а е т ,
потому что итератор
// для S o r t e d L i s t возвращает записи словаря,
которые
// включают к а к с т у д е н т а ,
так и ключ:
//
// Student s t u d e n t =
(Student)о;
// // Р а б о т о с п о с о б е н следующий в а р и а н т :
//
(обратите внимание на преобразование типов)
D i c t i o n a r y E n t r y de =
( D i c t i o n a r y E n t r y ) о,Student student =
(Student)de.Value;
s += s t u d e n t . T o S t r i n g ( ) + " \ n " ;

}

}

}

}

return

S ;

Д а н н ы й ф а й л у к а з ы в а е т , ч т о е г о с о д е р ж и м о е я в л я е т с я ч а с т ь ю п р о с т р а н с т в а имен
V S I n t e r f a c e . К л а с с U n i v e r s i t y с о с т о и т н е б о л е е ч е м и з и м е н и и отсортированной
коллекции студентов. М е т о д E n r o l l ( )

добавляет объекты типа S t u d e n t в S o r t e d ­

L i s t с и с п о л ь з о в а н и е м и м е н и с т у д е н т а в к а ч е с т в е к л ю ч а с о р т и р о в к и — д р у г и м и слова­
ми, студенты хранятся в списке в отсортированном порядке.

500

Часть VII. Дополнительные главы

Метод U n i v e r s i t y . T o S t r i n g ( ) выводит название университета, гимн и имена
всех студентов. Он делает это путем создания итератора, метод M o v e N e x t () которого
применяется для перехода от одного элемента списка к другому, получая каждый эле­
мент посредством свойства C u r r e n t . Поскольку класс S o r t e d L i s t , в котором хра­
нятся студенты, представляет собой словарь, итератор возвращает не сохраненные объ­
екты, а записи из словаря (объекты класса D i c t i o n a r y E n t r y ) , содержащие объект
вместе с ключом, применяемым для сортировки. Обратите внимание на использованные
преобразования типов.
Коллекции и их итерирование описаны в главах 15, "Обобщенное программирова­
ние", и 20, "Работа с коллекциями". Здесь вместо синтаксиса M o v e N e x t () можно
применить блок итератора, рассмотренный в предыдущей главе, а можно восполь­
зоваться циклом f o r e a c h :
foreach(DictionaryEntry

de

in

students)

{
Student student =
(Student)de.Value;
s += s t u d e n t . T o S t r i n g ( ) + " \ n " ;

}
Как вы вскоре убедитесь, цикл f o r e a c h предпочтительнее.

Преобразование классов в программу
Классы S t u d e n t и U n i v e r s i t y н е образуют программу. Консольное приложение
начинается со статического метода M a i n ( ) . Этот метод может быть в любом классе,
однако по умолчанию он находится в классе P r o g r a m .
Содержимое исходного файла P r o g r a m , c s изменено следующим образом:

// V S I n t e r f а с е - демонстрационная программа, состоящая из
// нескольких к л а с с о в . Классы S t u d e n t и U n i v e r s i t y находятся
// в своих собственных исходных ф а й л а х .
// Файл P r o g r a m . c s
using
System;
namespace
VSInterface

{
class

Program

{
static

void

Main(string[]

args)

{
University university =
new U n i v e r s i t y ( " И н с т и т у т случайных н а у к " ) ;
university.Enroll(new Student("Dwayne",
1234));
university.Enroll(new Student("Mikey",
1235));
university.Enroll(new Student("Mark",
1236));
Console.WriteLine(university.ToString() ) ;
Console.WriteLine();
// Ожидаем п о д т в е р ж д е н и я п о л ь з о в а т е л я
Console.WriteLine("Нажмите для " +
"завершения программы...");

Глава 21. Использование интерфейса Visual Studio

501

Console.Read();

}
}
}
При компиляции программы файл проекта говорит Visual Studio о том, что в одну
программу следует объединить все три ф а й л а — U n i v e r s i t y . c s , S t u d e n t . c s
и Program. cs.
При выполнении программы выводится простой (но отсортированный!) список сту­
дентов:
Институт случайных
Список студентов:
Dwayne
(1234)
Mark
(1236)
Mikey
(1235)
Press

Enter

to

наук

terminate...

Программы в этой книге написаны так, чтобы максимально сэкономить бумагу. До­
полнительные пустые строки опущены, код, который не представляет непосредственный
интерес для рассматриваемой темы, зачастую тоже, а оставшийся в основном линейно
организован. Должен ли любой ваш код выглядеть таким образом?
Далее приводится несколько рекомендаций по написанию кода, который легко читать
(человеку), обновлять, сопровождать и тестировать, и который хорошо организован
в концептуальном смысле.
Используйте дополнительные пробелы и пустые строки. Избегайте излишнего
сжатия кода, которое наблюдается в настоящей книге. Вот класс S t u d e n t , пере­
писанный в более свободном формате:
//
//
//

V S I n t e r f а с е - файл S t u d e n t . c s
Student - моделирование студента,
н а п и с а т ь с в о е имя

#region
using

Using

который

в

с о с т о я н и и сам

Directives

System;

#endregion
namespace

Using

Directives

VSInterface

{
III

/// S t u d e n t - учащийся
///

public
class
Student

школы

{

#region
//

502

Имена

Private

Data

переменных

Fields
начинаются

со

строчной

буквы,

но

Часть VII. Дополнительные главы

// слова внутри имени переменной
//
букв
private
string
sStudentName;
private int nID;
#endregion
#region

Private

с

прописных

Fields

Constructors

//

Student

//

прописной

public

Data

начинаются

-

конструктор
буквы,

и

как

Student(string

имя

и

классаначинаются

все

слова

sStudentName,

внутри
int

с

имени

nID)

{
this.sStudentName
this.nID = nID;

=

sStudentName;

}
#endregion
#region

Constructors

Public

///



///
///

Name - имя


public
///
///
///

string

Methods

and

Properties

учащегося

Name

{

get{

return

sStudentName;}

}


ToString - возвращает


public

override

string

имя

и

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

ToString()

{
return

String.Format("{o}

({l})",

sStudentName,

nID);

}
#endregion

}

Public

Methods

and

Properties

}

Используйте директивы Visual Studio # r e g i o n и

ttendregion

для отделения

разделов вашего кода. Это позволит сворачивать и скрывать разделы при работе
над другими частями кода. Нажмите , а затем < C t r l + 0 > , чтобы переклю­
читься между свернутым и развернутым состоянием. Давайте вашим разделам
описательные имена, такие как показаны в приведенном листинге.
Используйте X M L - к о м м е н т а р и и , начинающиеся с / / / . Данные символы по­
зволяют Visual Studio применять их в механизме автозавершения, выводя коммен­
тарии как документацию по данному методу прямо в окне кода при вызове одного
из ваших собственных методов. Механизм автозавершения рассматривался в гла­
ве 8, "Методы класса", и является одним из простейших способов получить спра­
вочную информацию о методе или классе — в том числе и по вашим собствен­
ным, если вы используете XML-комментарии.

Глава 21. Использование интерфейса Visual Studio

503

Вы можете также воспользоваться инструментом N D o c с открытым кодом
( h t t p : / / n d o c . s o u r c e f o r g e . n e t ) и автоматически сгенерировать
привлекательную документацию в стиле V i s u a l S t u d i o на основании ваших
XML-комментариев.
Комментируйте код, но делайте комментарии значимыми. Хороший
комментарий рассказывает о ваших намерениях и назначении кода, а не
о механике их реализации. Например, не пишите так:
/ / Ц и к л п о м а с с и в у с т у д е н т о в с в ы з о в о м м е т о д а D i s p l a y для
// каждого объекта типа S t u d e n t .
Вместо этого достаточно написать:
//

Вывод

информации

о

студентах.

Посмотрите на имена методов или классов, которые собираетесь
комментировать, и подумайте, нельзя ли их переименовать так, что­
бы комментарии стали излишни. Метод D i s p l a y A l l S t u d e n t s () не
требует никаких комментариев.
Используйте хорошие описывающие имена для переменных, методов,
классов и прочих объектов. Начинайте имена методов с глаголов
( D i s p l a y A l l S t u d e n t s ( ) ) , логические переменные или методы со слов
наподобие is или has ( i s V a l i d , h a s l t e m s , c a n P a s t e ) , и делайте все име­
на понятными и значащими. Не используйте слишком длинных имен. Избе­
гайте применения в именах аббревиатур, в особенности нестандартных.
Х о т я в этой книге и используется венгерская нотация (см. главу 3,
"Объявление переменных-значений"), с у щ е с т в у ю т и другие соглаше­
ния по именованию. Большинство программистов не используют венгер­
скую нотацию, в которой в качестве префикса применяется указание типа
(наподобие s для s t r i n g , d для d o u b l e и так далее). В предыдущем
примере использован другой стиль именования, который вы встречаете
в большей части документации и примеров.
Пишите короткие методы, которые проще для понимания, менее подвер­
жены ошибкам и легче тестируются. Везде, где это возможно, работа метода
должна использовать вызовы других методов. Это называется разложением
вашего кода. Если категорически не требуется иного, делайте ваши методы за­
крытыми. Даже однострочный код стоит выделить в отдельный метод, если это
делает код исходного метода понятнее. Предположим, например, что у вас есть
сложное составное логическое выражение, наподобие
i f ( ( y p o s == -1) & (vowelPos == - 1 ) )
...
Его достаточно сложно понять с первого взгляда. Можно использовать
комментарии для пояснения сути дела, но маленький метод с хорошим
именем ничуть не хуже:
public

bool

HasNoVowels(int
int

indexOfLetterY,
indexOfFirstVowel)

{
return

( i n d e x O f L e t t e r Y == -1) &
( i n d e x O f F i r s t V o w e l == -1);

}

504

Часть VII. Дополнительные главы

10

Этот код (из небольшого переводчика на Pig Latin , который я как-то писал) сле­
дует за кодом, который пытается найти первую гласную в целевом слове, если та­
ковая существует. Если е е нет, i n d e x O f F i r s t V o w e l принимает значение - 1 .
Однако буква у также может рассматриваться как гласная в некоторых ситуациях,
так что этот метод должен принимать во внимание и ее.
В методе, вызывающем Н а s N o V o w e l s ( ) , следующая строка гораздо проще для
понимания, чем исходное логическое выражение:
if ( H a s N o V o w e l s ( y p o s ,

vowelPos) )

{

return

'USE_WHOLE_WORD;

}

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

string

ConvertToPigLatin(string

return GetBackPart(word)

+

word)

GetFrontPart(word)

+

"ay";

Код написан на высоком уровне, с использованием имен методов, которые ясно
указывают их предназначение, не детализируя, как именно они работают —
с применением циклов, ветвлений и т.д. Легко увидеть, как минимум в общем, что
делает каждый вызов метода. Исходная версия этого метода была полна конст­
рукций i f , циклов, сложных логических выражений и локальных переменных.
Алгоритм "поросячьей латыни" прост, но некоторые его составные части несколь­
ко запутанны — как, например, поиск первой гласной для разбивки слова. Ис­
пользование описанного стиля работает сверху вниз (от общего к частному), от­
кладывая детали. Как можно предположить, методы G e t B a c k P a r t () и G e t ­
F r o n t P a r t () написаны одинаково, с явным указанием намерений на каждом
шагу и переносом деталей в подчиненные методы. Многие программы в этой кни­
ге можно улучшить посредством этого стиля, либо используя его изначально, либо
прибегая к рефакторингу.
Можно снизить сложность еще больше, если создать вспомогательные клас­
сы, инкапсулирующие часть работы, вместо одного или д в у х классов, тяну­
щих все на себе. Всегда старайтесь инкапсулировать мелкие детали в классах или
наборах методов. В частности, посмотрите, нет ли кода, который может изменить­
ся в будущем, и инкапсулируйте его в собственном классе. Моя любимая книга на
эту тему — Head First Design Patterns Фриманов (Freeman) (O'Reilly, 2004).
Эти и подобные методы помогут вам справиться с величайшей проблемой програм­
мирования: управлением сложностью. Плотный, закрученный код трудно понимаем,
а это — прямой путь к ошибкам.

10

"Поросячья латынь" — искажение слов английского языка по определенным правилам;
в чем-то аналог знакомого с детства "языка" в стиле "э-чи-то-чи дет-чи-ский-чи я-чи-зык-чи". —
Примеч. ред.

Глава 21. Использование интерфейса Visual Studio

505

Вряд ли вы обладаете настолько феноменальной памятью, чтобы помнить все классы и
методы даже из одного пространства имен, скажем, S y s t e m . Конечно, можно запомнить
синтаксис С# и несколько других деталей, но все же лучше не забывать о том, как пользовать­
ся справочной системой, поиск нужной информации в которой имеет несколько видов.
Окно справочной системы Visual Studio называется D o c u m e n t Explorer. Зна­
ние этого факта может помочь избежать определенной неразберихи.

F1
Помощь по клавише предоставляет быстрый доступ к информации о полях или
конструкциях в существующем коде, которые вы плохо помните или не вполне понимаете.
Например предположим, что вам не понятна разница между оператором n e w и одно­
именным модификатором метода. Вы можете щелкнуть на слове n e w в любом месте
в окне редактирования и нажать . Visual Studio откроет окно помощи, как показано
на рис. 21.10. (Если вы не понимаете разницу между терминами new, см. в главеб,
"Объединение данных — классы и массивы", описание оператора new, а^ в главе 12,
"Наследование", — наследования new. Или, как видно из приведенной копии экрана,
имеется еще ограничение n e w () у обобщенных классов, так что можно заглянуть
и в главу 15, "Обобщенное программирование".)

Рис. 21.10. Справочная система Visual Studio поможет разо­
браться с разными значениями ключевого слова new
Если справка содержит несколько статей, соответствующих вашему термину, вы уви­
дите маленькое плавающее окошко с перечислением доступных тем. Дважды щелкните
на нужной теме, чтобы увидеть ее.

506

Часть VII. Дополнительные главы

-

ж

• •

Visual Studio пытается обеспечить доступ к справочным файлам, инсталлиро­
ванным на вашем компьютере, и к дополнительным ресурсам Web. Если вы не
подключены к Интернету, то можете получить сообщение о том, что справоч­
ной системе не предоставляется доступ в Web.
Вы можете выбрать, где будет выводиться окно справочной системы. В Visual
Studio выберите команду меню T o o l s ^ O p t i o n s . В разделе E n v i r o n m e n t слева

щелкните на пункте Help, G e n e r a l . Выберите External Help V i e w e r или Inte­
grated Help V i e w e r из S h o w Help Using и щелкните на кнопке О К . Лично
я предпочитаю External Help V i e w e r , когда справочная система запускается
в виде отдельной программы и не мешает самому Visual Studio. Integrated Help
V i e w e r помещает справку в свернутое окно вместе с вашими исходными файла­
ми. Но попробуйте оба варианта и решите сами, что вам больше нравится.

Предметный указатель
Если справка < F 1 > — не то, что вам нужно, пбскольку у вас нет соответствующего
ключевого слова или идентификатора, вы можете продолжить поиск в предметном ука­
зателе (Index Help). Предметный указатель наиболее полезен, когда вы знаете тему, ко­
торая может вам помочь, но не уверены в деталях.
Например, вам может потребоваться коллекция некоторого вида, и при этом из­
вестно, что большинство классов коллекций находятся в пространстве имен
S y s t e m . C o l l e c t i o n s . Для поиска следует выбрать команду меню H e l p ^ l n d e x , а за­
тем в окне Index ввести collections в поле ввода Look For, что предоставит список
тем, связанных со словом collections. (Этот список находится в левой части окна Help.
В правой части выводится текст найденной вами темы.) Двойной щелчок на элементе
. N E T F r a m e w o r k в списке тем дает вам окно, показанное на рис. 21.11.

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

Затем следует щелкнуть на S y s t e m . C o l l e c t i o n s , и эта тема открывает список
членов пространства имен C o l l e c t i o n s . При прокрутке списка в нем можно найти
класс S o r t e d L i s t . В соответствии с кратким описанием справа это именно то, что
нужно. Итак, результаты поиска выглядят так, как показано на рис. 21.12.

Рис. 21.12. Найдена информация по определенному классу
Каждый член слева в окне тем представляет собой гиперссылку. Щелчок на S o r t e d L i s t открывает информацию об этом классе, включая гиперссылки на члены класса, так
что вы можете легко получить более детальную информацию.
Текстовое поле Filtered By в окне Index H e l p позволяет ограничить список
тем, в которых выполняется поиск. На рис. 21.11 и 21.12 поиск велся в рамках
"Visual С # " . Без этого ограничения поиск мог бы вернуть информацию о кол­
лекциях, не имеющих ничего общего с С#. Справочная система Microsoft De­
veloper Network (MSDN) существенно больше, чем справка по С#. Фильтрация
работает для предметного указателя, поиска и содержания.
Когда тема показана в окне Help, можно щелкнуть на кнопке S y n c with Table
of C o n t e n t s на панели инструментов (книга с белым кругом, на котором изо­
бражены левая и правая стрелки). Это выделит тему на вкладке содержания
C o n t e n t s . Вы можете выполнить прокрутку, чтобы увидеть, что тема, посвя­
щенная классу S o r t e d L i s t , находится в Class Library Reference для .NET
Framework Software Development Kit (SDK). Вкладка C o n t e n t s полезна для по­
лучения обзора информации.
Обратите внимание на опцию H e l p F a v o r i t e s в меню Help. Эта вкладка позволяет со­
хранить тему как "избранную". Позже вы сможете быстро к ней вернуться. На рис. 21.10
показано окно H e l p F a v o r i t e s с некоторыми из избранных тем. Чтобы добавить текущую
тему из предметного указателя в список избранного, щелкните на окне темы правой
кнопкой мыши и выберите команду A d d to H e l p Favorites.

508

Часть VII. Дополнительные главы

Поиск
Опция S e a r c h в меню Help наиболее полезна, когда вы в точности не знаете, что
именно вам нужно. Это полнотекстовый поиск по всем темам справочной системы.
Например, требуется коллекция, отсортированная в алфавитном порядке. Для поиска
следует выбрать H e l p O S e a r c h для того, чтобы открыть вкладку S e a r c h . Но при вводе
sorted в поле S e a r c h F o r полученные результаты оказываются не слишком полезны­
ми, так что лучше ввести collection classes.
На рис. 21.13 показаны результаты поиска для "collection classes". Если вы максими­
зируете окно Help, то увидите несколько закладок справа от окна: Local Help, M S D N

Online, C o d e z o n e C o m m u n i t y и Questions. По умолчанию вы получаете результаты
поиска в локальных файлах.

Puc. 21.13. Используйте полнотекстовый поиск, если вам не помогли ни контекст­
ный поиск по , ни предметный указатель
Если вы подключены к Интернету, справочная система вернет также темы, располо­
женные в других областях (если вы подключаетесь по телефонной линии, это будет
очень медленный поиск). Щелкните на закладке справа для вывода этих тем. Local Help
обращается к файлам, хранящимся на вашем компьютере. M S D N Online обращается
к справочным ресурсам на сайте Microsoft Developer Network (MSDN). C o d e z o n e C o m ­
munity обращается к множеству независимых сайтов, где вы часто можете найти допол­
нительную информацию и пообщаться с другими программистами на С# в форумах. За­
кладка Q u e s t i o n s позволяет выполнить поиск в группах новостей, посвященных Х#
и вопросам, связанным с .NET.
(Чтобы получить советы о том, как составлять хорошие запросы, откройте справку,

выберите H e l p ^ H e l p on Help, щелкните на T e c h n i q u e s for Locating Help, а затем на
Full-Text Searches.)
Глава 21. Использование интерфейса Visual Studio

509

Поиск может помочь найти класс или метод, который нужен вам для ваших це­
лей, но при этом можно потратить много времени, продираясь через дебри не­
нужных тем.
Такой широкий поиск, как "collection class", возвращает сотни возможных тем
(максимальное количество выводимых в окне — 500), так что вы получаете их так же,
как страницы с результатами поиска в Web. Для перехода к следующей или предыдущей
странице результатов поиска щелкните на стрелке в правом верхнем углу Local Help на
вкладке S e a r c h . Большинство этих тем будут для вас бесполезны.
Как и в случае предметного указателя, можно улучшить полнотекстовый поиск с по­
мощью фильтра. Можно фильтровать поиск по языку, технологии (такой как .NET Win­
dows Forms или Office Applications) и типу темы. На рис. 21.13 установлен весьма широ­
кий тип тем: Articles and Overviews (статьи и обзоры), Contains Code (с содержанием ис­
ходных текстов), How-Tos (краткие инструкции), Knowledge Base (базы знаний), Other
Documentation (прочая документация) и Syntax/API Reference (справка по синтакси- ]
су/API). Указывая конкретный тип темы, вы можете существенно снизить количество
мусора. Кроме того, вы можете выполнить поиск локально на вашем компьютере идя
глобально, в Web. (Чтобы получить дополнительную информацию о справке в Интерне­
те, найдите в предметном указателе раздел "Help, online content".)

Дополнительные возможности
Кроме избранных тем, можно сохранить в Help Favorites и поиски. Выполните поиск,
затем при активной вкладке S e a r c h щелкните на кнопке A d d to Help Favorites панели ин­
струментов Help (пиктограмма в виде странички с желтым знаком "плюс"). Теперь, открыв
Help Favorites, вы можете в любой момент повторить выполненный вами поиск.
Обратите внимание на кнопку H o w Do I на панели инструментов на рис. 21.13. Это
новый справочный ресурс со ссылками на все виды тем "how-to".
В качестве расширения меню Help Visual Studio 2005 предлагает новое меню Com­
munity, которое связывает ряд сетевых ресурсов и обеспечивает доступ к сообществу
программистов на С# во всем мире. Слушайте профессионалов, задавайте вопросы и на­
бирайтесь опыта...
Попробуйте поиграться с окном D y n a m i c Help. Оно предназначено для отра­
жения контекста того, с чем вы работаете в данный момент — класс библиоте­
ки .NET Framework, ключевое слово С# и так далее. Честно говоря, данное усо­
вершенствование не такое уж и важное, хотя идея, конечно, привлекательная.
Лично я считаю наиболее важными и полезными возможностями справочной
системы контекстную справку и предметный указатель. Старайтесь начи­
нать с контекстной справки . Затем переходите к предметному указателю.
Он напоминает предметный указатель книги. Если же и здесь вы не получили
помощь, переходите к полнотекстовому поиску. Поиск похож... ну, на прогул­
ку в Web, но не такую эффективную. И наконец, обратитесь к карте: вкладке
содержания. Содержание похоже на оглавление книги. Это неплохое место, ес­
ли вы хотите получить не напоминание, а обзор на какую-то тему.

510

Часть VII. Дополнительные главы

Автоперечисление членов
"Автоперечисление членов" в Visual Studio часто делает излишним обращение к ме­
ню Help. При вводе имени класса или метода Visual Studio пытается предоставить вам
справку на основании введенного во всплывающем окне.
Автоперечисление можно отключить. Выберите команду меню T o o l s ^ O p t i o n s .
В окне Options щелкните на пункте T e x t Editor в левой панели и выберите ко­
манду All L a n g u a g e s ^ G e n e r a l . И наконец, проверьте установку флага Auto List

Members.
Чтобы увидеть, чем может помочь указанная возможность, рассмотрим знакомую ситуа­
цию: я знаю, что класс коллекции некоторого типа хранит элементы в отсортированном по­
рядке. Поскольку я знаю, что этот класс находится где-то в пространстве имен S y s ­
t e m . C o l l e c t i o n s , следует поместить курсор на начало пустой строки в редакторе исход­
ного текста и ввести n e w S y s t e m . C o l l e c t i o n s . Как только будет введена точка в конце
" C o l l e c t i o n s " , Visual Studio откроет меню, в котором перечислены все классы, состав­
ляющие пространство имен C o l l e c t i o n s . Это самый быстрый и простой вид помощи.
Visual Studio перечисляет в данной ситуации только неабстрактные классы, по­
скольку только они могут быть инстанцированы с использованием ключевого
слова n e w . Подробнее об абстрактных и конкретных классах можно прочесть
в главе 13, "Полиморфизм".
В прокручиваемом списке возможных классов находится и класс S o r t e d L i s t . По­
сле выбора класса Visual Studio открывает его описание, как показано на рис. 21.14. По­
хоже, этот класс — именно то, что нужно.

Рис. 21.14. Автоперечисление —мощное подспорье в работе программиста

Глава 21. Использование интерфейса Visual Studio

511

После того как вы нашли то, что искали,
new
System.Collections.SortedList.

можно

удалить

временный теки

При нормальном течении событий при вводе реального кода автоперечисление явля­
ется частью автозавершения, о котором подробно рассказывалось в главе 8, "Методы
класса".

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

В этом разделе содержится программа с массой "особенностей". Моя задача состоит
в ее отладке с использованием инструментария, предоставляемого Visual Studio.

Жучки в программе: а дустом не пробовали?
В приведенной далее программе имеется ошибка (а может, и имеются).

// VSDebug - э т а п р о г р а м м а и с п о л ь з у е т с я в к а ч е с т в е
// демонстрационной для отладки; программа неработоспособна
// ( и с п р а в л е н н а я в е р с и я программы — VSDebugFixed)
using
System;
using
System.Collections;
u s i n g System.10;
n a m e s p a c e VSDebug

{
class

Program

{
static

void

Main(string[]

args)

{
//

512

Я

должен

вставить

это

предупреждение,

чтобы

Часть VII. Дополнительные главы

//
//

избежать тысяч
нее работает

писем

с

указанием,

что

моя

программа

Console.WriteLine("Эта
программа
не
работает!");
S t u d e n t si = new S t u d e n t ( " S t u d e n t 1 " ,
1);
S t u d e n t s2 = new S t u d e n t ( " S t u d e n t 2",
2);
//
display the
two
students
Console.WriteLine("Student 1
Console .WriteLine ( "Student 2
// Теперь
требуем от класса
//
студентов

=
",
si.ToString()) ;
=
",
s2.. T o S t r i n g ( ) ) ;
Student вывести всех

Student.OutputAllStudents();
//
Ожидаем п о д т в е р ж д е н и я
пользователя
Console.WriteLine("Нажмите для
"завершения

"

+

программы., . " ) ;

Console.Read();

}

}
public
{

s
p
p
p

class

t a t i
riva
riva
ubli

Student

c ArrayList
allStudents
te
string
sStudentName;
te
int nID;
c S t u d e n t ( s t r i n g sName,

sStudentName
nID = nID;

=

=

new

int

ArrayList();

nID

sName;

allStudents.Add(this);
// T o S t r i n g - в о з в р а щ а е т имя и и д е н т и ф и к а т о р
public
override
string
ToString()
s t r i n g s = String.Format("{0}

({l})",

sStudentName,
return
public

студента

nID);

s;

s t a t i c

void

OutputAllStudents()

IEnumerator i t e r = allStudents.GetEnumerator();
// Использую цикл for вместо обычного while
f o r ( i t e r . R e s e t ( ) ;
iter.Current
! = null;
iter.MoveNext())

{
Student

s

=

(Student) i t e r . Current-

Console .WriteLine ("Student

}

=

{0}",

s.ToString());

}
После удаления из программы всех ошибок времени компиляции можно перехо­

дить к делу.
Несмотря на то что все ошибки изгнаны, остались еще два предупреждения (они по­
являются в окне E r r o r List с пиктограммой в виде желтого треугольника). Предупрежде-

Глава 21. Использование интерфейса Visual Studio

513

ния означают потенциальные проблемы, которые недостаточно серьезны, чтобы считать
их ошибками. Однако это не значит, что их можно игнорировать. Хорошенько изучите
их, поскольку они могут указать на ошибки в вашем коде. Хотя, конечно, расшифровка
сообщений компилятора порой труднее, чем расшифровка египетских иероглифов.
Просто для интересующихся — на прилагаемом компакт-диске имеется де­
монстрационная программа V S D e b u g G e n e r i . e s , в которой вместо хране­
ния a l l S t u d e n t s в A r r a y L i s t используется L i s t < T > , метод O u t p u t A H S t u d e n t s () оказывается гораздо проще — и все это работает!

Пошаговая отладка
При обнаружении наличия ошибки в программе одним из наилучших первых шагов
в ее локализации и устранении является возможность отладчика, известная под названи­
ем пошагового выполнения (single stepping). Для этого воспользуйтесь командой меню

D e b u g ^ S t e p O v e r или клавишей .
Даже если вы не очень хорошо запоминаете функциональные клавиши, в этом
случае следует сделать исключение. Использовать меню D e b u g для доступа

к пошаговому выполнению Step O v e r и Step Into — непозволительно медлен­
но (панель инструментов лучше меню, но и ей далеко до клавиатуры).
Нажатие приведет к выполнению демонстрационной программы V S D e b u g до
открывающей фигурной скобки функции M a i n ( ) . Повторное нажатие выполнит
функцию M a i n ()

до первой выполнимой инструкции (executable statement) — инструк­

ции, которая что-то делает на самом деле. Комментарии и объявления такими инструк­
циями не являются, а вот C o n s o l e . W r i t e L i n e () определенно осуществляет некото­
р ы е действия. На рис. 21.15 изображено окно Visual Studio после двукратного пошагово­
го выполнения.

Puc. 21.15. Пошаговый режим приводит к выполнению программы по одной
инструкции

514

Часть VII. Дополнительные главы

Перед началом процесса отладки Visual Studio перекомпилирует программу,
так что не нервничайте преждевременно.

Обратите внимание, что первая строка программы подцвечена. Это — очередная ин­
струкция, которая будет выполнена в пошаговом режиме. Запомните — желтая строка
еще не выполнена.
Обратите также внимание на окно, открытое внизу, с именем Locals (оно может на­
ходиться в минимизированном состоянии; на рис. 21.15 оно показано открытым).
В этом окне выводится список трех локальных переменных, т.е. переменных, объяв­
ленных в текущей функции. Переменные s i и s 2 типа V S D e b u g . S t u d e n t имеют зна­
чения n u l l , поскольку им еще не были присвоены значения. Столбец Туре предостав­
ляет информацию о полном имени класса, включая пространство имен. Переменная
a r g s типа s t r i n g [] (массив строк) с длиной 0 означает, что программе не были пере­
даны аргументы. Это тоже очень важная возможность отладчика.
Еще одно нажатие приводит к выполнению вывода предупреждения функцией
W r i t e L i n e ( ) . Чтобы убедиться в этом, нажмите комбинацию клавиш для
того, чтобы переключиться на программу V S D e b u g . В консольном окне вы увидите одну
строку — Э т а п р о г р а м м а не р а б о т а е т !. Это именно то, что и ожидалось. Еще раз
нажмите для возврата в Visual Studio.
< A l t + T a b > — команда переключения между программами Windows, исполь­
зуемая для передачи управления от одной программы к другой. Она
"активизирует" главное окно программы, в которую вы переключаетесь. Когда
активен отладчик, программа V S D e b u g выполняется, но находится в приоста­
новленном состоянии. Клавиши можно использовать где угодно,
а не только в Visual Studio.
Естественно, очередное нажатие приводит к выполнению строки S t u ­
d e n t si = .... Поток управления останавливается на следующей строке функции
M a i n ( ) , выполняя конструктор первого объекта S t u d e n t з а один шаг. Конструктор
всегда выполняется, даже если вы этого и не видите.
В окне Locals переменная s 2 остается равной n u l l , но переменная s i теперь содер­
жит объект класса S t u d e n t . Небольшой знак "плюс" слева от si означает, что объект
можно открыть и посмотреть на его "внутренности". После щелчка на этом значке окно
Locals приобрело вид, показанный на рис. 21.16, раскрывая содержимое объекта s i .
Первая и вторая записи в экземпляре объекта — члены n I D типа i
N a m e типа s t r i n g . Третья з а п и с ь — заголовок списка статических
данном случае это единственный член данных a l l S t u d e n t s типа
скольку данный объект также имеет свое содержимое, слева от него
кий значок "плюс", позволяющий ознакомиться с этим содержимым.

nt и sStudentчленов к л а с с а — в
A r r a y L i s t . По­
находится малень­

Взгляните внимательнее на значения двух членов экземпляра: s S t u d e n t N a m e имеет
значение " S t u d e n t 1 " , которое выглядит вполне корректно, a n I D имеет значение О,
что корректным не назовешь. Это значение должно быть равно 1 — значению, передан­
ному конструктору. Что-то в конструкторе пошло не так...
Беспокоясь о программировании на С# вообще и о своей карьере в частности, я вы­
бираю команду меню DebugStop D e b u g g i n g . Затем я нажимаю три раза: один
раз для перезапуска программы, и два раза для того, чтобы пройти W r i t e L i n e () —

Глава 21. Использование интерфейса Visual Studio

515

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

Рис, 21.16. Окно Locals позволяет получить детальную информацию о со­
стоянии объекта
Действие клавиш и идентично, когда вы выполняете инструкцию,
не являющуюся вызовом функции некоторого вида. Пошаговое выполнение по­
средством (step into) приводит к пошаговому выполнению вызываемой
функции. Однако оба пошаговых режима не позволяют заходить в библиотечные
функции .NET. Исходный текст реализации библиотеки закрыт для входа.
В этот раз в окне появляется конструктор с выделенной первой строкой. Далее следу­
ет открыть интересующий объект t h i s в окне Locals. Затем несколько раз нажать
, чтобы перейти к точке инициализации n I D .
Каждое изменяемое значение в окне Locals выделяется красным цветом.

Весь в ожидании, я нажимаю еще раз. И н т е р е с н о — значение t h i s . n I D не
изменяется, несмотря на то что значение n I D в окне Locals стало равным 1.
Если вы скомпилируете демонстрационную программу V S D e b u g с использо­
ванием команды D e b u g ^ B u i l d V S D e b u g вместо применения клавиши
для пошагового прохода в отладчике, в окне E r r o r List вы увидите два упомя­
нутых предупреждения. Если вы посмотрите на конструктор класса S t u d e n t ,
то увидите волнистую пурпурную линию под присваиванием n I D = n I D , ука­
зывающую на проблему. Если бы вы сделали это перед тем, как переходить
к отладке, возможно, вы бы смогли исправить ошибку, не прибегая к отладчи­
ку. К сожалению, эта линия не видна на рис. 21.16, так как на нем показан код
в отладчике.

516

Часть VII. Дополнительные главы

Вернемся к анализируемой строке. Следующее выражение просто присваивает значе­
ние n I D самому себе:
nID = nID;

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

=

nID;

//

Присваивание

аргумента

переменной-члену

Можно прекратить отладку и перекомпилировать программу, после чего, по­
местив курсор над этим присваиванием, посмотреть еще раз на предупрежде­
ние во всплывающем окне. Оно гласит "Assignment made to same variable; did
you mean to assign something else?" ("Выполняется присваивание той же пере­
менной. Не намеревались ли вы выполнить иное присваивание?"). Можно ли
выразиться понятнее?
Можно попробовать изменить строку на t h i s . n I D = n I D и снова пошагово вы­
полнить программу. (С некоторыми ограничениями вы можете также просто изменить
исходный текст и продолжить отладку — эта возможность Visual Studio называется Edit
and Continue — поищите информацию о ней в справочной системе.)
В этот раз следует аккуратно проверить объекты si и s2 в окне Locals после их кон­
струирования, но, кажется, все выглядит хорошо. Однако пошаговое выполнение оче­
редного вызова W r i t e L i n e () дает странный вывод на экран:
Эта программа
Student 1 =

не

работает!

Что могло случиться на этот раз? Вероятно, T o S t r i n g () ничего не возвращает. Не­
обходимо начать сначала, так что пока что я прекращаю отладку.

Главное - вовремя остановиться
Вероятно, вы уже замаялись в очередной раз пошагово проходить программу.Поша­
говый проход большой программы представляется вообще сплошным кошмаром.
Отладчик Visual Studio позволяет указать, что вы хотите остановить программу в ее
конкретной точке. Это достигается путем создания так называемой точки останова
(breakpoint).
Для этого нужно щелкнуть мышью на области слева от интересующей команды
W r i t e L i n e ( ) , где предполагается приостановить выполнение программы. Возле стро­
ки появляется маленький красный кружок, а сама строка подцвечивается красным цве­
том, что свидетельствует о наличии точки останова. Теперь можно указать программе,
что она может начинать выполнение, посредством команды меню D e b u g ^ S t a r t или кла­
виши . В результате ни о чем не нужно беспокоиться, зная, что программа остано­
вится, дойдя до указанной строки.
Как и должно быть, программа начинает работу и останавливается в заданной точке,
как показано на рис. 21.17. Обратите внимание на желтую стрелку, появляющуюся
в красном кружке, и на подцвечивание инструкции W r i t e L i n e () желтым цветом.
Далее следует вновь три раза нажать , чтобы добраться до инструкции W r i t e ­
L i n e ( ) , которая выводит информацию о студенте 1, и затем — , чтобы попасть в
метод T o S t r i n g ( ) .

Глава 21. Использование интерфейса Visual Studio

517

Рис. 21.17. Желтая стречка указывает, где остановилась программа из-за
наличия точки останова
Я не должен был передавать результат вызова S t r i n g . F o r m a t О оператору r e ­
t u r n , как показано в следующей строке:
return

String.Format("{0}

({l})",

sStudentName,

nID);

Вместо этого следовало бы переписать T o S t r i n g () с использованием временной
переменной s:
public

override

string

ToString()

{
string s =
return s;

String.Format("{0}

({l})",

sStudentName,

nID);

}
Присваивание возвращаемого значения промежуточной переменной дает воз­
можность просмотреть его в отладчике. (Помимо этого, нет никаких иных при­
чин поступать таким образом, так что после отладки можно удалить эту про­
межуточную переменную.)
Нажмите < F U > для пошагового выполнения строки, вычисляющей значение s —
строки, возвращаемой функцией T o S t r i n g ( ) . В окне Locals все выглядит вполне кор­
ректно, как видно из рис. 21.18. (Примечание: \ t , которое вы видите в строке з, пред­
ставляет собой символ табуляции. Я нажал вместо пробела, когда вводил строку
S t r i n g . F o r m a t . На самом деле в этом нет ничего страшного. Вне отладчика выберите
команду меню E d i t ^ A d v a n c e d ^ S h o w W h i t e S p a c e для того, чтобы вместо пробелов
выводилась точка, а вместо символов табуляции — стрелочка. Отключить этот режим
можно аналогичным способом.)

Неприятность найдена
Проблема должна заключаться в самом вызове W r i t e L i n e ( ) . Следует дважды на­
жать , чтобы вернуться к этой строке. Ага! Управляющий элемент { о } , который

518

Часть VII. Дополнительные главы

д о л ж е н в ы в о д и т ь строку, в о з в р а щ а е м у ю T o S t r i n g ( ) , отсутствует. Т о есть в ф у н к ц и ю
п е р е д а н о з н а ч е н и е д л я него, н о с а м э л е м е н т з а б ы т . Ч т о б ы и с п р а в и т ь о ш и б к у , д в е коман­
ды W r i t e L i n e () надо переписать следующим образом:
// d i s p l a y t h e two s t u d e n t s
Console.WriteLine("Student 1
Console.WriteLine("Student 2

=
=

{o}",
{o}",

si.ToString());
s2.ToString());

Рис. 21.18. Возвращаемое значение корректно. Так что же происходит?
Вероятно, ошибка произошла в результате перепутывания двух видов функций
WriteLine():
// С и с п о л ь з о в а н и е м управляющего э л е м е н т а
Console .WriteLine ("Student 1 = " + s i . ToString ())';
// С и с п о л ь з о в а н и е м управляющего э л е м е н т а
C o n s o l e . W r i t e L i n e ( " S t u d e n t 1 = {о}",
si.ToString());

Подсказка о данных
Сейчас самое время рассказать об одном очень ценном нововведении в отладчике
Visual Studio 2005: подсказке о данных (DataTip). Такая подсказка представляет собой
небольшой прямоугольник, который появляется, когда вы останавливаете курсор над пе­
ременной во время останова в отладчике. После того как я дважды нажал для воз­
врата в функцию M a i n ( ) , я помещаю курсор над переменной s i внутри вызова W r i t e ­
L i n e ( ) (без щелчка) и вижу появившееся окошко с s i и его значением T o S t r i n g ( ) :
{ S t u d e n t ID ( 1 ) }. Я игнорирую маленький квадратик, происхождение которого из-за
введенного символа табуляции я пояснял ранее. Информацию о подсказке о данных
можно получить из раздела "DataTip" справочной системы.
Подсказки работают только когда переменная находится "в контексте", т.е. ли­
бо в изучаемой в настоящий момент функции, либо является членом-данными
текущего класса, и вы уже выполнили строку, в которой инициализируется эта
переменная.

Глава 21. Использование интерфейса Visual Studio

519

Теперь о существенном усовершенствовании подсказок, видном из рис. 21.19. По­
местите курсор мыши над знаком + в окошке s l . При этом откроется детальная инфор­
мация об объекте

s l . Если вы переместите курсор на значок +

перед S t a t i c

m e m b e r s , а п о т о м — перед a l l S t u d e n t s , а з а т е м — перед [ 0 ] — нулевым членом
A r r a y L i s t объекта a l l S t u d e n t s — то вы опять увидите sl — в этот раз уже внутри
A r r a y L i s t объекта a l l S t u d e n t s .

Рис. 21.19. Подсказка о данных— отличное средство, чтобы быстро разо­
браться с содержимым объекта в отладчике
Подсказки позволяют погружаться все глубже и глубже в сложные объекты. (Ранее
для получения этой информации необходимо было открывать окно W a t c h или QuickW a t c h для данной переменной, либо использовать окно Locals.)
Снова щелкните на красном кружке (см. рис. 21.17) для того, чтобы удалить
точку останова. (Вы можете также воспользоваться командой меню D e b u g s

Delete All Breakpoints.) В меню Debug имеется еще одна команда D e b u g s
W i n d o w s 1 ^ B r e a k p o i n t s , которая предоставляет доступ ко всем возможностям
точек останова.

Стек вызовов
Далее я ставлю точку останова н а вызове O u t p u t A l l S t u d e n t s ( ) , следующем не­
посредственно за двумя только что исправленными вызовами W r i t e L i n e ( ) . Я нажи­
маю для выполнения программы до этой точки и смотрю, что выведено в окне
C o n s o l e . Все выглядит как надо.
Затем я еще раз нажимаю , чтобы пропустить вызов O u t p u t A l l S t u d e n t s ( ) .
И вот тут-то это и происходит.
Появляется сообщение об ошибке наподобие показанного на рис. 21.20. Оно привя­
зано к строке с циклом f o r , а именно к условию цикла, в котором вызывается свойство
C u r r e n t итератора (об итераторах см. главу 20, "Работа с коллекциями"). Сообщение
о б ошибке гласит: E n u m e r a t i o n h a s n o t s t a r t e d . C a l l M o v e N e x t (Перечисление
не начато. Вызовите M o v e N e x t ) —- все, что следует знать о происшедшем.

520

Часть VII. Дополнительные главы

Рис. 21.20. Visual Studio говорит о том, что забыт начальный вызов MoveNext ()
Я закрываю окно сообщения об ошибке и, чтобы получить немного дополнительной
информации, командой меню D e b u g O W i n d o w s ^ C a l l Stack открываю окно стека вызо­
вов Call Stack, показанное на рис. 21.21 (здесь оно раскрыто для того, чтобы было луч­
ше видно представленную им информацию).

Puc; 21.21. Окно Call Stack полезно при поиске источника фатальной ошибки
и для определения вашего местоположения

Глава 21. Использование интерфейса Visual Studio

521

Здесь содержится вся информация, которую может предоставить отладчик — и, как
правило, ее достаточно много. Желтая стрелка и подцветка в окне редактора указывают
на выражение, вызвавшее проблемы. Окно Call Stack описывает, как именно мы попали
в точку генерации исключения: функция Main() вызвала OutputAllStudents ()
в строке 64. Информация о номерах строк в полосе состояния говорит, что строка 64 —
это заголовок цикла for, который уже был указан окном с сообщением об ошибке.
Вы можете вывести номера строк в окне редактирования — для этого воспользуй­
тесь командой меню Tools^OptionsOEditor^C* и выберите Line Numbers.

В данном случае стек вызовов не оказывает особой помощи, так как включает только
две функции: Main() и OutputAllStudents ( ) . Но в больших программах может
быть очень сложно обнаружить, как именно вы попали в эти жернова — и стек вызовов
окажет вам в таком случае неоценимую помощь.
Дополнительную информацию можно получить, щелкнув на ссылке View Detail в ок­
не сообщения
об исключении и воспользовавшись полем
InnerException
(единственное со знаком + перед ним). Поместите курсор над StackTrace и получите
дополнительную информацию. В верхней строке содержится фраза get_Current. Вот
где настоящая неприятность — в вызове свойства Current итератора.
Беглый взгляд на документацию по свойству IEnumerator. Current проясняет,
что не вызван метод MoveNext () перед попыткой получения первого элемента. Теперь,
когда стало понятно, в чем дело, следует остановить отладчик щелчком на кнопке Stop
Debugging в полосе инструментов Debug и вернуться в режим редактирования. (Кнопка
панели инструментов — это ярлык команды меню D e b u g ^ S t o p Debugging; аналогич­
ного эффекта можно добиться и с помощью клавиш .)
В этот момент у меня накапливается несколько симптомов, указывающих на
свойство Current итератора. Исключение подцвечивает условие цикла for,
где вызывается Current, StackTrace в InnerException также упомина­
ет закулисное имя Current — get_Current. (Свойства в действительности
реализуются за сценой с использованием методов с префиксами get_ или
set_.) Итак, вопрос — что же такого могло начудить свойство Current? От­
вет — я не вызвал сначала MoveNext ( ) , так что свойство Current не полу­
чило начальное значение — первый элемент данных в ArrayList.
Дальнейшее исследование показывает, что весь цикл for — одна большая ошибка.
Итератор завершает работу, когда MoveNext () возвращает false, а не когда Cur­
rent получает значение null. Обновленный цикл (теперь — более безопасный while)
выглядит следующим образом:
public

static void OutputAllStudents()

{
IEnumerator iter = allStudents.GetEnumerator();
while(iter.MoveNext()) // 'while 1 , а не 'for'
{
Student s = (Student)iter.Current;
Console.WriteLine("Student = {o}", s.ToString());

}

}
522

Часть

VII. Дополнительные главы

Теперь программа использует MoveNext () для итераций по контейнеру объектов
Student. Каждый Student возвращается свойством Current. Цикл завершает рабо­
ту, когда вызов MoveNext () возвращает false, что указывает на то, что в коллекции
больше нет не просмотренных элементов.
Цикл foreach также может помочь избежать описанных неприятностей—об­
ратитесь к материалу о блоках итераторов в главе 20, "Работа с коллекциями".

Я сделал это!
Очередной запуск программы наконец приводит к корректному выводу:
Эта программа не работает!
Student 1 = Student 1 (1)
Student 2 = Student 2 (2)
Student = Student 1 (1)
Student = Student 2 (2)
Нажмите для завершения

программы...

Хорошо, что главной в данной демонстрационной программе была ее отладка, а не
создание красивого вывода... Кстати, первая строка вывода более не актуальна.
Исправленная версия демонстрационной программы хранится на прилагае­
мом компакт-диске под именем VSDebugFixed.

Не важно, насколько мощный инструмент отладчик — чем меньше вы будете к нему
обращаться, тем лучше. Последние веяния в программировании, такие как первоначаль­
ная разработка тестов, непрерывный рефакторинг, шаблоны проектирования и другие
аспекты того, что именуется "экстремальным" программированием, могут существенно
снизить количество времени, проводимое в отладчике, и повысить производительность
программирования (не говоря об уменьшении количества седины у программиста). По­
ищите литературу или информацию в Web, посвященную вопросам экстремального про­
граммирования.
Visual Studio — очень богатая среда программирования, часто способная выполнить
одну задачу разными способами. Многие возможности наверняка не дождутся, когда вы
их примените. Но чем больше вы знаете о них, тем более эффективно вы сможете вы­
полнять свою работу в качестве программиста на С#.
Даже если вы используете Visual Studio, все равно стоит прочесть главу 22, "С# по
дешевке", в которой рассматриваются альтернативы Visual Studio. Это только углубит
ваше понимание среды программирования на С#.

ше главы

Глава

21.

Использование

интерфейса

Visual

Studio

523

Глава 22

С# по дешевке
В этой главе...
>

Поиск альтернатив Microsoft Visual Studio 2005

>

Работа без сети — но не без платформы .NET

>

Программирование на С# в SharpDevelop

>

Программирование на С# в TextPad

>

Использование отладчиков .NET вне Visual Studio 2005

>

Тестирование кода С# посредством инструментария NUnit

>

Проверка возможности запуска ваших программ пользователями

амым мощным средством для программирования на С# является, вне всяких со­
мнений, пакет Visual Studio 2005 компании Microsoft. Он объединяет весь про­
цесс разработки в одну интегрированную среду разработки (integrated development envi­
r o n m e n t — IDE), описанную в главе 21, "Использование интерфейса Visual Studio". Вы
можете создавать, отлаживать и выполнять свои программы С# в одной среде.
Пакет Visual Studio особенно полезен для разработки программ Windows с гра­
фическим интерфейсом пользователя (GUI) и приложений, основанных на Webстраницах с применением технологии ASP.NET, потому что этот пакет предоставля­
ет визуальные методы расположения окон и диалогов. Помимо этого, пакет обладает
богатым набором дополнений, без которых, как вы сами можете убедиться, работать
достаточно трудно.
Однако пакет Visual Studio стоит недешево. Если у вас его еще нет, вы можете думать:
"Я хотел бы попробовать программировать на С#, но как я могу это себе позволить?"
К счастью, в наши дни у вас имеется выбор. Одним вариантом может быть не­
сколько урезанная версия Express языка Visual С# (см. последний раздел данной
главы), другим является среда SharpDevelop IDE, которая бесплатно имитирует ба­
зовые функциональные возможности Visual Studio. Вы также можете программиро­
вать на С# в недорогом редакторе TextPad, как это делают многие программисты на
языках Java и Perl. (Прочие варианты можно найти, набрав в строке поиска Google
" С # development environment").
В этой главе рассматриваются инструментальные средства, которые позволят вам
работать без Visual Studio. Здесь вы познакомитесь с SharpDevelop, TextPad и NUnit,
узнаете, как устанавливать и использовать несколько очень дешевых рабочих сред
С#. Попутно вам даже будет показано, как написать простое приложение Windows
Forms с окном и элементами управления при полном отсутствии проектировщика
форм Visual Studio.

Первое, что вам понадобится, — это набор бесплатных элементов .NET. Независимо
от того, какие инструменты вы выберете, базовые составляющие для программирования
на С# включают в себя следующее:
текстовый редактор для написания кода, например Блокнот, TextPad или редактор
кода SharpDevelop;
компилятор С#, C s c . е х е ;
один из отладчиков, который поставляется вместе с языком С#: CorDbg. е х е или
DbgCLR. е х е , предпочтительнее последний;
окно командной строки Command. com или Cmd . c o m (в зависимости от вашей
версии Windows), которое входит в состав Windows.
Ряд других отличных бесплатных дополнений поставляется вместе с языком С#.
О нескольких из них чуть больше будет рассказано ближе к концу главы.
Для многих из этих составляющих в дополнение к документации по языку С#, кото­
рую можно загрузить со страницы компании Microsoft (о чем речь пойдет в следующем
разделе), вам понадобится дополнительная информация. Практически невозможно про­
граммировать на С# без справочной информации под рукой, так как у вас будет появ­
ляться все большее и большее количество вопросов.
Большинство из того, что вам необходимо, доступно из таких ресурсов, как база зна­
ний (Knowledge Base) Microsoft на Web-сайте сети разработчиков (Microsoft's Developer
N e t w o r k — MSDN) по адресу h t t p : / / m s d n . m i c r o s o f t . com. Там можно в избытке
получить информацию о языке С#, платформе .NET, Windows и многом другом. Чтобы
найти доступный для разработчиков инструментарий, поищите на сайте MSDN
"инструменты платформы .NET". Страница, посвященная Visual С#, расположена по ад­
ресу h t t p : / / m s d n . m i c r o s o f t . c o m / v c s h a r p / 2 0 0 5 / . Центр разработчиков плат­
формы .NET находится п о адресу h t t p : / / m s d n . m i c r o s o f t . c o m / n e t f r a m e w o r k / .
Эти страницы содержат ссылки на дополнительные ресурсы, включая группы новостей
и форумы, на которых вы можете задавать вопросы. Ссылки Communities и Newsgroups
на сайте Visual С# помогут вам найти информацию и помощь по языку С#. Кроме того, вы
можете обратиться к Web-сайтам, список которых приведен в конце введения.

Получение бесплатных компонентов
Вы можете получить инструменты, описанные в предыдущем разделе, следующими
способами.
Путем покупки пакета Visual Studio или Visual С# Express (конечно, это означает,
что вы не нуждаетесь в дешевом решении, и тем не менее эта глава может ока­
заться полезной для вас, поскольку в ней содержится уйма информации о том, что
происходит за прекрасным обличием Visual Studio).
Путем загрузки бесплатного набора инструментов для разработки программного
обеспечения (SDK) платформы .NET, который включает все необходимые инст-

526

Часть VII. Дополнительные главы

рументы. На сайте MSDN щелкните на вкладке Download, чтобы перейти в раздел
Download & Code Center. Там вы можете получить самую последнюю версию
набора .NET SDK, который содержит все, что вам необходимо. Выбирайте версию
в зависимости от вашего компьютера — вероятнее всего, вам необходима версия
х86. Доступны также 64-битовые версии, но для них вам нужен компьютер с 64битовым процессором.
Набор SDK велик по объему; вероятно, вам понадобится высокоскоростное со­
единение с Интернетом, но можно заказать этот же набор на компакт-диске на
сайте MSDN.
При любом из этих подходов устанавливается платформа .NET, программное обеспе­
чение, содержащее все типы данных и классов, на которых основано программирование
на С# — в частности, входящие в пространство имен System и другие.
У вас уже могут быть многие из необходимых инструментов, поскольку ряд из них
поставляется с последними версиями операционной системы Windows ХР. Поищите на
своем жестком диске компилятор С#, Csc. ехе. Если вы его найдете, вероятно, осталь­
ные инструменты у вас тоже есть.
Если вы уже установили платформу .NET, большинство инструментов С# обычно
расположено в папке С: \Windows\Microsof t. NET\Framework\v2 . О . п, где п оз­
начает номер версии. Во время написания этих строк я запускал вторую бета-версию
тестового выпуска платформы .NET версии 2.0, поэтому п у меня равен 50215, но этот
номер, конечно же, изменился, когда был выпущен пакет Visual Studio 2005. Вероятно,
на вашей машине эти инструменты расположены в такой же папке.
Наиболее вероятное альтернативное расположение некоторых инструментов — в папке
С: \Program Files. Туда обычно устанавливаются пакеты Microsoft .NET SDK и Vis­
ual Studio. Поищите папку \GuiDebug в иерархии папок SDK. Отладчик, который вам
нужен (DbgCLR. ехе), находится там. (Для поиска всегда можно использовать средства
Windows.)

Обзор цикла разработки
Основной шаблон разработки программы С# в любой среде программирования до­
вольно прост. Выполните следующие действия.
1. Напишите программу в текстовом редакторе (которым может быть Visual Studio,
SharpDevelop, TextPad или даже простой Блокнот). Избегайте текстовых процес­
соров, подобных Microsoft Word или WordPad. Они делают работу с простыми
текстовыми файлами слишком громоздкой.
2. Скомпилируйте программу с помощью компилятора С#, используя Visual Studio,
SharpDevelop, TextPad или командную строку. Блокнот для этой цели не подходит.
3. Вернитесь в редактор и при помощи справочной системы и чашки кофе устрани­
те ошибки, которые обнаружил компилятор, после чего снова скомпилируйте
программу.
4. Запустите программу для ее проверки с помощью Visual Studio, SharpDevelop,
TextPad, Windows Explorer, командной строки или инструмента NUnit, который
рассматривается далее в этой главе.

Глава 22.

С#

по дешевке

527

5. Посредством отладчика исправьте логические ошибки и другие дефекты, исполь­
зуя Visual Studio, TextPad, SharpDevelop или командную строку.
Намылить, сполоснуть, повторить...

Хорошей, но неполной заменой пакету Visual Studio является программа SharpDevelop, из­
вестная также как #develop. Программа SharpDevelop ( w w w . i c s h a r p c o d e . n e t ) является
бесплатной, как и большая часть программного обеспечения с открытым исходным кодом, до
тех пор, пока вы придерживаетесь довольно нетребовательного лицензионного соглашения.
И если вы не в состоянии позволить себе приобрести Visual Studio, то стоит попробовать по­
работать с SharpDevelop.
Программа SharpDevelop содержится на прилагаемом к книге компактдиске, так что испытать ее — дело не сложное.

Изучение SharpDevelop
SharpDevelop прекрасно подходит для написания, компилирования и выполнения
программы на С#. Эта программа совсем немного похожа на интегрированную среду
разработки Visual Studio (точнее, на более старую, чем Visual Studio 2005, версию, но
SharpDevelop работает с С# 2.0), как показано на рис. 22.1. На этом рисунке изображены
многочисленные окна документов и инструментов, в достаточной степени соответст­
вующие окнам в Visual Studio.
Возможно, вы заметили, что элементы имеют несколько отличающиеся имена в SharpDe­
velop и в Visual Studio. В табл. 22.1 сравниваются термины SharpDevelop с аналогичными
терминами Visual Studio.

528

Часть VII. Дополнительные главы

Рис. 22.1. Среда разработки SharpDevelop во многом выглядит
(и работает) наподобие Visual Studio
Окна инструментов пакета Visual Studio (например, Output, Toolbox, Properties)
в SharpDevelop называются "панелями" или "скаутами".
Если вы будете помнить об этих различиях в названиях, а также о некоторых других
вещах, о которых речь пойдет в следующем разделе, то сможете использовать многое из
главы 2 1 , "Использование интерфейса Visual Studio", в SharpDevelop — но не материал
разделов, посвященных справочной системе и размещению окон.

Сравнение возможностей SharpDevelop и Visual Studio
Для использования SharpDevelop необходимо создать новое объединение (Combain),
в которое будет добавлен формируемый проект. Вы можете просматривать файлы
и ссылки в этом объединении с помощью окна Project Scout. Из этого окна или из окна
Classes Scout можно открывать файлы в окне кода, в котором по умолчанию они появ­
ляются на вкладках. Редактирование кода практически идентично редактированию в Vis­
ual Studio, включая аналог автозавершения кода в SharpDevelop.
После окончания написания кода его можно скомпилировать посредством меню
Build, как и в Visual Studio. Ошибки появляются на панели Error List. Вы можете изме­
нить заданную по умолчанию конфигурацию Debug на конфигурацию Release, а также
определить свои собственные настройки.
Если объединение, которое вы создаете, предназначено для построения графического
приложения Windows, вы увидите форму, на которой можно разместить элементы
управления таким же образом, как и в Visual Studio (за исключением прелестей проекти-

Глава 22. С# по дешевке

529

ровщика формы в более новом пакете Visual Studio 2005, например, направляющих ли­
ний, и, возможно, кроме некоторых самых новых элементов управления Windows). Ус­
тановите свойства элементов управления в окне Properties Scout. Код элементов управ­
ления находится в файле формы с расширением . CS, как и в Visual Studio. Во время на­
писания этих строк код формы не разбивался на два класса, как это происходит в Visual
Studio 2005.
Наибольшее различие наблюдается при отладке ошибочной логики программы. Пока
что в SharpDevelop отсутствует такой мощный встроенный отладчик, как в Visual Studio.
Но позже будет рассмотрен еще один альтернативный вариант.
С другой стороны (вы заметили, что всегда имеется и обратная сторона?), работа
над SharpDevelop постоянно продолжается, пакет обладает большим количеством пре­
красных возможностей, хотя и с определенными недостатками и очень слабой доку­
ментацией (по крайней мере на момент написания этих строк). Например, в докумен­
тации отсутствует информация об использовании команды Debugger меню Tools про­
граммы SharpDevelop.
Итак, при отсутствии доступа к Visual Studio программа SharpDevelop является
неплохим выбором. Впрочем, прочтите остальную часть этой главы и познакомьтесь
с прочими альтернативными решениями, прежде чем сделать окончательный выбор.

Получение справочной информации
Ниже описано, как настроить в SharpDevelop получение информации из справочной
системы .NET SDK.
Настройте команду в меню Tools для открытия справки SDK в вашем Webбраузере. Выберите команду меню Tools^Options 1 ^Tools. Щелкните на кнопке
Add. Вызовите инструмент "Browse .NET Docs". В поле Command перейдите
к вашему Web-браузеру. Для Internet Explorer путь окажется, вероятно, следую­
щим: C:\Program Files\Internet Explorer\IExplore.exe. В поле
Arguments введите путь к документу StartHere.htm из папки пакета .NET
SDK. Этот пакет находится, вероятно, где-то в папке С: \Program Files. На
моей машине документ StartHere.htm расположен в папке C:\Program
Files\Microsof t Visual Studio 8\SDK\v2 . 0\StartHere . htm, кото­
рая является частью инсталляции пакета Visual Studio. Если этот пакет не установ­
лен, то необходимый документ нужно искать в папке установки пакета Microsoft
.NET SDK. Щелкните на кнопке ОК. Для просмотра документации выберите ин­
струмент в меню Tools программы SharpDevelop.
Можно просмотреть детальную справку по инструментам .NET SDK, таким как
отладчик, путем двойного щелчка на файле Cptools . chm в подкаталоге \Docs
в папке вашего пакета .NET SDK.
Также можно обратиться к разделу "Настройка остальных инструментов" далее
в этой главе. В нем рассматривается несколько дополнительных инструментов,
которые могут помочь вам получить больше информации. Эти инструменты
описаны во взаимосвязи с программой TextPad, но их можно использовать и с
SharpDevelop.

530

Часть

VII. Дополнительные главы

3. В правой части щелкните на кнопке Add.
Ниже станут доступными поля текстового ввода. Поле Title содержит текст
"New T o o l " .
4. Замените текст "New Tool" в поле Title на что-нибудь наподобие Debugger.
5. Щелкните на кнопке Browse рядом с полем ввода Command и перейдите
к каталогу с установленным пакетом . N E T Framework SDK. Откройте пап­
ку пакета SDK и затем подкаталог GuiDebug. Выберите файл DbgCLR.exe
и щелкните на кнопке Open.
Ранее уже рассматривался вопрос о том, где должен находиться ваш пакет SDK.
6. Вернитесь в окно Options, щелкнув на кнопке ОК.
Только что созданный инструмент Debugger открывает отладчик CLR и ничего бо­
лее. В следующих нескольких разделах объясняется, как запускать инструмент, загру­
жать в него файлы и использовать отладчик.

Запуск отладчика из SharpDevelop
После того как вы скомпилировали отладочную версию своей программы, ее можно
построчно проверить в отладчике CLR. В этом разделе объясняется, как начать исполь­
зование отладчика CLR.
CLR является визуальным отладчиком, который выглядит и в основном работает точ­
но так же, как и его коллега в Visual Studio.
Вы работаете в комфортном окне с кодом, который открыт перед вами, и получаете
удобные отметки наподобие желтой подцветки текущей линии и красной подцветки
строк с контрольными точками, а также можете вызывать знакомые команды из меню
Debug с помощью панели инструментов или комбинаций клавиш. Вы можете легко про­
верять содержимое переменных и отслеживать значения нескольких переменных одно­
временно. Вы можете исследовать стек вызовов, показывающий последовательности ме­
тодов, вызывавшихся перед тем методом, через который вы сейчас проходите.
Однако многие из возможностей отладчика Visual Studio 2005 здесь отсутствуют,
включая замечательную подсказку о данных, рассматривавшуюся в предыдущей
главе. В этом отладчике имеются только старые возможности, но они вполне при­
годны для работы.

Загрузка отладчика
Исправив все ошибки компиляции, вы можете обнаружить, что программа после за­
пуска ведет себя странно. Это именно тот случай, когда нужно воспользоваться отладчи­
ком CLR.
Для запуска отладчика не нужно выбирать меню Debug — его просто нет! Вместо
этого выберите в SharpDevelop команду меню Tools^Debugger (или аналогичную ко­
манду в TextPad).
Когда вы в первый раз отлаживаете какую-то программу, выполните следующие
действия.
1. В окне отладчика выберите команду меню D e b u g s Program to Debug. Вы­
берите в диалоговом окне исполняемый файл вашего проекта, например,

Mycode. ехе.
532

Часть VII. Дополнительные главы

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

1. Выберите команду меню Project ^ P r o j e c t Options.
2. В левой части диалогового окна Project Options выберите команду C o n ­

figurations^ Debug ^Runtime/Compiler.
3. В правой части в панели Compiler Version выберите необходимые версии
компилятора и среды выполнения . N E T .
Для использования последней версии 2.0 выберите v2 . 0 . п (где п — номер вы­
пуска для версии 2.0).
4. Щелкните на кнопке ОК.
После выполнения этих действий можно свободно использовать новые возможности
языка С# 2.0 в своих программах, включая обобщенное программирование и блоки ите­
раторов. Обобщенное программирование рассматривается в главе 15, "Обобщенное
программирование", а блоки итераторов — в главе 20, "Работа с коллекциями".
После выхода официального выпуска пакета Visual Studio 2005 вам не нужно
выполнять вышеперечисленные действия. Просто загрузите самую свежую
доступную версию программы SharpDevelop (она бесплатная) и используйте
принятую по умолчанию (Standard) версию компилятора.
Если у вас есть пакет Visual Studio или Visual С# Express, вы, вероятно, пред­
почтете один из них программе SharpDevelop.

Добавление инструмента для запуска отладчика
Самым главным недостатком программы SharpDevelop является отсутствие отладчи­
ка с возможностями встроенного отладчика Visual Studio (который описан в главе 21,
"Использование интерфейса Visual Studio"). Но здесь будет показано, как добавить в ме­
ню Tools программы SharpDevelop инструмент, который запускает еще один визуальный
отладчик компании Microsoft— CLR, поставляемый с пакетом .NET Framework SDK.
Затем вы узнаете, как запускать и использовать этот отладчик.
Этот отладчик можно использовать как в SharpDevelop, так и в TextPad.

Для добавления инструмента Debugger в программу SharpDevelop выполните сле­
дующее.
1. Выберите команду меню T o o l s ^ O p t i o n s .
2. В левой части окна Options выберите каталог T o o l s и затем External

Tools.
1ВЫ

Глава 22. С# по дешевке

531

Файл с расширением . ЕХЕ должен находиться в подкаталоге bin\Debug папки
вашего проекта.
2. Выберите команду меню F i l e ^ O p e n ^ F i l e и главный файл вашего проекта
с расширением . CS (это файл с функцией M a i n ( ) ) , к примеру, M y c o d e . c s .
3. Если программа состоит из нескольких файлов .CS, повторите процедуру
открытия файла, описанную во втором шаге, для каждого дополнительного
файла.
Выбор нескольких файлов в диалоговом окне File Open позволяет открыть их
все одновременно.

На рис. 22.2 показано окно отладчика с прерванным в точке останова проектом С#.

Рис. 22.2. Проект С#, открытый в отладчике CLR
При выборе команды меню File^Close Solution можно сохранить конфигурацию
отладчика, называемую решением, наподобие решения С# в Visual Studio. Файл
решения отладчика имеет расширение . DLN. В следующий раз, когда потребуется
загрузить информацию об отладке для этого проекта, вы можете загрузить только
файл решения, выбрав в отладчике команду меню File^Open Solution. Это избавит
от необходимости повторного открытия файла . ЕХЕ и всех файлов . CS.

Приготовился, настроил, отладил!
После настройки отладчика с использованием решения для вашей программы вы мо­
жете решать обычные отладочные задачи. Из-за большой схожести отладчика со встро­
енным в Visual Studio не стоит тратить время на посвящение вас во все подробности, так
как за ними можно обратиться к предыдущей главе.

Глава 22.

С# по дешевке

533

Отладчик CLR очень похож на отладчик Visual Studio, поскольку по сути он
основан на последнем. Этому отладчику не хватает всего лишь нескольких из
наиболее продвинутых возможностей отладчика Visual Studio.

Отсутствующие возможности отладчика
Чего же нет в отладчике CLR, что может обеспечить Visual Studio? В нем отсутствует
несколько вещей, но это не вызовет у вас большого огорчения. Ниже перечислены воз­
можности, которых не хватает отладчику CLR по сравнению с отладчиком Visual Studio.
Окна Registers, Disassembly и Auto в отладчике CLR работают не так, как в Vis­
ual Studio, так что они бесполезны для языка С#. Вероятно, вам не потребуются
окна Auto или Registers; а при необходимости вы можете использовать отдель­
ный инструмент для дизассемблирования — Ildasm.
Вы не сможете вызвать справку при нажатии на кнопку . Это неприятно, но
можно нормально работать и без этого. Используйте справку, предоставляемую
в пакете .NET Framework SDK, как было описано ранее в этой главе.
Этот отладчик можно использовать только для отладки программ платформы
.NET, но не "родной" код Win32. Ну и что? Язык С# полностью основан на плат­
форме .NET.
Нельзя использовать удаленную отладку. Если вам необходима подобная функция,
применяйте Visual Studio.

Одной из хороших, но более грубых альтернатив программе SharpDevelop является
редактор кода TextPad, показанный на рис. 22.3. С точки зрения комфорта, этот редактор
определенно можно считать шагом назад по сравнению с такими программами, как
SharpDevelop или Visual Studio. Он намного ближе к работе в командной строке — за ис­
ключением того, что в действительности вам не придется работать в ней, — и имеет
большое количество хороших возможностей для редактирования, предназначенных для
программистов.
Пробная версия редактора TextPad находится на прилагаемом компактдиске. Если вы захотите использовать этот редактор, вы должны его приоб­
рести. Посетите Web-сайт по адресу www. t e x t p a d . c o m , заплатите (около
30 дол.) и зарегистрируйте свой редактор.
На Web-сайте программы TextPad также находится форум, на котором можно полу­
чить помощь от опытных пользователей T e x t P a d — в основном приверженцев языков
Java и Perl, но все же их помощь может быть полезной и для программиста на С#. Неко­
торые программисты С# предпочитают редактор TextPad программе SharpDevelop, за
исключением программирования Windows Forms и Web-программирования. Но все же
вы должны попробовать обе программы, чтобы определить свои собственные предпоч­
тения. Эта глава может стать решающей причиной для программирования на С# с помо­
щью TextPad.
,

534

Часть

VII. Дополнительные главы

На прилагаемом компакт-диске находится демонстрационная программа Pri orityQueueTextPad, показывающая, как настроить файлы в "проекте" для
TextPad. Этот пример основан на программе PriorityQueue, которая рас­
сматривается в главе 15, "Обобщенное программирование".

Рис. 22.3. Редактор TextPad выглядит обманчиво просто
Поскольку редактор TextPad настолько универсален, его следует описать более под­
робно, отчасти как способ представить вам некоторые возможности платформы .NET,
которые я не мог бы объяснить в другом месте. Это такие возможности, которые слож­
ные среды разработки наподобие Visual Studio и SharpDevelop имеют тенденцию скры­
вать, но о них стоит знать. Без этого невозможно стать профессионально подготовлен­
ным программистом.
Хотя редактор TextPad на рис. 22.3 выглядит довольно просто, его можно настроить
для выполнения удивительного разнообразия полезных задач программирования. Далее
приведен список только некоторых возможностей, которые вы обнаружите, познако­
мившись с программой поближе.
Работает со многими языками программирования: TextPad специально разра­
ботан как редактор исходных текстов программ и отлично работает почти с лю­
бым языком программирования; по крайней мере, он имеет встроенную поддерж­
ку для очень многих из них, включая С# и Visual Basic.
Имеет возможность сворачивания документов: в редакторе TextPad вы можете
легко работать с множеством исходных файлов программ, как и в его более мощ­
ных собратьях.
Предоставляет отличные возможности редактирования: эти возможности
включают поиск пар скобок, закладки для часто посещаемых мест в файлах, ото­
бражение номера строки, проверку правописания, сортировку строк, способность
записывать и воспроизводить клавиатурные макрокоманды, ряд встроенных мак-

Глава 22.

С# по дешевке

535

рокоманд и многое другое. Блокнот на фоне T e x t P a d — черепаха по сравнению
с гепардом.
Обеспечивает хорошие встроенные средства управления файлами: они вклю­
чают в себя инструментарий для сравнения двух файлов, полезный в ряде ситуа­
ций отладки, и команду, которая открывает Windows Explorer в каталоге, содер­
жащем текущий файл.
Но самые главные возможности программы включают следующие действительно по­
лезные функции.
Вы можете настраивать классы документов. Например, можно настроить тип до­
кумента . CS языка С#, включая цвета синтаксиса, шрифт, опции печати и обра­
ботку табуляции для отступов. Каждый класс документа обрабатывается в редак­
торе по-своему. Это похоже на сервис, предоставляемый Visual Studio.
Как и SharpDevelop, редактор TextPad позволяет настраивать ваши собственные
команды инструментов в меню Tools. Это предоставляет возможность установить
команды для запуска компиляторов, отладчиков и других полезных инструментов
непосредственно из редактора. Вы можете также установить команду для запуска
своих скомпилированных программ.

Опции настройки инструментов
Prompt for Parameters (запрос параметров). Вашей программе необходимо по­
лучать параметры из командной строки? Это так, если вы планируете, что ваше
консольное приложение будет запускаться из командной строки, и вы действи­
тельно используете параметр a r g s функции M a i n ( ) . Обычно лучше пропустить
настройку этой опции.
Run Minimized (выполнять минимизированным). Выбирайте эту опцию, если
ваша программа абсолютно никак не взаимодействует (в смысле ввода-вывода)
с пользователем. У меня еще не было необходимости использовать эту функцию.
Save АН Documents First (сначала сохранять все документы). При выборе дан­
ной опции происходит сохранение любых документов, открытых вами в редакторе
TextPad, перед запуском команды. Я всегда устанавливаю эту опцию.
Capture Output (перехват вывода). Выбор данной опции зависит от того, может
ли выполнение команды приводить к сообщениям об ошибках и знаете ли вы пра­
вильный синтаксис "регулярных выражений" для перехвата ошибок. Далее будет
описан этот правильный синтаксис.
Одна возможная причина для выбора этой опции возникает, когда, а) вы настраи­
ваете инструмент для фактического запуска своей успешно скомпилированной
программы, и б) вы не предполагаете, что пользователь введет что-либо в команд­
ной строке во время выполнения программы. Если программа не является диало­
говой, вы можете при желании использовать эту опцию, чтобы направить поток
вывода программы в окно Command Results редактора TextPad вместо окна ко­
мандной строки. Для команды Run следует предпочесть окно командной строки,
так что можно не использовать опцию Capture Output.

536

Часть VII. Дополнительные главы

Но совсем другое дело команда Build, которая может (извините, будет!) возвра­
щать сообщения об ошибках. При знании синтаксиса волшебных регулярных вы­
ражений для языка С# вы можете выбрать опцию Capture Output для этих команд,
но не для других.
Suppress Output Until Completed (запрет вывода до завершения). Данная опция
запрещает вывод, пока программа не закончится. Я обычно оставляю эту опцию
неотмеченной.
Sound Alert When Completed (подача звукового предупреждения при заверше­
нии). Если ваша программа долго выполняется (например, долго компилируется),
и вы хотели бы в это время выполнять другую работу, эта опция предупредит вас
звуковым сигналом о завершении программы.
Чего недостает редактору TextPad, так это удобных инструментов наподобие Solution
Explorer, Class View, проектировщика форм и т. д. Вы можете предпочесть SharpDe­
velop, но даже в этом случае редактор TextPad удобен для быстрого редактирования про­
граммы или для универсальной замены Блокнота.
Как известно, существует множество других прекрасных редакторов программ,
включая Vi, Fte, Emacs и Brief, большинство из которых предлагают даже
больше возможностей (часто за деньги). Некоторые программисты даже кля­
нутся своим любимым редактором.
Из следующих разделов вы узнаете, как настроить редактор TextPad для компилиро­
вания, выполнения и отладки своих программ С#. Действия довольно сложные, но вы
попутно получите большое количество интересной дополнительной информации.
Необходимо настроить две вещи в редакторе TextPad, прежде чем он сможет рабо­
тать с С#: класс документов . CS для языка С# и несколько связанных с С# команд в ме­

ню Tools.

Создание класса документов .CS для языка С#
Для создания нового класса документов языка С# в редакторе TextPad выполните
следующие действия (которые не проиллюстрированы рисунком).

1. Выберите команду меню C o n f i g u r e ^ N e w Document Class.
2. В мастере Document Class Wizard введите C#, чтобы дать название классу,
и затем щелкните на кнопке Next.
3. Введите * . cs в качестве "члена класса". Щелкните на кнопке Next.
4. Установите флажок Enable Syntax Highlighting и затем из списка файлов
подцветки синтаксиса выберите c s h a r p . s y n . Щелкните на кнопке Next,
а затем — на кнопке Finish.
5. Выберите команду меню C o n f i g u r e ^ P r e f e r e n c e s . В иерархическом списке
слева выберите пункт Document C l a s s e s ^ C t t . Справа отметьте желаемые
опции для обработки файлов С#, как показано на рис. 22.4.

Я выбрал опции Maintain Indentation, Automatically Indent Blocks, Strip Trailing
Spaces from Lines When Saving и Check Spelling of Comments. Все остальные
опции я оставил такими, какими они были по умолчанию.

Глава 22.

С#

по дешевке

537

6. В иерархическом списке слева выберите подпункт Tabulation в пункте С#.
Введите количество пробелов табуляции и число пробелов для отступа. За­
тем щелкните на кнопке Apply.
Я выбрал размер табуляции и отступа, равный двум для сохранения пространства при
печати примеров программ в книге, но вы можете выбрать более удобные размеры. Я
также выбрал опции Convert New Tabs to Spaces и Convert Existing Tabs to
Spaces When Saving Files. Некоторые программисты предпочитают табуляцию.

Рис. 22.4. Здесь в редакторе TextPad настраивается
класс документов С#
7. Щелкните также слева на пункте Font и установите предпочитаемый шрифт
для программы. Затем щелкните на кнопке Apply.
Я выбрал шрифт Lucida Console, обычный, размер 9 пунктов для экрана и принтера.
8. Находясь в диалоговом окне Preferences, просмотрите прочие настройки
предпочтений и при желании измените что-либо.
Имейте в виду, что установки для индивидуальных классов документов отменяют
общие настройки.
9. При выполнении восьмого шага установите еще одну опцию, которая при­
годится позже. В левой панели окна Preferences щелкните на пункте Editor
(прямо под пунктами General и File). Справа отметьте последнее поле оп­

ции Use POSIX Regular Expression Syntax.
10. Наконец, когда вы закончите, щелкните на кнопке Apply и затем — на
кнопке ОК.

Добавление собственных инструментов: Build С# Debug
Настройка команд менюTools для компиляции, отладки и запуска программ С#
включает в себя добавление в меню инструмента для каждого действия, изменение тек­
ста, который появляется в меню Tools, и дальнейшую настройку инструмента.
В качестве примера выполните следующие действия, чтобы добавить инструмент
Build С# Debug для компиляции версии вашей программы, которая может выполняться
в отладчике.

538

Часть VII. Дополнительные главы

1. Выберите команду меню C o n f i g u r e d Preferences. В левой части диалогово­
го окна Preferences щелкните на пункте Tools.
2. В правой части выберите команду A d d d P r o g r a m . В диалоговом окне Se­
lect a File перейдите к папке, в которой находится исполняемый файл
(. ЕХЕ) инструмента.
Чтобы найти инструменты, рассматриваемые в этой главе, обратитесь к разделу
"Получение бесплатных компонентов". Обратите внимание, что отладчик CLR
находится в ином месте, чем большинство из них. Он описан в разделе "Запуск
отладчика из SharpDevelop" этой главы.
3. Выберите требуемый инструмент — в данном случае Csc. ехе.
В списке справа появится новая команда. В настоящий момент это название са­
мого исполняемого файла, но вы можете затем изменить это название на чтонибудь более значащее.
Между группами инструментов в меню можно разместить тонкую сплошную
разделительную линию. Начните с нее при создании нового инструмента. Вы­
берите команду A d d o M e n u Separator (вместо Program). Для перемещения
разделителя (или любого инструмента) используйте волнистые стрелки вверху
списка инструментов.
4. Для изменения названия инструмента щелкните на нем дважды в списке
справа — но немного медленнее, чем при двойном щелчке. Затем введите
текст, который появится в меню T o o l s для этой команды. По завершении
ввода нажмите и затем щелкните на кнопке Apply.
Для своей первой команды компилирования я ввел текст Build

С#

Debug.

5. Щелкните на новом инструменте, который вы добавили, в пункте T o o l s
диалогового окна Preferences, чтобы появились дополнительные опции,
как показано на рис. 22.5.
Справа появятся дополнительные опции, как показано на рис. 22.5, с настройками
по умолчанию. Их можно изменить, выполнив следующие действия. В правой части
поле Command уже должно содержать путь к исполняемому файлу инструмента,
в поле Parameters, возможно, содержится текст $ F i l e , а в поле Initial Folder —
$ F i l e D i r . Для большинства команд это совершенно верно, но вы измените эти
значения для нескольких первых инструментов, которые настраиваете.
6. В поле ввода Parameters введите следующее:
/debug

/out:$FileDir\bin\debug\Program.ехе

©refs.rsp

*.cs

7. Установите флажки Save All Documents First и Capture Output. Затем
щелкните на кнопке Apply.
Оставьте другие флажки неотмеченными.

8. Выберите поле Regular Expression to Match Output и аккуратно введите
следующую строку, затем щелкните на кнопке Apply:
Л

( [*.']+.св)\( ( [0-9]+) , ( [0-9]+)\)

Ниже будет кое-что пояснено из этого марсианского стихотворения.

Глава 22.

С# по дешевке

539

Рис. 22.5. Опции настройки вашего первого инструмента
9. У с т а н о в и т е п о л я Registers т а к , ч т о б ы в них б ы л и з н а ч е н и я 1, 2 и 3.
10. Щ е л к н и т е на к н о п к е Apply и затем на к н о п к е ОК, ч т о б ы з а к р ы т ь диалого­

вое окно Preferences.
Далее вы настроите дополнительные инструменты таким же двухэтапным способом,
описанным ниже.
1. Создайте и н с т р у м е н т , к а к описано в ы ш е .
В следующих разделах будут рекомендоваться названия для каждого инструмента.
2. Н а с т р о й т е свой н о в ы й и н с т р у м е н т путем его в ы б о р а в л е в о й ч а с т и диало­
гового о к н а Preferences и з а п о л н е н и я д е т а л ь н о й и н ф о р м а ц и и в правой
части.
В следующих разделах такая детальная информация будет представлена для каж­
дого инструмента.

Настройка инструмента для компиляции финальной версии
После полной отладки и проверки программы вы упаковываете финальную версию и
выпускаете ее в свет. Для настройки инструмента компиляции окончательной версии
следуйте приведенной инструкции.
P r o g r a m to R u n ( п р о г р а м м а д л я запуска). При создании инструмента перейдите
к C s c . е х е (ищите папку, описанную в разделе "Получение бесплатных компо­
нентов" этой главы).
M e n u N a m e (название меню). Введите B u i l d С #

Release.

C o m m a n d (команда). Настраивается, когда вы создаете пункт меню инструмента
и указываете программу, которая запускается для этого инструмента: C s c . е х е .
P a r a m e t e r s ( п а р а м е т р ы ) . При настройке инструмента введите в это поле текст
/out:$FileDir\bin\release\Program.ехе

©refs.rsp

*.cs

(теперь

без ключа /debug). Затем заполните пункты в оставшейся части этого списка.
Initial Folder ( н а ч а л ь н ы й к а т а л о г ) . Оставьте это поле без изменений.

540

Часть VII. Дополнительные главы

Options (опции). Установите флажки Save All Documents First и Capture Output
(остальные не установлены).
Regular Expression (регулярное выражение). Щелкните на кнопке Apply, снова
откройте инструмент Build С# Debug, выделите регулярное выражение, нажмите
комбинацию клавиш для его копирования, выберите инструмент Build
С# Release и поле Regular Expression и нажмите комбинацию клавиш
для вставки регулярного выражения. Щелкните на кнопке Apply. Избегайте по­
вторного ввода этого монстра!
Registers (регистры). Введите в ячейки 1,2 и 3.
Теперь в редакторе TextPad имеются два инструмента. Позже вы добавите еще не­
сколько, но те инструменты, которые были только что настроены, требуют небольшого
пояснения и некоторых настроек.

Объяснение опций настройки инструментов Debug и Release
Для некоторых компиляций командная строка может нуждаться в изменениях. В этих
случаях вы должны временно перенастраивать инструменты компилирования в редакто­
ре TextPad или создавать специальный файл для сохранения дополнительных опций.
Я предпочитаю последний подход.
В следующих разделах рассматриваются все опции в полях Parameters инструмен­
тов Build С# Debug и Build С# Release, а также объясняются опции Regular Expres­

sion и Registers.

Ключ /debug
Первым параметром в поле Parameters инструмента Build С# Debug (но не инстру­
мента Build С# Release) является ключ / d e b u g .
В некоторых программах используются ключи командной строки, которые предос­
тавляют пользователю возможность настраивать поведение программы. Для инструмен­
тов редактора TextPad, которым необходимы такие ключи, они вводятся в поле Pa­
rameters. Например, ввод строки /debug * .cs дает команду компилятору С# запус­
тить отладочную версию процесса компиляции.
Ключи командной строки отличаются от ее параметров. Символ $ F i l e — это
параметр, указывающий редактору TextPad имя файла, который является теку­
щим в данный момент в редакторе. Редактор TextPad передает эту информа­
цию инструменту при его запуске.
Но если вам, к примеру, нужно указать отладочную версию при запуске компилятора,
то необходимо использовать ключ, который понимает компилятор: для языка С# таким
ключом является / d e b u g . Другие команды определяют другие ключи. Выяснить, какие
ключи доступны, обычно можно путем ввода в окне команды имени команды, за кото­
рым следует пробел, символ наклонной черты и вопросительный знак. Например, можно
ввести esc. ехе / ? , чтобы узнать, какие ключи компилятора С# имеются (как и уточ­
нить остальную часть синтаксиса команды). Ключи имеют названия и начинаются с сим­
вола / или -.

Глава 22. С# по дешевке

541

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

/? Другими словами,

функция Main () должна получать аргументы командной строки и искать сре­
ди них ключ / ? , а затем выводить справочную информацию.

Ключ /out
Ключ /out в поле Parameters позволяет указать, где компилятор должен помещать
компилируемую программу для работы. Значением по умолчанию является $FileDir.
В следующем списке объясняется информация, использованная в ключе /out для ко­
манды Build С# Debug.
$FileDir: этот "макропараметр" сообщает редактору TextPad о месте
расположения ваших исходных файлов С# (* . cs). При компиляции про­
граммы из редактора TextPad компилятор помещает результирующие
файлы . ЕХЕ, . DLL и другие файлы компиляции, такие как информация
отладки (в файле . PDB), в тот же каталог по умолчанию.
Жизнь будет проще, если скопировать структуру каталогов, которая ис­
пользуется в программах Visual Studio и SharpDevelop. Это позволяет
компилировать одинаковую программу в любой из этих сред с аналогич­
ной структурой. В Visual Studio файлы компиляции помещаются в подкаталог
подкаталога $FileDir: для отладочной версии это $FileDir\bin\Debug,
для окончательной— $FileDir\bin\Release.
Для подражания программе Visual Studio создайте вручную подкаталоги
в папке вашего проекта и включите ключ /out в поле Parameters по об­
разцу:

/out: $FileDir\bin\release\Program.exe.

Выполняйте

эти действия как часть настройки каждого проекта.
Для поиска других макросов наподобие $FileDir введите "tools" в спра­
вочной системе TextPad и выберите тему "Tool Parameter Macros".
P r o g r a m . ехе: ключ /out можно использовать для определения имени
программы. В Visual Studio класс, включающий функцию Main ( ) , по
умолчанию называется P r o g r a m , и файл, содержащий этот класс, назы­
вается P r o g r a m , сs. (при желании можно изменить одно или оба эти на­
звания.) Вы также можете вручную изменить имя файла с расширением
. ЕХЕ в ключе / out на что-нибудь другое. Возможно, вам не нравится на­
зывать каждый файл с расширением

. ЕХЕ "Program". Для ввода другого

названия просто откройте диалоговое окно Configuration для инструмента
компиляции и замените строку P r o g r a m в ключе /out другим названи­
ем. Я использую строку "Program.exe" в качестве заполнителя, так как
в поле Parameters нужно что-нибудь ввести.

Параметр @refs.rsp
Параметр Orefs.rsp позволяет упростить уже пугающую командную строку для
инструментов компиляции редактора TextPad. Хитрость заключается в том, чтобы со­
хранить набор дополнительных параметров в текстовом файле в каталоге вашего проекта
и обращаться к нему из командной строки, указывая этот файл. Компилятор С# читает

542

Часть VII. Дополнительные главы

файл и добавляет его содержимое к командной строке. Этот файл используется как с от­
ладочной, так и с финальной версиями.
Для использования данной возможности командной строки вашего инструмента вы­
полните следующие действия.
1. В редакторе TextPad выберите команду меню F i l e ^ N e w для создания ново­
го файла.
2. В начале файла введите: # Файл н а с т р о й к и д л я компиляций С#.
3. Сохраните файл с именем r e f s. r s p в каталоге вашего проекта.
Убедитесь в том, что поле Save as Туре в диалоговом окне Save as установлено
в А 1 1 Files (*.*).

Что содержится в файле настройки
Эти файлы в основном применяются для ссылок на пространства имен, которые ис­
пользуются вами. Каждый раз, когда директива using, такая как using System;, по­
мещается в начало исходного файла С#, компилятору необходима информация о том, где
искать классы из этого пространства имен, т.е. имя и расположение файла . DLL, кото­
рый связан с пространством имен, упомянутым в директиве using.
В Visual Studio вы отправляете компилятор к соответствующему файлу . DLL
(называемому сборкой (assembly)), выбирая команду меню P r o j e c t ^ A d d Reference.
(Этот шаг не нужен для подмножества библиотеки .NET, для которой в компиляторе
уже имеются собственные предопределенные ссылки: обычно используемые элементы
находятся в файле Mscorlib. dll. Ниже будет дано объяснение, как определить, что
это за элементы.)
В инструментах компиляции редактора TextPad (Debug и Release) вы выполняете ту
же операцию посредством ключа /reference (/г для краткости). Вы могли бы помес­
тить свои ключи /г (по одному для каждой директивы using) прямо в командной стро­
ке (другими словами, в поле Parameters для ваших инструментов компиляции, вместо
файла @ref s . rsp).
Да, вы могли бы это сделать. Но только представьте себе, каким длинным стало бы
содержимое командной строки в таком небольшом поле Parameters!

Пример файла настроек
Размещение элементов наподобие ключей /г — это именно то, для чего предназна­
чен файл настройки. В приведенном далее файле содержится пара ключей /г:
# файл н а с т р о й к и д л я п р и л о ж е н и й С#
/г:System.Windows.Forms.dll
/ г : " С : \ P r o g r a m Files\NUnit 2 . 2 \ b i n \ n u n i t . f r a m e w o r k . d l l "
Этот простой файл настройки включает комментарий (первая строка, начинающаяся
с #) и два ключа / reference, каждый на отдельной строке. Первый ключ ссылается на
файл . DLL, содержащий классы в пространстве имен System. Windows . Forms, а вто­
рой — на файл . DLL для инструмента тестирования NUnit. Данный инструмент является
библиотекой классов .DLL сторонних производителей. Эти строки не являются кодом
языка С#, поэтому они не заканчиваются точкой с запятой. Вторая строка заключена
в кавычки, потому что путь к файлу содержит пробелы.

Большинство библиотечных классов, которые вы указываете в директивах us i n g (или в полностью квалифицированных именах в своем исходном тексте
наподобие S y s t e m . W i n d o w s . F o r m s . F o r m ) , являются частью библиотеки
базовых классов (Base Class Library — BCL) платформы .NET Framework. Дан­
ные классы — это файлы

. DLL, которые уже сохранены в глобальном кэше

сборок (Global Assembly Cache — GAC), центральном хранилище в каталоге
Windows. Компилятор может найти эти классы на основе ссылки, просто по
имени файла . DLL наподобие S y s t e m . W i n d o w s . F o r m s . d l l .
Но иногда необходимо использовать классы, определенные в библиотеках классов
сторонних производителей или в библиотеках классов, созданных вами самостоятельно.
Например, чтобы применить инструмент тестирования NUnit с вашим проверяемым ко­
дом, понадобится директива u s i n g N U n i t . F r a m e w o r k ; во всех файлах, содержащих
тестируемые классы; кроме того, нужна ссылка ( / г ) на каталог, в котором находится
файл n u n i t . f r a m e w o r k . d l l .
Файлы библиотеки инструмента NUnit не хранятся в GAC. Чтобы помочь компилято­
ру их найти, ссылка должна определить полный путь к файлу . D L L . Именно поэтому
в предшествующем примере во втором ключе /г содержится полный путь.
Следует упомянуть еще вот о чем: в файле настройки, как и в командной строке
(в поле Parameters для инструмента), необходимо определить ключи, параметры и име­
на файлов для компиляции в такой последовательности:
/debug и /out
Orefs.rsp

ключи
параметры

/ / например:
/ / например:

файлы

/ / например: * . c s

Ознакомьтесь с кратким описанием примера.
/ d e b u g — это ключ. (Его лучше разместить здесь, чем в поле Parameters, если
только вы не используете этот файл настройки для обеих компиляций — Debug

и Release).
© d e b u g . r s p — это параметр (но этот параметр должен быть в командной строке).
* . cs — список файлов для компиляции. (Вы можете иногда встретить дополни­
тельные ключи после имен файлов.)
Большинство из этих элементов могут встречаться как в командной строке, так и
в файле параметров.
Размещайте файл параметров в каталоге, на который указывает параметр
$ F i l e D i r в редакторе TextPad. Для команд компиляции это каталог, содер­
жащий исходные файлы . CS.
Во время компиляции компилятор раскрывает командную строку с использованием
информации из файла настроек © r e f s . r s p (в командной строке они разделялись бы
пробелами).
Компилятор С# всегда использует дополнительный заданный по умолчанию
файл настройки, называемый c s c . r s p , для ссылок на все общие сборки, такие
как S y s t e m . d l l . Таким образом компилятор знает о них без вашего вмеша­
тельства. Найдите этот файл в том же месте, где находится компилятор С#,
c s c . е х е , и просмотрите его.

544

Часть VII. Дополнительные главы

Работа над ошибками компиляции
Оба инструмента — и Build С# Debug, и Build С# Release — запускают компиля­
тор. В любом случае, даже при компиляции окончательной версии, вы можете получить
от компилятора сообщения об ошибках. При правильной настройке инструментов любые
сообщения компилятора об ошибках появляются в окне Command Results редактора
TextPad. Типичное сообщение об ошибке выглядит так:

mycode.cs(11,17): error CS0246: The type or namespace name
'joeyTypes' could not be found (are you
missing a using directive or an assembly
reference?)
Первая часть — это имя файла, в котором произошла ошибка. Числа в круглых скоб­
ках — номера строки и столбца в этой строке. Остальная часть просто подробнее описы­
вает ошибку.
Волшебное регулярное выражение, которое вы добавили при настройке инст­
рументов, предназначено для выделения трех частей информации из этого со­
общения: имени файла, номера строки и номера столбца. Редактор TextPad пе­
рехватывает сообщение об ошибке от компилятора, применяет регулярное вы­
ражение для извлечения этих элементов и помещает их в те "регистры",
которые вы настроили как 1, 2 и 3. Вот в чем был весь фокус-покус!
Синтаксис регулярного выражения примерно так же прост, как и общая теория отно­
сительности. (В действительности он не так уж и плох, если только вы поймете его суть.)
В окне Command Results можно дважды щелкнуть на сообщении об ошибке
и перейти к указанным строке и столбцу. Очень полезная возможность!

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

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

Два инструмента для запуска вашей программы
После успешной компиляции своей программы вы, как правило, захотите ее запус­
тить. Поскольку отладочная и финальная компиляции помещают результат в различные
каталоги, вам потребуются два инструмента. Чтобы настроить инструменты для запуска
вашей программы, используйте настройки из следующего списка.
Program to Run (программа для запуска). При создании инструмента укажите
следующую программу для его запуска: C s c . е х е . Чуть позже вы измените на­
звание программы на другое, не доступное во время создания инструмента. Эта
установка одинакова для обоих инструментов запуска, описанных здесь.

Глава 22. С# по дешевке

545

Menu Name (название меню). Введите Run Debug для одного инструмента
и Run Release для другого.
Command (команда). При настройке инструмента замените все, что находится
в этом поле, строкой $FileDir\bin\debug\Program. ехе для первого инст­
румента и

$FileDir\bin\release\Program.ехе для второго.

(Если вы из­

менили свои командные строки компиляции в поле Parameters, чтобы использо­
вать название, отличное от "Program", воспользуйтесь этим названием и здесь).
Затем завершите настройку остальных элементов в этом списке.
Parameters (параметры). Оставьте это поле без изменений для обоих инструментов.
Options (опции). Установите флажок Save All Documents First (остальные опции
не установлены).
Remaining options (остальные опции). Пропустите.
При использовании своего инструмента Run Debug или Run Release убеди­
тесь в том, что файл, содержащий функцию M a i n ( ) , находится в редакторе
TextPad на переднем плане.

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

уверенность
для классов
далее в этой
используйте

Program to Run (программа для запуска). При создании инструмента введите
название или перейдите к программе
C:\Program
Files\NUnit
2 . 2\bin\nunit-gui . ехе (хотя, возможно, у вас имеется более свежая версия
программы NUnit).
Menu Name (название меню). Введите Run NUnit.
Command (команда). Это поле устанавливается после создания инструмента.
Other options (другие опции). При настройке инструмента оставьте их все без
изменений.

Инструмент для отладки вашей программы
После того как вы скомпилировали свою программу с помощью команды Build С#
Debug, которую настроили ранее, вам зачастую необходимо запустить программу в отладчи­
ке для поиска ошибок. Чтобы настроить инструмент для запуска отладчика, добавьте инстру­
мент Debug .NET Program в редактор TextPad, используя следующие установки.
Program to Run (программа для запуска). При создании инструмента перейдите
к папке, в которой установлен пакет .NET Framework SDK, обычно она находится
в каталоге C:\Program Files. Затем найдите подкаталог \GuiDebug и от­
кройте файл DbgCLR. ехе. Обратитесь к разделу "Получение бесплатных компо­
нентов" выше в этой главе.
Menu Name (название меню). Введите Debug .NET Program.

546

Часть VII. Дополнительные главы

Command (команда). Это поле устанавливается после создания инструмента.
Parameters and Initial folder (параметры и начальный каталог). При настройке
инструмента очистите эти поля. Затем завершите настройку остальных элементов
этого списка.
Options (опции). Установите флажок S a v e All Documents First (никаких регу­
лярных выражений или регистров).
Эта команда работает для любого языка платформы .NET, а не только для С#. Под­
робнее применение данного отладчика было описано ранее в этой главе. Один и тот же
отладчик используется как в TextPad, так и в SharpDevelop.

Инструмент для просмотра документации пакета .NET SDK
Поскольку редактор TextPad ничего не знает о языке С# или библиотеке .NET Frame­
work (в отличие от SharpDevelop), вы будете вынуждены воспользоваться инструментом
для просмотра документации из .NET Framework SDK. Для настройки инструмента, ко-,
торый открывает документацию в вашем Web-браузере, добавьте инструмент Browse
.NET Docs, используя следующие настройки.
Program to Run (программа для запуска). При создании инструмента перейдите
к папке, в которой установлен ваш Web-браузер. Например, для использования
программы
Internet
Explorer
перейдите
к
программе
C:\Program
Files\lnternet E x p l o r e r \ I E x p l o r e . е х е .
Menu Name (название меню). Введите Browse

.NET Docs.

Command (команда). Это поле устанавливается после создания инструмента.
Parameters (параметры). В Windows Explorer найдите папку с пакетом .NET
SDK; вероятно, она находится в каталоге C:\Program Files. При настройке
инструмента введите путь к этой папке, за которым следует \StartHere . htm.
Other options (другие опции). Пропустите.
Существует еще один отличный инструмент. Программа WinCV позволяет про­
сматривать пространства имен и классы в библиотеках платформы .NET. Испол­
няемый файл WinCV. е х е расположен в папке \bin в каталоге пакета .NET SDK.
Все, что вам нужно — это указать путь в поле Command в окне Preferences.
Дизассемблер промежуточного языка (Intermediate Language Disassembler) позволит
вам получить удобочитаемую версию того, во что скомпилирована ваша программа С#.
Постепенно из полной абракадабры она будет превращаться во все более и более понят­
ный для вас код. Программа I l d a s m . e x e расположена в подкаталоге \bin каталога
.NET SDK. Все, что вам нужно — это указать путь в поле Command. При создании ин­
струмента можно выбрать опцию Close DOS Window on Exit.
Для получения специальной справки по инструментам SDK дважды щелкните на
файле \ D o c s \ C p T o o l s . chm в окне Windows Explorer в каталоге .NET SDK.

Итак, вы завершили настройку редактора TextPad для работы с языком С#. На
рис. 22.6 показан окончательный вид меню Tools. Теперь редактор TextPad стал очень
полезным инструментом для программирования на С#, а вы узнали, что делают про-

Глава 22.

С# по дешевке

547

граммы Visual Studio и SharpDevelop под своим блестящим покрывалом. Далее вы узнае­
те, как тестировать свои программы.

Рис. 22.6. Простое меню Tools в редакторе TextPad— вашей новой среде
программирования на С#

Выше объяснялось, как настроить инструмент для запуска NUnit. Эта программа дос­
тупна на Web-сайте по адресу w w w . n u n i t . o r g и на прилагаемом компакт-диске.
Это — программное обеспечение с открытым исходным кодом, поэтому вы можете сво­
бодно использовать его, придерживаясь лицензии.
Данный раздел применим как к работе в Visual Studio, так и к работе в TextPad
или SharpDevelop.

Запуск программы NUnit
Чтобы можно было запускать программу NUnit из какой-нибудь рассматриваемой
в этой главе среды разработки, вы должны сначала настроить ее в меню Tools. Озна­
комьтесь с командами для запуска программы NUnit.
Для запуска программы NUnit из пакета Visual Studio выберите команду меню

Tools'^Ваше название.
Для запуска программы NUnit из редактора TextPad выберите команду меню

T o o l s ^ R u n NUnit.
Для запуска программы NUnit из SharpDevelop выберите команду меню Tools^NUnit.

548

Часть VII. Дополнительные главы

Программу NUnit также можно запустить из меню Start в Windows, из Windows
Explorer или из командной строки.
Конечно, сначала необходимо кое-что подготовить. Программа NUnit проста в ис­
пользовании, но вы должны ознакомиться с ее соглашениями и методами.
На рис. 22.7 показана программа NUnit с частично успешным тестовым запуском, ко­
торый только что завершился.

Рис. 22.7. Тестирование в NUnit и простое, и мощное

Тестирование
Эта книга о программировании на языке С#, поэтому вы можете удивиться, почему
в ней говорится о тестировании, и к тому же — зачем вы должны этим заниматься.
Если вы ожидаете пользы от программы, то требуется некоторая гарантия, что она
действительно делает то, для чего предназначена. Без соответствующей проверки у вас
не будет уверенности даже для самостоятельного использования программы, не говоря
уж о ее распространении среди других пользователей. Попрограммируйте некоторое
время, и вы поймете, что имеется в виду.
Но, может, кто-то другой несет ответственность за тестирование? Да, в больших про­
граммных проектах обычно имеется отдельная группа тестировщиков, которая проверяет
программное обеспечение. Но если вы работаете в одиночку, то вы и есть эта группа
в полном составе. Но даже если вы работаете в команде, вы все равно несете ответствен­
ность за тестирование программы, которую пишете.
Тестирование, осуществляемое в NUnit, относится к типу модульного тестиро­
вания. "Модули", которые вы проверяете, обычно являются отдельными клас­
сами и их методами. Чтобы убедиться в том, что класс BankAccount не будет
обманывать вашего работодателя (банк) или его клиентов, вы должны выпол­
нить множество вводов информации для его методов и удостовериться в кор­
ректности получаемых результатов.

Глава 22.

С# по дешевке

549

Модульное тестирование выполняет две задачи. Первая заключается в провер­
ке правильности поведения программы. Вторая состоит в поиске ошибок. Если
они в программе есть (а это неизбежно), вы должны обнаружить их и искоре­
нить. Пишите свои тесты так, чтобы вы могли найти ошибки.
Типичный класс тестирования в NUnit содержит значительное количество — обыч­
но небольших — методов тестирования. Каждый метод тестирования проверяет один
из методов вашего класса. Может потребоваться несколько или даже множество мето­
дов тестирования, чтобы охватить все основные компоненты для метода, который вы
проверяете: правильный ввод, плюс различные виды неверного ввода, включая ввод вне
допустимого диапазона, пустой ввод, опасный ввод, глупый ввод, никогда не случаю­
щийся ввод и т.д. Люди, которые фактически используют вашу программу, будут оче­
редными Эйнштейнами в поиске способов "наступить на грабли" в ней и будут давать
знать о себе обычно в самое неподходящее время.
Что касается времени, когда следует проводить тестирование, то лучшим будет время
написания программы. Если исходить из опыта, то при создании метода следует всегда
потратить несколько минут на написание одного или нескольких тестов для него. После
того как вы приобретете некоторый опыт, обычно на все это уже не потребуется много
времени, и затем вы сможете легко запускать тесты — некоторые из них или все — сно­
ва и снова, так что количество правильных выполнений вашей программы будет возрас­
тать, а вы будете знать, что новый код не нарушит старый. Старайтесь не уклоняться от
этого принципа — позже это окупается сэкономленным на отладке программ временем.

Написание тестов NUnit
Для работы в программе NUnit добавьте один или несколько классов тестирова­
ния — они могут быть в любом проекте решения программы, хотя зачастую в целях дос­
тупности их легче поместить в тот же проект, в котором находится тестируемая про­
грамма. (Если поместить эти классы в отдельный проект, то методы, которые тестируют­
ся, должны быть объявлены как public, чего обычно делать не следует. Общим
правилом должно являться сохранение методов скрытыми, когда это только возможно).
Класс тестирования NUnit может иметь следующий вид:
using System;
using NUnit.Framework;
// Эта директива необходима
namespace NUnitTestExample

{
// Элементы в скобках

[]

называются

"атрибутами"

[TestFixture]

// Атрибут: это термин NUnit для класса
// тестирования
class MyTestClass

public
{
// Здесь размещаются все данные, необходимые для
// большинства или всех тестов, в виде переменных// членов, а также конструктор(ы) (если они
// необходимы)
public MyTestClass() {
}

// Метод настройки — вызывается перед каждым методом
// тестирования; используется для установки одинаковых
550

Часть

VII. Дополнительные главы

// начальных условий для тестов, так что ни один тест не
// влияет на результаты последующих тестов
[SetUp]
// Атрибут SetUp (обратите внимание на
// правильное написание)
public void M y S e t u p O
{
// Здесь выполняется настройка, необходимая для всех
// методов тестирования

}
// Здесь вы пишете методы тестирования
// Методы тестирования имеют "атрибут" [Test]
// объявлены как public void, без аргументов
[Test]
public void StringLengthsGoodTest()

и всегда

{
}
[Test]
public void CountSpacesTest()

{
}
// Вспомогательный метод для тестов,
// атрибута [Test])
private int CountSpaces(string s)

но не сам тест

(нет

{
}
// другие тесты...
Вы можете найти законченный пример реального класса тестирования NUnit
NUnitTestExample на прилагаемом компакт-диске. На рис. 22.8 показан
пример класса тестирования NUnit в редакторе TextPad. Этот же класс тес­
тирования прекрасно работает в Visual Studio и SharpDevelop (в последнем
вы можете запускать тесты прямо в среде SharpDevelop с поддержкой про­
граммы NUnit).

Изучение класса тестирования программы NUnit
Вот что необходимо знать о классах тестирования.
Директива u s i n g и ссылка. Вам необходима директива using для NUnit.
Framework, а также ссылка (ключ /г) на файл nunit. framework. dll в подката­
логе \bin в папке программы NUnit на вашей машине; вероятно, это С:\Program
Files\NUnit.
Приспособление для теста и тесты. Программа содержит класс тестирования,
или "приспособление для теста" (test fixture), который состоит из методов тести­
рования и, возможно, дополнительных методов поддержки. Скоро вы познакоми­
тесь с методами тестирования, а на компакт-диске в демонстрационной программе
NUnitTestExample представлено несколько их разновидностей.
Демонстрационная программа представляет собой простую программу, основан­
ную на методе TrimAndPad () в демонстрационной программе AlignOutput

ie главы

Глава 22.

С#

по дешевке

551

из главы 9, "Работа со строками в С # " . Полный текст программы N U n i t T e s t E x a m p l e здесь приводиться не будет.

Рис. 22.8. Класс тестирования, загруженный в редактор TextPad, выглядит
так же, как и в Visual Studio
Атрибуты. Атрибуты — это возможность языка С#, которая больше нигде не
рассматривается в данной книге. Они своего рода "украшение", которое можно
одеть на классы, методы и другие объекты языка С# для различных целей. Класс
тестирования украшен атрибутом [TestFixture], а методы тестирования —
атрибутом [Test].
NUnit определяет разные атрибуты, используемые этой программой, чтобы про­
браться в ваш скомпилированный файл .ЕХЕ или . D L L посредством методики,
называемой отражением (эта тема также выходит за рамки данной книги). С по­
мощью "включения отражения" сборки .NET программа NUnit может определить
все классы (по атрибуту [TestFixture] !) и все методы (по атрибуту [Test])
тестирования. Затем она может просто вызывать только те методы, которые вы­
полняют ваши тесты (без реального запуска всей программы).
Вы увидите несколько других атрибутов в этой главе и в примере NUnitTestEx­
ample на компакт-диске.
Установка и очистка. Кроме методов тестирования, можно при желании опреде­
лить по одному методу с атрибутами [Setup], [TearDown], [FixtureSetUp]

и [FixtureTearDown].
Методы тестирования должны быть разработаны так, чтобы ни один тест не влиял на
любой другой тест. Каждый метод тестирования получает новую песочницу для игр.
Программа NUnit вызывает метод, который вы украсили атрибутом [ S e t u p ] , вся­
кий раз перед запуском каждого метода тестирования. Это дает возможность обес­
печить любые необходимые действия настройки. Например, вы можете использо­
вать этот метод для открытия соединения с базой данных, создания необходимого

552

Часть

VII. Дополнительные главы

объекта, инициализации массива с некоторыми тестовыми вводами и так далее.
Аналогичным методу
[Setup]
является метод
[TearDown]. Ваш метод
[TearDown], который вы должны выбрать, вызывается прямо после запуска каж­
дого метода тестирования. Используйте его для очистки после тестирования, уста­
новки ссылок объектов в null, отключения от базы данных или сети и так далее.
Программа NUnit вызывает аналогичные методы [FixtureSetUp] и [FixtureTearDown], если вы их предоставляете, один раз для каждого полного выполнения
испытаний. Метод [FixtureSetUp] запускается перед запуском любого теста, а ме­
тод [FixtureTearDown] после завершения работы всех тестов. Иногда можно
локализовать действия установки и очистки так, чтобы они не повторялись для
каждого метода тестирования, устраняя дублирование.

Написание тестовой программы NUnit
Методы тестирования программы NUnit имеют достаточно стандартную структуру,
хотя можно, конечно, проявить творческий подход и использовать их для проверки чегонибудь значительно большего. (Например, существует программа, которая проверяет,
был ли установлен определенный пиксель на экране!) Вот метод тестирования из про­
граммы NUnitTestExample, находящейся на компакт-диске.
// StringLengthsGoodTest — проверяет корректный ввод (в
// противоположность ошибочному вводу)
// Методы тестирования начинаются с "атрибута" [Test]
[Test]
public void StringLengthsGoodTest()
// всегда public void,
// без аргументов

{
Console.WriteLine("StringLengthsGoodTest:");
// Здесь выполняется настройка ввода и тому подобного
// (если вы еще не сделали этого в методе S e t u p )
stringsln = new string[] {"Joe
", "Rumpelstiltskin",
" Vanderbilt" };
// Вызов тестируемого метода
// Генерируются измененные строки
stringsOut = Program.TrimAndPad(stringsln);
// Сравнение фактических результатов вызова с ожидаемыми
// В этом тесте мы ожидаем, что все строки будут длиной 16
// символов. NUnit.Framework предоставляет класс Assert с
// несколькими методами, включая IsTrue, IsFalse и
// AreEqual — все они проверяют выполнение логических
// условий. Первый параметр метода IsTrue является
// проверяемым логическим условием, вторым параметром
// является сообщение, которое выводится NUnit, если
// логическое условие ложно
Assert.IsTrue(stringsOut[0].Length == 16,
"строкаО имеет неверную д л и н у " ) ;
Assert.IsTrue(stringsOut[1].Length == 16,
"строка1 имеет неверную д л и н у " ) ;
Assert.IsTrue(stringsOut[2].Length == 16,
"строка2 имеет неверную д л и н у " ) ;

}
Глава 22.

С#

по дешевке

553

Далее описана обычная последовательность метода тестирования в NUnit.
1. Выполняются все необходимые настройки теста (если вы еще не сделали
этого в методе с атрибутами [ S e t u p ] или [ F i x t u r e S e t U p ] ) .
2. Вызывается тестируемый метод, получаются результаты его вызова.
Если метод ничего не возвращает, вам, вероятно, придется проявить творческий
подход. Например, если метод копирует файлы из одного места в другое, вам
придется использовать язык С#, чтобы подсчитать количество файлов в источни­
ке и получателе, или сравнить все имена файлов, например, применяя методы на­
подобие описанных в главах 19, "Работа с файлами и библиотеками", и 20,
"Работа с коллекциями". Вы можете передать часть работы вспомогательным
функциям, а платформа .NET Framework предоставляет большое число классов,
которые можно использовать для помощи.
Это служит лишним подтверждением тому, что лучше использовать короткие,
простые м е т о д ы — отчасти потому, что их проще проверять. Вы можете
"разложить" большой метод на несколько меньших и протестировать их.
(Посетите Web-сайт www. ref actoring. com).
3. Выполняется фактический тест: сравнение полученных результатов с ре­
зультатами, которых вы ожидаете. Если они совпадают, тест пройден. Если
нет, то тест завершился неудачно.
Вы должны знать, чего ожидаете от функции, вводя в нее данные. Исполь­
зуйте контролируемые условия для тестирования и выполняйте тесты с
"тестирующими", а не с реальными данными.
Если тест пройден, в окне программы NUnit он будет отмечен зеленой точкой.
Если не пройден, он будет отмечен красным, и отобразится информация о том,
где произошла ошибка, и что при этом происходило. Используйте эту информа­
цию для поиска неприятностей в коде своей программы.
4. Выполните все необходимые действия по "уборке за собой".
В частности, не должны сложиться условия, которые могут влиять на последующие
тесты. Это можно сделать в методах [TearDown] или [FixtureTearDown] или
же в конце самого метода тестирования.
Иногда может потребоваться добавить один или несколько вспомогательных методов
или свойств к проверяемому классу, чтобы облегчить тестирование. Я присваиваю таким
методам доступ internal (не private и не public) и помечаю их как поддержку
тестирования в блоке #region/#endregion. Вы можете также создать в своей про­
грамме специальный класс TestSupport. Делайте методы и свойства этого класса ста­
тическими, чтобы их можно было вызывать так, как показано в следующей строке:

TestSupport.StoreSomethingForTestToCheck(something);
Дополнительные преимущества этого вызова состоят в том, что он сохраняет боль­
шую часть связанного с тестом кода вне программы и помечает элемент поддержки, не
внося беспорядка. В своем классе TestSupport вы можете предоставить член для хра­
нения данных, а также метод или свойство, которое может использоваться в тестах для
получения этих сохраненных данных.

554

Часть

VII. Дополнительные главы

Использование Assert
Механизмом для проведения фактических испытаний и передачи их результатов
в программу NUnit для отображения является класс Assert, который предоставляет
NUnit. Класс Assert имеет многочисленные методы для сравнения объектов и выпол­
нения логических проверок.
Методы класса Assert включают IsTrue (), IsFalse (), IsNull О , IsNotNull (), AreEqual (), AreSame (), Fail () и Ignore ( ) . Для получе­
ния информации о классе Assert и его методах перейдите в подкаталог \doc
в каталоге NUnit и дважды щелкните на файле Assertions.html. (Другие
расположенные там файлы содержат полезную информацию об NUnit, доступ­
ную также на Web-сайте по адресу www. nunit. org.)
В языке С# используется похожая технология Assert, в частности в классе
System.Debug.

Идея заключается в том, чтобы "сделать заявление" (или "утверждать"), что имеет ме­
сто некоторое логическое условие. Утверждение терпит неудачу, если это условие ложно,
или оно успешно, если условие истинно. В случае неудачного исхода в NUnit отображается
сообщение, которое передается в качестве последнего параметра методам Assert.
Вы можете проявить творческий подход в тестировании утверждений. Например, ес­
ли вашим тестирующим вводом является массив данных, который вы передаете тести­
руемому методу, можно поместить код утверждения внутри цикла так, чтобы утвержде­
ние выполнялось для каждого элемента массива, как в приведенном ниже фрагменте из
еще одного метода тестирования в программе NUnitTestExample.
// Подсчет количества пробелов в строке и проверка
// ожидаемого результата
for(int i = 0; i < stringsOut.Length; i++)

{
// Вспомогательный метод
int nSpaces = CountSpaces(stringsOut[i]);
// Проверка утверждения для каждого элемента массива
// stringsOut
Assert.AreEqual(nSpaces, spacesExpected[i],
"Строка " + i + " содержит неверное " +
"количество п р о б е л о в " ) ;

}
Это хороший способ — проверять сразу целый диапазон вводов в одном тестирую­
щем методе.

Запуск набора тестов NUnit
После того как вы написали свою программу и тесты для нее, необходимо запускать
тесты следующим образом.
1. Скомпилируйте свою программу.
Компиляция должна пройти без ошибок, прежде чем вы сможете запускать тесты
NUnit.
2. Запустите отдельную программу NUnit.
Глава 22.

С#

по дешевке

555

Она существует в двух вариантах — в форме консольного и графического прило­
жений. Вы, вероятно, предпочтете графическую версию.
В программе SharpDevelop можно запустить NUnit из меню Tools, и результат
будет отображаться прямо в среде SharpDevelop в окне Unit Tests.
Вы также можете загрузить инструмент TestDriven.NET (с Web-сайта www.
testdriven. net), который предоставляет похожее отображение результатов
тестирования NUnit в среде Visual Studio.
3. В

NUnit

выберите

команду

меню

File^Open

и

перейдите

к

папке

\bin\Debug или \bin\Release, где находится программа, только что
скомпилированная вами, в виде файла .ЕХЕ или .DLL. В окне открытия
файла NUnit дважды щелкните на файле . ЕХЕ или . DLL, который содержит
вашу тестируемую программу.
В NUnit будет отображен иерархический список приспособлений для тестирова­
ния (классов), а также методов тестирования в окне Test.
4. Щелкните на кнопке Run.
В NUnit запустятся тесты (в алфавитном порядке!), и серые точки перед названи­
ем каждого метода тестирования станут зелеными (успешно завершенный тест)
или красными (произошла ошибка).
5. Взгляните на вкладку Errors and Failures справа для просмотра информа­
ции о неудачных тестах.
Щелкните на записи об ошибке, чтобы просмотреть стек вызовов в панели под
методом, потерпевшим неудачу: вы увидите последовательность вызовов метода,
которая привела к неудаче; для каждого вызова приведены имя файла и номер
строки (стек вызовов описан в главе 21, "Использование интерфейса Visual
Studio"). (В NUnit отображается и другая полезная информация, такая как обыч­
ный вывод программы в консоль на вкладке Console.Output и ошибочный вывод
программы на вкладке Console.Error.)
В следующий раз, когда вы захотите выполнить тесты, перекомпилируйте свою про­
грамму и переключитесь в окно NUnit. Программа автоматически перезагрузит ваш из­
мененный код (вы также можете сделать это вручную посредством команды Reload
в меню File). Щелкните на кнопке Run.
Вы также можете щелкнуть на одном тесте в программе NUnit для запуска
только его.
На рис. 22.7,приведенном выше, показан ряд тестов программы NUnit, которые только
что были выполнены. Один из них прошел неудачно (это было сделано преднамеренно).
FailingTest отмечен более темной (красной) точкой (и, как результат, MyTestClass
и другие элементы иерархии, расположенные над ним). Индикатор выполнения под кнопкой
Run также красный. Успешно завершенные тесты отмечены светлыми (зелеными) точками.
В программе отображается также стек вызовов для неудачного теста.

556

Часть

VII. Дополнительные главы

Исправление ошибок в проверяемой программе
Не исключено, что вы могли совершить ошибки и в самих тестирующих методах, так
что любые результаты, полученные в программе NUnit, являются ошибочными незави­
симо от проверяемого метода или от метода тестирования. Б р р р . . . Кто будет следить за
наблюдателями? Когда такое происходит, было бы хорошо иметь возможность пройти
методы тестирования в отладчике, чтобы можно было проверить переменные и отсле­
дить проблему.
Имеется только одна загвоздка: вашу проверяемую программу запускает NUnit, а не
Visual Studio или TextPad. И что знает программа NUnit об отладчиках для языка С#?
Однако все же имеется способ отладить тестирующую программу. (В действительно­
сти их несколько. Вам будет показан один из них.)

Зачем присоединяться к процессу NUnit?
Каждая запущенная программа запускает в Windows процесс, отдельную об­
ласть памяти, выделенную только для данной программы. Внутри этой облас­
ти программа сохраняет свои переменные и работает практически так же, как
если бы она имела в своем распоряжении весь компьютер. Когда программы Visual
Studio и NUnit выполняются одновременно, каждая из них находится в своем собст­
венном процессе, и вы должны предоставить программе Visual Studio (или отладчику
CLR) доступ к процессу NUnit для отладки.
В следующих разделах описано, как отлаживать тесты NUnit из таких отладчиков:
отладчик Visual Studio (когда вы работаете в Visual Studio);
отладчик CLR (когда вы работаете в SharpDevelop, TextPad или из ко­
мандной строки).

Запуск тестов NUnit в отладчике Visual Studio
Выполните следующие действия для отладки своей программы тестирования, если вы
работаете в Visual Studio (в следующем разделе рассматривается работа в TextPad или
SharpDevelop, или из командной строки).
1. Скомпилируйте программу без ошибок времени компиляции.
2. Запустите NUnit и убедитесь, что загружен проект, который вы хотите отладить.
Не забывайте об этом шаге и обязательно проверьте, файл какой именно про­
граммы вы загрузили в NUnit.
3. В Visual Studio откройте файл, содержащий класс тестирования. Установите
точку останова в строке, в которой хотите остановить отладчик.
Эта строка будет в одном из ваших методов тестирования.
4. В Visual Studio выберите команду меню D e b u g s Attach to Process.
О том, зачем это делать, рассказывается во врезке "Зачем присоединяться к про­
цессу NUnit?"

5. В диалоговом окне Attach to Process найдите в списке Available Proc­
esses процесс n u n . i t - g u i . е х е (в левой колонке).

Глава 22. С# по дешевке

557

В поле Title этого "процесса" будет написано что-то вроде N U n i t T e s t E x a m p l e .
е х е — N U n i t (в зависимости от названия тестируемой программы). На рис. 22.9 по­
казано, как выглядит диалоговое окно Attach to Process с выбранным процессом
NUnit для присоединения.

Рис. 22.9. Присоединитесь к NUnit как ко внешнему процессу для
отладки программы тестирования
6. Щелкните на кнопке Attach.
7. Нажмите комбинацию клавиш для переключения в программу
NUnit. Щелкните на кнопке Run.
Отладчик остановится на строке с точкой останова и подцветит ее. Вы можете начать
пошаговое выполнение программы с этой точки.
Можно установить точки останова как в программе тестирования, так и в тес­
тируемой программе, но в тесте такая точка должна быть только одна. С нее
и начинается отладка.

Запуск тестов NUnit в отладчике CLR
Выполнение тестов NUnit в отладчике CLR очень похоже на их выполнение в отлад­
чике Visual Studio. Вам необходимо делать это, когда вы работаете с NUnit из программ
TextPad или SharpDevelop.
Для запуска программы тестирования в отладчике CLR выполните следующие действия.
1. Запустите отладчик CLR как обычно, из меню T o o l s в программе TextPad
или SharpDevelop, или при необходимости запустите его как самостоятель­
ное приложение.

558

Часть

VII. Дополнительные главы

2. Запустите NUnit (графическую или консольную версию).
3. В отладчике, как обычно, загрузите программу для отладки и соответст­
вующие ей файлы . CS.
4. Установите в отладчике точку останова в строке исполняемой программы

в одном из ваших методов тестирования.
5. Выберите команду меню D e b u g ^ P r o c e s s e s . В диалоговом окне Processes
выберите процесс n u n i t - g u i . е х е (или n u n i t - c o n s o l e . е х е ) и щелкните
на кнопке Attach. Затем — на кнопке Close.
6. Нажмите клавишу для выполнения программы с пропуском вызовов
функций или для пошагового выполнения с заходом в функции.
7. Отладчик остановится в установленной вами точке останова.
8. Продолжайте пошаговое выполнение, проверку переменных и т.д.
Этот процесс может перевести вас из испытательного кода в код вашей програм­
мы по вызову испытательным методом. Ошибка может быть в любой из данных
областей.

Прекращение отладки
Для того чтобы прекратить отладку, переключитесь в Visual Studio (или от­
ладчик CLR) и выберите команду меню DebugStop Debugging. Это при­
ведет к отсоединению от процесса NUnit, и вы сможете продолжать про­
граммирование.
В любой момент, когда вы захотите отладить тесты вашего проекта, повторите пре­
дыдущие шаги.

Одной из наиболее недостающих возможностей в TextPad является отсутствие визу­
альных средств для размещения управляющих элементов в ваших формах Windows.
Большинство читателей этой книги хотят создавать программы для Windows, и все, что
можно здесь сделать — это порекомендовать SharpDevelop. Но можно показать и еще
одну альтернативу — написание такого кода вручную.

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

Глава 22.

С#

по дешевке

559

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

Работа в стиле визуального инструмента
Написание такого же кода, как и создаваемый визуальными инструментами, упирает­
ся в знание формата.
В главе 1, "Создание вашей первой Windows-программы на С#", вы вкратце знакоми­
лись с программированием Windows Forms и создавали маленькое приложение, которое
выводило форму (окно) с двумя текстовыми полями и одной кнопкой. При щелчке на
кнопке программа копировала текст из верхнего поля в нижнее.
Если у вас нет Visual Studio, вы не сможете следовать инструкции из гла­
вы 1, "Создание вашей первой Windows-программы на С # " , так что иден­
тичный код предоставлен в демонстрационной программе ImitatingFormDesigner на прилагаемом компакт-диске. Вы можете увидеть тот же
код, что и в главе 1, "Создание вашей первой Windows-программы на С # " ,
но он написан мною самостоятельно, без визуального инструментария.
За красивыми формами, создаваемыми при помощи мыши, Visual Studio скрывает
код, который создает все эти управляющие элементы и заставляет их работать.
Visual Studio создает весь код за исключением того, что происходит при взаимодей­
ствии с пользователем. Например, когда пользователь щелкает на кнопке, выполняются
какие-то действия, и написать код для этих действий — это уже ваша забота. В описан­
ном примере действие представляет собой одну строку на С#, которая копирует текст из

textBoxl в textBox2:
textBox2.Text = textBoxl.Text;
Когда вы создаете новое приложение Windows (в отличие от консольных приложе­
ний, которые использовались во всей этой книге), Visual Studio 2005 генерирует три ис­
ходных файла. Два из них содержат часть класса Forml, который лежит в основе фор­
мы, представляющей пользовательский интерфейс вашей программы. В третьем файле
находится класс Program, содержащий функцию Main ().
Вот более детальное описание полученных вами файлов.
Forml.cs: этот файл содержит объявление частичного класса (partial class) для
класса Forml. (Что такое частичный класс, будет объяснено позже.) Это только по­
ловина класса Forml (вторая его половина находится в другом файле), но это та по­
ловина, в которой вы пишете весь код, который должен быть создан вручную.
Forml .Designer, cs: данный файл содержит вторую половину кода частично­
го класса Forml. Это часть, которую Visual Studio 2005 создает для вас.
Не трогайте этот файл. Добавляйте код только в первую половину класса Forml
в файле Forml. cs. Когда вы добавляете или модифицируете управляющий элемент
в проектировщике форм, он сам изменит код — вы не должны в это вмешиваться.
Program, cs: этот файл содержит функцию Main ().

560

Часть

VII. Дополнительные главы

В демонстрационной программе ImitatingFormDesigner я написал определен­
ный код в файле Forml. cs для того, чтобы программа выполняла некоторые действия:
обработчик щелчка buttonl копирует текст из textBoxl в t e x t B o x 2 , как и в гла­
ве 1, "Создание вашей первой Windows-программы на С#", в которой на рис. 1.10 пока­
зано, как это выглядит на экране компьютера.
Взгляните на код второй демонстрационной программы на прилагаемом компактдиске — Forms YourWay. Этот код похож на неприкасаемый код из файла
Forml .Designer, cs демонстрационной программы ImitatingFormDesigner, но
в него включены комментарии, поясняющие, что и как работает.
Главное заключается в том, что при возможности использовать проектировщик
форм Visual Studio (или его коллегу из SharpDevelop) дизайнер выполняет мас­
су работы за вас, но все, что он делает — это всего лишь код, который — само
собой, с большими затратами труда — вы можете написать и самостоятельно.

Частичные классы
Частичные классы — новая возможность С# 2.0. Идея заключается в разбиении клас­
са на два или большее количество файлов. Каждый файл содержит частичный класс
(partial class), помеченный новым ключевым словом partial:
// Файл 1
public partial class MyClass
{
// Часть класса MyClass, но не весь — остальная его часть
// находится в другом файле

}
// Файл 2
public partial class MyClass // Тот же заголовок
{
// Еще одна часть класса MyClass, но не весь он — словом,
// идея вам понятна?

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

Глава

22.

С#

по дешевке

561

Позже вы сможете перенести код из сгенерированной части в написанную вами,
но это приведет к тому, что дизайнер не сможет с ним работать. Например, если
вы перенесли весь код, касающийся некоторой кнопки, включая ее создание, в
вашу половину, то кнопка больше не будет появляться в дизайнере, хотя будет
выводиться при работе программы. Именно так вы можете добавлять визуальные
элементы в форму или модифицировать ее динамически, во время выполнения
программы. Если вы переместите только часть кода, при повторной генерации
дизайнер может восстановить ее, что приведет к дублированию и ошибкам ком­
пиляции. Такое перемещение следует выполнять очень осторожно.
Как уже упоминалось, частичные классы связаны в первую очередь с кодом дизайне­
ра форм Visual Studio, что наглядно показывает демонстрационная программа Imitat­
ingFormDesigner.

Самостоятельное написание
Чтобы посмотреть, какой код вы обязаны написать при отсутствии визуаль­
ного инструментария, взгляните на предельно упрощенную схему демонст­
рационной программы FormsYourWay на прилагаемом компакт-диске.
Комментарии в этом коде указывают, куда следует добавить код управляю­
щих элементов.
Код демонстрационной программы FormsYourWay состоит в основном из фигурных
скобок, комментариев и кода, связанного с управляющими элементами формы.
Одна из интересных особенностей этой программы состоит в том, что в ней нет фай­
ла Program, cs с функцией Main ( ) . Вместо этого функция Main () сделана методом
самого класса Forml. Данная функция может находиться в любом классе. Просто убеди­
тесь, что в программе имеется ровно одна функция Main ( ) . Кстати, взгляните на эту
функцию Main ( ) , чтобы иметь представление, как выглядит типичная функция Main ()
для графического приложения Windows.
Ознакомьтесь со сводкой элементов кода, необходимых для размещения управляю­
щих элементов в форме с помощью одного лишь кода, без применения дизайнера.
Объявить управляющие элементы (как члены-данные класса формы). Они должны
иметь классы наподобие Button, TextBox или Label из пространства имен
System.Windows.Forms.
System.Windows.Forms.Button

buttonl;

При желании можно использовать директиву using для пространства имен Sys­
tem .Windows.Forms.
Инстанцировать каждый управляющий элемент (в конструкторе формы или мето­
де, который он вызывает).
buttonl

= new System.Windows.Forms.Button()

Установить свойства каждого управляющего элемента (в конструкторе формы или
методе, который он вызывает). Вы должны поэкспериментировать, чтобы опреде­
лить корректные координаты Location и значения Size (обратите внимание,
что в примере параметр у у Size не используется, так что указано значение 0).
buttonl.Location = new System.Drawing.Point(40,
buttonl.Name = "buttonl";
562

Часть

80);

VII. Дополнительные главы

buttonl.Tablndex = 1;
buttonl.Text = "Copy";
Добавить обработчик для каждого события, которое вы хотите обработать, для
каждого из управляющих элементов (в конструкторе). Добавление обработчика
для кнопки выглядит примерно так:
buttonl.Click += new Sys­
tem. EventHandler (this . buttonl_Click) ;
В представленном примере единственное событие, требующее обработки, — это собы­
тие Click кнопки. Однако вам могут потребоваться и другие события. Аргумент
в скобках именует метод Forml, который отвечает на щелчок кнопки. В данном слу­
чае он называется buttonl_Click ().
Добавить каждый уггоавляющий элемент в коллекцию Controls (в конструкторе),
this.Controls.Add(buttonl);
Написать обработчики для всех событий, связанных с каждым управляющим эле­
ментом (в качестве методов формы; после конструктора). Метод обработчика
кнопки имеет примерно следующий вид:
private
e)

void

buttonl_Click(object

sender,System.EventArgs

{
// Некоторые действия в ответ на щелчок на кнопке
MessageBox.Show("Hello");
// Или, как в демонстрационной программе FormsYourWay:
textBox2.Text = textBoxl.Text;

}
Чрезвычайно
ет единственное
вами, результат
dows-программы
друг от друга.

простой код демонстрационной программы FormsYourWay использу­
небольшое интерактивное окно, показанное на рис. 1.10. Другими сло­
идентичен полученному в главе 1, "Создание вашей первой Win­
на С#", и ранее в этой главе. Однако сами коды несколько отличаются

Вам потребуется масса справочной информации о свойствах классов управ­
ляющих элементов, таких как Button, TextBox, ListBox и т.д., а также
о классах Form и его базовых классах. Все это можно найти в справочной сис­
теме .NET SDK.

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

Глава

22.

С#

по дешевке

563

Что, если на целевой машине не установлен .NET? Да, политика Microsoft такова, чтобы
предельно забить винчестеры пользователей всем, что только можно продать, ну а вдруг?...
Вы не Microsoft и не должны требовать от ваших пользователей установить .NET
SDK. Вместо этого вы можете распространять с вашей программой пакет .NET redistrib­
utable package D o t n e t f x . е х е . Его можно бесплатно загрузить с MSDN и распространять
с вашими программами. Сделайте установку этого пакета частью установки вашей програм­
мы, и пользовательская машина будет корректно настроена для выполнения программ
. N E T — включая все необходимые библиотеки времени выполнения, библиотеки базовых
классов и компилятор ЛТ. Если на целевом компьютере уже имеется старая версия .NET, но
не та, в которой нуждается ваша программа, данная инсталляция не затронет ее и установит
новую версию "рядом", так что они обе будут к услугам нуждающихся в них программ (само
собой, D o t n e t f x . е х е изменяется с каждой новой версией .NET).
Один из типов проектов, которые можно создавать в Visual Studio (или
SharpDevelop) — проект установки. Он создает . MS I-файл (Microsoft Installer),
на котором пользователь может дважды щелкнуть мышью для установки вашей
программы на своем компьютере. Это возможность выходит за рамки настоя­
щей книги, но документирована в .NET SDK. Однако для небольших программ
можно не создавать . MS I-файл, а просто скопировать ее на пользовательский
жесткий диск. Microsoft также предоставляет способ инсталляции Интернетприложений, написанных с использованием ASP.NET, через Интернет.

Если вы попробуете программировать в Visual Studio, вы обязательно захотите при­
обрести его.
Учтите, что наряду с запредельно дорогими профессиональными версиями
имеются недорогие (около 50 долл.) версии Visual Studio. В главе 19, "Работа
с файлами и библиотеками", упоминалась версия Visual С# Express — поду­
майте о возможности ее приобретения.
Но тем не менее все описанные в данной главе инструменты — весьма достойная за­
мена Visual Studio.

564

Часть VII. Дополнительные главы

н

к
Класс, 775
object, 273
Абстрактный, 302
Базовый, 263
Библиотека, 430
Конструктор, 244
Конструктор по умолчанию, 246
Метод, 177; 182
Обобщенный, 339
Оболочка, 348
Объект, 117
Ограничение доступа, 231
Опечатывание, 308
Определение, 116
Определение метода, 181
Определение функции, 179
Перегрузка конструкторов, 253
Свойство, 242
Статические члены, 123
Статическое свойство, 243
Функции доступа, 242
Функция-член, 141
Частичный, 561
Члены, 116
Экземпляр, 117; 181
Классификация, 227
Код ошибки, 395
Коллекция, 474
Комментарий, 57
Документирующий, 794; 499
Конструктор по умолчанию, 246

Л
Логическое сравнение, 77

м
Массив, 724
Индекс, 725
Свойство Length, 729
Фиксированного размера, 725
Метод, 182
Модуль, 235; 421

Предметный

указатель

Наследование, 230; 261
Конструктор базового класса, 276
Недетерминистическая деструкция, 287
Нулевой объект, 720

О
Область видимости, 704
Обобщенные классы, 339
Создание, 347
Объект, 777
Нулевой, 720
Объявление, 59
Окно DOS, 50
Окно документа, 38
Окно управления, 38
Округление, 67
Оператор, 73
as, 274
break, 98
continue, 98
if, 86
is, 272
Безусловного перехода, 777
Бинарный, 74
Декремента, 77
Деления по модулю, 74
Инкремента, 76
Префиксный и постфиксный, 76
Приоритеты, 74
Присваивания, 58; 75
Присваивания составной, 76
Составной логический, 79
Тернарный, 83
Умножения, 73
Отладка, 572
Пошаговое выполнение, 574
Точка останова, 577
Отношение
МОЖЕТ_ИСПОЛЬЗОВАТЬСЯ_КАК,

311
СОДЕРЖИТ, 269
ЯВЛЯЕТСЯ, 263

567

п
Перегрузка функции, 753; 253; 284
Передача аргументов в программу, 767
Переменная, 57
Инициализация, 59
Объявление, 57
Переполнение буфера, 467
Перечислитель, 462; 474
Повышение типа, 81
Позднее связывание, 292
Полиморфизм, 230; 291
Полностью квалифицированное имя, 425
Понижение типа, 82
Преобразование типов, 70
Присваивание, 59
Пробельный символ, 207
Проект, 174; 420; 494
Пространство имен, 421; 422
Объявление, 422
Пузырьковая сортировка, 755
Пустая строка, 67

Р
Работа с файлами, 434
Разделение программы, 419
Разложение классов, 300
Разложение кода, 504
Регистр, 67
Рекурсия, 290
Рефакторинг, 148; 505
Решение, 174; 420; 494

С
Сборка, 427
Сборка мусора, 727
Связанный список, 457
Символ
Пробельный, 207
Соглашения по именованию, 69
Сокращенное вычисление, 80
Сокрытие метода базового класса, 285
Специальные символы, 66
Сравнение
Чисел с плавающей точкой, 78
Ссылка, 720; 131

568

Строка, 67; 799-227
Использование switch, 205
Конкатенация, 200
Неизменность, 207
Сравнение, 207
Сравнение без учета регистра, 205
Форматирование, 272; 218
Модификаторы, 218
Форматная, 275
Структура, 327
Конструктор, 329
Предопределенные типы, 333

т
Тип
Повышение, 81
Понижение, 82
Типы с плавающей точкой, 62
Типы-значения, 67
Точка останова, 57 7

У
Уровень абстракции, 226
Усечение, 67

Ф
Файл
FileStream, 438
StreamWriter и StreamReader, 435
Проекта, 420; 494
Форма, 36
Функция, 747
Аргументы, 749
Передача по значению, 756
Передача по ссылке, 757
По умолчанию, 756
Возврат значения, 762
Вызов, 88
Перегрузка, 755; 253; 284
Функция-член, 747

ц
Целочисленные типы, 60
Цикл, 93

Предметный

указатель

do...while, 98
for, 104
foreach, 134
while, 93
Бесконечный, 203
Вложенный, 106
Счетчик, 97

щ
Шаблон консольного приложения, 47
Шаблоны программы, 33
Э
Экземпляр, 117; 227
Элемент управления, 38

Числа с плавающей точкой, 61

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

569

Научно-популярное

издание

Стефан Рэнди Дэвис, Чак Сфер
С# 2005 для "чайников"
В издании использованы карикатуры американского художника Рича Теннанта
Литературный редактор
Верстка
Художественные редакторы
Корректор

Т.Г. Сковородникова
О.В. Романенко
В.Г. Павлютин, ТА. Тараброва
Л.А. Гордиенко

Издательский дом "Вильяме"
127055, г. Москва, ул. Лесная, д. 43, стр. 1
Подписано в печать 14.11.2007. Формат 70x100/16.
Гарнитура Times. Печать офсетная.
Усл. печ. л. 27,6. Уч.-изд. л. 46,44.
Тираж 1000 экз. Заказ № 5558
Отпечатано по технологии CtP
в ОАО "Печатный двор" им. А. М. Горького
197110, Санкт-Петербург, Чкаловский пр., 15.