Вы находитесь на странице: 1из 340

Серия «Для программистов»

C#
без лишних
слов

Уильям Робисон

Москва
УДК 004.438C#
ББК 32.973.26-018.1
Р58

Робисон У.
Р58 C# без лишних слов: Пер. с англ. - М.: ДМ К Пресс. - 352 с.: ил. (Серия
«Для программистов»),

ISBN 5-94074-177-0

Язык программирования C# - одна из важнейших составных частей


платформы .NET, разработанной компанией Microsoft. В предлагаемом из­
дании содержится ясное, полное и лаконичное описание языка. На первый
взгляд кажется, что C# похож на C++ и Java, но в данной книге говорится
и о существенных различиях между ними. Приводится также полная грам­
матика языка, рассказывается о наиболее часто употребляемых классах из
библиотеки классов (BCL).
Самая интересная часть книги - это рассказ о различных приемах про­
граммирования, проиллюстрированный большим числом примеров, кото­
рые вы сможете с успехом применить в собственных программах. Основное
внимание уделяется вопросам синтаксиса и построения программ, представ­
ляющим интерес для практикующих программистов.

Authorized translation from the English language edition, entitled PURE C#, 1st edition by
ROBISON, WILLIAM, published by Pearson Education, Inc., publishing as Sams, Copyright @
2002 by Sams Publishing.

All rights reserved. No part of this book may be reproduced or transm itted in any form or by any
means, electronic or mechanical, including photocopying, recording or by any information storage
retrieval system, without permission from Pearson Education, Inc.

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

ISBN 0-672-32266-8 (англ.) Copyright © by Sams Publishing


ISBN 5-94074-177-0 (рус.) © Перевод на русский язык, оформление
ДМК Пресс
Содержание
Введение ...........................................................................................................14

Ч А С Т Ь I. О с н о в ы я з ы к а C # ................................................................. 15

Г л а в а 1. Э л е м е н т ы я з ы к а ......................................................................... 16
С труктура п р о гр а м м ы ...................................................................................16
Типы и о б ъ я в л е н и я ........................................................................................ 18
Встроенные значащие типы ......................................................................... 18
Типы классов ................................................................................................ 23
Тип и н т е р ф е й с а ..............................................................................................33
П оток у п р а в л е н и я .......................................................................................... 36
Нормальное выполнение ..............................................................................36
Д е л е ги р о в а н и е ............................................................................................... 40
Исключения .................................................................................................. 42
Н а сл е д о в а н и е ................................................................................................. 49
Н ебезопасны й к о д ......................................................................................... 53
Вызов внешних функций...............................................................................53
Написание небезопасного кода................................................................... 54
Д ирективы п р е п р о ц е с с о р а ........................................................................ 56
Р е з ю м е .............................................................................................................. 58

Г л а в а 2 . Р а б о т а с п р и л о ж е н и я м и ........................................................60
Пром ежуточны й язы к и единая среда и с п о л н е н и я ............................ 60
Промежуточный язык ................................................................................... 60
Единая среда исполнения.............................................................................61
Исполняем ы е файлы, сб орки и к о м п о н е н т ы ........................................62
Сборки...........................................................................................................62
Процедура объединения...............................................................................63
Компоненты.................................................................................................. 64
Атрибуты ком понентов и с б о р о к ...............................................................65
С редства р а з р а б о т к и ................................................................................... 68
Компилятор c sc.............................................................................................68
Управление компиляцией с помощью программы nmake............................ 71
■ ■ ■ Ill C# без лишних слов

Построение сборок с помощью программ sn и a l.........................................75


Управление сборками с помощью программы gacutil..................................79
Отладка на платф орме .N E T .......................................................................80
Отладка с помощью программы DbgCLR..................................................... 81
Структура откомпилированной сборки.........................................................84
Р е з ю м е .............................................................................................................. 86

Г л а в а 3 . Б и б л и о т е к а б а з о в ы х к л а с с о в ............................................ 87
Архитектура и п р о ф и л и ................................................................................ 87
С троки и регулярны е в ы р а ж е н и я ............................................................. 88
К о н т е й н е р ы ...................................................................................................... 92
С е р и а л и за ц и я ................................................................................................. 96
Ввод и в ы в о д ....................................................................................................98
Сетевые к о м м у н и к а ц и и ............................................................................. 102
Сокеты........................................................................................................ 103
Коммуникация с помощью сокетов........................................................... 104
Вспомогательные классы для сетевого программирования.................... 108
Р е з ю м е ............................................................................................................ 111

Г л а в а 4 . П е р е м е н н ы е и т и п ы ................................................................ 112
Простые типы д а н н ы х ................................................................................ 112
Создание и использование ...................................................................... 112
Строки и их преобразования ..................................................................... 113
Преобразование и приведение типов....................................................... 118
К л а с с ы ............................................................................................................ 120
И н те р ф е й с ы ................................................................................................... 123
С тр у кту р ы ......................................................................................................125
Перечислим ы е т и п ы ....................................................................................126
Р е з ю м е ............................................................................................................ 127

Ч А С Т Ь I I. Т е х н и к а п р о г р а м м и р о в а н и я ................................... 129

Глава 5. К л а с с ы и к о м п о н е н т ы .........................................................1зо
О пределение сущ ностей и к л а с с о в ....................................................... 130
М е т о д ы ............................................................................................................ 132
С в о й с тв а ..........................................................................................................136
Содержание Ι Ι Ι Ι · · · Η

П ространства и м е н ..................................................................................... 143


Р е з ю м е ............................................................................................................ 145

Г л а в а 6 . У п р а в л е н и е п а м я т ь ю и C # ................................................. 146
Управление памятью в каркасе .NET F ra m e w o rk ...............................146
Интерфейс IDisposable ............................................................................. 148
Чистильщики.............................................................................................. 152
Слабые ссылки.......................................................................................... 156
И спользование памяти в C # .....................................................................158
Предложения fixed и using......................................................................... 158
Эффективное управление памятью.......................................................... 159
Р е з ю м е ............................................................................................................ 160

Гл ава 7. У п р а в л е н и е п о т о к о м в ы п о л н е н и я п р о г р а м м ы 161
Потоки ............................................................................................................. 161
С и н х р о н и за ц и я ............................................................................................. 165
Д е л е г а т ы .........................................................................................................170
С о б ы ти я ........................................................................................................... 174
Р е з ю м е ............................................................................................................ 177

Г л а в а 8 . Н е б е з о п а с н ы й к о д ...................................................................178
У к а з а т е л и ........................................................................................................178
Сложности при работе с указателями ...................................................... 178
Решение..................................................................................................... 179
Память и вызов функций платформенного API ........................................ 180
Н ебезопасны е контексты ......................................................................... 183
Н ебезопасны е конструкц ии я з ы к а ......................................................... 184
Управление памятью в небезопасном к о д е ........................................187
Р е з ю м е ............................................................................................................ 189

Г л а в а 9 . М е т а д а н н ы е и о т р а ж е н и е .................................................. 190
И спользование а т р и б у т о в ........................................................................ 190
С оздание нестандартны х а т р и б у т о в .....................................................193
О тражение и динам ическое с в я з ы в а н и е .............................................196
Отражение и статически связанные элементы ........................................ 196
Динамическая загрузка и связывание...................................................... 197
Р е з ю м е ............................................................................................................204
■ ■ ■ Ill C# без лишних слов

Глава 10. К о н ф и гу р и р о в а н и е к о м п о н е н т о в
И П р и л о ж е н и й ................................................................................................ 205
Конф игурирование с б о р о к ....................................................................... 205
Уровни конфигурирования......................................................................... 205
Манипулирование конфигурационными файлами .................................... 206
Управление р е с у р с а м и ..............................................................................209
Ресурсы, не зависящие от региона.............................................................210
Ресурсы, зависящие от региона................................................................. 212
Резю ме ............................................................................................................216

Глава 11. И с п о л ь з о в а н и е S D K ............................................................ 217


Ком пиляция и ко м п о н о в к а ........................................................................ 217
Основные этапы компиляции......................................................................217
Интеграция с СОМ+ ....................................................................................222
Отладка и и н с п е к ц и я .................................................................................. 227
Развертывание со зд ан но го р е ш е н и я ................................................... 228
Р е з ю м е ............................................................................................................230

Ч а с т ь I I I . С п р а в о ч н о е р у к о в о д с т в о .......................................... 231

П р и л о ж е н и е А . Г р а м м а т и к а я з ы к а C # .......................................... 232
С труктурны е э л е м е н т ы ..............................................................................232
Ф ункциональны е э л е м е н ты ......................................................................245

П р и л о ж е н и е В. К р а т к и й с п р а в о ч н и к
ПО ОСНОВНЫМ Т И П а М ....................................................................................270
Класс A p p lic a tio n E x c e p tio n ........................................................................ 270
Класс A rg u m en tO u tO fR an ge E xce p tion ................................................... 270
Класс A rithm eticE xception ......................................................................... 271
Класс A r r a y ..................................................................................................... 271
Класс A ttrib u te ............................................................................................... 274
П еречисление A ttrib u te T a rg e ts ................................................................ 276
Класс A ttrib u te U s a g e A ttrib u te ................................................................... 277
Класс B itC o n v e rte r....................................................................................... 277
С труктура B o o le a n ....................................................................................... 278
С труктура B y te .............................................................................................. 279
С труктура C h a r.............................................................................................. 280
Содержание 9

Класс C o n s o le ............................................................................................... 282


Класс C o n v e rt................................................................................................ 283
С труктура D a te T im e .....................................................................................285
П еречисление D a yO fW e e k........................................................................ 290
Класс DBNull ..................................................................................................291
С труктура D e c im a l....................................................................................... 291
Класс D e le g a te .............................................................................................. 295
С труктура D o u b le ......................................................................................... 297
Класс E n v iro n m e n t....................................................................................... 298
П еречисление E n v iro n m e n t.S p e c ia lF o ld e r........................................... 300
Класс E v e n tA rg s ............................................................................................301
Д елегат E v e n tH a n d le r................................................................................. 301
Класс E x c e p tio n .............................................................................................301
Класс F la g s A ttrib u te .....................................................................................302
Класс G C ..........................................................................................................302
И нтерф ейс IC o m p a ra b le .............................................................................303
С труктура In t1 6 .............................................................................................303
С труктура Int32 ........................................................................................... 304
С труктура Int64 .............................................................................................306
Класс M a rs h a lB y R e fO b je c t........................................................................ 307
Класс M ath ..................................................................................................... 307
Класс M u ltic a s tD e le g a te .............................................................................310
Класс N o n S e ria liz e d A ttrib u te .....................................................................311
Класс O b je c t...................................................................................................311
Класс O b s o le te A ttrib u te ..............................................................................312
Класс O p e ra tin g S yste m ............................................................................... 312
Класс R a n d o m ............................................................................................... 313
С труктура S B y te ............................................................................................313
Класс S e ria liz a b le A ttrib u te ......................................................................... 315
С труктура S in g le ...........................................................................................315
Класс S tr in g ....................................................................................................316
Класс T h re a d S ta tic A ttrib u te .......................................................................322
С труктура T im e S p a n ....................................................................................323
Класс T im e Z o n e .............................................................................................326
П еречисление T y p e C o d e ........................................................................... 327
С труктура U ln t1 6 ...........................................................................................327
■ ■ ■ Ill C# без лишних слов

С труктура U ln t3 2 ......................................................................................... 328


С труктура U ln t6 4 ...........................................................................................329
Класс U r i..........................................................................................................331
Класс U riB u ild e r.............................................................................................334
П еречисление U riH ostN am eT ype............................................................ 335
П еречисление U riP a rtia l.............................................................................335
Класс V e rs io n ................................................................................................. 336

Предметный у ка з а те л ь .............................................................337
Марку, Нику, Брэнди
и всем сотрудникам группы компаний Enterprise Family, доказавшим,
что мы способны предвидеть и добиваться цели
Illll C# без лишних слов

Об авторе
Уильям Робисон - начальник отдела корпоративных приложений компании
Enterprise Social Investment Corporation (Колумбия, штат Мэриленд) и обладатель
сертификата MCSE. Робисон имеет четырнадцатилетний стаж проектирования
и разработки информационных систем. За это время он занимал различные ад­
министративные и технические должности на предприятиях ВВС и в частных
компаниях. Робисону довелось работать на различных платформах, включая на­
стольные ПК и рабочие станции, сервера под управлением ОС NT и UNIX, а также
большие ЭВМ фирмы IBM. В представленной книге сконцентрирован его опыт
программирования на языках C++, Java, а теперь и С#. В сферу профессиональных
интересов Робисона входят распределенные системы, моделирование, симулиро­
вание и визуализация.

Благодарности
Чтобы написать книгу, недостаточно иметь опыт работы и ввести текст. Сна­
чала следует подыскать интересную тему, а платформа .NET - это именно то, что
нужно. Сотрудники компании Microsoft неплохо потрудились, и я благодарен им
за то, что они нашли время поделиться своими идеями. Отдельное спасибо Конни
Салливан (Connie Sullivan) за помощь в работе над этой книгой (и за восхититель­
ные пикники в Сиэттле!). Я желаю ей всего самого наилучшего.
За помощь в доведении этой работы до логического завершения я благодарю
Нейла Роуи (Neil Rowe). Не могу также не отметить усилия Сьюзен Хоббс (Susan
Hobbs), Барбары Хача (Barbara Hacha), Маттиаса Сьегрена (M attias Sjogren
и Джорджа Недефа (George Nedeff) по исключению из текста всего лишнего. Спа­
сибо всем вам - работать с вами было истинным удовольствием.
Однако я не смог бы написать эту книгу без поддержки своих родных и друзей.
Особо хочу поблагодарить Брэнди Спицер (Brandi Spitzer) за то, что она помог­
ла мне уложиться в график и мирилась с тем, что я несколько месяцев провел,
уединившись в своем кабинете. Не будь ее, книга никогда не увидела бы света.
И, наконец, мои благодарности Деби, Терезе, Джиму, Эду и Хэлен. Я очень
ценю вашу поддержку.
Об авторе І) Ц Н І 13

Сообщите нам ваше мнение


Вы - читатель этой книги - наш самый главный критик и рецензент. Мы
ценим ваше мнение и хотим знать, что сделано правильно, что можно было бы
сделать лучше, по каким темам стоило бы напечатать другие книги. Сообщите,
что вам понравилось, а что не понравилось в этой книге, и что, на ваш взгляд,
следует предпринять, чтобы наши издания стали лучше. Издательства «ДМК
Пресс» и «Sams» ждут ваших комментариев. Вы можете отправить их по факсу,
по электронной или обычной почте. В своем письме не забудьте, пожалуйста,
указать название и авторов книги, а также ваше имя и почтовый адрес. Мы
внимательно изучим все замечания и передадим их авторам и редакторам, рабо­
тавшим над книгой.

E-mail (Sams): feedback@quepublishing.com


E-mail (ДМ К Пресс): editor-in-chief@dmkpress.ru
Введение
Здравствуйте. Купив эту книгу, вы открываете окно в будущее программирования
на платформе Microsoft. Язык C# - это составная часть семейства технологий под
общим названием «платформа .NET», на базе которых Microsoft предлагает строить
приложения нового поколения. Трудно отрицать, что описанная технология являет­
ся мощной и устремленной в будущее, а язык C# - ее неотъемлемая часть.
Книга состоит из трех частей. Часть I представляет собой компактное изложе­
ние концепций самого языка. Хотя C# напоминает языки C++ и Java, его внутрен­
нее устройство существенно отличается. Здесь вы узнаете, чем именно.
Часть II - как раз то, ради чего написана книга. Здесь приведено множество
примеров, иллюстрирующих различные приемы программирования на языке С#.
В главах 5-11 вы сможете найти фрагменты кода, показывающие, как можно ре­
шить стоящую перед вами проблему
Часть III содержит некоторые справочные материалы. В приложениях А и В
дана формальная грамматика языка и приведено описание наиболее часто исполь­
зуемых классов из библиотеки базовых классов (Base Class Library).
Настоящая книга не ставит целью научить вас создавать программы для плат­
формы .NET, и уж тем более это не учебник по C# для программистов на Java.
Я предполагаю, что вы умеете программировать на каком-то другом языке и знако­
мы с базовыми понятиями, поэтому можете сразу приступить к освоению нового
материала. Конечно, я остановлюсь на некоторых аспектах платформы .NET, пос­
кольку именно для нее и разработан изучаемый язык, но, если потребуется сделать
выбор между более подробным рассказом о .NET или о С#, я предпочту С#.
Разумеется, я надеюсь, что вы читаете эти строки, уже купив книгу. Если так,
спасибо вам за покупку! Если же вы сейчас стоите в книжном магазине и пытае­
тесь выбрать книгу по языку С#, надеюсь, что вы остановите свой выбор именно
на этой. Более компактного, очищенного от словесной шелухи и полезного спра­
вочника по C# вам все равно не найти.
Билл Робисон,
осень 2001 года
Часть
Основы языка C#
Гл ава Л . Элементы языка
Глава 2 . Работа с приложениями
Гл ава 3 . Библиотека базовых классов
Гл ава 4 . Переменные и типы
Язык C# - это результат критического пересмотра и расширения языка C++. Но
даже опытному программисту на C++ предстоит многому научиться, прежде чем
он сможет с той же продуктивностью работать на С#. В части I изложены основ­
ные сведения, необходимые для программирования на языке С#. В главе 1 описан
синтаксис языка и его основные конструкции. В главе 2 приведены программы,
поставляемые в составе .NET Framework SDK, с помощью которых вы можете
откомпилировать и связать программы и библиотеки. В главе 3 представлен об­
зор библиотеки времени исполнения, которой вы можете пользоваться в своих
приложениях.

Глава 1. Элементы языка


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

Структура программы
Синтаксис языка C# сильно напоминает C++. Начнем с базовой структуры
программы. В листинге 1.1 приведен простой пример программы на языке С#,
которая печатает текстовое сообщение.
Листинг 1.1. Первое знакомство с C#
using System;

/// <summary>
III Демонстрация структуры простейшей программы на С#.
Ill </summary>
class SimpleStart
{
static void Main(string[] args)
Структура программы н и ш

9
10 // Вывести текст на экран.
11 Console.WriteLine("Это совсем простая программа.\п");
12
13
Код на C# представляет собой последовательность предложений, разделя­
емых точкой с запятой. Некоторые предложения могут содержать внутри себя
другие предложения при условии, что они заключены в фигурные скобки. При­
мером может служить предложение class SimpleStart {} в листинге 1.1. Как
правило, предложения записываются в свободном формате, то есть пробелы не
принимаются во внимание, но внутри ключевого слова, идентификатора и других
подобных элементов языка пробелы недопустимы. Так, следующие конструкции
корректны:
int X = 4444;
string у = "This is a string.";
а вот такие - уже нет:
int X = 44 44; // Пробел внутри лексемы недопустим.
string у = "This is // Символ перевода строки внутри
a string."; // строковой константы тоже недопустим.
Из этого примера видно, что комментарий может начинаться двумя символами
косой черты ( / / ). Можно также использовать традиционную для языка С форму
( / * комментарий * / ) или три идущих подряд символа косой черты для выде­
ления фрагментов документации в формате XML.
Предложения могут быть декларативными; так, предложение c l a s s в строке 6
листинга 11 объявляет элемент программы. Предложения могут также быть импе­
ративными, то есть осуществлять некоторое действие во время выполнения про­
граммы; примером служит предложение Console. WriteLine ( . . . ) в стр о к е П .
В языке C# применяется концепция пространства имен для организации опре­
делений символов. Любой элемент программы, на который вы ссылаетесь, должен
быть объявлен либо в пространстве имен, где находится ссылка, либо в пространс­
тве имен, импортированном с помощью предложения using, либо ссылку нужно
полностью квалифицировать. В последних двух случаях пространство имен долж­
но быть или частью вашей программы, или принадлежать сборке, которая стала
доступна вашей программе в результате процедуры объединения (fusion), выпол­
няемой каркасом .NET Framework. Поскольку в этом примере в строке 11 приме­
няется объект System.Console, то в самом начале программы импортируется
пространство имен System, принадлежащее единой среде исполнения (CLR -
Common Language Runtime), которая находится в глобальном кэше сборок.
C# отличается от C++ тем, что все переменные, функции и другие элементы
программы объявляются в каком-то классе. Никаких глобальных констант, опе­
режающих объявлений функций и других подобных конструкций не существует.
В C# отсутствуют также заголовочные файлы, поскольку в среде .NET они не
нужны.
Illll Элементы языка

Примечание Утверждение о том, что не существует глобальных констант и


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

Типы и объявления
В языке C# есть две разновидности типов: значащие и ссылочные. Ссылочные
типы описывают объекты, их экземпляры размещаются в куче. Значащие типы
предназначены для оптимизации простых типов, таких как целые числа и числа
с плавающей точкой; их экземпляры хранятся в стеке, к ним не применяются
процедуры инициализации и уничтожения. Однако с экземпляром любого знача­
щего типа можно работать как с объектом, что достигается за счет так называемой
процедуры обертывания (boxing).

Встроенные значащие типы


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

Таблица 1.1. Значащие типы в языке C#


Ключевое слово Значения Тип в среде исполнения
sbyte Знаковое 8-разрядное целое SByte
byte Беззнаковое 8-разрядное целое Byte
short Знаковое 16-разрядное целое Int16
ushort Беззнаковое 16-разрядное целое Ulnt16
int Знаковое 32-разрядное целое Int32
uint Беззнаковое 32-разрядное целое Ulnt32
long Знаковое 64-разрядное целое Int64
ulong Беззнаковое 64-разрядное целое Ulnt64
float 32-разрядное с плавающей точкой Single
double 64-разрядное с плавающей точкой Double
decimal 128-разрядное с плавающей точкой Object (Decimal)
bool Булевское Boolean
char Широкий символ Char
Типы и объявления I I I · · ·

Таблица 1.1. Значащие типы в языке C# (окончание)


Ключевое слово Значения Тип в среде исполнения
enum Определяется пользователем Int32
struct Определяется пользователем Переменный,
по умолчанию Int32

Встроенные операторы
В табл. 1.2 перечислены в порядке убывания приоритета операторы языка С#,
применимые к значащим типам. Каждый знакомый с языком C++ не встретит
никаких трудностей в применении операторов С#. Правда, бросается в глаза от­
сутствие операторов для работы с указателями (*, ->) и области действия класса
(: :). В C# есть только оператор «точка» ( . ) для выбора члена, причем неважно,
принадлежит ли член к значащему типу, ссылочному типу или является статичес­
ким. Ясность программы от этого несколько пострадала, но количество ошибок,
особенно допускаемых программистами, которые только начинают знакомство
с языком, должно уменьшиться.

Таблица 1.2. Операторы языка C#


Тип Оператор Действие
Первичные Выбор члена (например, myObj.member)
[] Индекс элемента массива или индексатора
о Вызов функции (например,
MyFunc( aParam ))
а+ +,а— Постинкремент/постдекремент
new Выделение памяти
typeof Определение типа во время выполнения
(un)checked Включение (выключение) контроля
границы массива
Унарные +,- Знак
I Булевское отрицание (NOT)
- Поразрядная операция НЕ
++a, —a Прединкремент/предекремент
(Typename)a Явное приведение типа (например, (int)f)
Мультипликативные *, / Умножение, деление
Όo, Деление по модулю
Аддитивные +, - Сложение,вычитание
Сдвига <<, >> Поразрядный сдвиг влево, вправо
Условные <, >, < =,>= Меньше, больше, меньше или равно,
больше или равно
is Определение типа во время выполнения
as Безопасное приведение типа
Сравнения 5 !- Проверка на равенство
Illll Элементы языка

Таблица 1.2. Операторы языка C# (окончание)


Тип Оператор Действие
Поразрядное И Бит результата установлен, если
установлены соответствующие биты
обоих операндов
Поразрядное Бит результата установлен, если
Исключающее ИЛИ установлен соответствующий бит ровно
в одном из операндов
Поразрядное ИЛИ Бит результата установлен, если установ­
лен соответствующий бит хотя бы в одном
из операндов
Булевское И 8с 8с Результат принимает значение «истина»,
если оба операнда истинны
Булевское ИЛИ II Результат принимает значение «истина»,
если хотя бы один операнд принимает
значение «истина»
Булевский выбор Выбрать одно из двух выражений
в зависимости от значения булевского
выражения, например:
BoolExp ? trueAction() :
falseAction()
Присваивание Присвоить правую часть левой части
=> /= Умножить (разделить) левую часть на правую
часть и присвоить результат левой части
Разделить по модулю и присвоить
Сложить (вычесть) и присвоить
<<=,>>= Сдвинуть влево (вправо) и присвоить
&=> л=> I Выполнить поразрядную операцию
и присвоить

Операторы можно применять к различным объектам в С#, но иногда это прос­


то не имеет смысла. Так, поразрядный сдвиг неприменим к строкам. Кроме того,
вы можете и самостоятельно определять необходимые операторы. Поскольку язык
C# сильно типизирован, то операнды любого оператора должны удовлетворять
правилам полиморфизма. Иными словами, если вы, скажем, присваиваете один
объект другому, как, например:
а = Ь;
то b должен принадлежать тому же типу, что и а, или производному от него, либо
должно существовать неявное преобразование, которое из объекта типа b создает
объект типа а.

Работа с переменными
Порядок объявления и использования переменных такой же, как в других язы ­
ках. Вы объявляете экземпляр типа, указывая имя типа, за которым идет имя пере­
менной. За именем переменной может следовать необязательный инициализатор:
int mylntVar = 5;
Типы и объявления Ι Ι ΙΙ · · · · Ε 0

При объявлении массива используются квадратные скобки (оператор взятия


индекса в С#):
int[] myArrayVal = new int[] { 1, 2, 3, 4 };
В этих примерах переменная одновременно объявляется и инициализируется.
При инициализации массива можно опускать часть new typename:
int[] myArray = { 1, 2, 3, 4, 5 };
Инициализировать массив во время объявления необязательно, но, если ини­
циализатор опущен, необходимо провести инициализацию где-то в другом месте
до первого использования, например:
int mylntVar;
int[] myIntArr;

mylntVar = new int(5); // Создать переменную типа int


II с начальным значением 5.
mylntArr = new int[25]; // Создать массив и обнулить его
// элементы.
В этом примере для создания экземпляров соответствующих типов (int и мас­
сива из 25 элементов типа int) я использовал оператор new.
Какой бы способ инициализации вы ни выбрали, переменная должна быть
инициализирована до первого использования. Microsoft называет это требование
«позитивной инициализацией» и почти во всех случаях расстается с практикой
неявной инициализации переменных нулевым значением. Единственное исклю­
чение составляют массивы. При создании нового массива все его элементы ини­
циализируются значением по умолчанию для значащих типов и значением null
для ссылочных.

Ключевые слова struct и enum


Структуры (struct) и перечисления (enum) - это довольно необычные пред­
ставители значащих типов. Перечисления объявляются с помощью ключевого
слова enum и применяются, главным образом, для присвоения символических
имен константам. Но пользоваться перечисляемыми типами не так просто, как
хотелось бы. Рассмотрим, например, такое объявление:
enum Direction { Up, Down, Left, Right }
Здесь объявляются целочисленные символы Up, Down, Left и Right, которые
призваны упростить восприятие кода. Имея такое объявление, можно объявить и
использовать переменную типа перечисления:
Direction steerDirection = Direction.Up;
Другой пример определяемого пользователем значащего типа - это структу­
ра struct. Структура - это компактный тип с низкими накладными расходами,
призванный сгруппировать небольшое число взаимосвязанных полей. По способу
Illll Элементы языка

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


ми типами и могут служить для оптимизации хранения объектов, не нуждающихся
в поддержке, которая предоставляется ссылочным типам. Следующий пример
демонстрирует объявление структуры:
public struct Vertex
{
public int X, у, z;
public Vertex( int newX, int newY, int newZ )
{
x = newX;
у = newY;
z = newZ;
}
}
Здесь объявляется структура Vertex (вершина) с тремя членами и конс­
труктором. Экземпляры этой структуры будут трактоваться как значащие типы
и размещаться в стеке. Но, в отличие от других значащих типов, можно объявить
переменную типа структуры без использования оператора new. Вполне достаточно
просто поименовать переменную. С другой стороны, структуры напоминают ссы­
лочные типы в том отношении, что могут содержать конструктор, который можно
вызвать при создании экземпляра структуры.

Примечание Конструктор - это особый метод типа, который автоматически


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

В следующих примерах продемонстрированы оба способа создания экзем­


пляра структуры Vertex:
Vertex vl( 1, 1, 1 ); // Инициализация с явным вызовом
// конструктора.
Vertex v2; // Инициализация по умолчанию.
В объявлении переменной v l явно вызывается описанный в объявлении
конструктор, который инициализирует поля структуры заданными значениями.
Второе объявление полагается на умение компилятора генерировать конструктор
по умолчанию (то есть без параметров), который обнуляет все поля структуры. В
обоих случаях переменные инициализируются сразу после объявления. Кстати
говоря, конструктор по умолчанию нельзя переопределить; при попытке объ­
явить в программе конструктор без параметров для структуры компилятор выдаст
ошибку.
Синтаксис доступа к членам структуры прост: имя переменной, точка, имя чле­
на. Так, для доступа к членам переменной myVar, принадлежащей к типу Vertex,
следовало бы написать myVar .x, myVar .у, myVar .z.
Я уже говорил выше, что со значащими типами можно работать, как с объек­
тами, пользуясь механизмом обертывания. Логически значащие типы и являются
объектами класса, производного от System.Ob ject. Но если вы явно не попроси­
Типы и объявления Ι Ι Ι Ι Μ Η Β

те, то дополнительная структура, необходимая для проявления их объектной при­


роды, не создается - хранится только значение. Тем не менее каждому простому
значащему типу соответствует некоторый класс. При использовании любого ме­
тода класса, например при вызове метода ToString () для переменной типа int
с целью получить ее строковое представление, C# обертывает эту переменную,
то есть создает объект соответствующего класса, инициализирует его значением
переменной и вызывает метод объекта. Такое «своевременное» создание объект­
ных оберток позволяет существенно снизить издержки по сравнению с чистым
объектно-ориентированным подходом - объект создается лишь тогда, когда он
действительно нужен.

Типы классов
Термин ссылочный тип в языке C# обычно применяется к классу, то есть к
определению типа, экземпляр которого программа может создать в виде объекта.
Как правило, в документации термины тип и класс взаимозаменяемы, и употреб­
ление одного вместо другого не искажает смысла. Это связано с тем, что в .NET, а
стало быть, и в C# практически все представляется классами. Поэтому я начну с
рассмотрения объявлений классов.
В объявлении класса могут быть следующие элементы:
□ атрибуты;
□ модификатор сокрытия членов (только для вложенных классов);
□ модификаторы видимости (public, protected, private, internal);
□ модификаторы наследования (sealed или abstract);
□ имя типа;
□ базовые классы и интерфейсы;
□ переменные-члены (в C# они называются полями);
□ константы;
□ свойства;
□ события;
□ операторы;
□ индексаторы;
□ конструкторы и деструкторы;
□ функции-члены (в C# они называются методами).
Я не стану давать формальное определение того, что такое объявление (вы
можете познакомиться с ним в приложении А), а просто приведу пример. По ходу
изложения я буду ссылаться на листинг 1.2.
Л истинг 1.2. Пример объявления в C#
[Obsolete("Воспользуйтесь чем-нибудь другим")]
sealed class MyClass : Object, IDisposable
{
private int myField =5; // Закрытая инициализированная
переменная.
private int[] myArray; // Закрытый массив,

public const int myConst = 30; // Открытая константа.


Illll Элементы языка

9 protected int Multiply! int param ) Защищенный метод.


10
11 return myField * param;
12
13
14 public void Dispose!) // Открытый метод.
15
16 myArray = null;
17 GC.SuppressFinalize(this);
18
19
20 public MyClass() Конструктор.
21
22 myArray = new int[25];
23
24
25 MyClass() Деструктор,
26
27 if ( myArray != null )
28 myArray = null;
29
30 public int Field Открытое свойство.
31
32 set { myField = value; }
33 get { return myField; }
34
35
36 public int this[int ind] // Открытый
// индексатор.
37
38 get { return myArray[ind]; }
39 set { myArray[ind] = value; }
40
41
42 }

Объявление класса
Объявление класса включает (в указанном порядке): атрибуты, модифика­
торы, ключевое слово class, имя типа, список базовых классов и тело класса.
Ключевое слово c l a s s , имя типа и тело обязательны, остальные элементы могут
отсутствовать. В листинге 1.2 представлены все элементы, а их назначение объ­
ясняется ниже.
В строке 1 демонстрируется использование атрибута класса. Атрибут - это
модификатор объявления, который обычно действует на конструкцию, следую­
щую непосредственно за ним. Атрибуты заключаются в квадратные скобки (я упо­
требил атрибут Obsolete, чтобы пометить элемент, которым больше не следует
пользоваться в программе). Подробнее атрибуты обсуждаются в главе 9.
Типы и объявления

В строке 2 начинается объявление собственно класса. Оно содержит ключевое


слово class, за которым следует имя класса. При этом в объявлении присутс­
твует ключевое слово sealed, означающее, что этому классу нельзя наследовать.
Вместо него можно употребить слово abstract, которое говорит, что у класса
обязательно должны быть подклассы. Очевидно, что к одному классу нельзя при­
менить оба модификатора.
Для обозначения факта наследования и реализации интерфейсов после имени
класса стоит двоеточие, а за ним идет список разделенных запятыми имен базовых
классов (нуль или одно) и интерфейсов (нуль или более). В данном случае класс
MyClass наследует классу Object и реализует интерфейс IDisposable (все
типы неявно являются производными от Obj ect, но для демонстрации синтак­
сиса хватит и этого). В отличие от языка Java, ключевое слово implements не
нужно.
Если один класс наследует другому, то он получает в свое распоряжение все
члены родительского класса. Смысл же реализации интерфейса в том, что про­
грамма обещает реализовать все члены, объявленные в интерфейсе, с сохранением
семантики. Это напоминает модель контракта в СОМ, только СОМ разрешает
реализовывать лишь члены-функции, а интерфейсы C# могут содержать любую
комбинацию методов, свойств, индексаторов и событий. C# допускает множес­
твенное наследование интерфейсов. Недостатки, присущие множественному
наследованию реализаций классов, широко известны, но такие библиотеки, как
C++ Standard Template Library, убедительно продемонстрировали ценность этого
механизма для спецификации интерфейсов.
В оставшейся части объявления класса представлены примеры большинства
встречающихся членов. Объявление каждого члена начинается с модификатора
доступа, который говорит о том, в каких частях программы этот член можно ис­
пользовать. В табл. 1.3 перечислены все модификаторы доступа.
Таблица 1.3. Модификаторы доступа в C#

Модификатор Доступен Применим к членам


public Везде class или struct
protected Только в этом классе и в его подклассах class
private Только в этом классе class или struct
internal Только в этом проекте class или struct
protected internal Только в этом классе и в его подклассах class или struct

Если для некоторого члена не указан модификатор доступа, то по умолчанию


член считается закрытым (private).

Поля, имеющие значения


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

В отсутствие модификатора хранения поле считается полем экземпляра, то


есть для каждого объекта класса создается отдельная копия поля. Но можно вос­
пользоваться модификатором static для создания поля класса - тогда все объ­
екты класса будут пользоваться единственной копией этого поля, возможно в
сочетании с модификатором readonly, который запрещает изменять значение
после инициализации.
В строке 5 объявляется неинициализированный массив. Как и для простых
полей, инициализация во время объявления допускается, но не является обяза­
тельной. Как обычно, массив необходимо инициализировать до первого исполь­
зования, что я и сделал в конструкторе класса (строка 22).
В строке 7 демонстрируется объявление константного поля с помощью мо­
дификатора const. Так объявляются поля, обычно открытые, которые содержат
неизменяемые величины, например константу «пи» или постоянную Планка. Для
константных полей обязательно должен присутствовать инициализатор, так как
язык запрещает их модификацию после объявления.

Методы
В строках с 9 по 18 листинга 1.2 объявлены два метода. Метод Multiply ()
защищенный (protected) и, значит, может применяться только в этом классе
и его подклассах. Метод Dispose () открытый (public), он доступен в любой
точке программы. На самом деле метод Dispose () - это реализация интерфейса
IDisposable, тем самым контракт, «подписанный» в объявлении класса, оказы­
вается выполненным.
Общая форма объявления метода такая же, как в большинстве языков, произ­
водных от С и C++: объявление состоит из типа возвращаемого значения, имени
метода, списка параметров и блока кода, представляющего собой тело метода. Этот
код исполняется, когда программа обращается к данному методу
В объявлении метода могут употребляться модификаторы static, virtual,
abstract и override. Модификатор static говорит, что метод имеет область
действия класса, то есть не связан ни с каким конкретным экземпляром. Такой
метод не может обращаться к членам экземпляра. Если в объявлении метода есть
модификатор virtual, то реализуются механизмы, необходимые для того, чтобы
в производных классах его можно было заместить (override). Однако сам вир­
туальный метод не требует наличия ключевого слова override. Модификатор
abstract говорит о том, что метод обязательно следует заместить в подклассах;
в таком случае у метода не должно быть тела. Подробнее об этих модификаторах
речь пойдет в разделе «Наследование» в этой главе.
Методы могут принимать нуль или более параметров, каждый из которых
может быть значением, ссылкой или выходным параметром, что обозначается
соответствующими модификаторами. Параметр без модификатора передается
по значению, в этом случае метод получает копию значения, хранящегося в вы­
зывающей программе. Хотя метод может изменить значение такого параметра,
вызывающая программа этого изменения не увидит.
Типы и объявления Н ІМ І 27

Ссылочный параметр обозначается модификатором r e f - как в объявлении,


так и при вызове:

// Объявление метода со ссылочным параметром,


public void DoSomething( ref int a ) {}

// Вызов метода с параметром myVar.


mcVar.DoSomething( ref int myVar );

Ссылочный параметр не копируется при вызове метода, и вызывающая про­


грамма видит все изменения, произведенные над ним в теле вызванного метода.
Поскольку незнание этого может привести к неожиданным побочным эффектам,
язык требует, чтобы ключевое слово r e f присутствовало не только в объявлении
функции, но и при ее вызове. Тем самым вы подтверждаете, что знаете о возмож­
ных последствиях. Переменная, передаваемая по ссылке, должна быть инициали­
зирована перед вызовом метода.
Выходные параметры в некотором смысле противоположны ссылочным. Ссы­
лочный параметр может быть модифицирован внутри метода, но для передачи
параметра по ссылке существует и много других причин. Напротив, выходной па­
раметр должен быть инициализирован методом, в который он передан. Выходные
параметры - это способ передать из метода в вызывающую программу не только
возвращаемое значение, но и иную информацию. Единственный недостаток состо­
ит в том, что нельзя выйти из метода, не инициализировав каждый выходной пара­
метр; вызывающая программа должна быть уверена, что после возврата из метода
каждому выходному параметру присвоено значение. Для обозначения выходного
параметра применяется ключевое слово o u t.
Методы можно перегружать, то есть объявлять разные методы с одним и тем же
именем. Однако у каждого такого метода должны быть различающиеся списки пара­
метров (имя метода, число и типы параметров в совокупности составляют его сигна­
туру). Модификаторы и тип возвращаемого значения не принимаются во внимание
при решении вопроса о том, какой метод должен быть вызван, - только сигнатура
имеет значение. Компилятор определяет, какой метод вызвать, сравнивая типы
переданных параметров с сигнатурами перегруженных методов. Будет вызван
тот метод, сигнатура которого лучше всего соответствует типам параметров. C#
запрещает объявлять методы с одинаковым названием и одинаковыми сигнатурами.

Конструкторы и деструкторы
Вы можете определить две группы специальных методов: конструкторы и
деструкторы. Работают они как обычно. Конструктор вызывается при создании
экземпляра класса еще до того, как станет возможен доступ к членам класса. Де­
структор же вызывается при уничтожении экземпляра.
В классе могут быть конструкторы класса и конструкторы экземпляра. Конс­
труктор класса, обозначаемый модификатором s t a t i c , выполняется до того, как
28 ■ ■ ■ Ill Элементы языка

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


экземпляра класса. Поэтому конструктор класса полезен для инициализации не­
константных статических членов. Что касается конструктора экземпляра, то он
используется для инициализации полей каждого создаваемого экземпляра класса.
В классе может быть несколько перегруженных конструкторов экземпляра
с различными списками параметров. К ним применяются те же правила разре­
шения и уникальности, что и к любому перегруженному методу. Программисты
часто реализуют несколько конструкторов, чтобы пользователи класса могли
не передавать лишние параметры при создании объекта; конструктор в этом
случае подставит разумные значения по умолчанию для полей, которые не были
инициализированы явно. В листинге 1.3 приведен пример такого использования
перегруженных конструкторов.
Листинг 1.3. Конструкторы с увеличивающейся степенью подробности
class Simple
{
private int myA;
private string myS;
public const int defaultA = 0;
public const string defaults = "S";

public SimpleStart() : this(defaultA, defaults) {}


public SimpleStart( int a ) : this(a, defaults) {}
public SimpleStart( int a, string s )
{
myA = a ;
myS = s ;
}

} // Конец класса Simple.


Здесь представлено три конструктора. Первому вообще не передаются пара­
метры, второму передается один параметр, а третьему - два. При желании можно
было бы еще добавить конструктор, который принимает только один строковый
параметр. Такой подход обеспечивает необходимую пользователям гибкость, но
при этом гарантирует, что созданный экземпляр класса всегда имеет известное
состояние, а код инициализации сосредоточен в одном месте. Заметим, однако,
что в отличие от C++ из одного конструктора нельзя напрямую вызвать другой,
обратиться к нему можно лишь посредством списка инициализации в заголовке
конструктора.

Объявление операторов
В классе можно объявлять собственные операторы; это не более, чем еще один
частный случай метода, позволяющий подстраивать семантику операторов под
нужды вашего класса. Такой прием удобен в математических программах (класси­
ческий пример - реализация классов векторов и матриц). Как и в случае методов,
переопределение уже существующих операторов называется перегрузкой.
Типы и объявления ΙΙΙΗ Ι
Большинство операторов являются либо унарными, либо бинарными1 в зави­
симости от того, сколько у них операндов - один или два. Например, в предложе­
нии X = -у ; оператор = бинарный, а оператор - (вычисление противоположного
числа) - унарный (действует только на переменную у). Операторы, которые мож­
но перегружать, перечислены в табл. 1.4.

Таблица 1.4. Перегружаемые операторы в C#


Оператор Вид Естественная семантика
Унарный Унарный плюс и минус (вычисление
противоположного числа)
Унарный Инкремент, декремент (заметим, что в C# нельзя
по-разному перегружать префиксные
и постфиксные операции инкремента
и декремента)
Унарный Логическое отрицание
Унарный Поразрядное отрицание
true, false Унарный Зависит от приложения; требует наличия
обратного оператора
Бинарный Сложение, вычитание
Бинарный Умножение, деление
Бинарный Деление по модулю
<<, >> Бинарный Поразрядный сдвиг влево, вправо
<. >. <=. >= Бинарный Меньше, больше, меньше или равно, больше
или равно; требуют наличия обратного оператора
Бинарный Проверка на равенство, неравенство; требуют
наличия обратного оператора
Бинарный Поразрядное И
Бинарный Поразрядное Исключающее ИЛИ
Бинарный Поразрядное ИЛИ

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


операторы (&& и I I ), но можно перегрузить операторы, на которых они основаны.
Например, оператор «сложить и присвоить» (+=) зависит от оператора +, так что
на семантику += можно повлиять путем реализации оператора +.
Помимо традиционных операторов, вы можете определить операторы преобра­
зования для приведения одного типа к другому. Операторы преобразования могут
быть явными (explicit) или неявными (implicit) в зависимости от потребностей.
На перегрузку операторов налагаются некоторые ограничения. Во-первых,
объявления операторов должны быть статическими и принимать либо один па­
раметр (для унарных операторов), либо два (для бинарных). Оператор преобразо­
вания должен принимать ровно один параметр типа класса и возвращать значение
того типа, в который производится преобразование. Нельзя определять операторы

1 Иногда в литературе унарные операторы называют одноместными, а бинарные - двумест­


ными. - Прим. перев.
Illll Элементы языка

для предопределенных типов, хотя разрешается иметь бинарный оператор, в ко­


тором предопределенный тип имеет только один операнд. Операторы сравнения
должны определяться парами; если вы определили operator >, то должны опре­
делить и operator < (операторы, для которых необходимо задать пару, помечены
в табл. 1.4 фразой «требует наличия обратного оператора»). Наконец, если вы
определяете операторы проверки на равенство (= - и ! -), то должны также пере­
определить методы Obj ect.Equals () и Obj ect.GetHashCode (), иначе ваш
класс не будет согласован с правилами, диктуемыми системой типов.
В листинге 1.4 приведены примеры трех видов операторов.
Листинг 1.4. Примеры определения операторов в C#
1 public class Cpplnt32
2 {
3 public int val;
4 public Cpplnt32( int і
5 {
6 val = i;
7 }
8 public Cpplnt32()
9 {
10 val = 0;
11 }
12 public static bool operator true( Cpplnt32 і )
13 {
14 if ( і .val != 0 )
15 return true;
16 else
17 return false;
18 }
19 public static bool operator false( Cpplnt32 і )
20 {
21 if ( і .val = = 0 )
22 return true;
23 else
24 return false;
25 }
26 public static Cpplnt32 operator -( Cpplnt32 ci )
27 {
28 return Cpplnt32( -ci.val );
29 }
30 public static Cpplnt32 operator +( Cpplnt32 ci, int і )
31 {
32 return ci + i;
33 }
34 public static Cpplnt32 operator *( Cpplnt32 ci, int і )
35 {
36 return ci * i;
37 }
Типы и объявления I I I · · ·

38 public static bool operator ==( Cpplnt32 ciA, Cpplnt32 ciB )


39
40 if ( ciA.val == ciB.val )
41 return true;
42 else
43 return false;
44
45 public static bool operator !=( Cpplnt32 ciA, Cpplnt32 ciB )
46
47 if ( ciA.val != ciB.val )
48 return true;
49 else
50 return false;
51
52 public static implicit operator Cpplnt32( int і )
53
54 return new Cpplnt3 2 (і);
55
56
Здесь реализован класс, обладающий тем же поведением, что целое в языке
C++. В частности, объект такого класса может использоваться самостоятельно в
условном выражении. Такую способность он приобретает в результате определе­
ния операторов operator true () и operator false () (строки 12-25). Оба
оператора возвращают значение типа bool, но что именно означают «истина» и
«ложь», конечно, зависит от конкретного класса. В данном случае true значит «не
равно 0», a false - «равно 0».
В строках 26-29 реализован унарный оператор «минус» для вычисления про­
тивоположного значения. Этот оператор принимает только один параметр типа
Cpplnt32 и возвращает объект того же типа, представляющий противоположное
(в смысле обычной арифметики) число.
В строках 30-37 определены два базовых арифметических оператора. Для них
не существует никаких особых требований за исключением того, что оба должны
принимать два параметра и возвращать значение соответствующего типа. В дан­
ном случае возвращается значение типа Cpplnt3 2, и мы получаем возможность
выполнять такие операции, как с і = с і * 2 ;.
Оператор эквивалентности в строках 38-44 тоже не преподносит никаких
сюрпризов; он просто сравнивает значения переданных параметров и возвращает
true, если они равны, и false - в противном случае. Если определен оператор
operator -=, то должен быть определен и парный ему operator ! -, что и сде­
лано в строках 45-51.
Наконец, в строках 52-55 реализован оператор преобразования из типа int
в тип Cpplnt32. В данном случае я воспользовался модификатором implicit,
чтобы разрешить простое присваивание без явного приведения типа. Подобные
операторы преобразования между родственными типами могут намного облегчить
вашу жизнь.
Illll Элементы языка

Составные поля: свойства и индексаторы


В строках 30-34 листинга 1.2 приведено объявление свойства. Одна из приме­
чательных особенностей языка C# - это встроенная поддержка свойств. При этом
само поле остается закрытым, а для манипулирования им определяются функции
доступа (в документации Microsoft функция получения значения свойства назы­
вается «getter», а функция его установки - «setter»). В языке Visual Basic тоже
есть аналогичный механизм, но, поскольку C# ведет свою родословную от C++,
синтаксис объявления куда более лаконичный. Строка 30 напоминает обычное
объявление переменной, но за ней следует предложение, содержащее блоки set
и get, по наличию которых мы можем сказать, что речь идет о свойстве. Когда
вызывающая программа присваивает свойству значение, компилятор автомати­
чески создает переменную с именем value, равную этому значению, и передает в
блок set. С другой стороны, блок get исполняется, когда вызывающая программа
хочет получить значение свойства.
В строках 36-40 объявляется индексатор класса. Индексатор позволяет вызы­
вающей программе обращаться к классу следующим образом:
MyClass mcVar;

mylntVar = mcVar[5];
В объявлении индексатора должны быть указаны по меньшей мере вид до­
ступа, тип возвращаемого значения, ключевое слово this, а также типы и имена
индексных параметров. Объявление public int this [int ind] годится, так как
я в данном случае создаю индексатор в самом классе. Если бы я хотел объявить
индексатор для интерфейса, реализуемого данным классом, то должен был бы
указать в начале имя интерфейса:
class MyClass: Ilndexedlnterfасе
{
object Ilndexedlnterfасе.this[int index]
{

}
}
Индексатор может иметь несколько индексных параметров разных типов - это
позволяет моделировать многомерные массивы и словари. Однако не следует
злоупотреблять данной возможностью. Если вам надо продублировать поведение
системных контейнеров, лучше воспользоваться готовыми средствами, а не изоб­
ретать собственные.
Индексаторы тоже можно перегружать. При решении вопроса о том, какой
индексатор вызывать, применяются обычные правила, только имя индексатора
не принимается во внимание (оно всегда одно и то же: this). Объявлять два ин­
дексатора с одинаковыми сигнатурами запрещено.
События
Событие - это член класса, объявленный с модификатором event. Оно позво­
ляет клиентам класса получать извещения о том, что с объектом что-то произош­
Тип интерфейса ΙΙΙΙ· · · · Ε Ξ

ло. При использовании совместно с делегатами (delegate) события заменяют


указатели на функции в C++ и методы вида ОпХХХ в Visual Basic, давая объектам
возможность выразить интерес к получению событий.
Идея событий не нова. Самый распространенный пример их работы - это гра­
фические интерфейсы пользователя (ГИП). Так, форма, содержащая кнопку, заин­
тересована в получении события щелчка по кнопке. Однако графическими интер­
фейсами применение событий не ограничивается. События происходят постоянно -
тики таймера, поступление пакета из сети и т.д. C# - это первый язык, содержащий
элегантный встроенный механизм для поддержки событий.
Поскольку события тесно связаны с делегатами, я отложу рассмотрение при­
мера до момента обсуждения делегатов в разделе «Поток управления» ниже в
данной главе.

Тип интерфейса
C# —это объектно-ориентированный язык, от которого естественно ожидать
поддержки наследования и полиморфизма. Но в современных системах все боль­
шее распространение получает еще один аспект программирования, отсутствую­
щий в традиционных описаниях того, что такое «объектная ориентированность».
Речь идет об интерфейсах. Создание и использование «шаблона» класса является
неотъемлемой частью современных моделей программирования, поэтому неуди­
вительно, что в языке C# имеется обширная поддержка интерфейсов. Именно ее
наличие и позволяет ограничиться только одиночным наследованием.
Объявление интерфейса очень похоже на объявление класса. Как и класс, ин­
терфейс может содержать методы, свойства и события. Но есть и существенные
отличия:
1. Интерфейс не может наследовать классам, только другим интерфейсам.
2. Интерфейс не может содержать полей и индексаторов.
3. Интерфейс не может содержать тел тех членов, которые обычно определя­
ются в классе, к примеру методов и свойств.
4. В интерфейсе нельзя объявлять операторы.
5. Интерфейс не может ограничивать доступ к членам. Поскольку его назначе­
ние - предоставить шаблон для доступных извне атрибутов, это просто не
имеет смысла.
6. В интерфейсе не может быть конструкторов и деструкторов.
Интерфейсы применяются для того, чтобы определить, как должны выглядеть
другие типы. Типы, производные от интерфейса, должны соответствовать его
спецификации, иначе компилятор выдаст ошибку. Это значит, что класс должен
реализовать все члены, объявленные не только в интерфейсе, которому он на­
следует непосредственно, но и во всех предках данного интерфейса. Программа,
работающая с объектами такого типа, может трактовать их как экземпляры ин­
терфейса. Следовательно, полиморфизм удается реализовать без множественного
наследования. Важно понимать, что интерфейс не объявляет тип класса, а только
описывает другие типы.
Illll Элементы языка

В библиотеке времени исполнения применяется множество интерфейсов. Н а­


пример, важной особенностью среды исполнения является наличие сборщика
мусора. Но поскольку сборщик мусора вызывается, когда удобно среде, и, значит,
нет гарантии, что объекты будут освобождены в конкретный момент времени, то
среда предоставляет интерфейс IDisposable. Если ваш класс наследует этому
интерфейсу, то в нем должен быть определен метод вида:
public void Dispose()
{
// Здесь можно освободить ресурсы.
}
Реализовав интерфейс IDisposable, вы даете пользователям класса возмож­
ность сообщить, что объект больше не нужен. А это значит, что ресурсы можно
освободить, не дожидаясь, пока сборщик мусора вызовет деструктор объекта.
В объявлении и использовании интерфейса нет ничего сложного. Начать нужно
с объявления самого интерфейса. Предположим, например, что я хочу поместить
набор объектов в связанный список и при этом каждый объект должен уметь запи­
сывать себя в файл, чтобы потом список можно было быстро прочитать из файла.
Но ограничиваться только одним типом объекта нежелательно (надо же смотреть
в будущее!), поэтому я определяю интерфейс, содержащий члены, которые необхо­
димы для взаимодействия с узлом связанного списка (см. листинг 1.5).
Листинг 1.5. Объявления интерфейса как прототипа классов

1 public interface IListNode


2 {
3 // В этом свойстве хранится ссылка на следующий узел.
4 // IListNode next.
5 {
6 get;
7 set ;
8 }
9
10 // Этот метод вызывается для записи объекта в поток
// outStream.
11 void Save( ref FileStream outStream );
12
13 // Этот метод вызывается для чтения объекта из потока
// inStream.
14 void Read( ref FileStream inStream );
15 }
В интерфейсе IListNode определен член next для организации односвязно­
го списка и два метода - Save () и Read (), принимающие в качестве параметра
объект типа FileStream для записи и чтения из дискового файла. Теперь можно
создавать классы, наследующие этому интерфейсу, и использовать их экземпляры
в своей программе. Обратите внимание, что функции доступа get и set не имеют
тел, равно как и методы. В листинге 1.6 демонстрируется реализация интерфейса
IListNode.
Тип интерфейса ΙΙΙΗ Ι
Листинг 1.6. Наследование интерфейсу и его реализация
как средство поддержки полиморфизма
1 class IntNode IListNode
2 {
3 protected int nodeValue;
4 protected IListNode nextNode;
5
6 // Члены интерфейса узла списка,
7 public IListNode next
8 {
9 get { return nextNode; }
10 set { nextNode = value; }
11 }
12 public void Save( ref FileStream outStream )
13 {
14 BinaryWriter bWriter = new BinaryWriter
( outStream );
15 bWriter.Write( nodeValue );
16
17
18 public void Read( ref FileStream inStream )
19 {
20 BinaryReader bReader = new BinaryReader
( inStream );
21 nodeValue = bReader.Readlnt32();
22 }
23
24 // Свойство для доступа к значению,
// хранящемуся в узле,
25 public int val
26 {
27 get { return nodeValue; }
28 set { nodeValue = value; }
29
30
31
32
33
34 IntNode iNode = new IntNode();
35 FileStream fs = new FileStream( aFileName,
FileMode.Create );
37 iNode.val = 5;
38 iNode.Save( ref fs );
39
40 fs.Seek( 0, SeekOrigin.Begin );
41 iNode = new IntNode();
43 iNode.Read( ref fs );
В строках 1-30 объявляется класс IntNode, производный от IListNode.
В классе IntNode реализовано свойство next (строки 4 -1 1 ), а также методы
36 ■ ■ ■ Ill Элементы языка

Save () и Read () (строки 12-22). Особенностью, характерной только для класса


IntNode, являются данные. Например, я определил в нем единственное свойство
типа int. Поскольку интерфейс реализован, я могу использовать класс в своей
программе, как показано в строках 34-43.

Поток управления
Говоря о потоке управления, мы подразумеваем языковые конструкции, кото­
рые позволяют управлять тем, когда определенный участок кода должен выпол­
няться. К ним относятся предложения ветвления и цикла, делегаты для реализа­
ции обратных (в том числе групповых) вызовов и предложения, предназначенные
для обработки исключений. В языке C# синтаксис обычных предложений передачи
управления и обработки исключений очень похож на C++, но делегаты - это новое
и полезное добавление.

Нормальное выполнение
Для управления ходом нормального выполнения в C# используются предло­
жения ветвления, цикла и выбора. Все они перечислены в табл. 1.5.
Таблица 1.5. Предложения для управления порядком выполнения кода в C#
Ключевое слово Назначение
break Выход из объемлющего блока
continue Переход к следующей итерации цикла
do Цикл с постусловием
for Цикл с итерациями
foreach Цикл обхода набора
goto Переход на метку или ветвь предложения switch
if Двузначный выбор
return Выход из метода (возможно, с возвратом значения)
switch Многозначный выбор
while Цикл с предусловием

Предложения i f и s w i t c h позволяют выбрать одно из двух или нескольких


действий соответственно. Предложение i f состоит из ключевого слова i f , за ко­
торым следует помещенное в скобки условное выражение, а затем предложение
или блок предложений, выполняемых, если условие истинно. Затем может идти
необязательное ключевое слово e l s e и блок предложений, выполняемых в случае,
когда условие ложно. В отличие от C++ значением условного выражение должна
быть булевская величина, а не целое число или значение какого-то другого типа.
Следующий пример иллюстрирует применение предложения i f :
if ( myInt > 0 )
CallSomeFunc();
else
{
Поток управления ΙΙΙΗ Ι
myInt = 1;
CallSomeOtherFunc();
}
Если значение my Int положительно, то выполняется простое предложение
(вызов CallSomeFunc () ), в противном случае - блок предложений.
Предложение switch состоит из ключевого слова switch, за которым следует
помещенное в скобки выражение выбора, а далее - несколько вариантов, обрам­
ленных фигурными скобками:
switch ( mylntVal )
{
case 1:
Console.WriteLine("Число равно единице.");
break;
case 2:
Console.WriteLine("Число равно двум.");
break;
default:
Console.WriteLine("Число не равно ни единице, ни двум.");
break;
}
Как видно из приведенного примера, каждый вариант выбора обозначается
ключевым словом case, за которым следует одно из возможных значений и код,
выполняемый в случае, если выражение принимает именно это значение. Можно
включить также вариант default, который выбирается в том случае, когда выра­
жение принимает значение, отличное от всех явно перечисленных. В данном при­
мере будет выполнен код из первого варианта, если переменная my IntVal равна 1,
код из второго варианта - если она равна 2, и код из варианта по умолчанию - если
переменная не равна ни 1, ни 2. Программа находит первый подходящий вариант
и начинает выполнять ассоциированные с ним предложения. В одном варианте
может быть несколько предложений. Каждый вариант следует завершать каким-
то предложением выхода из switch. Я использовал для этой цели предложение
break, но можно было употребить также continue, goto или return.
В языке C# есть четыре предложения цикла: for, foreach, do и while. Пред­
ложение for эквивалентно одноименному предложению в C++ и имеет вид:
for (init_exp; term_exp; control_exp ) предложение

□ init_exp - это выражение, обычно присваивание, которое устанавливает


начальное значение переменной (или переменных) цикла;
□ control_exp - выражение, изменяющее переменную цикла на каждой
итерации;
□ term_exp - булевское выражение, управляющее моментом завершения
цикла.
Сначала вычисляется выражение init_exp, в котором выполняются началь­
ные установки цикла. Затем C# проверяет, равно ли true значение выражения
Illll Элементы языка

term_exp. Если это так, то выполняется тело цикла. После каждой итерации
вычисляется выражение control_exp и снова проверяется значение выражения
term_exp. Цикл выполняется до тех пор, пока term_exp равно true. Рассмот­
рим пример:
int а ;
int[] b = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for ( а = 0 ; а < 10 ;а++ )
{
b [а] /= 2;
}
Вначале переменной цикла (а) присваивается значение 0. Затем вычисляется
выражение а < 10 и, поскольку оно истинно, выполняется тело цикла. После этого
вычисляется управляющее выражение а+ +, в котором значение а увеличивается
на 1. Цикл повторяется, пока а не станет равным 10, после чего управление пере­
дается предложению, которое следует за скобкой, завершающей тело цикла.
Кстати говоря, управляющую переменную необязательно объявлять вне цикла
for. Если вам нужен лишь временный счетчик, то объявить соответствующую
переменную можно прямо в заголовке цикла:
for ( int с = 0; с < 10; C++ )
b [с ] /= 2 ;
Этот пример функционально эквивалентен предыдущему, только переменная с
объявлена и инициализирована одновременно. Когда цикл завершается, перемен­
ная выходит из области действия и прекращает свое существование.
Цикл for представляет собой мощное средство, но иногда для его инициали­
зации требуется больше усилий, чем реально необходимо, а небрежность в этом
отношении может стать причиной ошибок. Например, в предыдущем примере
условие а < 10 часто приводит к нарушению граничных условий. Так, програм­
мист, пишущий на языке Visual Basic и привыкший к индексированию массивов
начиная с 1, мог бы легко задать а <= 10, что привело бы к выходу за границы
массива. Чтобы избежать таких ситуаций при обходе массива или набора, в C#
включено предложение for each, имеющее следующий вид:
foreach ( type item in collection ) предложение
Цикл foreach выполняет предложение по одному разу для каждого эле­
мента набора collection. Item - это временная ссылка на текущий элемент
набора типа type, к которому можно обращаться из предложения. При тех же
объявлениях, что и в предыдущем цикле for, элементы массива допустимо про­
суммировать так:
а = 0;
foreach (int X in b)
a += X ;
Здесь для каждого элемента массива исполняется предложение а += х. На
первой итерации х ссылается на b [ 0 ], на второй - на b [ 1 ] и т.д. Согласитесь, это
гораздо проще, чем в стандартном цикле for.
Поток управления Н ІМ І 39

Еще два предложения цикла - do и while - почти одинаковы и отличаются


только моментом вычисления условия цикла. Они записываются в следующем
виде:
do предложение while ( выражение )
while ( выражение ) предложение
В обоих случаях предложение выполняется до тех пор, пока истинно выра­
жение. Но, как следует из порядка предложений, в цикле do выражение вычис­
ляется после каждого выполнения тела цикла, а в цикле whi 1е - до начала каждой
итерации. Иными словами, цикл while выполняется с предусловием, а цикл do -
с постусловием. Это различие часто полезно, но не забывайте, что в цикле while
тело будет выполнено только тогда, когда начальное условие истинно, а в цикле
do тело всегда будет выполнено хотя бы один раз. Пример ниже иллюстрирует
использование каждого вида цикла:
do
{
а = а - 1;
}
while ( а > 1 );

while ( а < 10 )
а++;
В любом цикле разрешается воспользоваться предложениями break и cont і-
nue для изменения нормального порядка выполнения предложений. Если вы
хотите вообще выйти из объемлющего составного предложения (например, for,
switch и т.п.), вам поможет предложение break. Вы уже видели, как оно приме­
няется на примере предложения switch, так что я не буду повторяться.
Предложение continue позволяет прекратить текущую итерацию цикла и
сразу перейти к следующей, не покидая цикла. Вот пример:
1: public void ChangeArray( Object [] a )
2: {
3: foreach ( Object о in a )
4: {
5: // Если текущий объект равен null, пропустить его
6: / / и перейти к обработке следующего.
7: if ( о == null )
8: continue;
9:
10: // Здесь обрабатывается элемент массива.
11: }
12 : }
Здесь метод ChangeArray () принимает в качестве параметра массив объек­
тов неуказанной длины. Я воспользовался предложением foreach для обхода
массива, поскольку C# знает его размер, а мне это неинтересно (если вам все-таки
хочется вычислить длину массива, то ее можно получить с помощью свойства
Length). В данном случае я не хочу ничего делать с пустыми элементами, поэтому
Illll Элементы языка

в строке 7 проверяю, равен ли текущий элемент null. Если это так, то предложе­
ние continue в строке 8 возвращает управление на начало цикла foreach (на
строку 3), переменной о присваивается ссылка на следующий элемент массива,
и тело цикла выполняется снова.
Нравится вам это или нет, но в языке C# есть предложение goto. С его помощью
можно перейти на помеченный участок программы, как в следующем примере:
1: public int Findlnt( int val, int [] ia )
2: {
3: int і;

5: // Проверить, равен ли текущий элемент массива ia


// значению val.
6: for ( і = 0; і < іa.Length; і++ )
7: if ( іа[і] == val )
8: goto done;
9:
10: // Если мы сюда попали, то значение val не было найдено.
11 : return -1;
12 :
13: done:
14: return і;
15: }
Здесь функция Findlnt () ищет в массиве некоторое значение и возвращает
индекс первого элемента, равного этому значению, или -1 , если таковых не най­
дено. Ситуация как раз подходит для применения goto, поскольку все остальные
варианты (использование стражей на границе массива, специальной переменной
found и т.п.) сложнее и не так прозрачны. В основном цикле поиска (строки 6 -8 )
проверяются все элементы массива. Если нужное значение найдено, предложение
goto в строке 8 осуществляет переход на предложение return в строке 14 (первое
предложение после метки done :). Если цикл выполнится до конца, то мы попадем
в строку 11 и вернем вызывающей программе значение -1 , показывающее, что
поиск завершился неудачей.
В этом же примере демонстрируется последнее из предложений управления
нормальным выполнением программы - return. Его можно использовать для
возврата в вызывающую программу с указанием возвращаемого значения или без
него (если функция объявлена как void).

Делегирование
Делегирование в языке C# - это средство для явной поддержки функций об­
ратного вызова, которые в современном программировании необходимы довольно
часто. Один из примеров - уже упомянутые выше графические интерфейсы
пользователя. Мне как-то пришлось работать над приложением, в котором тре­
бовалась центральная точка для регистрации различных модулей, вызываемых
для обработки поступающих данных, при изменении состояния системы и т.д.
Делегирование ΙΙΙ····Κ 2
Поскольку я использовал тогда язык C++, код регистрации пришлось писать са­
мому Если бы в моем распоряжении был С#, то механизм делегирования избавил
бы меня от лишних усилий.
Вернемся, однако, к событиям. Их применение основано на предположении
о том, что на состояние объекта влияют внешние воздействия, происходящие
в различные моменты времени. Объект обычно создается программой, которая
реагирует на некоторое подмножество внешних воздействий. В примере с кнопкой
большая часть клиентского кода не желает реагировать на событие перерисов­
ки, но хочет участвовать в обработке события щелчка по кнопке. Если немного
поразмыслить, выяснится, что нам нужны три способа: способ сообщить, какие
события могут возбуждаться внутри класса, способ задать реакцию программы
и способ связать событие с кодом его обработки во время выполнения.
Для решения данной задачи C# предлагает механизм делегирования. Чтобы
связать событие с кодом обработки, вы сначала определяете делегата, который
объявляет, как должны выглядеть методы, способные обработать событие, и на
что должно быть похоже объявление самого события. Это нетрадиционный спо­
соб структурирования связи между событием и обработчиком, и его проще
показать на примере, чем описать словами. Рассмотрим следующее объявление:
public delegate void BoomEvent ( int dB );
Оно говорит: «Я собираюсь объявить событие типа BoomEvent (взрыв).»
Любой обработчик такого события должен принимать один параметр типа int, в
котором передается громкость взрыва.
В программе, возбуждающей такое событие, оно должно быть объявлено как
объект типа BoomEvent (то есть того же типа, что и делегат):
public class EventGenerator
{
public event BoomEvent GoBoom;
}
Чтобы прореагировать на событие, которое будет обработано объявленным
выше делегатом, необходимо объявить в классе метод с такими же параметрами
и таким же типом возвращаемого значения, что и у делегата. Написав код обра­
ботчика, вы можете связать его с событием:
1 public class UserClass
2 {
3 private EventGenerator anEventGenerator;
4 public void MyHandler( int dB )
5 {
6 // Здесь должен находиться код обработчика.
7 };
8 UserClass()
9 {
10 anEventGenerator = new EventGenerator();
11 anEventGenerator.GoBoom += new BoomEvent(MyHandler);
Illll Элементы языка

12 : }
13 : }
В строке 3 мы объявляем экземпляр класса, который возбуждает событие.
В строках 4 -7 объявляется метод с теми же типами параметров и возвращаемого
значения, что и у делегата. Наконец, в строке 10 члену anEventGenerator при­
сваивается ссылка на новый экземпляр класса EventGenerator, а в строке 11
создается экземпляр делегата, инициализированный функцией обработки, и этот
делегат регистрируется с помощью оператора +=, добавляющего новый элемент
в список объектов, которые должны быть оповещены в случае возникновения
события.
Чтобы возбудить событие, в коде класса, являющегося источником события,
нужно вызвать событие, как если бы это была функция:
public class EventGenerator
{
public event BoomEvent GoBoom;
protected void BurnFuse()
{
GoBoom( 12 0 );
}
}
Здесь в результате вызова GoBoom ( 12 0 ) управление попадает к экземпля­
ру делегата, который вызывает все зарегистрированные обработчики (см. выше
определение класса UserClass). Если того требуют условия задачи, вы можете
зарегистрировать несколько обработчиков одного и того же события.

Исключения
Третий вид предложений передачи управления - исключения - представляют
собой конструкции, предназначенные для обработки неожиданных ситуаций в
программе. Например, исключение возбуждается в том случае, когда программа
пытается обратиться к объекту по нулевой ссылке или возникает арифметическое
переполнение в процессе вычислений. Программисты часто неправильно поль­
зуются исключениями для нормального возврата результатов. Это неразумно;
исключения нужны, прежде всего, для обработки ситуаций, которые не должны
возникать при нормальном функционировании программы. Исключение может
завершить программу, поэтому использовать его для возврата значений или вы­
ходных параметров не следует.
Когда возбуждается исключение, выполнение программы в текущем контексте
прекращается, и в стеке ищется ближайший обработчик исключения. Если такого
не нашлось, среда исполнения завершает программу.
Исключения - это полнофункциональные типы; в конечном итоге любое ис­
ключение наследует классу System.Except ion из библиотеки каркаса .NET
Framework. В своей программе вы можете создавать собственные классы исклю­
чений, отражающие специфику задачи. При разумном использовании такой под­
ход позволяет построить механизм обработки ошибок, согласованный с тем, что
применяется в среде исполнения.
Делегирование ιιιη ι

Предложения try, catch и finally


Первый шаг при работе с исключениями состоит в том, чтобы научиться об­
рабатывать исключения, возбуждаемые системой. Поскольку необработанное ис­
ключение сразу же завершает программу, надо знать, какие исключения могут воз­
никать при обращении к среде CLR. Ключевыми конструкциями, применяемыми
при обработке исключений, являются t r y , c a t c h и f i n a l l y .
Рассмотрим наивный код, представленный в листинге 1.7.
Листинг 1.7. Рискованный способ сетевого программирования
// Проверяет, работает ли Web-сервер, пытаясь соединиться
// с ним.
bool retVal = false;
Socket so = new Socket( AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Top );

// Разрешить адрес и порт (80 - это порт по умолчанию


// для HTTP).
IPAddress svrAddr = Dns.Resolve( server ).AddressList[0];
9 IPEndPoint ep = new IPEndPoint( svrAddr, 80 );
10
11 // Соединиться,
12 so.Connect( ep );
13 if ( so.Connected )
14 retVal = true;
15
16 so.Close();
17
18 return retVal;
Этот короткий фрагмент мог бы быть телом процедуры, которая проверяет,
работает ли Web-сервер. Однако при написании сетевой программы мы с уверен­
ностью предполагаем лишь одно: что-то пойдет не так, как надо. В данном случае
запрос к службе доменных имен (DNS) в строке 8 может завершиться неудачей
и вы не получите имени хоста. Или произойдет ошибка при попытке установить
соединение в строке 12.
В C# такого рода проблемы приводят к возбуждению исключений средой ис­
полнения. В листинге 1.8 показана более корректная версия примера.
Листинг 1.8. Немного лучше: некоторые ошибки уже отлавливаются
II Проверяет, работает ли Web-сервер, пытаясь соединиться
II с ним.
bool retVal = false;
Socket so = new Socket( AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Top );

try
{
ιη ιιι Элементы языка

9: II Разрешить адрес и порт.


10: IPAddress svrAddr = Dns.Resolve
( server ) .AddressList [0] ;
11: IPEndPoint ep = new IPEndPoint( svrAddr, 80 );
12 :
13: // Соединиться.
14: so.Connect( ep );
15: if ( so.Connected )
16: retVal = true;
17 : }
18: catch ( SocketException )
19 : {
20: // Вы могли бы реализовать и болееизощренную обработку
21: // ошибок. Но здесь ничего большего не надо.
22: retVal = false;
23 : }
24 :
25: so.Close() ;

27: return retVal;


Программисты, пишущие на языках C++ и Java, поймут эту программу без вся­
ких затруднений, поскольку применяемый синтаксис ничем не отличается. Код, в
котором возможна ошибка, заключается в блок, предваряемый ключевым словом
try. За блоком находятся одно или несколько предложений catch. Перехватить
можно конкретное исключение (что я и сделал в строке 18), если указать после
слова catch имя его типа. В данном примере, если возникает исключение типа
Socket Exception, оно обрабатывается в блоке, следующем за словом catch.
Все прочие исключения не обрабатываются, и, если выше в стеке вызовов не най­
дется подходящего обработчика, программа аварийно завершится.
Использовать конструкцию catch разрешается еще двумя способами. Если
после имени типа поставить идентификатор переменной, то с помощью этой пе­
ременной можно манипулировать объектом исключения и, в частности, получить
дополнительную информацию об ошибке. Вот пример:
catch ( SocketException se )
{
System.Console.WriteLine( se.Message );
retVal = false;
}
Разрешается также вовсе опустить спецификацию исключения, если вы хотите
перехватить любые ошибки:
catch
{
retVal = false;
}
Поскольку исключения - это объекты классов, допустимо перехватить целое
семейство исключений, указав в качестве типа имя общего для них базового клас-
Делегирование ιιιη ι

са. Поэтому, если вы хотите перехватывать все исключения (как в случае, когда
спецификация опущена), но при этом манипулировать объектом перехваченного
исключения, укажите в качестве типа System. Exception, которому наследуют
все классы исключений:
using System;

catch ( Exception e )
{
System.Console.WriteLine( e .TargetSite.Name + ": "
+ e .Message );
retVal = false;
}
При попытке соединиться с несуществующим сервером этот код выведет на
консоль следующую строку:
GetHostByName: No such host is known
С одним блоком try может быть связано несколько блоков catch. В таком
случае они вычисляются сверху вниз, пока не будет найден блок, в котором ука­
занный тип исключения соответствует типу возникшего исключения. Однако,
поскольку блок catch перехватывает не только исключения указанного в нем
типа, но и всех типов, производных от него, располагать эти блоки следует так,
чтобы самые специализированные исключения оказались в начале, а более общие
шли за ними. Такая проблема часто возникает в ситуации, когда вы перехватыва­
ете ожидаемые исключения, но в то же время хотите обработать и более общие
исключения, даже если их появление маловероятно. Например, следующий код
написан с самыми лучшими намерениями, при этом компилироваться он не будет:
try

// Здесь содержательный код.

catch ( Exception e )

catch ( SocketException se )

Его идея в том, чтобы обработать исключение при работе с сокетом, а если
возникнет какая-то другая ошибка, то тоже сделать что-то разумное. Однако ком­
пилятор просматривает программу сверху вниз и видит, что обработчик класса
Exception маскирует обработчика класса SocketExcept ion, а потому генери­
рует ошибку следующего содержания:
A previous catch clause already catches all exceptions of this or
a super type ("System.Exception").
Предыдущий блок catch уже перехватывает все исключения этого типа
или супертипа ("System.Exception").
46 ■■■Ill Элементы языка

Чтобы избежать этого, нужно изменить порядок блоков c a t c h на такой:

{
// Здесь содержательный код.
}
catch ( SocketException se )
{
}
catch ( Exception e )

В таком виде код компилируется нормально.


При написании программ вы все время создаете и уничтожаете объекты, ко­
торые пользуются теми или иными ресурсами или взаимодействуют с другими
объектами. Если возникает исключение, то нормальный порядок выполнения
кода нарушается, и код очистки может быть пропущен. Справиться с этой про­
блемой помогает блок f i n a l l y , куда разрешается поместить код, исполняемый
вне зависимости от того, вышли мы из блока t r y через закрывающую скобку или
в результате исключения. В листинге 1.9 приведен окончательный вариант сетевой
программы.
Листинг 1.9. Окончательная отказоустойчивая версия программы
1 : // Проверяет, работает ли Web-cepBep,
// пытаясь соединиться с ним.
2 bool retVal = false;
3 Socket so = null;
А
Чи
5 try
6 {
7 so = new Socket( AddressFamily.InterNetwork
8 SocketType.Stream,
9 ProtocolType.Tcp );
10
11 // Разрешить адрес и порт.
12 IPAddress svrAddr = Dns.Resolve( server
AddressList[0];
13 IPEndPoint ep = new IPEndPoint( svrAddr,
14
15 // Соединиться.
16 so.Connect( ep );
17 if ( so.Connected )
18 {
19 retVal = true;
20 }
21 }
22 catch ( Exception e )
23 {
Делегирование ιιιη ι

24 // Что-то не так, но мне это безразлично.


25 System.Console.WriteLine(e .TargetSite.Name +
+ e .Message);
26 retVal = false;
27 }
28 finally
29 {
30 // Если исключение возникло после соединения,
// закрыть сокет.
31 if ( so != null && so.Connected )
32 so.Close();
33
34
35 return retVal;
В этой версии программы я перенес все, что может стать причиной исключе­
ния, внутрь блока t r y , и при этом перехватываю любое исключение в строке 22
и выполняю финальную очистку в блоке f i n a l l y (строки 28-33). Если в блоке
t r y возникает исключение, то управление сначала попадает в блок c a t c h . По
выходе из него мы попадаем в блок f i n a l l y , где проверяем, был ли создан и
соединен сокет. Если это так, соединение закрывается.
Внутри блоков c a t c h и f i n a l l y следует избегать конструкций, которые
могут возбудить повторное исключение.

Возбуждение исключений
Вторая сторона работы с исключениями - методика их возбуждения. При воз­
буждении исключения вы покидаете текущий контекст и попадаете в родительский,
в котором исключение может быть обработано. В общем случае, натолкнувшись
в программе на исключение, вы должны первым делом освободить все захваченные
ресурсы (возможно, в блоке f i n a l l y ) , а затем передать исключение дальше.
Для возбуждения исключения применяется предложение th ro w , причем сам
объект исключения помещается в скобки:
throw new ArithmeticException( "Ошибка деления на нуль." )
Поскольку в большинстве случаев исключение не представляет интереса в том
месте, где возбуждается, обычно используется форма, в которой объект создается
оператором new прямо в предложении th ro w .
При написании программы, желающей обрабатывать исключения, часто воз­
никает необходимость очистить текущий контекст в блоке c a t c h и повторно
возбудить то же самое исключение, чтобы передать его «наверх». Например, в
классе, входящем в состав слоя работы с данными можно перехватить исключение,
относящееся к базе данных, освободить соответствующие ресурсы и передать его
слою бизнес-логики. Именно это и делается в листинге 1.10.
Листинг 1.10. Повторное возбуждение исключения
1: using System.Data;
2: using System.Data.SqlClient;
Illll Элементы языка

3
4
5
6 SqlConnection conn = null;
7 SqlCommand cmd = nu11;
8 SqlDataReader dr = null;
9
10 try
11 {
12 // Здесь идет работа с базой данных,
13 }
14 catch
15 {
16 / / Освободить ресурсы.
17 if ( cmd != null )
18 cmd.Dispose();
19
20 if ( conn != null )
21 {
22 conn.Close () ;
23 conn.Dispose();
24
25
26 // Передать исключение выше, повторно возбудив его.
27 throw;
28 }
Здесь код внутри блока try, занимающийся работой с базой данных, может
получить исключение по самым разным причинам, поэтому трудно сказать, что
именно уже инициализировано. В блоке catch (строки 14-28) проверяются все пе­
ременные, при инициализации которых возможен захват ресурсов, и необходимые
объекты очищаются методом Dispose. Затем в строке 27 происходит повторное
возбуждение исключение, в результате чего оно передается в объемлющий код.
Пока что я демонстрировал только работу с исключениями, определенными
в библиотеке среды исполнения. Но иногда полезно создать собственное исклю­
чение, имеющее смысл в контексте вашей программы. Для этого нужно объявить
класс, производный от System.Exception. Можно ограничиться предоставля­
емым по умолчанию поведением или расширить его. Нередко достаточно самого
факта наличия исключения некоторого типа. В листинге 1.11 показано, как ис­
пользовать нестандартные исключения.
Листинг 1.11 .Создание нового класса исключения
using System;

// Объявить нестандартное исключение, возбуждаемое,


// если программа пытается обратиться к этой библиотеке
class BadLibraryException : Exception
{
Наследование ιιιη ι

7 public string Additionallnfо ;


8
9 public BadLibraryException()
10
11 Additionallnfо = "Not specified";
12
13
14 public BadLibraryException( string s )
15
16 Additionallnfо = s;
17
18 }
19
20 [Obsolete("Пользоваться этой библиотекой больше нельзя.")]
21 public class OldClass
22 {
23 public void SomeFunc()
24 {
25 throw new BadLibraryException("Class OldClass");
26 }
27 }
В данном примере я предполагаю, что библиотечный класс больше не работа­
ет и не должен применяться в программе. Поэтому я объявил класс исключения
с именем BadLibraryException (строки 5 -1 8 ) и включил в него поле для хра­
нения подробной информации. Впрочем, это поле можно и не использовать. Имея
такой класс, я могу задействовать его в других классах из той же библиотеки, что
и показано в строках 20-27. В классе OldClass (он помечен атрибутом Obsolete,
чтобы насторожить программистов, если те по ошибке захотят им воспользовать­
ся) код метода SomeFunc () заменен предложением throw, которое возбуждает
нестандартное исключение.

Наследование
Мы уже встречались с наследованием применительно к интерфейсам. Я пола­
гаю, что вы знакомы с концепциями объектно-ориентированного программирова­
ния, так что ограничусь лишь описанием их реализации в языке С#.
Отношение наследования обозначается двоеточием после имени класса в его
объявлении. В списке базовых классов может присутствовать не более одного
класса, но сколько угодно интерфейсов. В итоге в состав членов класса входят
также члены его базового класса, члены всех родительских интерфейсов, члены
их родителей и так далее вплоть до вершины дерева наследования.
Ради совместимости типов ссылку на экземпляр класса Т можно присваивать
экземпляру как класса Т, так и любого производного от него.
Класс не может наследовать другому классу с меньшей областью видимос­
ти; так, попытка унаследовать класс с атрибутом public от класса с атрибутом
private или internal приведет к ошибке. В языке C# в отличие от C++ нет
Illll Элементы языка

управления доступом при наследовании, поэтому к членам, унаследованным от


базового класса, предоставляется тот же уровень доступа, что и в объявлении ро­
дительского класса. Кроме того, нельзя изменять уровень доступа к члену класса
путем замещения его членом с более ограниченным доступом.
Поскольку уровень всех определений в текущем пространстве имен одинаков,
можно попытаться объявить классы с круговыми зависимостями в наследовании,
то есть унаследовать класс А от класса В, который, в свою очередь, наследует от
А. Однако очевидно, что это приведет к ошибке компиляции, так как компилятор
не будет знать, где кончается цепочка наследования.
Разрешается объявлять члены, которые скрывают или замещают члены базо­
вого класса. Для интерфейсов вы обязаны предоставить реализацию всех членов,
но базовые классы дают гораздо большую свободу. Впрочем, как и во всех ос­
тальных случаях, C# пытается заставить вас максимально точно выразить свои
намерения.
В простейшем случае вы можете объявить метод, который семантически скры­
вает одноименный член базового класса:
class Superclass
{
public int A ()
{
// Сделать что-то.
}
}

class Subclass : Superclass


{
public new int A()
{
// Сделать нечто иное.
}
}
В данном случае метод Subclass .А () будет вызван, если программа об­
ращается к методу А () экземпляра класса Subclass. Поскольку объявление
в подклассе члена с тем же именем, что и в базовом классе, - типичная ошибка, то
компилятор предупредит вас о возможных последствиях, если вы не вставите
модификатор new в объявление члена производного класса.
Но сокрытие члена базового класса - это частная мера. В предыдущем опре­
делении замена носит лишь семантический характер и не порождает полиморф­
ного кода. Иными словами, обращение к методу А () приведет к вызову метода
из класса Subclass, только если оно производилось через объект этого класса.
Обращение же через объект класса Superclass приведет к вызову метода, опре­
деленного именно в данном классе, поскольку ничего не сделано для того, чтобы
этому воспрепятствовать.
Пытаясь понять, как разрешается вопрос о вызове метода производного класса,
скрывающего одноименный метод базового класса, вы можете получить странные
на первый взгляд результаты (листинг 1.12).
Наследование ιιιη ι

Листинг 1.12. Взаимодействие доступности и наследования не всегда очевидно


1 class Superclass
2 {
3 public virtual int A()
4 {
5 return 5;
6 }
7
8
9 class Subclass : Superclass
10 {
11 private new int A()
12 {
13 return 6;
14 }
15
16 private int В ()
17 {
18 return 4;
19 }
20
21 public int С ()
22 {
23 // Вызывается Subclass.A
24 return A ();
25 }
26
27
28 class MainClass
29 {
30 public static int Main( string[] args )
31 {
32 Subclass sc = new Subclass();
33
34 // Вызывается Superclass.A ().
35 System.Console.WriteLine( sc.A().ToString
36
37 // Ошибка компиляции.
38 int b = sc.В ();
39
40 return 0;
41
42 }
Программа из этого примера работает так, как описано в комментариях. В
производном классе метод В () объявлен закрытым (private), поэтому попытка
вызвать его из функции Main () в строке 38 приводит к ошибке компиляции. Од­
нако вызов А () в строке 35 разрешается в пользу метода Superclass .А (), так
Illll Элементы языка

как Subclass .А () невидим в клиентском коде из-за модификатора private.


Обращение к Subclass .С () вызывает метод, определенный в самом подклассе
Sub Class, поскольку этот метод доступен. Такое поведение отлично от того, что
мы наблюдаем в других языках; так, в C++ оба вызова приведут к ошибке.
Чтобы обеспечить полиморфное поведение, C# содержит ключевые слова
virtual и override. Они используются как модификаторы объявлений членов
класса, содержащих код, который вы хотите заместить с целью расширения или
изменения функциональности базового класса. В листинге 1.13 показано, как эти
ключевые слова применяются.
Листинг 1.13. Использование ключевых слов virtual и override
для реализации полиморфного поведения
1: class Superclass
2: {
3: public virtual int A()
4: {
5: return 5;
6: }
7: }
8:
9: class Subclass : Superclass
10 : {
11: public override int A()
12 : {
13: return 6;
14 : }
15: }
Употребив в объявлении метода А () базового класса модификатор virtual, я
сказал, что этот метод является кандидатом для замещения; клиентский класс, вы­
зывающий его через ссылку на объект базового класса, получит в свое распоряже­
ние механизм для корректного выбора метода во время выполнения. Модификатор
override в объявлении на строке 11 говорит, что одноименный метод в производ­
ном классе замещает метод базового класса, тем самым довершая картину
При замещении унаследованного метода вы можете воспользоваться моди­
фикатором sealed совместно с override, чтобы сообщить компилятору, что
такое замещение окончательно и не может быть переопределено в последующих
производных классах. Употреблять модификатор sealed применительно к неза­
мещенным методам не разрешается; если вы не хотите, чтобы метод можно было
замещать, уберите модификатор virtual.
Последний модификатор, имеющий отношение к наследованию, - это abst­
ract. Он говорит, что соответствующий член не предоставляет реализации, то есть
воздействует на отдельные члены класса точно так же, как интерфейс действует
на все свои члены. Например, чтобы объявить абстрактное свойство, понадобится
примерно такой код:
abstract class AbstractProperty
{
Небезопасный код м м
public abstract int X
{
get ;
}
// Здесь продолжение класса.
}
В таком случае любой класс, производный OTAbstractClass, обязан реали­
зовать предназначенное только для чтения свойство X.
Ключевые слова, относящиеся к наследованию, могут применяться к свойс­
твам, методам и событиям. Однако модификатор abstract можно употреблять
только для членов класса, в объявлении которого также есть слово abstract.

Небезопасный код
Как и другие языки высокого уровня, к примеру Visual Basic или Java, C#
ставит себе целью изолировать код от машины, на которой он исполняется. Такой
подход повышает надежность программы, но при необходимости «приоткрыть
капот» и работать непосредственно с компьютером возникают проблемы. В слу­
чае Visual Basic или Java для ее разрешения приходится изучать другой язык,
помещать низкоуровневый код в библиотеку и вызывать его через некоторый
специализированный интерфейс.

Вызов внешних функций


Как и следовало ожидать, C# учел прошлые ошибки и предоставляет куда более
элегантный интерфейс для выполнения так называемого небезопасного (unsafe)
кода. Если вам нужно обратиться к библиотеке, написанной на языке низкого уров­
ня, достаточно включить в объявление метода модификатор extern, указать имя
вызываемой функций и имя динамически загружаемой библиотеки, в которой она
находится. Вот пример:
class ExtAPIs
[
[Dlllmport("kernel32", SetLastError=true)]
public static extern uint CreateFile(String lpFileName,
int dwDesiredAccess,
int dwShareMode,
SECURITY_ATTRIBUTES
IpSecurityAttributes,
int dwCreationDisposition,
int dwFlagsAndAttributes,
int hTemplateFile);
}
Хотя внешние методы использовать можно, реальная польза от этого невелика.
Они не нужны для получения доступа к библиотекам и другим контролируемым
компонентам или C O M -объектам. В отличие от других «безопасных» языков,
C# тесно связан с линейкой операционных систем Windows, поэтому большая
часть возможностей платформы доступна через системные классы. И, наконец,
Illll Элементы языка

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

Написание небезопасного кода


C# содержит модификатор u n s a f e для типов и предложений. Он позволяет
работать напрямую с памятью, указателями и другими «небезопасными» объекта­
ми непосредственно в тексте вашей - в остальном контролируемой - программы.
Часто такой метод называют «С внутри С#», поскольку небезопасный код син­
таксически очень близок к языку С. Правда, включаемых файлов и макросов вы
лишены, но базовый синтаксис к вашим услугам.
Модификатор u n s a f e допустимо применять к объявлению типа в целом или
отдельных его членов. Если нужно написать короткий фрагмент кода, то этим
модификатором можно пометить блок. В листинге 1.14 продемонстрированы все
три метода:
Листинг 1.14. Для небезопасного кода можно использовать модификатор unsafe
1 class UnsafeMembers
2
3 unsafe char * pc;
4
5 public void SafeCodeO
6 {
7 // Здесь небезопасный код может встречаться только
8 // в предложениях, помеченных unsafe.
9 }
10
11 public void UnsafeStatement()
12 {
13 // code here is safe code still
14 unsafe
15 {
16 // Здесь разрешается использовать небезопасный код,
17 }
18 }
19
20 public unsafe void UnsafeMethod()
21 {
22 // Здесь тоже небезопасно.
23 }
24
25
26 unsafe class CoreDump
27 {
28 public void Crash()
29 {
30 // Небезопасный код способен привести
II к переполнению буферов.
31 // Поведение в этом случае не определено!
Небезопасный код м м

32: char * рс = stackalloc char[25];


33: for ( int і = 0; і <= 25; i++ )
34 : *pc = "\0";
35 : }
36 : }
В листинге 1.14 первый класс - это обычный контролируемый класс, в кото­
ром, правда, есть небезопасные члены: указатель на char в строке 3 и небезопас­
ный метод в строках 20-23. Но все же класс в целом остается безопасным, поэтому
в методе Saf eCode () нельзя манипулировать небезопасными членами. Хотя бе­
зопасному коду разрешено обращаться к небезопасным методам, манипулировать
указателями он не может, даже если нужно лишь передать одному небезопасному
методу указатель, возвращенный другим небезопасным методом.
В строках 11-18 демонстрируется небезопасное предложение, оно состоит из клю­
чевого слова unsafe, за которым следует блок, содержащий небезопасный код.
На использование небезопасного кода налагается несколько ограничений.
Во-первых, при компиляции такого кода компилятору csc следует задать флаг
/unsafe, иначе он выдаст ошибку, как только встретит небезопасный элемент.
Во-вторых, хотя вы и пользуетесь синтаксисом С, некоторые вещи не работают.
Например, следующая конструкция хорошо знакома программистам на С и C++:
1: public unsafe void шешсру( byte * from, byte * to, int len
)
2: {
3: if ( from == null II to == null II len == 0 )
4: throw new System.ArgumentException();
5:
6: do
7: {
8: *to++ = *from++;
9: }
10: while ( (len— ) > 0 );
11 : }

Цикл копирования в строках 6-10 - это образец стандартной техники програм­


мирования на С. На каждой итерации в строке 8 разыменовываются указатели, и
байт, на который указывает from , копируется в байт, на который указывает to .
После этого операторы постинкремента ( ++) сдвигают указатели на следующую
позицию. Но есть одна тонкость: тот факт, что в С булевские значения представ­
ляются целыми, а указатели также неявно являются целыми, позволяет записать
проверку в строке 3 следующим образом:
if ( !from II !to II len == 0 )
Да и сравнение в строке 10 можно переписать так:
while ( len— );
Увы, даже в небезопасном коде вы должны использовать значение типа b o o l;
i n t для этой цели не годится, а уж указатели тем более.
56 ■ ■ ■ Ill Элементы языка

Приведенного короткого введения в вопросы написания небезопасного кода


вполне достаточно для первого знакомства. В главе 8 я продолжу эту тему.

Директивы препроцессора
Директивы препроцессора позволяют динамически управлять тем, как и какой
код нужно компилировать. Сам термин восходит к ранним компиляторам языков
программирования, когда компиляция осуществлялась в два этапа: сначала код
обрабатывался препроцессором и зачастую при этом так или иначе модифициро­
вался, а потом запускался настоящий компилятор, который создавал p-код или
исполняемый машинный код.
В языке C# нет препроцессора, одна видимость. И директивы препроцессора,
и сам код обрабатываются за один шаг, но концептуально вы можете считать, что
есть два прохода. Директивы, перечисленные в табл. 1.6, предназначены главным
образом для поддержки условной компиляции и позволяют включать или исклю­
чать участки кода в зависимости от определенных вами символов.
Таблица 1.6. Директивы препроцессора
Символ Применение
#define symbol Определяет символ
ttundef symbol Отменяет определение ранее определенного символа
#if condjexp Начинает условную компиляцию
#else Начинает альтернативный блок кода, компилируемый,
если условие в директиве #if ложно
#eiif condjexp Начинает следующую альтернативу, эквивалентно else if
#endif Завершает блок, начатый директивой #if
#warning msg Заставляет компилятор вывести сообщение msg в виде
предупреждения
#error msg Заставляет компилятор прекратить работу и вывести сообщение
msg как ошибку
#line num [ file ] Устанавливает текущую строку и, возможно, имя файла
для вывода диагностических сообщений
#region name В среде Visual Studio открывает секцию, которую можно свернуть
в редакторе; требует парной директивы ttendregion
#endregion Закрывает секцию

Замечание Не все имеющиеся в C+ + средства макрообработки включены в


С#. Но даже то, что осталось, лучше, чем полное их отсутствие в
языке Java.
Чаще всего используются директивы # d e f i n e / # u n d e f и # i f . Они служат
для того, чтобы определить или удалить символ во время компиляции и за счет
этого включить или исключить те или иные участки кода. В листинге 1.15 иллюс­
трируется типичное применение - для создания отладочной и выпускной версий
кода с помощью символа DEBUG.
Директивы препроцессора Н ІН І

Листинг 1.15. Использование директив препроцессора


для условного включения кода
1: #define DEBUG
2
3
4
5: public void ProcessSomething()
6: {
7: int V = DoSomeWork();
#і f DEBUG
Console.WriteLine( "Возврат из DoSomeWork() " +
V .ToString() );

10 : #endif
11 : }
Директива # define в строке 1 определяет символ DEBUG, используемый ниже.
В строках 8 -10 компиляция предложения Console .WriteLine () обусловлена
директивой # i f , которая проверяет, определен ли символ DEBUG. Если это так,
предложение вывода включается в программу, иначе компилятор пропускает его.
Таким образом, препроцессор позволяет полностью убрать отладочный код из
программы, просто отменив определение символа DEBUG.
Возможны и более сложные ситуации. Например, можно управлять версиями,
включив в текст программы несколько условий:
#i f VERI
// Код версии 1.
#elif VER2
// Код версии 2.
#elif VER3
// Код версии 3.
#endif
Помимо условной компиляции, препроцессор можно использовать для вы­
вода предупреждений или даже прекращения компиляции. Этой цели служат
директивы #warning и terror. Например, если вы хотите, чтобы некоторый мо­
дуль компилировался только в отладочной версии, примените директиву #error
следующим образом:
#іf NODEBUG
#error Этот код нельзя включать в выпускную версию.
#endif
В результате компиляция прервется, когда будет определен символ NODEBUG.
Если вы не хотите прибегать к столь радикальным мерам, ограничьтесь предуп­
реждением:
#if NODEBUG
#warning Этот код нельзя включать в выпускную версию.
#endif
Любое сообщение об ошибке или предупреждение, выдаваемое компилятором,
содержит имя файла и номер строки, в которой обнаружена ошибка. Этой инфор­
Illll Элементы языка

мацией манипулируют с помощью директивы #line. Такая возможность очень


полезна для разработчиков инструментальных средств. Например, препроцессор
встроенного SQL сначала обрабатывает исходную программу на С, содержащую
операторы языка SQL, и создает новый текст уже на чистом С, который переда­
ется компилятору При этом в случае обнаружения ошибки компилятор должен
указать имя и номер строки в исходном файле, а не в том, который создан пре­
процессором.
Директива #line как раз и устанавливает имя файла и номер строки, распро­
страняемые на участок кода после этой директивы:
#line 12 3 "myData.cs"
Console.WriteLine("Выберите карту, любую карту.";
Компилятор сообщит об ошибке в этом коде (нет закрывающей скобки в вы­
зове метода), но, какой бы файл в действительности ни компилировался и в каком
бы месте ни была обнаружена ошибка, директива #line заставит его сказать, что
ошибка имела место в строке 123 файла с именем myData.с s.
Последние две директивы - #region и #endregion - пока имеют смысл
только для пользователей Visual Studio. Редактор, входящий в состав Visual Studio.
NET, позволяет сворачивать секции кода примерно так же, как элемент управле­
ния TreeView в других приложениях. По умолчанию Visual Studio осуществляет
свертку по границам пространств имен, классов и методов, а с помощью этих
директив можно свернуть иные участки текста. На рис. 1.1 показана раскрытая
секция кода, а на рис. 1.2 та же секция свернута.

Резюме
Глава 1 была посвящена краткому введению в язык С#. Хотя в этом языке
отсутствуют некоторые из наиболее мощных средств C++, здесь появился ряд
совершенно новых возможностей. Однако для того, чтобы приступить к созда­
нию программ на С#, вам необходима дополнительная информация: о том, как
компилировать и связывать программы и библиотеки, и о том, что происходит,
когда программа загружается и исполняется. В главе 2 описано, как с помощью
инструментов, входящих в .NET Framework SDK, превратить текст на языке C#
в работающее приложение.
Резюме Н ІН І

|<Ки Simple - M icrosoft Visual C#.NET [design] - Classl.cs І-ІПІХІ


File Edit View Project Build Debug Tools Window Help

►Debug Щ operator Щ І Я Й 1*
1 ► ■ и 1+ ^ **-= 1Неї 1® т - 1 Щг %i й=і А- Ξ = Л % % % т ! Ш Ш|й J
This I Disassembly I Class View - Simple I Object Browser Class l.cs

Simple. SimpleMain ^1 |^ M a in (s trin g [] args)


Solution 'Simple' (1 project)
Jfregion Main B - [j]p Sim ple
class SimpleMain Ш ■ References
{ static void M a i n (s t ring[] args)
Й - __l bin
Й - __l obj
{ yEl Assemblylnfo.cs
Cpplnt32 і = neu Cpplnt32(); 1 Classl with class.cs
\ Classl with file code.cs
і = 2;| \ Classl with Sockets.cs
і *= З і ■■ B ] Classl.cs
1 Classl.saved.es
if < і )
1 Copy of Classl with dass.es
Console.W r i t eLine( "The value is t r u e ." );
*1 nspace.cs
else
1 nspace.exe
Console.W r i t eLine( "The value is false." );
1 Simple, vsd
I— cE] usedclass.es
Н у Struct s;
s.h = 1; Solution Items
s.у = 2; ■■■■ [^ ] Simple.vsd
s .z = Э ;

Output

Ξ Output I

Рис. 1.1. Раскрытая секция кода

Oil Simple - Microsoft; Visual C#.NET [design] - Classl.cs ■ _lnjxl


File Edit View Project Build Debug Tools Window Help

j Ш- Щ- & У 9 jt ^ ю - - tP - ^ Debug operator т й ні* 3^


! ► и ■ a + ^ '-i Hex т ІЦ % a± % Ί \ a % % a й Ί
Solution Explorer - Simple

ш\\Ш -Й
class Socketeer... Solution 'Simple' (1 project)
class DataClass... В ΐϋΡ Sim ple
IE X с ept і oriHandl ingj В ■ References
ЕЁ··· ' _ | bin
[inter faces] Й - __l obj
yEl Assemblylnfo.cs
IOp e r at о r s| 1 Classl with dass.es
\ Classl with file code.cs
|Structs| ~j Classl with Sockets.cs
Ц І Classl.cs
1 Classl.saved.es
\ Copy of Classl with dass.es
fl'emo coding technique =]
\ nspace.cs
[Demo all kinds of class members]
\ nspace.exe
\ Simple.vsd
yEl usedClass.es
B- ^ Solution Items
[д~| Simple.vsd

ІҐ
Output

I Щ Output I

Рис. 1.2. Та же секция, но в свернутом виде


Глава 2. Работа с приложениями
В главе 1 был в основных чертах описан язык С#, но это только полдела. Ни один
современный язык программирования не существует сам по себе, независимо от
окружения, в котором выполняются созданные на этом языке программы. Ниже
как раз и представлено такое окружение - способ построения и запуска приложе­
ний, а также наиболее часто используемые инструменты, входящие в состав .NET
Framework SDK.

Промежуточный язык и единая среда исполнения


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

Промежуточный язык
Программы, исполняемые в контролируемой среде, транслируются не в ма­
шинный код, а в изобретенный компанией Microsoft промежуточный язык (MSIL).
Этот язык напоминает машинные команды, но не зависит ни от какой конкрет­
ной архитектуры процессора и обладает дополнительными возможностями для
поддержки объектно-ориентированного программирования. Если интересно, мо­
жете прочитать спецификацию языка MSIL (а заодно и прочих составных частей
среды исполнения) в документе, который находится в каталоге <sdk_install_
dir>\Tool Developers Guide (для получения оглавления документа откройте
файл StartDocs .htm).
Некоторые люди, поверхностно знакомые с контролируемой средой, заклю­
чают, что C# - это Java в интерпретации Microsoft. Однако платформа .NET не
зависит ни от какого языка. Среда исполнения Java требует, чтобы программа была
написана именно на Java, а для .NET вполне достаточно, если компилятор умеет
генерировать код на языке MSIL. Коль скоро это условие выполнено, исходный
язык не имеет никакого значения. Уже сейчас существует более 20 языков, при­
годных для платформы .NET, так что вы можете выбрать тот, который лучше всего
подходит для решения конкретной задачи, или продолжать пользоваться тем, к
которому привыкли.
Другой эффект применения MSIL состоит в кросс-языковой совместимости,
начиная с уровня инфраструктуры и выше. На платформе .NET могут одновремен­
но работать модули, написанные на любых ,ΝΕΤ-совместимых языках. Можно даже
Промежуточный язык и среда исполнения Н ІМ І 61

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


другом языке.
Примечание Вы можете составить представление о том, что такое IL, восполь­
зовавшись утилитой ild a s m . e x e , входящей в SDK. Это дизассемб­
лер промежуточного языка, который открывает откомпилирован­
ные динамически загружаемые библиотеки или исполняемые файлы
и выводит содержащийся в них код в виде, напоминающем язык
ассемблера. Я еще скажу об этой программе в конце главы, а допол­
нительную информацию вы можете почерпнуть из файла IL D a sm
A d v a n c e d O p t io n s . d o c в каталоге T o o l D e v e lo p e r s G u id e .

Единая среда исполнения


Единая среда исполнения (Common Language Runtime - CLR) - это платфор­
ма, на которой работают ваши программы. CLR реализует единую языковую инф­
раструктуру Common Language Infrastructure (спецификация среды исполнения
на платформе .NET) и отвечает за загрузку, компиляцию, связывание и контроли­
руемое исполнение программы. Помимо того, что традиционно принято относить
к среде исполнения, CLR предоставляет службы для своевременной компиляции,
отладки и профилирования. Библиотеки .NET, содержащие классы для органи­
зации пользовательского интерфейса, доступа к данным и к API операционной
системы, находятся поверх CLR и взаимодействуют с ней.
Что касается данных, CLR реализует единую систему типов (Common Туре
System - CTS) - важную часть архитектуры .NET. Типы, определенные в CTS
и реализованные в CLR, перечислены в табл. 2.1. Этот список очень напоминает
систему типов языка С#, хотя есть и несколько исключений. Так, значащие типы
struct, enum и decimal не отображаются напрямую на типы CTS и реализуются
не средой исполнения, а компилятором.
Таблица 2 .1 . Базовые типы, определенные в единой системе типов
Ключевое слово Значения
in t 8 Знаковое 8-разрядное целое
u n sig n e d in t 8 Беззнаковое 8-разрядное целое
i n t 16 Знаковое 16-разрядное целое
u n sig n e d i n t 16 Беззнаковое 16-разрядное целое
in t3 2 Знаковое 32-разрядное целое
u n sig n e d in t3 2 Беззнаковое 32-разрядное целое
i n t 64 Знаковое 64-разрядное целое
u n sig n e d i n t 64 Беззнаковое 64-разрядное целое
f lo a t3 2 32-разрядное число с плавающей точкой
f l o a t 64 64-разрядное число с плавающей точкой
n a tu r a l in t Целое естественного размера
■ ■ ■ Ill Работа с приложениями

Таблица 2 .1 . Базовые т ипы , определенные в единой системе типов


(окончание)
Клю чевое слово З начения
n a tu r a l u n sig n e d in t Беззнаковое целое естественного размера
b o o l Булевское значение
ch ar Широкий символ
s t r in g Строка символов
obj e c t Ссылочный тип
ty p e d r e f Типизированный указатель

В большинстве сред разработки имеются инструменты для создания описа­


ний тех сервисов, которые предоставляет программа. Это описание прилагается
к коду и распространяется в одном с ним пакете. К примеру, Windows использует
библиотеки типов для описания CO M -компонентов, а в Java применяются описи
и отражение. Программы и библиотеки на C# всегда содержат такого рода опи­
сания, которые называются метаданными, и для их создания не надо приклады­
вать никаких усилий. Однако вы можете повлиять на состав метаданных и даже
включить собственные элементы описания с помощью атрибутов, о которых мы
будем говорить ниже.
Метаданные, вставленные компилятором в готовый продукт, содержат полные
описания типов, созданных в вашей программе и видимых внешним программам
и инструментальным средствам. Сочетание метаданных и кода на промежуточном
языке позволяет получать самоописываемые, независимые от платформы .NET-
компоненты, готовые к распространению. Располагая этой информацией, CLR
может очень гибко размещать код в памяти, разрешать ссылки на библиотеки и
гарантировать, что связи между компонентами и типами будут правильно уста­
новлены во время выполнения.

Исполняемые файлы, сборки и компоненты


Физической единицей кода на платформе .NET по-прежнему остается файл
в формате Portable Executable (РЕ). Результатом компиляции программ и библи­
отек являются EXE и DLL-файлы, но в рамках каркаса .NET Framework любой
исполняемый элемент связывается с единой средой исполнения CLR, которая
обеспечивает компиляцию и выполнение кода.

Сборки
Логической единицей развертывания в .NET является сборка (assembly). В со­
став сборки входит опись (manifest), то есть набор метаданных, описывающих фай­
лы и типы, которые сборка раскрывает другим приложениям. Опись может также
содержать сильное имя (strong name) - комбинацию имени сборки, информации
о версии и необязательных региональных параметров. Для сборки с сильным име­
нем метаданные содержат цифровую подпись на открытом ключе, которую CLR
Исполняемые файлы, сборки и компоненты И Н І 63

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


Кроме того, сборка может включать цифровую подпись, созданную с помощью
сертификата Authenticode, которая удостоверяет происхождение кода и позволяет
CLR проверить подлинность подписи сборки.
Сборки бывают приватные и разделяемые. Приватные сборки используются
только тем приложением, которое их установило, тогда как разделяемые заносятся
в глобальный кэш сборок (Global Assembly Cache - GAC), за ведение которого
отвечает каркас. GAC применяет технику подсчета ссылок и пользуется инфор­
мацией о номерах версий сборок для управления библиотеками (не исключая
и библиотеку базовых классов BCL). Он взаимодействует с CLR, стремясь не
допустить, чтобы наличие одновременно установленных версий библиотек обра­
тилось в «проклятие DLL».
Нельзя недооценивать важность решения о том, должна ли сборка быть при­
ватной или разделяемой. Устанавливая сборку в GAC, вы получаете возможность
иметь одну копию, используемую несколькими приложениями. Но в таком случае
инсталлировать приложение придется с помощью программы установки, напри­
мер Windows Installer. Если же вы готовы сделать сборку приватной, то для ин­
сталляции достаточно процедуры, которую Microsoft называет «развертыванием
с помощью XCOPY»: это означает, что установку можно произвести путем копи­
рования файлов на целевой компьютер. Microsoft по данному поводу говорит, что
пространство на диске стоит дешевле, чем усилия, потраченные на установку и
поддержку программ. В общем-то, я с этим согласен, но решать вам, принимая во
внимание особенности конкретного приложения.

Процедура объединения
Во время загрузки программы CLR считывает из файла метаданные, чтобы
узнать, какие типы потребуются программе во время выполнения. Затем кар­
кас отыскивает все библиотеки, на которые ссылается программа, выполняя так
называемую процедуру объединения (fusion). Эта процедура намного сложнее,
чем простой поиск в каталогах, перечисленных в переменной окружения PATH,
причем ее можно конфигурировать для организации сценариев безопасности в
различных архитектурах.
Задача разрешения ссылок состоит из трех основных частей. Прежде всего, если
ссылка содержит сильное имя типа, к которому нужен доступ, то среда исполнения
определяет версию нужной программе сборки. Затем она пытается найти сборку,
пользуясь «советами» приложения, публикатора, а также конфигурационными
файлами. Если это не дает результата, то «апробируются» файлы, находящиеся
либо в инсталляционном каталоге приложения, либо в месте, которое определя­
ется из конфигурационного файла приложения или по контексту вызова, либо в
специальных подкаталогах инсталляционного каталога приложения.
Программа может включать как статические ссылки на используемые в коде
типы, так и динамические ссылки, задействующие механизм отражения. Но и те,
и другие разрешаются одинаково.
В П Н І І І І Работа с приложениями

Примечание Детали конфигурирования на платформе .NET выходят за рамки


этой книги. Более подробную информацию о размещении сборок
можно найти в документации по .NET Framework SDK, точнее в
разделах «Deploying .NET Framework Applications» (Развертывание
приложений) и «How the Runtime Locates Assemblies» (Как среда ис­
полнения находит сборки).
Процесс загрузки гарантирует лишь нахождение сборок первого уровня, ссыл­
ки на дополнительные сборки разрешаются «на лету» в процессе выполнения
программы. В результате код, который пользователю не нужен, загружаться не бу­
дет. Эффект от подобной оптимизации очевиден даже на локальном компьютере,
но еще важнее это для приложений, исполняемых в среде Web, поскольку таким
образом удается сократить объем данных, передаваемых по сети.

Компоненты
Идея компонентов имеет для программирования огромную значимость. Смысл
ее в том, чтобы получить дискретную единицу программного обеспечения, которую
можно независимо распространять и многократно использовать в различных при­
ложениях для достижения четко определенной цели. На платформе .NET термин
компонент объединяет как невизуальные элементы, например соединения с базой
данных или Web-сервисы, так и элементы управления, то есть компоненты, реа­
лизующие автономную единицу пользовательского интерфейса, скажем флажок
или кнопку Поддержка компонентного программирования - это одна из основных
целей платформы .NET в целом и языка C# в частности.
Строго говоря, компонент - это класс (а не сборка), реализующий интерфейс
IComponent. Сказанное означает, что должны быть реализованы интерфейсы
IDispose (базовый для IComponent) и ISite, описывающий доступное для
чтения и записи свойство с именем Site. Компонент всегда содержится в неко­
тором контейнере (site), а взаимодействие между компонентом и объемлющим его
контейнером осуществляется с помощью событий, возбуждаемых через свойство
Site. Компоненты могут предоставлять и другие сервисы, в том числе необходи­
мые для разработки инструментальных средств и для удаленного вызова функций
приложения.
Вы не обязаны самостоятельно реализовывать интерфейс IComponent. Для
создания пользовательских интерфейсов и объектов, допускающих удаленный
вызов, каркас предоставляет следующие базовые классы:
□ System.ComponentModel.MarshalByValueComponent;
□ System.ComponentModel.Component;
□ System.Windows.Forms.Control and UserControl.
Первый класс, MarshalByValueComponent, полезен для создания компо­
нентов, которые передаются между контекстами клиента и сервера, но не требуют
наличия устойчивой ссылки. Хорошим примером такого рода служит запрос к базе
данных; после того, как информация передана клиенту, сервер может освободить
ресурсы, выделенные для удовлетворения запроса. Класс Component позволяет
Атрибуты компонентов и сборок П Н ! 65

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


а клиент обязан сохранить ссылку на него на протяжении выполнения нескольких
операций. Такое различие проводится потому, что поддерживающие состояние
объекты (скажем, в СОМ+), которые должны хранить ссылки, потребляют гораздо
больше ресурсов, чем передаваемые по значению, и никогда не достигнут той же
гибкости, что лишенные состояния объекты.
Control - это базовый класс для объектов пользовательского интерфейса.
Вы можете наследовать ему для создания нестандартных элементов управления.
При желании разрешается наследовать подклассу Us er С on tгol класса Control,
который более точно определяет место нестандартного элемента в иерархии на­
следования.
Примечание По определению, всякий компонент для платформы .NET должен
реализовывать интерфейс IC o m p o n e n t. На практике понятие
компонента интерпретируется более вольно: под этим понимают
всякий повторно используемый объект, о чем я и говорил в начале
этого раздела. Вам следует помнить о таком разнобое в термино­
логии, поскольку в материалах Microsoft слово «компонент» при­
меняется в обоих смыслах. Точно так же под «элементом управ­
ления» (control) изначально понималось нечто, реализующее часть
пользовательского интерфейса; однако в приложениях ASP.NET мы
говорим о серверных элементах управления, которые выступают в
роли компонентов (в широком смысле).

Это введение в компоненты было очень кратким, что не отражает их роли на


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

Атрибуты компонентов и сборок


Вы уже видели, как атрибуты применяются для включения дополнительной
информации в классы. При создании сборок и компонентов в вашем распоряже­
нии есть целый ряд встроенных атрибутов, описывающих процедуры построения
двоичных файлов, их развертывания и использования. Хорошим примером ат­
рибутов сборки является набор, определенный в классе System.Ref lection
(см. табл. 2.2).

Таблица 2 .2 . Атрибуты сборки


Атрибут Назначение
A ss em b ly A lg o rі th m ld A ttr ib u te Управляет работой алгоритма хэширования,
применяемого при создании описи сборки
A ssem b ly C o m p a n y A ttrib u te Строка, содержащая название компании,
выпустившей сборку
ιη ιιι Работа с приложениями

Таблица 2 .2 . Атрибуты сборки (окончание)


Атрибут Назначение
A sse m b ly C o n fig u r a tі o n A ttr ib u te Строка, описывающая конфигурацию
построения сборки
A sse m b ly C o p y r ig h tA ttr ib u te Строка, описывающая правообладателя сборки
A sse m b ly C u ltu r e A ttr ib u te Код региона для совместимости сборки. Перечень
кодов регионов см. в документе RFC 1766
A s s e m b ly D e fa u ltA lia sA ttr ib u te Дружественное имя сборки, используемое вместо ее
настоящего имени
A ssem b ly D ela y S і g n A ttr ib u te Булевское значение, показывающее, используется ли
в сборке отложенное подписание
A s se m b ly D e sc r ip tі o n A ttr ib u te Строка, содержащая краткое описание сборки
A sse m b ly F ile V e r s io n A ttr ib u te Строка, содержащая номер версии файла, если он
отличается от номера версии сборки
A sse m b ly F la g sA ttr ib u te Флаг, управляющий использованием сборки.
В настоящее время он описывает лишь возможность
«равноправного» выполнения(одновременного
исполнения различных версий сборки).
Принимает следующие значения:
0x0000 - без ограничений
0x0 010 - только не в одном прикладном домене
0x0 02 0 - только не в одном и том же процессе
0x0 03 0 - только не на одном компьютере
A ssem b ly In fo rm a tі o n a l Определяет номер версии «для сведения» (атрибут
имеющий смысл для пользова­
V e r s io n A tttr ib u te ),
телей, но не для среды исполнения
A ss em b lyK eyF і 1 eA t t r i b u t e Содержит имя файла с парой ключей для подписания
сильного имени сборки
A ss em blyK eyN am eA tt r ib u t e Содержит имя контейнера ключей, предоставляемого
криптографическим сервис-провайдером. В нем
хранится пара ключей для подписания сильного
имени сборки
A sse m b ly P r o d u c tA ttr ib u te Содержит имя продукта, с которым
ассоциирована сборка
A ssem b lyТ іt le A t t r ib u t e Дружественный заголовок сборки
A ssem b lyT rad em ark A ttrib u te Информация о торговой марке
A sse m b ly V e r sio n A ttr ib u te Номер версии для совместимости типов

Названия всех атрибутов в табл. 2.2 заканчиваются словом Attribute. Это


соглашение нашло отражение в способе применения атрибутов: компилятор сам
добавит суффикс «Attribute» к названию атрибута, если вы забудете это сделать.
Например:
using System.Reflection;

[assembly: AssemblyTitle("MyAssembly")]
[assembly: AssemblyDescription("Пример сборки")]
Атрибуты компонентов и сборок ШШ\

[assembly: AssemblyConfiguration("debug")]
[assembly: AssemblyVersion("1.0.*") ]
Атрибуты сборки задаются в квадратных скобках с добавлением специфика­
ции цели assembly. Для импорта пространства имен, содержащего используемые
атрибуты, следует включить предложение using. Вслед за двоеточием идет имя
атрибута и в скобках - необходимые параметры. Вообще говоря, состав атрибутов
сборки оставлен на ваше усмотрение, но если вы хотите, чтобы у сборки было
сильное имя, то должны включить по меньшей мере атрибуты AssemblyVersion,
AssemblyName и AssemblyCulture.
У компонентов также могут быть специальные атрибуты, представленные
в табл. 2.3. Все они определены в пространстве имен System. ComponentModel.
Таблица 2 .3 . Атрибуты компонента
Атрибут Назначение
A m b ie n tV a lu eA ttr ib u te Идентифицирует свойство элемента управления,
полученное от родительского объекта
Вin d a b le A ttr ib u te Говорит, что элемент управления безопасно
использовать для привязки к данным
B r o w sa b le A ttr ib u te Разрешает или запрещает отображать свойство
или событие в среде разработки
Сa t e g o r y A t t r i b u t e Задает категорию, в которой среда разработки будет
показывать данное свойство или событие
D e fa u ltE v e n tA ttr ib u te Обозначает событие компонента по умолчанию
D e fa u ltP r o p e r ty A ttr ib u te Устанавливает свойство компонента по умолчанию
D e fa u ltV a lu e A ttr ib u te Устанавливает значение по умолчанию для свойства
D esc r i p t і o n A ttr ib u te Содержит описание компонента
D e s ig n e r A ttr ib u te Идентифицирует библиотеку и вид дизайнера
компонента
D e s ig n e r C a te g o r y A ttr ib u te Определяет категорию дизайнера компонента
D e s ig n e r S e r ia liz a t io n Говорит, должен ли дизайнер сохранять свойство
v i s i b i l і t y A t t r i b u t e и, если да, как именно
D e sig n O n ly A ttr ib u te Говорит, что значение свойства можно установить
только во время проектирования в среде разработки
E d ito r A tt r ib u te Определяет, какой редактор следует использовать
для модификации свойства во время проектирования
E d ito r B r o w sa b le A ttr ib u te Говорит, следует ли разрешать редактирование
свойства во время проектирования
Im m u tab leO b j e c t A t t r i b u t e Помечает компонент, все свойства которого запреще­
но модифицировать во время проектирования
In h e r ita n c e A ttr ib u te Содержит уровень наследования компонента
I n s ta lle r T y p e A ttr ib u te Определяет вид инсталлятора, применяемого
для установки целевого компонента
L ic e n s e P r o v id e r A ttr ib u te Показывает, что этот тип поддерживает
лицензирование
L is tB in d a b le A ttr ib u te Говорит, можно ли привязывать компонент к списку
IIIIIL Работа с приложениями

Таблица 2 .3 . Атрибуты компонента (окончание)


Атрибут Назначение
L o c a lі z a b le A ttr ib u te Помечает свойства, для которых во время генерации
кода должны существовать локализованные ресурсы
M e r g a b le P r o p e r ty A ttr ib u te Определяет, можно ли данное свойство отображать
вместе с другими в окне свойств во время
проектирования
N o tify P a r e n tP r o p e r ty A ttr ib u te Помечает свойство, родитель которого должен быть
извещен об изменении значения во время
проектирования
P a r e n th e s iz e P r o p e r ty Управляет заключением имени свойства
в скобки при отображении его
N a m eA ttrib u te
на вкладке Properties в среде разработке
P r o p e r ty T a b A ttr ib u te Описывает вкладку Property и, возможно, область
действия компонента для среды разработки
P r o v id e P r o p e r ty A ttr ib u te Помечает элемент как расширитель свойства
R e a d o n ly A ttr ib u te Помечает свойство, которое в дизайнере разрешено
только читать
R ecom m en dedA sC on figurable Говорит, что данный атрибут следует сделать
конфигурируемым пользователем приложения
R e fr e s h P r o p e r tie s A ttr ib u te Определяет, какого вида перерисовка нужна
для обновления внешнего вида свойства после
изменения его значения в дизайнере
R u n ln s t a lle r A tt r ib u te Говорит, нужно ли запускать инсталлятор
при установке компонента
T y p e C o n v e r te r A ttr ib u te Ассоциирует с атрибутом конвертор типов

Большинство атрибутов, перечисленных в табл. 2.3, нужны для поддержки


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

Средства разработки
Будучи новой средой для разработки программного обеспечения, платформа
.NET требует и новых инструментальных средств. Хотя большинство программис­
тов будут создавать приложения в среде Visual Studio.NET, в состав .NET Frame­
work SDK включены (бесплатно) и все остальные инструменты, необходимые для
программирования на языке С#.

Компилятор CSC
Разумеется, самый главный инструмент, о котором вы должны знать, - это
компилятор с языка C# с sc. В табл. 2.4 приведены флаги компилятора.
Средства разработки П Н !

Таблица 2 .4 . Наиболее употребительные флаги компилятора C#


Ф лаг С окращ ение Н азначение
/ a d d m o d u l e :m o d f i l e _ l i s t Нет Включить в сборку файлы из списка
modfile_list
/ d e b u g [+1 - ] Нет Вставлять или не вставлять отладочную
информацию
/ d e f i n e :sy m b o l(s ) / d :sy m b o l(s ) Определить символы препроцессора
/ d o c : f i l e Вывести документацию в формате XML
в файл с указанным именем
/ l i b :p a t h _ l i s t Искать библиотеки в каталогах,
перечисленных в списке path_list
/in c r e m e n t a l[+1-] / i n c r [+1-] Разрешить или запретить некоторые виды
оптимизации
/ lin k r e s o u r c e : f i l e _ l i s t / l i n k e r e s : Сохранить в выходном файле
file_list ссылки на указанные файлы
/ o p t i m i z e [+1 - ] /o [ + 1-] Разрешить или запретить оптимизацию
/ o u t : f i l e Нет Присвоить выходному файлу имя filename
/ r e f e r e n c e : f i l e _ l i s t / r :f i l e _ l i s t Использовать поименованные файлы
сборок для разрешения ссылок
/ r e s o u r c e : f i l e _ l i s t / r e s :f i l e _ l i s t Включить в выходной файл ресурсы,
содержащиеся в файлах из списка
file_list
/ t a r g e t :e x e / t :e x e Создать консольное приложение
/t a r g e t: w in e x e / t :w in ex e Создать приложение с оконным
интерфейсом
/ t a r g e t :lib r a r y / t :lib r a r y Создать динамически загружаемую
библиотеку
/ta r g e t:m o d u le / t :m od u le Создать модуль для сборки

Элементы списков файлов и путей (path_list, file_list), встречающихся


в табл. 2.4, разделяются запятыми. Для запуска компилятора следует ввести ко­
манду с sc с соответствующими флагами и в конце указать имена файлов, которые
нужно откомпилировать:
csc /out:program.exe filel.cs file2.cs
Эта команда создаст выходной файл program, ехе путем компиляции двух
файлов: filel.cs и file2.cs. Заметим, что при таком запуске компилятора
никакого дополнительного связывания не нужно, выходной файл можно сразу
исполнять.
Но приложения редко состоят из одного исходного и одного исполняемого
файла. Если вы помните, как трудно было создавать DLL и управлять ими в пре­
дыдущих версиях Windows, вы будете поражены тем, как все изменилось в .NET.
Поскольку в каждый P E -файл включается опись его содержимого, вам остается
лишь сказать компилятору, из чего собрать готовую программу.
70 ■ ■ ■ Ill Работа с приложениями

В листинге 2.1 представлен простой класс, содержащий одно свойство и конс­


труктор. Я помещу этот класс в DLL, которую потом буду вызывать из других
приложений.
Листинг 2.1. Простой библиотечный класс
public class UsedClass
{
private string myName;
public string Name
{
get
{
return myName;
}
set
{
myName = value;
}
}
public UsedClass()
{
myName = "Это объект класса UsedClass.";
}
}

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


Чтобы откомпилировать его и поместить в DLL, достаточно следующей команды:
csc /out:UsedClass.dll /t:library UsedClass.es
Флаг out говорит компилятору, как должен называться выходной файл, -
в данном случае UsedClass. dll.Я воспользовался сокращенной записью флага
target, чтобы сообщить компилятору о необходимости создать библиотеку. И в
конце команды я указал имя исходного файла (UsedClass.cs).
Чтобы использовать эту DLL в другом проекте, нужно лишь сослаться на нее при
компиляции. Например, программа в листинге 2.2 пользуется классом UsedClass.
Листинг 2.2. Использование класса UsedClass
using System;
public class SimpleMain
{
public static void Main()
{
UsedClass uc = new UsedClass();
Console.WriteLine( uc.Name );
}
}
Средства разработки ЩЩ\

Если этот код сохранить в файле MainFile .cs, то для компиляции програм­
мы нужно будет набрать команду:
csc /out:a.exe /t:exe /г:usedClass.dll MainFile.cs
Здесь флаг / t :exe говорит компилятору что нужно создать исполняемый
файл, а флаг /out:а .ехе задает имя этого файла. Поскольку в программе ис­
пользуется класс UsedClass, то компиляция завершится с ошибкой из-за нераз­
решенной ссылки, если не задать флаг /г :UsedClass .dll, который показывает
компилятору, что искать внешние типы нужно в указанной DLL. В результате
будет создан исполняемый файл, который во время работы при необходимости
загрузит библиотеку UsedClass .dll.

Управление компиляцией с помощью программы птаке


Для компиляции небольшого числа файлов программы csc вполне достаточ­
но, но для нетривиального проекта нужны более развитые средства. В SDK имеется
утилита шпаке, предназначенная для управления созданием программ, состоящих
из многих файлов. Эта программа работает на основе правил, описывающих за­
висимости между файлами. Анализируя временные штампы файлов, она опреде­
ляет, какие модули нужно перестроить после внесения изменений, и запускает
для них указанную команду. Однако зависимости не обнаруживаются автомати­
чески; вы должны сами создать файл управления проектом (makefile), поместив
в него правила и команды.
Маке-файл может содержать комментарии, объявления и правила. Коммента­
рии представляют собой простой текст, начинающийся с символа #. Объявления -
это просто объявления переменных, такие же, как в пакетных файлах или сценар­
ных языках.
# это простое объявление
SRC=SourceFile.cs
Здесь объявлена переменная SRC, принимающая значение SourceFile .cs,
ее можно использовать ниже в таке-ф айле.
Основу таке-ф айла составляют правила. Типичное правило включает цель,
за ней следуют одна или несколько команд (каждая в отдельной строке), которые
нужно выполнить, если цель оказывается устаревшей. В примере с DLL библиоте­
ка UsedClass .dll зависит от файла UsedClass .cs. Такую зависимость можно
записать в виде следующего правила:
UsedClass.dll: UsedClass.cs
csc /out:UsedClass.dll /t:library usedClass.es
В первой строке указано имя зависимого файла UsedClass .dll. За ним идет
двоеточие и список файлов, от которых зависит DLL, - в данном случае это всего
один исходный файл UsedClass .cs. Приведенное правило говорит шпаке, что
нужно проверить временной штамп файла UsedClass.cs,H если он датирован
более поздним временем, чем DLL, то DLL необходимо построить заново. Вторая
72 ■ ■ ■ III Работа с приложениями

строка, которая должна начинаться символом табуляции, описывает, какая коман­


да будет решать задачу
В одном файле может быть много правил, управляющих компиляцией и дру­
гими действиями, которые необходимы для построения проекта. В листинге 2.3
приведен простой шаке-файл, который управляет построением приложения из
примера выше.
Листинг 2.3. Простой таке-файл для построения небольшого приложения
а .ехе: MainFile.cs UsedClass.dll
csc /out:a.exe /t:exe /r:UsedClass.dll MainFile.cs

UsedClass.dll: UsedClass.es
csc /out:UsedClass.dll /t:library UsedClass.es

Когда nm ake читает такой файл, она предполагает, что самое первое правило
описывает главный файл, который следует построить, и соответственно интерпре­
тирует остальные правила. В данном случае первое правило говорит, что нужно
построить исполняемый файл. Зная это, nm ake смотрит на второе правило, кото­
рое описывает, как надо строить DLL, если потребуется. Совместно приведенные
правила содержат всю информацию, необходимую nm ake для построения ис­
полняемого файла и DLL в случае, если будут изменены исходные файлы. Если
сохранить правила в файле с именем m a k e f i l e (без расширения), то достаточно
набрать в командной строке nmake, и цель будет построена.
nmake по умолчанию читает файл с именем m a k e f ile . Но с помощью флага
/ f можно задать любое другое имя. В табл. 2.5 перечислены некоторые полезные
флаги nmake.
Таблица 2.5. Наиболее употребительные флаги программы шпаке
Флаг Назначение
/а Перестроить все цели (не обращая внимания на временные
штампы)
/і Игнорировать ошибки (обычно nmake останавливается,
как только обнаружена ошибка)
/п Выводить на экран команды, но не выполнять их
(полезен для отладки таке-файлов)
/s Подавить вывод на экран самих выполняемых команд
/t Изменить временные штампы файлов, не перестраивая проект
η Вывести справочную информацию

Этот иллюстративный пример, разумеется, слишком прост. По мере роста


проекта вы наверняка захотите использовать более развитые возможности nmake.
В листинге 2.4 приведен таке-ф ай л для построения того же проекта, более при­
ближенный к реальности.
Листинг 2.4. Более изощренный таке-файл для построения того же приложения
1: # makefile: 7/20/2001 wmr
2: #
Средства разработки ШШ\

3 # Этот файл управляет компиляцией DLL и EXE для примера


4 # из главы 2 настоящей книги.
5 #
6 # (с) 2001 Pearson Education
7
8 # Объявляем группы файлов проекта.
9 LIB=UsedClass.dll
10 LIBSRC=UsedClass.cs
11 LIBDEP=$(LIBSRC) makefile
12
13 EXE=a.exe
14 EXESRC=MainFile.cs
15 EXEDEP=$(LIB) $(EXESRC) makefile
16
17 # Объявляем правила построения.
18 $ (EXE): $(EXEDEP)
19 CSC /out:$(EXE) /t:exe /r:$(LIB) $(EXESRC)
20
21 $ (LIB): $(LIBDEP)
22 csc /out:$(LIB) /t:library $ (LIBSRC)
23
24 clean:
25 del $ (EXE) $ (LIB)
Строки 1-6 содержат комментарий. Как и при написании любого другого кода,
вы должны включать информацию о назначении и функционировании фрагмента.
В строках 8-15 объявлены несколько переменных, при этом иллюстрируется
и порядок их использования. В строке 9 объявлена переменная LIB, содержащая
имя DLL. Переменная LIBSRC (строка 10) содержит имя исходного файла, а пере­
менная LIBDEP - имена файлов, от которых зависит библиотека, а именно LIBSRC
и сам makefile. Если makefile изменился, то библиотеку необходимо перестроить на
случай, если модификации подверглись шаги построения. Обратите внимание на
синтаксис переменной LIBSRC в строке 11: ее имя заключено в фигурные скобки
и перед ним стоит знак доллара. Таким образом, значением переменной LIBDEP,
объявленной в строке 11, будет «UsedClass .cs makefile».
Строки 17-22 содержат, по существу, тот же код, что и в первом примере, но
переписаны с использованием ранее объявленных переменных. Если запустить
nmake без указания цели, то она начнет построение с первого встретившегося
в файле правила, поэтому я сделал первым правило для построения исполняемого
файла, а за ним поместил правило для построения DLL.
При запуске nmake в командной строке можно указать имя цели. В строках
24 и 25 для иллюстрации этой функции показана цель clean. В ходе обычного
построения проекта эта цель никогда не будет выполняться, поскольку никакая
другая цель от нее не зависит. Цель clean часто включают в таке-ф айл, чтобы
иметь возможность удалить все промежуточные и целевые файлы, созданные
в процессе построения. В предположении, что файл с правилами назван make file,
для удаления всего, кроме самого исходного файла, нужно выполнить команду
IIIIIL Работа с приложениями

nmake clean
Эта команда говорит nmake, что следует построить цель clean. Поскольку
данная цель ни от чего не зависит, то будут просто выполнены ассоциированные
с ней команды, а точнее следующая:
del а .ехе UsedClass.dll
Сразу не очевидно, почему второй т а к е -ф ай л лучше первого, но по мере
роста проекта все сомнения отпадут. Так, в первом случае для добавления
в проект новых файлов придется изрядно потрудиться. Во втором же случае
для включения в библиотеку нового исходного файла (допустим, Another.
cs) достаточно добавить его имя в определение переменной LIBSRC (см. строку
10 в листинге 2.5).
Листинг 2.5. Улучшенный таке-файл для построения приложения,
включающего библиотеку
1 # makefile: 7/20/2001 wmr
2 #
3 # Этот файл управляет компиляцией DLL и ЕХЕ для примера
4 # из главы 2 настоящей книги.
5
6 # (с) 2001 Pearson Education
7
8 # Объявляем группы файлов проекта.
9 LIB=UsedClass.dll
10 LIBSRC=UsedClass.cs Another.cs
11 LIBDEP=$(LIBSRC) makefile
12
13 EXE=a.exe
14 EXESRC=MainFile.cs
15 EXEDEP=$(LIB) $(EXESRC; makefile
16
17 # Объявляем правила построения.
18 $(EXE): $(EXEDEP)
19 csc /out:$(EXE) /t:exe /r:$(LIB) $ (EXESRC;
20
21 $ (LIB): $(LIBDEP)
22 csc /out:$(LIB) /t:library $ (LIBSRC)
23
24 clean:
25 del $(EXE) $ (LIB)
Теперь nmake будет учитывать новый файл при каждом построении без до­
полнительных усилий с вашей стороны.
Это лишь краткое знакомство с тем, что можно делать с помощью nmake.
Программисты для Windows привыкли к интегрированным средам разработки, но
на UNIX-платформах большинство проектов управляются с помощью программы
make. Хотя статей, относящихся к nmake, в документации немного, в .NET SDK
Средства разработки ЩЩ\

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


программой, да и в нашей книге вы еще встретитесь с ней.

Построение сборок с помощью программ sn и а/


Строить DLL для использования в собственной программе полезно, и при
этом не возникает конфликта версий. Но создание сборок без какой бы то ни
было дополнительной информации лишает CLR возможности проверить вер­
сию во время исполнения, поэтому вся ответственность за управление своими
библиотеками лежит на вас. Однако если вы подпишете сборку сильным именем
перед распространением, то получите и механизм нумерации версий, и воз­
можность поместить свою сборку в глобальный кэш, так что ей смогут пользо­
ваться сразу несколько приложений. Хотя процедура называется «подписание
сборки», напомним, что она вериф ицирует не источник поступления кода,
а лишь номер версии.
Для создания сборки с сильным именем нужно выполнить два шага:
1. Получить пару цифровых ключей.
2. Подписать сборку во время компиляции закрытым ключом.
В состав .NET Framework SDK включена утилита sn, которая генерирует клю­
чи для создания сильных имен. В простейшем случае она вызывается с флагом /к
и генерирует файл ключей (отметим, что sn различает регистр при задании ключей):
sn /к:key.dat
Эта команда создает в текущем каталоге файл k e y . d a t, содержащий пару крип­
тографических ключей, которые используются для подписания сборок. Если есть
необходимость, то пары ключей можно хранить в контейнере, предоставляемом
криптографическим сервис-провайдером (CSP). Для этого употребляется флаг / і:
sn /і файл_ключей имя_контейнера
Другие флаги утилиты sn перечислены в табл. 2.6.

Таблица 2.6. Наиболее употребительные флаги программы sn


Ф лаг Назначение
/D assyl assy2 Сравнить сборки assyl и assy2, чтобы убедиться,
что они отличаются только подписями
/к keyfile Сгенерировать файл ключей
/і keyfile cont Поместить ключи в контейнер с именем cont,
предоставляемый CSP
/d cont Удалить предоставленный CSP контейнер с именем cont
/R assy keyfile Подписать (или переподписать) сборку assy ключом
из файла keyfile
/Rc assy cont Подписать (или переподписать) сборку assy ключом
из предоставленного CSP контейнера с именем cont
■■■Ill Работа с приложениями

Таблица 2 .6 . Наиболее употребительные флаги программы sn (окончание)


Ф лаг Н азначение
/ V [ f] assy Верифицировать подпись сборки assy; если включен еще флаг f ,
то верификация производится даже в том случае, когда сборка
помечена для пропуска этой операции
/Vr assy [user_list] Временно пометить сборку assy для пропуска операции
верификации подписи
/Vu assy Отменить пометку сборки assy для пропуска операции
верификации подписи

Предупреждение Утилита s n позволяет пропускать верификацию подписи,


чтобы во время разработки можно было пользоваться удоб­
ными ключами. Но в этом таится опасность: если позже
вы решите отменить пропуск верификации, то каркас не
станет проверять ранее пропущенные сборки. В результате
в глобальном кэше могут остаться неверифицированные
сборки.
Подписать сборку не труднее, чем создать ключ: можно либо применить ключ
с помощью программы sn, либо указать его в качестве значения флага для компо­
новщика сборок - программы al.
Чтобы подписать сборку с помощью sn, надо включить в нее по меньшей мере
следующие атрибуты:
[assembly: AssemblyVersion("1.О .О .О")]
[assembly: AssemblyCulture("")]
Указанные атрибуты входят в состав метаданных, необходимых для формиро­
вания сильного имени сборки. Затем вы обычным образом компилируете сборку.
Когда сборка откомпилирована, вы подписываете ее следующей командой:
sn /R сборка файл_ключей
Здесь сборка - это имя библиотечного файла, а файл_ключей - имя файла,
содержащего пару ключей для подписания сборки. Так, чтобы подписать биб­
лиотеку UsedClass с помощью ключей из файла key.dat, нужно выполнить
следующую команду:
sn /R UsedClass.dll key.dat
Для более крупных сборок, состоящих из нескольких файлов, вы можете от­
компилировать код в промежуточные модули, а затем воспользоваться компонов­
щиком сборок a l для создания DLL, которая будет содержать ссылки на файлы
модулей. При этом увеличивается число файлов, подлежащих распространению,
зато повышается производительность во время выполнения, поскольку загружать
придется только те модули, к которым реально происходит обращение из про­
граммы. Пока программе не понадобился тип, хранящийся в файле модуля, среда
исполнения может даже не проверять существование этого файла.
Средства разработки ЩЩ\

Возвращаясь к примеру библиотеки UsedClass, модифицируем таке-ф айл


так, чтобы создавалась подписанная сборка. Результат представлен в листинге 2.6.
Листинг 2.6. Маке-файл для создания сборки
1 makefile: 7/20/2001 wmr
2
3 Этот файл управляет компиляцией DLL и EXE для примера
4 из главы 2 настоящей книги.
5
6 (с) 2001 Pearson Education
7
8 # Объявляем группы файлов проекта.
9 LIB=UsedClass.dll
10 LIBSRC=UsedClass.cs Another.cs
11 LIBMOD=UsedClass.mod Another.mod
12 LIBDEP=$(LIBMOD) makefile
13 LIBKEY=/keyf:key.dat
14
15 EXE=a.exe
16 EXESROMainFile .cs
17 EXEDEP=$(LIB) $(EXESRC) makefile
18
19 # Объявляем правила построения.
20 $(EXE): $ (EXEDEP)
21 CSC /out:$(EXE) /t:exe /r:$(LIB) $ (EXESRC;
22
23 $ (LIBMOD) : $ (LIBSRC)
24 CSC /out:$@ /t:module $* cs
25
26 $ (LIB) : $ (LIBDEP)
27 al $ (LIBKEY) /out $ (LIB) /t:library $ (LIBMOD)
28
29 newkey:
30 sn --k $ (LIBKEY)
31
32 clean:
33 for ;i in ( $(EXE) $ (LIB) $ (LIBMOD) ) do dei
Я добавил в исходный таке-ф ай л переменную LIBMOD для хранения имен
создаваемых модулей (строка 11) и переменную LIBKEY, содержащую флаг про­
граммы al, который именует файл ключей (строка 13). Изменив эту переменную,
я смогу перейти к использованию другого способа хранения ключей.
Правило построения исполняемого файла не изменилось, все различия каса­
ются только правил построения сборки. В строках 23 и 24 я воспользовался воз­
можностью nmake применять одно правило сразу к нескольким файлам. В строке
23 говорится, что все файлы, перечисленные в переменной LIBMOD, зависят от
файлов, указанных в переменной LIBSRC. Поэтому nmake применяет к каждому
файлу из LIBSRC команду, находящуюся в строке 24. В строке 24 используются
78 ■■■Ill Работа с приложениями

два специальных макроса nmake; вместо $@ подставляется имя генерируемого


целевого файла, а вместо $ * - базовое имя цели (без расширения). Таким образом,
для цели UsedClass .mod nmake создает и выполняет следующую команду:
csc /out:UsedClass.mod /t:module UsedClass.es
В результате из каждого исходного файла получается один . mod-файл. В стро­
ках 26 и 27 создается сама сборка - выполняется команда ale флагом, объявлен­
ным в строке 13, именем DLL, взятым из значения переменной LIB (строка 9),
и именами файлов связываемых модулей. Созданная таким образом DLL не со­
держит кода, а лишь опись со ссылками на модули, входящие в сборку. В табл. 2.7
представлены другие флаги команды al.

Таблица 2 .7 . Наиболее употребительные флаги программы a l


Ф лаг С окращ ение Назначение
/baseaddress:address /base Задает базовый адрес сборки
/company:cy_infо /comp Задает атрибут компании
(см. AssemblyCompanyAttribute)
/configuration:info /config Задает строку конфигурации
(с м .ConfigurationAttribute)
/copyright:info /copy Задает строку авторского права
(с м .AssemblyCopyrightAttribute)
/culture:info /с Задает строку региона
(см. AssemblyCultureAttribute)
/delaysign[+1 - ] /delay Говорит, нужно ли отложить подписание сборки
/description:info /descr Задает строку описания
(CM. AssemblyDescriptіonAttribute)
/evidence:file /e Включает в цель файл file как ресурс,
поименованный Security .Evidence
/embedresource:file /embed Включает в цель файл file как ресурс.
Дополнительно после имени файла
можно указать имя ресурса (name)
и признак приватности (private)
в формате [, name[, private]]
/fіleversion:version Задает номер версии для сведения
(с м .AssemblyFileVersionAttribute)
/flags:flags Задает флаги сборки
(CM. AssemblyFlagsAttribute)
/fullpaths Заставляет al выводить полные пути
во всех сообщениях об ошибках
/help Отображает информацию о порядке вызова
/key file:f і1ename /key f Задает имя файла ключей для
подписания сборки
/keyname:info /keyn Задает имя криптографического
контейнера, содержащего ключи
для подписания сборки
Средства разработки ЩЩ\

Таблица 2.7. Наиболее употребительные флаги программы al (окончание)


Ф лаг С окращ ение Назначение
/linkresource:file /link Связывает файл file как ресурс
со сборкой. Дополнительно можно
указать имя ресурса (name), имя цели,
в которую копируется файл (target) ,
и признак приватности (private)
в формате [,name[, target[, private]]]
/main:method_name Задает имя точки входа в сборку
/nologo Подавляет вывод информации о самой
программе al
/out:file Обязательный флаг, задает имя
выходного файла
/product:info /prod Задает строку описания продукта
(CM. AssemblyProductAttribute)
/productversion:info /productv Задает строку версии продукта
(см. Assemblylnformational-
VersіonAttribute)
/target:lib /t :lib Создает DLL
/target:win /t :win Создает приложение с оконным интерфейсом
/target:exe /t :exe Создает консольное приложение
/template:file Задает имя сборки, от которой наследуются
метаданные (используется для создания
сопутствующих сборок)
/title:file Задает дружественный заголовок
(CM. AssemblyTitleAttribute)
/trademark:file /trade Задает строку торговой марки
(CM. AssemblyTrademarkAttribute)
/version:version /V Задает номер версии сборки
(CM. AssemblyVersionAttribute)
/win32icon:file Задает файл пиктограммы (с расширением
.ico), которая будет сопровождать данный
файл в программе Explorer
/win32res:file Задает имя ресурсного файла (с расширением
.res), включаемого в выходной файл
(ifile Задает имя файла, из которого al должна
читать значения флагов

Порядок вызова программы a l следующий:


al source_spec option_spec
Здесь s o u r c e _ s p e c - последовательность имен файлов модулей, а также флаги
/e m b e d r e s o u r c e или / l i n k r e s o u r c e , a o p tio n _ s p e c - все остальные флаги.

Управление сборками с помощью программы gacutil


Подписанную сильным именем DLL можно применять как обычную библи­
отеку, но дополнительно вы получаете возможность установить ее в глобальный
80 ■■■III Работа с приложениями

кэш сборок, так что она станет доступной другим приложениям. Для добавления
сборки в глобальный кэш и удаления ее оттуда можно пользоваться программой
Windows Explorer, но имеется также специальная утилита g a c u t i l , которую
удобно задействовать в сценариях установки и таке-файлах.
Порядок вызова g a c u t i l следующий:
gacutil [ флаги ] [ файл_с6орки ]
В табл. 2.8 перечислены флаги утилиты g a c u t i l .
Таблица 2.8. Флаги программы gacutil
Ф лаг С окращ ение Назначение
/cdl Удаляет все компоненты из кэша загруженных сборок
/help /h ИЛИ /? Выводит информацию о порядке вызова
/і assembly Устанавливает сборку в глобальный кэш
/nologo Подавляет вывод информации о самой программе
gacutil
/silent Подавляет вывод любых сообщений
/ungen nspace /U Удаляет сборку из глобального кэша. Когда
используется длинный, а не сокращенный флаг,
то сборка удаляется также из «родного» кэша сборок,
если таковой существует

Обратите внимание, что очистить кэш загруженных сборок можно (с помощью


флага /cdl), а поместить в него новые компоненты - нет.
Устанавливая сборку в кэш с помощью утилиты gacut i1, вы сообщаете имя фай­
ла сборки, а при удалении ее из кэша - имя самой сборки. Чтобы установить сбор­
ку UsedClass в GAC, разрешается либо перетащить ее в каталог %systemroot%\
assembly, пользуясь программой Windows Explorer, либо ввести в командной
строке следующую команду:
gacutil /і UsedClass.dll
Напротив, для удаления сборки применяется команда:
gacutil /и UsedClass

Отладка на платформе .NET


Отладка приложения подразумевает две вещи: анализ среды исполнения
(собственно, это обычно и называется отладкой) и изучение информации, кото­
рая выводится во время построения программы. В состав .NET Framework SDK
входят два отладчика: командный сor dbg и оконный DbgCLR, известный также
под названием Microsoft CLR Debugger. Каким отладчиком пользоваться, зависит
от того, что вам нужно, cordbg предоставляет обширный набор команд, поз­
воляющий изучать сопряжение CLR с вашей программой, но для его освоения
придется изрядно потрудиться. С другой стороны, DbgCLR обладает почти такими
же возможностями, но гораздо проще в работе (а следовательно, и полезнее, хотя
Отладка на платформе .NET I I I · · · 81

у вас может быть иная точка зрения). В SDK также включены утилиты для ассем­
блирования и дизассемблирования сборок: і1asm и ildasm, которые позволяют
заглянуть внутрь откомпилированных приложений или системных компонентов.
Совет Вам придется модифицировать переменную окружения PATH,
чтобы упростить запуск отладчика. Программа c o r d b g находит­
ся в каталоге . . . \ M i c r o s o f t .N E T \b in вместе со всеми осталь­
ными инструментами, входящими в SDK, но оконный отладчик
расположен в отдельном каталоге GuiDebug.

Отладка с помощью программы DbgCLR


Работа с отладчиком DbgCLR не вызовет никаких затруднений у тех, кто при­
вык к семейству средств разработки от компании Microsoft. Он предоставляет
практически те же функции, что и отладчик, встроенный в интегрированную сре­
ду Visual Studio.NET, только без средств управления разработкой и решениями.
У вас есть возможность сохранить сеанс отладки в виде файла с расширением
.din, чтобы впоследствии при каждом входе в отладчик воссоздавать одно и то
же окружение.
Запускается программа из командной строки, двойным щелчком по ней в окне
программы Explorer либо путем создания ярлыка в меню Пуск или на Рабочем
столе (лично я предпочитаю последний метод). В любом случае начальное окно
программы выглядит, как показано на рис. 2.1.

'У·* M ic ro s o ft C LR D e b u g g e r [d e s ig n ] = | χ ΐ
File Edit View Debug Tools W indow Help

I & ^ fi| I ► {3 и □ - .

Command Window - Immediate

Р и с . 2.1. Интерфейс программы Microsoft CLR Debugger всем хорошо знаком

Чтобы начать работу, отладчик должен знать имя программы. Напомним, что
для отладки программы следует откомпилировать все модули, которые вас инте­
ресуют, с флагом /debug+.
82 ■ ■ ■ Ill Работа с приложениями

В большинстве случаев вы будете пользоваться пунктом Program to Debug


(Отлаживаемая программа) в меню Debug (Отладка), чтобы сообщить отладчи­
ку имя исполняемого файла. Эта команда открывает диалоговое окно, в котором
задаются имя файла, аргументы командной строки и рабочий каталог. Указав все
необходимое, вы можете пользоваться иконками по образцу кнопок на передней
панели видеомагнитофона для запуска, прекращения и приостановки процесса
отладки.
Разрешается также подключить отладчик к уже работающему процессу, если
воспользоваться пунктом Debug Processes (Отладка процессов) в меню Tools
(Инструменты). При выборе этого пункта открывается диалоговое окно, пока­
занное на рис. 2.2. В верхней части окна представлены процессы, работающие на
компьютере, а в нижней части - те из них, к которым уже подключен отладчик (от­
лаживаемые процессы). В отличие от предшествующих инструментов Microsoft,
которые завершали отлаживаемый процесс по выходе из отладчика, в .NET вы
можете подсоединиться к управляемому процессу, поработать с ним в отладчике,
а потом отключиться, не завершая процесса. Допустимо одновременно отлажи­
вать несколько процессов, что полезно в случае мультипроцессных приложений.
Однако отладчик, поставляемый в составе .NET Framework SDK, не может под­
ключаться к удаленным процессам, тогда как входящий в состав интегрированной
среды Visual Studio.NET способен и на это.

P rocesses

Transport Default Close

Name: EARTH RISE Help j


Available Processes -

Process I ID I Title I Type I Ξ... I Debugger | |


explorer.exe 71Б V:\csharp\WindowsApplication1\bin\Debug Win32 0
NTVDM.EXE 165г Collage Capture <ORIGINALSET> Win32 0 Refresh
sqlmangr.exe 1600 Win32 0
WindowsApplica... 1472 Inetlog Browser .NET 0 Microsoft CL...

Г Show system processes Г Show processes in all sessions

Debugged Processes

Process I ID I Title I Transport | Machine/Port | Break j


WindowsApplica... 1472 Inetlog Browser Default EARTHRISE
Detach

Terminate

When debugging is stopped: | Detach from this process

Рис. 2.2. Подключение отладчика DbgCLR к процессу

После того как вы загрузите программу или подключитесь к ней, в вашем рас­
поряжении окажется множество инструментов для исследования кода и порядка
его исполнения. На рис. 2.3 представлены раскрытые меню отладки. Это должно
дать вам некоторое представление о полноте инструментария.
Отладка на платформе .NET IIIH I
'У ·* M is c e lla n e o u s Files - M icrosoft C LR D e b u g g e r [b re ak ] - M ainF orm .cs [R ea d Only] □Ш З
File Edit View Debug I Tools W indow Help

j ІШ ► Windows И Breakpoints Ctrl+Alt+B

► II aI Program To Debug.. Bunning Documents Ctrl+Alt+N

j Program [1472] V\ ► Continue F5 Watch


II Break All Ctrl+Alt+Break Autos Ctrl+Alt+V, A
MainForm .cs
■ Stop Debugging Shift+F5 Locals Ctrl+Alt+V, L
іг е г - Mis.. _*l
Detach All This Ctrl+Alt+V, T
olutionZ' (0 project
П Be st art Ctrl+Shift+F5 Immediate Ctrl+Alt+I aneous Files
Us) Processes... iForm.cs
Call Stack Ctrl+Alt+C
Exceptions... Ctrl+Alt+E Threads Ctrl+Alt+H
^ Step Into F11 Modules Ctrl+Alt+U
Step Over F1 □ Memory
^■1 Step Out S hift+F11 Disassembly Ctrl+Alt+D
rirf QuickWatch.. Ctrl+Alt+Q Registers Ctrl+Alt+G

^ New Breakpoint... Ctrl+B


Clear All Breakpoints Ctrl+Shift+F9
^ϋ) Disable All Breakpoints

Autos “ Output
Name Value Type Debug лі
1U i n d o u s A p p l i c a t i o n l . e x e 1 : Loaded 1f : \ u i n n t A a ^ J

Рис. 2.3. Инструменты отладки, имеющиеся в программе DbgCLR

В табл. 2.9 описаны пункты меню D ebug (Отладка).

Таблица 2 .9 . Средства отладки, имеющиеся в программе DbgCLR


Пункт м еню Н азначение
Continue, Break, Stop, Управление выполнением программы
Restart (Продолжить, Приостановить, Завершить, Перезапустить)
Processes Подключение к процессу и отключение от него
Exceptions Управление воздействием исключений на ход выполнения
программы
Step Into, Over, Out Пошаговое выполнение (Войти внутрь, Обойти, Выйти наружу)
QuickWatch Вычисление выражений, просмотр значений переменных
New, Clear, Disable Работа с контрольными точками
Breakpoints (Поставить, Убрать, Дезактивировать)

Для представления различных аспектов исполнения программы вы можете


пользоваться окнами, перечисленными в табл. 2.10. Доступ к ним дает меню
D ebug/W indow s (О тладка/О кна).

Таблица 2 .1 0 . Окна отладчика DbgCLR


Окно Н азначение
Running Documents Отображает документы, загруженные в отлаживаемое приложение;
позволяет отлаживать сценарии и элементы управления
в этих документах
Watch Отображает указанные вами переменные или выражения
η ιιιι Работа с приложениями

Таблица 2 .1 0 . Окна отладчика DbgCLR (окончание)


Окно Назначение
Autos Отображает локальные переменные и другие элементы
в текущем контексте исполнения
Locals Отображает переменные, объявленные в текущем контекте,
включая объект t h i s
This Окно специально предназначено для просмотра объекта t h i s
Immediate Окно команд, в котором можно набирать выражения и предложения
на языке текущего отлаживаемого модуля; полезно для модификации
переменных, вызова методов и т.д.
Call Stack В момент останова в контрольной точке показывает стек вызовов,
предшествующих текущему кадру, и позволяет передвигаться вверх
и вниз по стеку, чтобы исследовать состояние программы
в предшествующих кадрах
Threads Отображает все потоки текущей программы и позволяет изменять
контекст отладки, а также приостанавливать и возобновлять исполнение
потоков
Modules Отображает информацию о модулях, загруженных программой,
включая номера версий
Memory Отображает неформатированные области памяти, используемые
приложением
Disassembly Дизассемблирует программу и показывает машинный код приложения;
не показывает код на промежуточном языке (IL)
Registers Показывает значения, находящиеся в арифметических и сегментных
регистрах, а также флаги состояния процессора

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


хода выполнения программы. Принципиальное отличие между ними и полномас­
штабным отладчиком, включенным в среду Visual Studio.NET, состоит в том, что
у последнего есть средства, ориентированные на работу в масштабе предприятия.
Учитывая, что в комплекте с .NET Framework SDK поставляются некоторые ми­
нимальные утилиты для управления проектом, можно сказать, что для решения
большинства задач программирования вполне достаточно SDK в сочетании с
хорошим синтаксически-ориентированным редактором.

Структура откомпилированной сборки


Хотя этому часто не уделяют должного внимания, понимание внутренней
структуры и порядка исполнения откомпилированного кода может сильно повы­
сить эффективность отладки. Платформа .NET вводит дополнительный уровень
сложности, связанный с наличием промежуточного языка, но она же предостав­
ляет инструменты для работы с ним. Отладчик способен показать машинный код
на языке ассемблера; для доступа к коду на языке IL используется дизассемблер
промежуточного языка ild a s m .
Утилиту ild a s m допустимо запускать в оконном или текстовом режиме. В
текстовом режиме выходная информация выводится на консоль или в файл и
содержит дизассемблированные метаданные и код внутри сборки; вы можете
Отладка на платформе .NET I I I · · · 85

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


программа. В оконном режиме исследовать опись и код сборки разрешается ин­
терактивно, как показано на рис. 2.4.

f M a in F o r m : :M a in : v o i d ( )
ШШ1
.method private hidebysig static void MainQ cil managed
~2
. e n tr y p o m t
.custom instance void [mscorliblsystem.STAThreadAttribute::.ctorО = £ 01 00 00 00 ]
// Code size 11 £0 xtO
.maxstack S
ГL_0 000 newobj instance void Inetlog.MainFormctorQ
ΓΙ__ΰ0ΰ5 calΊ voi d [System,wi ndows ГForms]System.windows.Forms.Appi icati on::Runnel ass
ll__000 a ret
} // end of method MainForm::маіn

Рис. 2.4. Просмотр дизассемблированного кода в программе ildasm

Точный состав информации, которая выводится в оконном или текстовом


режиме, управляется флагами, задаваемыми в командной строке. Эти флаги опи­
саны в табл. 2.11.
Таблица 2 .1 1 . Флаги программы ild a s m
Ф лаг Н азначение
/all Сокращенное обозначение комбинации флагов /bytes /header /tokens
/bytes Выводить байтовые значения кода
/header Выводить информацию из заголовка РЕ-файла
/item=itemspec Дизассемблировать только itemspec, где itemspec - полностью
квалифицированное имя члена или типа,
например UsedClass: :get_Name
/linenum Включать номера строк исходного текста
/nobar Не выводить во время исполнения информацию о состоянии
/noil Не выводить код на промежуточном языке
/out=file Задать имя файла, в который будет выводиться информация
в текстовом режиме
/pubonly Включать только открытые элементы
/quoteallnames Заключать имена в одиночные кавычки
/raweh Выводить код обработчика исключений
/source Включить исходный текст в виде комментариев к листингу
на промежуточном языке
/text Запустить ildasm в текстовом режиме для вывода информации
на консоль или в файл
/unicode Выводить текст к кодировке Unicode
/tokens Включать лексемы метаданных
/visibility=tla Выводить только элементы с видимостью [ + t l a . . . ],
где tla - это трехбуквенное обозначение области видимости
/U TF8 Выводить текст в кодировке UTF-8
86 ■■■Ill Работа с приложениями

Совет Подробная информация о языке MSIL находится за рамками дан­


ной книги, но полное справочное руководство по самому языку и по
среде исполнения включено в .NET Framework SDK и размещается
в каталоге < s d k _ i n s t a l l _ d i r > \ F r a m e w o r k S D K \ T o o l
D e v e - lo p e r s G u id e \d o c s .

Для любителей докапываться до корней имеется также утилита ila s m , ко­


торая ассемблирует сборку на основе кода, порожденного ild a s m , другими инс­
трументальными программами или созданного вами самостоятельно. Впрочем,
большинству программистов на C# никогда не придется писать код сборки непос­
редственно на языке MSIL; я упомянул эту утилиту только для полноты.

Резюме
Каркас .NET Framework - это весьма развитая платформа для разработки
приложений. Среди инструментальных программ, имеющихся в составе .NET
Framework SDK, есть компиляторы, отладчики и множество других утилит для
создания приложений и управления конфигурированием и развертыванием. Неко­
торые из них были приведены в данной главе. В следующей главе мы завершим
описание среды программирования, предоставляемой каркасом .NET Framework,
познакомившись с библиотекой базовых классов (Base Class Library), к которой
можно обращаться во время исполнения.
Глава 3. Библиотека
базовых классов
Как и любой другой язык, C# зависит от библиотеки времени исполнения. Но для
него таковой является библиотека базовых классов (Base Class Library - BCL),
входящая в состав каркаса .NET Framework. Библиотека BCL должна присутс­
твовать во всех реализациях единой среды исполнения; она включает все классы
из пространства имен System, за исключением следующих:
□ S y s te m . D a ta
□ S y s te m .N e t
□ S y s te m . R e f l e c t i o n
□ S ystem .W eb
□ S y ste m .W in d o w s . Form s
□ S ystem .X m l
BCL упрощает программирование, расширяя предоставляемую CLR поддерж­
ку исполнения и компиляции за счет обширной библиотеки классов, контейнеров
и системных API, доступных всем языкам программирования и компиляторам
на платформе .NET. Программистам, работающим на нескольких языках, больше
нет нужды изучать различные модели программирования и библиотеки времени
выполнения. Код и данные теперь могут использоваться приложениями, написан­
ными на разных языках.
Библиотека BCL расширяема. В .NET вы можете наследовать классам, даже
если у вас нет их исходного текста, при условии, что класс не помечен в сборке
атрибутом s e a l e d . Во время исполнения производный класс трактуется так, как
если бы вы сами разработали и базовый класс.

Архитектура и профили
Платформа .NET выстроена с использованием многоуровневой архитектуры
(рис. 3.1). CLR предоставляет базовые средства, a BCL находится выше. На этом
фундаменте создаются дополнительные библиотеки, приложения и инструмен­
тальные программы.
На платформе .NET существует концепция профиля - предопределенной кон­
фигурации BCL, CLR и некоторых других библиотек, обеспечивающая конкрет­
ную функциональность. Профиль подстраивается под нужды аппаратуры или
приложения, например для мобильного телефона или домашнего бытового прибо­
ра. На данный момент определено только два профиля: Kernel (Ядро) и Compact
(Компактный).
Illll Библиотека базовых классов

Прикладные программы Средства разработки

Классы для работы с данными, XML, отражением и сетью

Библиотека базовых классов

Библиотека инфраструктуры времени выполнения

Единая среда исполнения

Рис. 3.1. Библиотеки каркаса .NET Framework расположены


на разных уровнях

Профиль Kernel обеспечивает минимум, необходимый для соответствия спе­


цификации CLI. В нем нет поддержки математики с плавающей точкой, сложных
массивов, отражения и возможностей взаимодействия с удаленными объектами.
Он содержит только BCL, компилятор и исполняющие механизмы.
Профиль Compact добавляет поддержку XML, работы с сетью и отражения.
Этот профиль предназначен для компактных приложений .NET, используемых
в мобильных устройствах, бытовой технике и другом подобном оборудовании,
которое не обладает достаточным количеством ресурсов.

Строки и регулярные выражения


Язык C# имеет развитые средства для манипулирования строками, которые
на самом деле реализованы на уровне CLR. Строки можно гибко форматировать,
а также сравнивать и осуществлять поиск, пользуясь классами из пространства
имен S y s te m . T e x t . R e g u la r E x p r e s s io n s .
Применять строки можно по-разному. В листинге 3.1 проиллюстрировано
несколько способов.
Листинг 3.1. Примеры действий со строками
using System.Text;

string si, s2;


StringBuilder sb;

// Простая инициализация; строкам присваиваются ссылки


// на литеральные объекты,
si = "This is a test.";
s2 = "this is a test.";
Строки и регулярные выражения I I I · · · · 89

sb = new StringBuilder();
// Сравнение с учетом и без учета регистра,
if ( string.Compare( si, s2 , true ) == 0 )
sb.Append( "Строки одинаковы." );
else
sb.Append( "Строки не одинаковы." );

char с = sl[7]; // Получить символ в позиции 7.


char [] ar = new char[20];

sl.CopyTo( 0, ar, 0, si.Length ); // Копировать


// диапазон символов.

CharEnumerator се = si.GetEnumerator(); // Перебор,


while ( ce.MoveNext() )
{
sb.Append( ce.Current );
}

if ( sl.EndsWith( "тест." ) ) // Проверка окончания строки,


sb.Append( " Строка кончается словом тест." );

s2 = (string)si.Clone(); // Копирование ссылки - s2 теперь


// указывает на строку si.

int і = si.IndexOf("is"); // Возвращает 2.


і = si.LastIndexOf("is"); // Возвращает 5.

string s3 = si.Insert(5, "value"); // Необходимо создать


// новую строку,
// так как строки
// неизменяемы.
sb.Append(" " + s3);

s3 = si.PadLeft(20, " "); // Выравнивает si на правую


// границу в области шириной
// 20 символов.
si = s3.Trim(); // Удаляет заполняющие пробелы;
// можно также использовать
// TrimLeftO и TrimRight () .
s3 si.ToLower(); // Переводит в нижний регистр.
Единая среда исполнения предоставляет также пространство имен Sys­
tem. Text .RegularExpressions, в котором находятся классы для сопостав­
ления с образцом. Функциональность многих методов поиска внутри строки (на­
пример, EndsWith () ) легко реализовать с помощью регулярных выражений.
В листинге 3.2 демонстрируются некоторые способы применения класса Regex.
Ε 3 3 ····ΙΙ Библиотека базовых классов

Листинг 3.2. Использование регулярных выражений


1 // Иллюстрирует некоторые способы применения регулярных
II выражений.
2 protected string MatchEval( Match m )
3 {
4 // Добавить символ новой строки и вернуть управление
5 return m .ToString() + "\n";
6 }
7
8 private void DemoRegexp()
9 {
10 StringBuilder sb = new StringBuilder();
11 string si, s2;
12 bool b;
13
14 // Подготовить строки, они будут использоваться
15 // на протяжении оставшейся части примера.
16 si = "This is a test?";
17 s2 = "this is a test.";
18
19 Regex r = new Regex("test");
20
21 int і = r.Match( si ).Index; // To же, что
// sl.IndexOf( literal
22
23 r = new Regex("test.$");
24 b = r .IsMatch(si, 0); // To же, что
25 // sl.EndsWith( literal
26
27 s2 = "This is a really long, aimless sentence that has
28 + "real purpose but to illustrate using regular
expressions.";
29
30 // Разбить предложение на слова.
31 r = new Regex(@"\S*[ \.]");
32 MatchCollection me = r .Matches(s2, 0);
33 foreach ( Match m in me )
34 {
35 // Обработать каждое слово -
// я просто вывожу их на экран.
36 System.Windows.Forms.MessageBox.Show
37 ( m .Captures[0 ].ToString (), "Capture" );
38 }
39
40 // Вывести все слова предложения по одному в строке.
41 s2 = г.Replace ( s2, new MatchEvaluator( MatchEval )
42 System.Windows.Forms.MessageBox.Show (s2, "Capture");
43 }
Строки и регулярные выражения I I I · · · · 91

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


ке текстов. Впервые я столкнулся с ними, когда программировал на платформе
UNIX в 1990 году, и уже тогда они не были новостью. Примеры в листинге 3.2
раскрывают лишь малую толику возможностей регулярных выражений, но все же
иллюстрируют основные идеи.
Регулярное выражение - это образец, который может содержать обычный
текст и специальные символы (называемые также метасимволами), управляющие
порядком сопоставления. Такой образец передается конструктору класса Regex,
как показано в строке 19. В данном случае образец представляет собой простой
литерал - строку из одного слова test. Затем (строка 21) я использую метод
Match объекта Regex, чтобы найти этот образец в строке si.
В строке 23 используется другой образец, который содержит два метасимво­
ла: точку и знак доллара. Regex считает, что точка соответствует одному любому
символу, а символ $ - концу строки1. Поэтому регулярное выражение test.$
сопоставляется с любой строкой, в которой между словом test и концом строки
есть ровно один символ. Следовательно, в строке 24 булевской переменной b будет
присвоено значение true, так как слово test отделяет от конца строки si только
вопросительный знак.
В строках 31-38 демонстрируется способность регулярных выражений (и клас­
са Regex) находить несколько соответствий за один раз. Образец в строке 31
ищет последовательности символов, заканчивающиеся либо пробелом, либо кон­
цом строки. Символ @перед образцом облегчает его запись, отключая обработку
еБсаре-последовательностей в С#; иначе мне пришлось бы вместо каждого символа
\ записывать два таких же.
Первым элементом регулярного выражения является последовательность \ S *.
Метасимвол \ S соответствует любому непробельному символу. Звездочка говорит,
что число таких символов может быть произвольным (в том числе равным нулю).
Следующий элемент - это список символов, заключенный в квадратные скобки,
[ \ . ]. В позиции этого списка Regex будет искать любой символ из числа пере­
численных внутри скобок, то есть в данном случае - пробел или точку (послед­
няя экранирована, чтобы трактовалась как обычный, а не специальный символ2).
Таким образом, данное регулярное выражение ищет любую последовательность
непробельных символов, заканчивающуюся либо пробелом, либо точкой, иначе
говоря - отдельные слова в предложении.
Подготовив образец для поиска, я в строке 32 использую его для поиска со­
ответствий, но теперь меня интересуют все вхождения, а не только первое. Метод

1 Здесь и ниже под «строкой» понимаются два разных образования: последовательность


символов, завершающихся символом ' \п', и объект класса String. Рассматриваемые
в примере регулярные выражения прекращают просмотр по достижении конца строки
в любом из этих смыслов. Но в принципе существует возможность продолжить сопостав­
ление, не останавливаясь на символе ' \ п '. - Прим. перев.
2 Это не нужно, так как внутри квадратных скобок точка (а равно и некоторые другие ме­
тасимволы) теряет свое специальное значение. - Прим. перев.
Illll Библиотека базовых классов

C ap ture f*l Regex .Mat ches () возвращает все найденные соответствия


This в виде Ha6opaMatchCollection, состоящего из объектов клас­
са Match. Каждый объект Match включает набор Captures,
really
long, который содержит фрагмент текста, сопоставленный с образцом.
aimless
sentence Для простого образца типа рассмотренного выше в этом наборе
that
has будет всего один элемент, но можно построить и более сложные
nc
real регулярные выражения, которые сопоставляются с несколь­
purpose
but кими фрагментами. В конце (строки 36 и 37) каждое слово по
to
illustrate
очереди показывается в окне сообщения.
using
regular
Регулярные выражения пригодны не только для поиска,
expressions.
их используют также для изменения текста с помощью метода
OK Replace (). Вы можете просто передать замещающую найден­
ный текст строку или пойти дальше - указать метод, который
будет обрабатывать каждое соответствие; допустимо также ис­
Рис. 3.2. Пре­
образованная
пользовать оба способа одновременно. В строках 41 и 42 приме­
строка, которая няется делегирующий метод, который подключает функцию
выводится MatchEval () к процессу выполнения замены. В строке 41 вызы­
программой вается метод г . Replace (), которому передается строка s2 и деле­
в листинге 3.2 гат MatchEvaluator, инициализированный ссылкой на функцию
MatchEval (). Объект г класса Regex вызывает этот делегат для
каждого соответствия, найденного в исходной строке; строка, возвра­
щаемая функцией MatchEval (), заменяет найденный текст в строке, которую воз­
вращает Replace (). В данном случае регулярное выражение выделяет каждое слово,
aMatchEval () добавляет к нему в конец символ новой строки (строка 5). Получивша­
яся в результате замены строка выводится в окне сообщения в строке 42 (рис. 3.2).
Это было лишь краткое введение в регулярные выражения на платформе .NET.
О них написаны целые книги, но мы должны двигаться дальше. Более подробную
информацию о регулярных выражениях можно найти в Internet или в книге Alexia
Prendergrast «Teach Yourself Regular Expressions in 24 Hours» издательства Sams.

Контейнеры
Наборы и контейнеры применяются в программировании часто, и .NET вклю­
чает различные библиотечные классы такого рода, в том числе упорядоченные
и неупорядоченные списки, стеки, очереди и словари. В табл. 3.1 перечислены
основные контейнерные классы, имеющиеся в каркасе .NET Framework.
Таблица 3 .1 . Контейнерные классы на платформе .NET
Контейнер Назначение
ArrayList Динамически растущий список с доступом, как к массиву
BitArray Массив битовых (булевских) значений
Hashtable Словарь, организованный в виде хэш-таблицы
Queue FIFO-очередь (first in, first out - первым пришел, первым обслужен)
SortedList Словарь, отсортированный по ключу
Stack LIFO-стек (last in, first out - последним пришел, первым обслужен)
Контейнеры I I I · · · ·

В листинге 3.3 продемонстрировано применение этих классов.


Листинг 3.3. Использование основных контейнеров
1 : public class KeyValue
2: {
З: string key;
4: string val;
5:
6: public KeyValue( string newKey,string newValue )
7: {
8: key = newKey;
9: val = newValue;
10 : }
11 :
12: public override string ToStringO
13 : {
14: return "Класс KeyValue: Ключ: " + key + " Значение
+ val ;
15: }
16 :
17: public string GetKey() { returnkey; }
18: public string GetValue() { returnval; }
19 : }
20 :
21: class CollectionDemo
22 : {
23: static void Main(string[] args)
24 : {
25: KeyValue [] kv = new KeyValue[]
26 : {
27: new KeyValue( "1", "значение 1" ),
28: new KeyValue( "2", "значение 2" ),
29: new KeyValue( "3", "значение 3" ),
30: new KeyValue( "4", "значение 4" ),
31: new KeyValue( "5", "значение 5" )
32: };
33 :
34: bool[] b = { true, false, false, true, true, false
35 :
36: ArrayList al = new ArrayList();
37: foreach (KeyValue k in kv )
38 : {
39: //Добавить в массив.
40: al.Add( k );
41 : }
42 :
43: Hashtable ht = new Hashtable();
44: Queue q = new Queue();
45: Stack s = new Stack();
46 :
Библиотека базовых классов

47: Console.WriteLine( "\nArrayList:" );


48: foreach ( KeyValue k in al )
49 : {
50: Console.WriteLine( k );
51:
52: // Добавить в хэшированнуютаблицу.
53: ht.Add( k.GetKey(), k.GetValue() );
54 :
55: // Поместить в очередь.
56: q.Enqueue( k );
57 :
58: // Поместить в стек.
59: s .Push( к );
60 : }

62: // П о и с к по ключу.
63: Console.WriteLine( "\nHashtable:" );
64: Console.WriteLine( ht["4"] );
65 :
66: // Объекты возвращаются в порядке добавления.
67: Console.WriteLine( "\nQueue:" );
68: while ( q.Count > 0 )
69 : {
70: Console.WriteLine( q.Dequeue() );
71: }
72 :
73: // Объекты возвращаются в противоположном порядке.
74: Console.WriteLine( "\nStack:" );
75: while ( s.Count > 0 )
76: {
77: Console.WriteLine( s .Pop() );
78 : }

80: Console.WriteLine( "\nBitArray:" );


81: BitArray ba = new BitArray(b);
82 : foreach ( bool bv in ba )
83 : {
84: Console.WriteLine( bv );
85: }

87: // Добавление в отсортированный список


// в произвольном порядке.
88: SortedList si = new SortedList();
89: int [] order = { 3, 4, 1, 2, 0 };
90: foreach ( int і in order )
91: sl.Add( k v [і].GetKey(), k v [і].GetValue() );
92 :
93: // Получение объектов, отсортированных по ключу.
94: Console.WriteLine( "\nSortedList:" );
95: foreach ( DietionaryEntry de in si )
Контейнеры I I I · · · ·

96 Console.WriteLine( de.Key + + de.Value );


97
98
Для демонстрации использования контейнеров нужно множество объектов,
которые будут находиться в контейнере, поэтому листинг 3.3 начинается с объ­
явления класса KeyValue для хранения пар строк. В этом классе переопределен
метод Obj ect.ToString () для вывода объектов на экран. В строках 25-32 со­
здается и заполняется массив из пяти таких объектов. Для тестирования класса
Bit Array в строке 34 объявляется дополнительный массив из булевских вели­
чин.
Первым в примере исследуется класс ArrayList, экземпляр которого объяв­
лен в строке 36. Этот класс сочетает поведение массивов и списков, предоставляя
неупорядоченный контейнер, к которому можно обращаться с помощью синтакси­
са доступа к элементам массива (например, al [ 3 ] ), или использовать нумератор,
как в примере. В строках 37-41 в массив вставляются все объекты типа KeyValue,
после чего в цикле foreach, который начинается в строке 48, они поочередно
выводятся на консоль.
При обходе контейнера ArrayList в цикле foreach объекты заодно копи­
руются в контейнеры Hashtable, Queue и Stack. Контейнер Hashtable может
хранить любые объекты и пользуется методом GetHashCode () для получения
целочисленного хэшированного значения ключа каждого объекта. Этот метод
можно переопределить, задав собственный алгоритм хэширования, или, как сде­
лано в листинге 3.2, положиться на реализацию в классе Obj ect. Для получения
объектов из контейнера Hashtable применяется нотация доступа к элементам
массива (строка 64). Ради повышения эффективности класс Hashtable раскла­
дывает объекты по «ящикам» в соответствии с их хэш-кодом и производит поиск
только в том ящике, куда попал хэш-код запрошенного ключа.
Класс Queue предназначен для хранения объектов в порядке вставки и извле­
чения их в том же порядке. Для помещения объекта в контейнер служит метод
Enqueue () (строка 56), а для извлечения объекта из очереди - метод Dequeue ()
(строка 70).
Класс Stack работает противоположным образом. Объекты извлекаются из
стека в порядке, обратном порядку вставки. Для помещения (заталкивания) объек­
та в стек применяется метод Push (), а для извлечения (выталкивания) - метод
Pop (). В строках 74-78 демонстрируется извлечение объектов из стека.
В контейнере BitArray хранятся битовые значения, возвращаемые как ве­
личины типа b o o l. В строке 34 объявлен массив элементов типа b o o l, а в строке
81 он используется для инициализации контейнера BitArray. В конструкторе
объекта разрешается также указать размер битового массива. Доступ к отдельным
элементам массива осуществляется с помощью нотации [ ] с целочисленным ин­
дексом. Контейнер BitArray может также выполнять над своими элементами
операции AND, NOT, OR и XOR. Например, следующий код маскирует первые два
бита каждого элемента массива Ьа с помощью операции поразрядного логического
умножения:
Ε1 ····ΙΙΙ Библиотека базовых классов

bool[] Ь2 = { false, false, true, true, true, true };


BitArray ba2 = ba.And( new BitArray( b2 ) );
Последний контейнер, который мы рассмотрим, - SortedList - упорядочи­
вает свое содержимое по значениям ключей. В строках 88-91 создается экземпляр
класса SortedList, затем в него вставляются тестовые элементы в произвольном
порядке. Однако извлекаются элементы уже в порядке возрастания ключа:
SortedList:
1, значение 1
2, значение 2
3, значение 3
4, значение 4
5, значение 5
Как и к другим контейнерам, к SortedList можно обращаться либо с помо­
щью индексатора, либо по значению ключа:
string str = (string)si["1"];
Это основные контейнеры, предоставляемые каркасом. Однако для специ­
альных элементов есть и другие. Все они реализуют интерфейс ICollection и
описаны в справочном руководстве по каркасу .NET Framework.

Сериализация
Сериализация - это представление состояния объекта в виде, допускающем
сохранение на диске или передачу по сети, с последующим восстановлением
объекта в другом контексте. Платформа .NET поддерживает сериализацию за
счет разделения концепций пункта назначения (файл, сетевое соединение и т.д.),
трансформации (преобразования объекта в поток байтов и обратно) и собственно
объекта.
Преобразование объекта в поток байтов и обратно выполняется объектами-
форматерами. Они содержатся в пространствах имен, вложенных в System.
Runtime .Serialization.Formatters. В состав .NET входят два готовых фор­
матера: один записывает двоичную копию объекта (BinaryFormatt er), а другой
сохраняет объект в виде SOAP-конверта (SoapFormatter). BinaryFormatter
преобразует объект в компактное двоичное представление, которое быстро считы­
вается; это решение годится для сохранения объекта на диске или для передачи
объектов по сети между аналогичными платформами. С другой стороны, представ­
ление в формате конверта SOAP можно считать и использовать на любой платфор­
ме, поэтому оно полезно для работы в гетерогенных системах. Однако ничто не
дается даром: разница в размере между двоичным и SOAP-представлением одного
и того же объекта весьма значительна.
В листинге 3.4 демонстрируется сериализация объектов для записи на диск
и последующего считывания в память.
Л ист инг 3.4. Сериализация с помощью форматеров
1: using System;
2: using System.IO;
Сериализация I I I · · · · 97

з using System.Collections;
4 using System.Runtime.Serialization;
5 using System.Runtime.Serialization.Formatters.Binary;
6 using System.Runtime.Serialization.Formatters.Soap;
7
8
9
10 [Serializable]
11 class StreetAddress
12 {
13 public int id;
14 public string name, streetl, street2 , city, state, zip;
15
16 public StreetAddress()
17 {
18 name = streetl = street2 = city = state = zip =
19 id = 0;
20 }
21
22 public StreetAddress(int inld, string inName, string
23 inStreetl, string inStreet2,
24 string inCity, string instate, string inZip)
25 {
26 id = inld;
27 name = inName;
28 streetl = inStreetl;
29 street2 = inStreet2;
ЗО city = inCity;
31 state = instate;
32 zip = inZip;
33 }
34
35
36 class Serializer
37 {
38 static void Main(string[] args)
39 {
40 ArrayList addresses = new ArrayList(10);
41
42 // Создать список адресов.
43 for ( int id = 0; id < 10; id++ )
44 {
45 addresses.Add( new StreetAddress(id, "AName",
46 "12 3 Main St.", "Ste. 800",
47 "Anywhere", "AK", "12345") );
48 }
49
50 // Вывести информацию в формате XML.
51 IFormatter soapFmt = new SoapFormatter();
52 Stream s = File.Open( "outfile.xml", FileMode.Create
ιη ιιι Библиотека базовых классов

53: soapFmt.Serialize( s, addresses );


54 : s .Close();
55 :
56: // Вывести информацию в двоичном виде.
57: IFormatter binFmt = new BinaryFormatter();
58: s = File.Open("outfile.bin", FileMode.Create);
59: binFmt.Serialize( s, addresses );
60 :
61: s .Close() ;
62 :
63: // Заново открыть файл и прочитать данные.
64: s = File.Open( "outfile.bin", FileMode.Open );
65: addresses = binFmt.Deserialize( s ) as ArrayList;
66 :
67: for ( int і = 0; і < addresses.Count; i++ )
68: Console.WriteLine(
69: ((StreetAddress)addresses[i]).id.ToStringO
+ " " +
70: ((StreetAddress)addresses[i]).name);
71: }
72 : }
В классе StreetAddress хранится типичная адресная информация. Этот
класс помечен атрибутом Serializable, означающим разрешение сериализа­
ции. Класс Serializer создает набор таких объектов (строки 40-48), а затем
пользуется объектом SoapFormatter для вывода SOAP-версии списка адре­
сов и объектом BinaryFormatter для вывода двоичной версии того же списка.
Необходимо лишь создать форматер (строки 51 и 57), создать поток, в который
будет записываться информация (строки 52 и 58), и вызвать метод Serialize ()
форматера.
В большинстве объектов состояние не исчерпывается набором значений, име­
ются еще и ссылки на другие типы, которые также необходимо воссоздать в ходе
десериализации объекта. Форматер берет на себя исследование объекта и выявля­
ет члены-ссылки. Он обходит граф ссылок и сериализует каждый встретившийся
объект.
Для десериализации объекта из потока используется метод Deserialize ()
форматера того же вида, который выполнял сериализацию. В листинге 3.4
класс Serializer считывает обратно массив адресов из двоичного файла,
для чего предварительно открывает его как поток в строке 64, а затем вызыва­
ет метод BinaryFormatter .Deserialize () в строке 65. Поскольку метод
Deserialize () возвращает значение типа Object, оно приводится к типу
ArrayList.

Ввод и вывод
Даже на совсем новой платформе нужно решать некоторые старые задачи.
Одна из них - обмен информацией между вашей программой и каким-то потре-
Ввод и вывод I I I · · · · 99

бителем. На платформе .NET есть классы для ввода/вывода - как потокового,


так и с произвольным доступом, а также класс System.Console, члены которого
используются консольными приложениями для доступа к стандартным потокам
ввода и вывода. Помимо ввода/вывода в файл и на консоль, .NET также предла­
гает классы для организации других потоков, а именно: в памяти, строковых и
сетевых.
В листинге 3.5 представлена реализация основных файловых операций в но­
тации языка С#.
Листинг 3.5. Базовый ввод/вывод
1: // Создать файл, содержащий данные.
2: string str = "Это произвольный текст.";
3: FileStream fs = File.Create( "testfile.txt" );
4 : byte [] buff;
5:
6: // Преобразовать каждый символ строки str в байты
7: / / и записать их в файл.
8: buff = Encoding.Unicode.GetBytes( str );
9: fs.Write( buff, 0, buff.Length );
10 :
11 : fs .Close() ;
В листинге 3.5 объект FileStream используется для создания текстового
файла и последующей записи в него строки. Статический метод File .Create ()
создает файл на диске и возвращает ассоциированный с ним потоковый объект
FileStream. В строке 8 мы получаем байты, составляющие строку, а в строке 9
выводим их в поток. Наконец, в строке 11 поток закрывается.
Работать напрямую с массивами байтов можно, если в этом действительно есть
необходимость, но часто такой низкий уровень ни к чему. Поэтому в библиотеке
BCL есть классы более высокого уровня, упрощающие ввод и вывод. В листинге
3.6 показано, как достичь того же результата более простым способом с помощью
класса StreamWriter.
Листинг 3.6. Использование класса StreamWriter для упрощения ввода/вывода
string str = "Это произвольный текст.";
StreamWriter sw = new StreamWriter("testfile.txt", false);
sw.Write( str );
sw.Close();
Класс StreamWriter упрощает считывание и запись в поток строковых зна­
чений. Кроме того, он автоматически записывает в текстовый файл преамбулу
Unicode (OxFEFF), так что операционная система и другие программы могут рас­
познать кодировку файла и порядок байтов. Если вы хотите записать обычный
ASCII-текст, передайте объекту StreamWriter кодировку:
StreamWriter sw = new StreamWriter("testfile.txt", false,
System.Text.Encoding.ASCII);
100 ■■■III Библиотека базовых классов

Чтобы еще больше упростить операции ввода/вывода, .NET поддерживает


композицию потоков. Например, класс BinaryWriter позволяет считывать и
записывать значения базовых типов, избавляя вас от необходимости выполнять
промежуточные шаги по получению их двоичных представлений. В классе Binary
Writer определены различные методы Write () для каждого из базовых типов,
а также для массивов элементов типа byte и char. Приведенный ниже код выво­
дит массив байтов в файл:
byte [] ar = new byte[10];

// Инициализировать массив значениями от 0 до 9.


for ( byte і = 0; і < 10; ar[i] = і, і++ );

FileStream fs = File.Create( "testfile.dat" );


BinaryWriter bw = new BinaryWriter( fs );
b w .Write( ar );
b w .Close();
Записать массив символов ничуть не сложнее:
string str = "Это произвольный текст.";
char [] cha = str.ToCharArray();

FileStream fs = File.Create( "testfile.dat" );


BinaryWriter bw = new BinaryWriter( fs );

b w .Write( cha );
b w .Close();
Для чтения из потока имеются симметричные классы. Так, считывание из
строки программируется следующим образом:
string str;

StreamReader sr = File.OpenText( "testfile.txt" );


str = sr.ReadToEnd();

Console.WriteLine(str) ;

sr.Close();
До сих пор мы видели только синхронные вызовы, когда управление не воз­
вращается вызывающей программе до полного завершения операции. Иногда,
однако, необходим асинхронный ввод/вывод, особенно если речь идет о чтении
с консоли или из сети. Для включения асинхронного режима нужно сконстру­
ировать объект класса FileStream с подходящими параметрами и вызвать его
метод BeginRead (), начинающий операцию ввода/вывода. Этот метод принимает
в качестве одного из параметров делегат, инициализированный адресом метода,
который будет вызван, когда ввод/вывод завершится. Другим параметром Begin­
Read () может быть переменная состояния, передаваемая методу обратного вызо­
ва. В листинге 3.7 демонстрируется эта техника.
Ввод и вывод I I I · · · ·

Листинг 3.7. Использование асинхронного ввода/вывода


1 struct Readlnfo
2 {
3 public FileStream fs;
4 public byte [] ba;
5 public long bufSz;
6 public ManualResetEvent ev;
O' 00

9 static void ReadCallback( IAsyncResult res )


10 {
11 Readlnfo ri = (Readlnfo)res.AsyncState;
12 for ( int і = 0; і < ri.bufSz; i++ )
13 Console.Write( ri.ba[i] );
14
15 ri.fs .Close () ;
16 ri.ev.Set() ;
17 }
18
19 static void Main(string[] args)
20 {
21 // Создаем делегат для обратного вызова.
22 AsyncCallback ас = new AsyncCallback( ReadCallback );
23 Readlnfo ri = new Readlnfo() /
24
25 // Открываем файл.
26 ri.fs = new FileStream(
27 "testfile.dat", // Путь.
28 FileMode.Open,// Режим открытия.
29 FileAccess.Read, II Запрошенные права доступа
30 FileShare.None, II Режим совместного
II использования.
31 256 , II Размер буфера.
32 true II Асинхронно?
33 );
34
35 // Задаем члены структуры, в которой передается состояние
36 ri.bufSz = ri.fs.Length;
37 ri.ba = new byte [ri.bufSz];
38 ri.ev = new ManualResetEvent ( false );
39
40 // Начинаем операцию чтения.
41 ri.fs.BeginRead(
42 ri.ba, II Буфер.
43 0, II Смещение от начала.
44 (int)ri.fs.Length, II Сколько байтов считать.
45 ac, II Обратный вызов.
46 ri II Объект состояния.
47 );
■ ■ ■ ■ Ill Библиотека базовых классов

48
49 // Ждем завершения дочернего потока,
50 ri.ev.WaitOne();
51
Для хранения различной информации, относящейся к операции ввода/вы-
вода, - потока, адреса буфера, размера буфера и объекта синхронизации - мы
вначале объявляем структуру Readlnfo. Затем необходимо определить метод,
который будет получать данные после завершения операции считывания, - Read
Callback (). И, наконец, код в методе Main () связывает все воедино.
В строке 22 создается делегат AsyncCallback, инициализированный ме­
тодом ReadCallback (), а в строке 23 - объект состояния операции. В строках
26-33 я задаю новый поток FileStream с подходящими параметрами, самым
важным из которых является последний - булевский признак, установленный
в true, чтобы сообщить объекту FileStream о необходимости организовать
асинхронный режим. Открыв поток, я воспользовался свойством Length для ус­
тановки размера буфера в структуре r і (строки 36 и 37), а затем создал событие
синхронизации, которому будет послан сигнал о завершении операции.
По окончании предварительной подготовки метод BeginRead () в строке 41
начинает операцию считывания. Ему передается буфер, позиция внутри буфера,
начиная с которой следует помещать данные (в нашем случае 0), число ожидаемых
байтов, делегат и структура, описывающая состояние. Потом главному потоку
остается только ждать события синхронизации.
Метод BeginRead () возвращает управление немедленно, но, разумеется, это
не означает, что считывание закончилось. Собственно операция выполняется в от­
дельном потоке, потому мы и создали событие ManualReset Event. Пока главный
поток ждет сигнала для этого события, система выполняет операцию и передает
ее результаты методу ReadCallback (), который выводит полученные данные на
консоль (строки 11-13), закрывает файл (строка 15) и сигнализирует событию.
Теперь главный поток возобновляет выполнение в строке 51, где программа нор­
мально завершается.
Хотя для организации асинхронного режима приходится потрудиться, во мно­
гих случаях, особенно когда речь идет о серверах или распределенных приложе­
ниях, асинхронный ввод/вывод абсолютно необходим. Для небольших блоков
данных это, конечно, излишне, но если размер передаваемых данных велик или
заранее неизвестен, то асинхронный режим позволит избежать зависания програм­
мы в ожидании завершения ввода/вывода.

Сетевые коммуникации
Модель сетевой коммуникации в C# такая же, как в традиционных языках, но
пользователю не нужно заботиться о различных мелких деталях. Программи­
рование на уровне сокетов по-прежнему остается непростым делом, но сущес­
твуют классы-обертки, упрощающие решение типичных задач. Пространства
имен System.Net технически не являются частью библиотеки базовых классов; я
включил их обсуждение в эту главу, поскольку сетевые коммуникации сегодня вы­
Сетевые коммуникации I I I · · · · 103

ходят на первый план. Каркас и сама операционная система Windows поддерживают


различные протоколы, но я решил ограничиться протоколом IP (Internet Protocol)
и программированием сокетов, так как большинство читателей будет заниматься
сетевыми задачами только в рамках семейства протоколов T C P /IP .
В настоящее время наиболее распространены два протокола: Transmission
Control Protocol (TCP - протокол управления передачей) и User Datagram Protocol
(U D P - протокол пользовательских дейтаграмм). В каждом из них клиент посылает
дейтаграмму, в которой, в частности, указан его собственный IP -адрес и номер порта
- логический идентификатор программы или сервиса, работающего на компьюте­
ре. К примеру, Web-cepBep обычно прослушивает порт 80 в ожидании входящих
запросов.
Протокол U D P организует ненадежную доставку пакетов от одного компью­
тера к другому, не устанавливая постоянного соединения. Под ненадежностью
протокола понимается тот факт, что он не содержит механизма для проверки, до­
ставлены ли данные получателю. U D P также не может гарантировать, что удален­
ный компьютер получит пакеты в том же порядке, в котором они были отправлены.
Программы, пользующиеся протоколом UDP, должны самостоятельно контроли­
ровать ошибки или реализовывать механизм квитирования.
Напротив, TCP - это протокол с постоянным соединением, который гаранти­
рует, что данные, отправленные одной программой, будут доставлены получателю
в том же порядке, а если это невозможно, то отправитель будет уведомлен об
ошибке. По приходе каждого ожидаемого пакета получатель посылает отправите­
лю подтверждение. Если отправитель не получит подтверждения в установленное
время, он пошлет потерявшийся пакет заново. Если число ошибок превысит
максимальный порог, TCP известит приложение, пытающееся послать данные,
об ошибке. Программировать сетевые коммуникации в таком режиме проще,
чем в случае UDP, но для передачи одного и того же объема информации здесь
потребляется больше сетевых ресурсов.

Сокеты
Сокет - это оконечная точка коммуникации между двумя программами, через
которую информация может передаваться в обоих направлениях. Чтобы орга­
низовать канал с использованием сокетов, необходимо задать пять элементов:
IP -адреса и номера портов отправителя и получателя, а также протокол обмена
(обычно TCP или U D P).
Первая программа, желающая принять участие в обмене данными, создает
сокет и привязывает его к указанным порту и протоколу на своем хосте. Вторая
программа создает свой сокет и с его помощью соединяется с первой. На момент
установления соединения (в случае TC P) или отправки пакета (в случае U D P)
пять элементов, однозначно определяющих канал, уже известны: адрес и номер
порта машины на каждом конце канала и вид протокола.
В случае использования протокола U D P постоянное соединение между при­
ложениями отсутствует; пакет посылается в надежде, что он дойдет до адресата.
При работе по протоколу T C P между двумя маш инами устанавливается ви р­
104 ■■■III Библиотека базовых классов

туальный канал, который существует, пока то или другое прилож ение его не
закроет. Канал на каждом конце обслуж ивается T C P -провайдерами, которые
обмениваются информацией о состоянии для управления транспортировкой
данных.

Коммуникация с помощью сокетов


В большинстве сценариев работы с сокетами одно приложение запускается
и начинает прослушивать сокет. Позже запускается другое приложение, которое
посылает информацию первому, уже работающему. Для удобства я буду называть
первое приложение сервером, а второе - клиентом.
Первым шагом процедуры подготовки к обмену данными, который должны
выполнять и клиент, и сервер, является создание объекта S o c k e t и задание для
него адресного семейства, адреса и номера порта, с которыми будет ассоцииро­
ван сокет. Дальнейшее зависит от того, как именно вы собираетесь осуществлять
коммуникацию.
Для T C P -сокетов, ориентированных на наличие соединения (потоковых соке­
тов), сервер переводит сокет в состояние прослушивания и ожидает запросов на
соединение. Программа на стороне клиента отправляет запрос на установление
соединения, указывая адрес сервера и номер порта. Как только соединение будет
установлено, программы приступают к обмену данными через сокет.
U D P -сокеты, не нуждающиеся в соединении, не требуют дополнительной
настройки. Поскольку между машинами не существует постоянного соединения,
сервер сразу приступает к прослушиванию порта, а клиент может посылать сооб­
щения. Поскольку сервер не возвращает подтверждений, операция отправки сооб­
щения немедленно возвращает управление. В листинге 3.8 показано, как посылать
и принимать сообщения в виде дейтаграмм.
Листинг 3.8. Обмен дейтаграммами через UDP-сокет
1: using System;
2: using System.Net;
3: using System.Net.Sockets;
4: namespace Comm
5: {
6: class Communication
7: {
8: const int sendPort = 20000;
9: const int bufSize = 256;
10: voidStart( string ipremote )
11 : {
12: Socket soc;
13: int bytesMoved = 0; // Число переданных байтов.
14: byte [] buf = new byte[bufSize]; // Буфер данных.
15: // Настроить локальный адрес сокета.
16: IPEndPoint localEp = new IPEndPoint(IPAddress.Any,
sendPort);
17: // Создать сокет для обмена дейтаграммами.
18: soc = new Socket(
Сетевые коммуникации ΙΙΙΗ Ι
19: AddressFamily.InterNetwork,
20: SocketType.Dgram,
21: ProtocolType.Udp );
22: // Привязать оконечную точку обмена к сокету.
23: soc.Bind( localEp );
24: if ( ipremote == "" )
25: {
26: // Режим сервера - прослушивание.
27: bytesMoved = soc.Receive( buf );
28 : }
29: else
30 : {
31: // Режим клиента - передача.
32: IPEndPoint remote =
33: newIPEndPoint( IPAddress.Parse(ipremote),
sendPort);
34: for ( int і = 0; і < bufSize; buf[i] = (byte)і, i++ );
35: bytesMoved = soc.SendTo( buf,remote );
36: }
37: Console.WriteLine( "Transferred {0}bytes.",
bytesMoved );
38 : }
39 :
40: static void Main(string[] args)
41: {
42: Communication comm = newCommunication();
43: // Если в командной строке задан ІР-адрес
// удаленного хоста,
44: // работать в режиме клиента, иначе - в режиме сервера.
45: if ( args.Length == 1 )
46 : {
47: comm.Start( args[0] );
48 : }
49: else
50: comm.Start( "" );
51: }
52 : }
53 : }
Здесь импортируются пространства имен System, System.Net и System.
Net. Sockets. Большинство классов, необходимых для работы с сокетами, нахо­
дятся в пространстве имен System.Net .Sockets, но нужны также некоторые
классы из пространства имен System.Net.
Точкой входа в программу является функция Main (), начинающаяся в стро­
ке 40. Программа запускается в режиме клиента или сервера в зависимости от
того, указан ли в командной строке ІР-адрес. Main () вызывает функцию Start ( ) ,
передавая ей либо ІР-адрес, либо пустую строку, а функция Start () уже выпол­
няет основную работу.
106 ■ ■ ■ III Библиотека базовых классов

Если в параметре remote передан IP -адрес, то функция Start () посылает


блок данных удаленному компьютеру, в противном случае ждет поступления дан­
ных. Первый шаг (строка 16) заключается в создании объекта класса IPEndPoint,
где хранится локальный IP -адрес и номер порта, через который программа будет
обмениваться данными. В строках 18-21 задается новый дейтаграммный сокет для
отправки информации по протоколу UDP. Подготовка сокета к работе завершается
привязкой к оконечной точке с помощью метода Socket.Bind () в строке 23.
Совет Константы S o cke t T y p e . Dgram и P ro t o c o l T y p e . Udp практи -
чески всегда используются вместе; то же можно сказать о конс­
тантах S o c k e tT y p e . S tre a m и P r o t o c o lT y p e . Тср, применяе­
мых для настройки потокового ТСР-сокета.
В строке 24 устанавливается режим работы программы - в зависимости от
того, передан ли IP -адрес. Если программа выступает в роли сервера, то в строке
27 вызывается метод Socket .Receive (), который будет ждать поступления
входной информации. Когда придут данные, библиотека запишет их в буфер b u f
и вернет число полученных байтов. Если же программа работает в режиме клиента,
то она создает оконечную точку с адресом и номером порта для отправки данных
(строка 32), помещает в буфер отправляемые данные (строка 34) и посылает их
(строка 35).
Я перечислил все, что нужно для отправки U D P -дейтаграмм. Как просто было
бы жить, если бы этим сетевое программирование и ограничивалось. Увы, про­
стота U D P объясняется тем, что в нем нет никакого механизма для управления
доставкой. В следующем примере (листинг 3.9) мы используем протокол TCP,
который устанавливает соединение и гарантирует, что данные, отправленные кли­
ентом, будут получены сервером.
Л ист инг 3.9. Обмен данными через потоковый ТСР-сокет
1: using System;
2: using System.Net;
3: using System.Net.Sockets;
4: namespace Comm
5: {
6: class Communication
7: {
8: Socket soc;
9: const int sendPort = 20000;
10: const int bufSize = 256;
11 :
12: void Start( string ipremote )
13 : {
14: int bytesMoved = 0;
15: byte [] buf = new byte[bufSize];
16: IPEndPoint localEp = new IPEndPoint(IPAddress.Any,
sendPort);
17 : soc = new Socket(
18: AddressFamily.InterNetwork,
Сетевые коммуникации III···· 107

19 SocketType.Stream,
20 ProtocolType.Tcp );
21
22 // Привязать посылающий сокет,
23 soc.Bind( localEp );
24
25 if ( ipremote == "" )
26 {
27 // Режим сервера - начать прослушивание,
28 soc.Listen(1);
29
ЗО // Подготовиться к приему запроса на соединение.
31 Socket readSock = soc.Accept();
32
33 // По выходе из Accept() в readSock
34 // будет новый уже соединенный сокет.
35 bytesMoved = readSock.Receive( buf );
36
37 // Все сделано.
38 readSock.Shutdown(SocketShutdown.Receive);
39 readSock.Close () ;
40
41 // soc не соединен, его можно закрыть,
42 soc.Close();
43 }
44 else
45 {
46 // Клиент.
47 IPEndPoint remote =
48 new IPEndPoint
( IPAddress.Parse(ipremote),
49 sendPort);
50 for ( int і = 0; і < bufSize; buf[і] = (byte)і, i++ );
51
52 // Соединиться с сервером,
53 soc.Connect(remote);
54 bytesMoved = soc.Send( buf );
55
56 // Разорвать соединение.
57 soc.Shutdown(SocketShutdown.Send);
58 soc.Close();
59
60
61 Console.WriteLine( "Передано {0} байтов.", bytesMoved );
62 }
63
64 static void Main(string[] args)
65 {
66 Communication comm = new Communication();
67
■ ■ ■ ■ Ill Библиотека базовых классов

68 // Установить режим клиента или сервера,


69 if ( args.Length == 1 )
70
71 comm.Start( args[0] );
72
73 else
74 comm.Start( "" );
75
76
77
На первый взгляд эта программа делает то же, что предыдущая, только с помо­
щью потокового T C P-сокета. Первое изменение мы видим в строках 19 и 20, где
тип сокета и протокол заменены на SocketType .Stream и ProtocolType .Тер
соответственно. Локальная оконечная точка привязывается к сокету, как и раньше
(строка 23).
При работе в режиме сервера программа настраивает сокет для прослушива­
ния запросов на входящие соединения путем вызова метода Socket. List en ()
(строка 28), после чего готова подтверждать прием таких запросов методом
Socket .Accept (). Метод Accept () возвращает новый сокет, уже соединен­
ный с клиентом. Хотя я этого делать не стал, но можно было бы вернуться к
ожиданию новых запросов на соединение по исходному сокету. Между моментом,
когда Accept () возвращает объект Socket, и моментом следующего обращения
к Accept () есть промежуток времени, когда программа вообще не прослушивает
сокет, а запросы тем не менее могут поступать. Параметр backlog, передаваемый
методу Listen (), говорит, сколько таких запросов система способна поместить в
очередь в ожидании нового обращения к Accept ( ) .
Получив соединенный сокет, программа в строке 35 вызывает его метод
Receive (), который будет ждать поступления данных и поместит их в предо­
ставленный буфер. На этом операция приема завершается, но нужно еще кое-что
сделать, чтобы нормально закры ть T C P -сокет. П оскольку в сокете хранит­
ся информация о состоянии соединения, наша программа должна сообщить
программе на другом конце, что она хочет закрыть соединение; вызов метода
Socket. Shutdown () в строке 38 инициирует этот процесс. Затем реализации
протокола TCP на обоих концах обмениваются между собой управляющ ими
сообщениями, после чего метод Socket .Close () освобождает ресурсы, свя­
занные с сокетом.
На другом конце соединения работает клиент. Чтобы запросить соединение
с сервером, он вызывает метод Socket .Connect () для сокета, привязанного
к оконечной точке, в которой задан ІР-адрес и номер порта удаленного компью­
тера. Поскольку в T C P -сокете хранится информация о состоянии, то при вызове
метода Send () не нужно в качестве параметра передавать оконечную точку, как
в методе SendTo (), который применялся при работе с протоколом UDP. От­
правив данные, клиент закрывает свою сторону соединения, вызывая методы
Shutdown() и Close( ) .
Сетевые коммуникации ΙΙΙΗ Ι
Вспомогательные классы для сетевого программирования
Программирование на уровне сокетов в C# намного проще, чем в С или C++,
но трудности при написании и отладке кода все равно остаются. В каркасе .NET
Framework есть классы UdpClient, TcpClient и TcpListener, призванные
облегчить решение типичных задач.
Класс UdpClient берет на себя детали коммуникации по протоколу UDP.
В нем также реализован метод Connect (), который позволяет работать с объек­
том так, как если бы имелось соединение (хотя на самом деле никакого соедине­
ния, конечно, нет). В листинге 3.10 показано, как можно реализовать функцию
Start () из предыдущих примеров, если воспользоваться классом UdpClient.
Листинг 3.10. Обмен данными с помощью класса UdpClient
1 : void Start( string ipremote )
2: {
3: byte [] buf;
4:
5: UdpClient uc = new UdpClient(sendPort);
б:
7: if ( ipremote == "" )
8: {
9: // Параметры для инициализации оконечной точки
10: // не используются, но инициализировать ее
// без параметров нельзя.
11: IPEndPoint remote = new IPEndPoint( IPAddress.Any,
sendPort );
12 :
13: // Прочитать данные.
14: buf = u c .Receive(ref remote);
15: }
16: else
17 : {
18 : // Клиент.
19: buf = new byte[bufSize];
20: for ( int і = 0; і < bufSize; buf[i] =(byte)і , i++ );
21: uc.Send( buf, buf.Length, ipremote, sendPort );
22 : }
23 :
24: Console.WriteLine( "Передано {0} байтов.", buf.Length );
25: }
Обмен данными по протоколу UDP сам по себе несложен, но класс UdpClient
еще больше упрощает дело. В строке 5 создан объект этого класса, которому пе­
редан порт на локальном компьютере. Для принимающей стороны в строке 11
создается объект IPEndPoint, которому присваиваются IP -адрес и номер порта
программы-отправителя, а в строке 14 происходит прием данных. Что касается
отправляющей стороны, то все необходимое для посылки данных находится в стро­
110 ■ ■ ■ ■ III Библиотека базовых классов

ке 21; метод Send () разбирает представленный строкой IP -адрес, устанавливает


нужный порт и пересылает данные.
Применение вспомогательных классов для TCP сложнее, но все равно это
легче, чем программировать сокет напрямую. В листинге 3.11 приведен пример.
Листинг 3.11. Обмен данными с помощью классов TcpListener и TcpClient
1 : void Start( string ipremote )
2: {
3: int bytesMoved = 0;
4: byte [] buf = new byte[bufSize];
5:
6: if ( ipremote == "" )
7: {
8: // Подготовить слушателя и принять запрос
// на соединение.
9: TcpListener tl = new TcpListener( sendPort );
10 : tl.Start() ;
11: TcpClient tc = tl.AcceptTcpClient();
12 : 11 .Stop() ;
13 :
14: // Прочитать данные.
15: NetworkStream stm = tc.GetStream();
16: bytesMoved = stm.Read(buf, 0, buf.Length);
17 : tc.Close() ;
18 : }
19 : else
20 : {
21: // Клиент.
22: TcpClient tc = new TcpClient(ipremote, sendPort);
23: NetworkStream stm = tc.GetStream();
24 :
25: stm.Write(buf, 0, buf.Length);
26: bytesMoved = buf.Length;
27 : tc.Close() ;
28 : }

30: Console.WriteLine( "Передано {0} байтов.", bytesMoved );


31 : }
Код сервера в строках 9-16 концептуально аналогичен тому, что вы уже виде­
ли при непосредственном программировании сокета. В строке 9 создается объект,
прослушивающий указанный порт, а в строке 10 он начинает выполнять свою ра­
боту. Строка 11 блокирует программу в ожидании запроса на соединение. Метод
TcpListener .AcceptTcpClient () возвращает объект класса TcpClient, со­
единенный с удаленным клиентом. Вызов метода Stop () объекта TcpListener
прекращает прием запросов на установление соединения.
Для чтения данных из соединения применяется объект NetworkStream, ко­
торый возвращает метод TcpClient .GetStream (). В этом классе реализован
Резюме III···· 111

стандартный интерфейс потока. В строке 16 с его помощью из соединения считы­


ваются данные, а в строке 17 соединение закрывается.
На стороне клиента строка 22 создает объект класса ТсрС l i e n t , указывая
ІР-адрес и номер порта сервера, с которым надо соединиться. В строке 25 данные
выводятся в сокет, а в строке 26 соединение закрывается. Обратите внимание, что
ни в коде сервера, ни в коде клиента нет явного обращения к методу sh u td o w n ,
разрывающему соединение.

Резюме
Библиотека базовых классов (BCL) содержит основные сервисы, которые
каркас .NET Framework предоставляет всем приложениям. Диапазон их весьма
широк: от простых контейнеров до высокоуровневых классов, поддерживающих
сетевые коммуникации. BCL существенно упрощает применение таких сложных
средств, как асинхронный ввод/вывод. Описанием библиотеки BCL я завершаю
краткое введение в среду программирования. В части II я более детально расскажу
о написании программ на языке С#.
Глава 4. Переменные и типы
В состав типов, имеющихся в языке С#, входят простые типы, классы, интерфейсы,
структуры и перечисления. В этой главе демонстрируется работа с каждым из них;
показывается, как выполняются преобразования типов, в частности преобразования
между строками и другими типами. Особое внимание будет уделено примерам фор­
матирования с помощью класса StringBuilder. Кроме того, иллюстрируются
доступ к информации о производительности и применение перечислимых типов
для управления состоянием. При написании программ на C# вы сможете восполь­
зоваться приведенными приемами для создания типов, реализующих потребности
вашего приложения.

Простые типы данных


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

Создание и использование
Использовать простые типы просто (а чего еще вы ожидали?). Для создания
переменной нужно указать имя типа и идентификатор переменной:
int с; // Создает переменную, ее еще предстоит
// инициализировать.
int 1 = 1 ; // Создает и одновременно инициализирует переменную.
Инициализировать переменную простого типа допустимо литералом или вы­
ражением. Выражение может содержать ссылки и литералы, например:
int а = 3; // Инициализация литералом.
int b = а * 2; // Инициализация выражением.
Значения с плавающей точкой во многом похожи, но при использовании лите­
ралов типа float и decimal необходимо явно указывать принадлежность к тому
или иному типу:
1: float а = 3 .OF;
2: double b = 1.5;
3: decimal с = (decimal)a * 2.ОМ;
Здесь в строках 1 и 3 показано применение суффиксов для обозначения типа
литерала; по умолчанию числовой литерал, содержащий дробную часть, в C# при­
надлежит к типу double. Таким образом, чтобы присвоить литералу значение с
Простые типы данных І ІІ ІМ Н Ш
одинарной точностью (типа float) или 128-разрядное значение (типа decimal),
необходимо либо явно привести его к нужному типу, либо использовать специ­
фикатор типа. В строке 3 продемонстрировано также приведение типа для пре­
образования переменной типа float к типу decimal. Спецификаторы типов
перечислены в табл. 4.1, их можно записывать как большими, так и маленькими
буквами.

Таблица 4 .1 . Спецификаторы типов в языке C#


С пециф икатор типа Р езультирую щ ий тип
F ИЛИ f float
D или d double
М или m decimal (тип decimal предназначен для представления
денежных величин, отсюда и буква м - сокращение от «money»)

Со значениями интегральных типов в С#, как и в C++, можно обращаться либо


как с целыми числами, либо как с последовательностями битов. Вот примеры того
и другого:
int і = 0xFFE7; // Инициализирует переменную
// шестнадцатеричным значением.

і <<= 4; // То же, что і *= (int)Math.Pow( 2, 4 ).


i Sc- OxFF; // Маскирует младший байт.
Хотя многих конструкций, имеющихся в C++, вы в C# не найдете, экономия
выразительных средств позаимствована.

Строки и их преобразования
Строки в C# представляют собой полноценные классы, и для действий с ними
применяются операторы, как в Visual Basic. Например, две строки можно скла­
дывать:
String s;
s = new String( "One string".ToCharArray() );
s += " another string
Это законно, но не оптимально. C# отличается от других языков тем, что
строки неизменяемы: после создания значение строки уже нельзя изменить. Так
сделано, прежде всего, для повышения производительности; если бы строки раз­
решалось модифицировать, то при манипулировании ими пришлось бы постоянно
перемещать в памяти ассоциированный со строкой буфер, а среда исполнения при
каждой такой операции должна была обновлять все ссылки на нее.
Чтобы упростить типичную задачу конструирования строк, не идя на издерж­
ки, связанные с их изменяемостью, среда исполнения предоставляет класс String-
Builder. Вы пользуетесь им, чтобы собрать строку из кусочков, а затем вызываете
метод ToString (), который возвращает окончательный объект класса String.
Ниже показано, как это делается:
1: int i = 0xFFE7; // Объявляем шестнадцатеричное
114 ■ ■ ■ ■ I l l Переменные и типы

// значение.
2: StringBuilder sb = new StringBuilder() ;
З:
4: sb.AppendFormat( "{0:X} - начальное значение; ", і );
5:
6: і <<= 4; // То же, что і *= (int)Math.Pow( 2, 4 ).
7: і &= OxFF; // Замаскировать младший байт.
8: sb.AppendFormat( "{0:Х} - окончательное значение.", і );
9:
10: Console.WriteLine( sb.ToString() );
В строке 2 создается объект StringBuilder, затем с его помощью в строках 4
и 8 форматируются два строковых значения, а в строке 10 выводится следующий
результат:
FFE7 - начальное значение; 7 0 - окончательное значение.

Совет Я воспользовался классом S t r i n g B u i l d e r , поскольку конструиро-


вал строку по частям. Если бы я хотел создать строку за один шаг,
то применил бы вместо этого метод S t r i n g . F o r m a t ( ) .
Ф орм атная строка, используем ая в методах StringBui lder .Append
Format () и String.Format () довольно необычна. Вам на выбор предостав­
ляется стандартное форматирование или форматирование по шаблону. Стандарт­
ное форматирование аналогично тому, к чему вы привыкли в функции print f,
применяемой в C++; в ней имеются спецификаторы формата, управляющие пре­
образованиями во время выполнения. Методы форматирования принимают фор­
матную строку в качестве первого параметра. Специальные последовательности
показывают, в какое место нужно вставить строки, полученные преобразованием
остальных параметров. Каждая последовательность символов форматирования
выглядит так:
{л [ : fint] }
Здесь η - индекс параметра, следующего за форматной строкой (нумерация начи­
нается с нуля), a fmt - спецификатор формата. Спецификатор управляет тем, как
будет форматироваться указанный параметр, а результирующая строка заменяет
специальную последовательность в форматной строке. Если спецификатор fmt
опустить, то библиотека вызывает метод ToString () объекта-параметра для
преобразования его в строку.
В табл. 4.2 перечислены спецификаторы числовых форматов и способы их
действия.
Таблица 4 .2 . Спецификаторы форматов
С пециф икатор Ф ормат Вы вод
Сп Денежная величина "$ппп.пп" (положительная)
" ($nnn.nn)" (отрицательная)
Генерирует η цифр после запятой,
если η задано, иначе две цифры
Простые типы данных ІІІИ І
Таблица 4 .2 . Спецификаторы форматов (окончание)
С пециф икатор Ф ормат Вы вод
Dn Десятичный nnn.nnn (положительное)
-nnn.ппп (отрицательное)
Генерирует не менее η цифр,
дополняя по мере необходимости
нулями
Eprec Экспоненциальный ηηηΕ+ηηη
Не нормализует экспоненту
до ближайшего кратного трем
Fprec С фиксированной точкой То же, что D, но не более ргес цифр
после запятой
Gprec Общий Возвращает наиболее короткую
из строк, генерируемых по
форматам Ергес или Fprec
Nprec Числовой То же, что D, но для разделения
тысяч используется региональная
настройка. Если ргес задано,
выводится именно столько цифр после
запятой, иначе ровно две
Pprec Процентный Порождает представление дробного
числа в виде процента, причем 1
равна 100%. Если ргес задано, выводит-
СЯ
именно столько цифр после запятой
R Возвратный Создает значение, которое может быть
преобразовано средой исполнения
обратно к исходному типу с помощью
класса System.Convert; числа
с плавающей точкой при этом могут
потерять точность
Xn Шестнадцатеричный Порождает шестнадцатеричное
значение; если η задано, выводится
по меньшей мере η цифр

Спецификаторы формата, перечисленные в табл. 4.2, нечувствительны к


регистру, но если в отформатированном значении есть буквы, то они будут за­
писаны в том же регистре, что и спецификатор. Ниже иллюстрируется данный
эффект:
Console.WriteLine( "exp:" + (0.000123456).ToString( "е5"));
Console.WriteLine( "EXP:" + (0.000123456).ToString( "E5"));
Console.WriteLine( "hex:" + (OxABCD).ToString( "x" ));
Console.WriteLine( "HEX:" + (OxABCD).ToString( "X" ));

Если выполнить этот код, будут выведены следующие результаты:


ехр:1.23456е-004
ЕХР:1.2345бЕ-004
hex:abed
HEX:ABCD
116 ■ ■ ■ Ill Переменные и типы

Для форматирования даты и времени применяются особые спецификаторы,


перечисленные в табл. 4.3. В отличие от спецификаторов форматирования чисел,
они зависят от регистра.
Таблица 4 .3 . Спецификаторы форматов даты и времени
С пециф икатор Ф ормат Вы вод
d Короткая дата m/d/y
D Длинная дата w, m d, у
f Полная дата и время w , m d , у h :m ар
(длинная дата
и короткое время)
F Полная дата и время w , m d, у h:m:s ap
(длинная дата
и длинное время)
Я Общий формат m/d/y h:m ap
G Общий формат
(длинное время) m/d/y h:m:s ap
М или m Месяц и день m d
R или Г Формат согласно RFC1123 w , d m у h :m :s GMT
(день недели и месяц
записываются сокращенно)
s Формат для сортировки y-m-d h:m:s
t Короткое время h :m ap
T Длинное время h :m :s ap
u То же, что s, y-m-d h :m : sZ (добавляется Z
но приводится к зоне GMT для временной зоны Zulu)
(«универсальное время)
и Длинное универсальное To же, ЧТО F, но приведено
время к зоне GMT
Y или у Месяц и год m, у

Прочие библиотечные объекты определяют способ преобразования внутрен­


него представления в строку по своему усмотрению.
Форматирование по шаблону позволяет гораздо более точно контролировать
формат чисел - примерно так же, как предложение PRINT USING в старых диалек­
тах языка BASIC. Шаблон форматирования - это строка, определяющая представ­
ление числа. Шаблон может включать специальные символы и текст. В табл. 4.4
приведены все поддерживаемые специальные символы.

Таблица 4 .4 . Символы, применяемые в шаблонах форматирования


Символ Н азначение
0 Позиция заполняется нулем; подставляется либо цифра числа,
либо незначащий нуль
# Позиция цифры
Простые типы данных ΙΙΙΗ Ι
Таблица 4 .4 . Символы, применяемые в шаблонах форматирования (окончание)
Символ Назначение
Десятичная точка
Разделитель тысяч, также может использоваться
для округления чисел
Процент - форматирует числа в виде процентной величины,
причем 1.0 = 100%
E+ fm t ИЛИ E -fm t Экспоненциальный формат; fm t - шаблон, который может
содержать специальные символы для форматирования чисел
При использовании + отображается знак показателя степени
\p fc h a r Еэсаре-последовательность в смысле функции p r i n t f языка С,
например \п для символа новой строки
« lit s t r » Текст, воспринимаемый как литерал
{ и } В форматной строке, переданной одной из разновидностей
библиотечной функции Format, парные фигурные скобки следует
использовать для включения в выходную строку шаблона.
Метод ty p e . T o S trin g () игнорирует фигурные скобки
Разделяет секции, применяемые к положительным, отрицательным
и нулевым значениям

Следующие примеры иллюстрируют работу шаблонов форматирования сов­


местно с методами ToString () различных типов, а также с функцией String.
F o r m a t ().
decimal m = 1238767987б .2ОМ;
float f = 0.9 999F ;

Console.WriteLine( Обычный: " + m .ToString("###,###,###.00 ")


Console.WriteLine( Экспоненциальный: " + m.ToString
("000.###E-00") );
Console.WriteLine( Округление до млн: " + m.ToString
("###,###,, млн.")
Console.WriteLine( Процент: " + f .ToString("00.000%") );

String s = String.Format( "{0:00.000%} - это процент.", f );


Console.WriteLine( s );
Если выполнить эти предложения, будут напечатаны следующие резуль­
таты:
Обычный: 12,387,679,876.20
Экспоненциальный: 123.877Е08
Округление до млн: 12,3 88 Million
Процент: 99.990%
99.990% - это процент.
Обратите внимание: наличие запятых в конце строк в третьем примере автома­
тически приводит к округлению числа, а символ форматирования в виде процента
автоматически умножает значение на 100%.
118 ■ ■ ■ ■ I I I Переменные и типы

Преобразование и приведение типов


Преобразования типов в C# могут быть явными и неявными. Неявный ме­
ханизм применяется тогда, когда имеется корректно определенное, однозначное
преобразование из исходного типа в целевой. Для числовых типов неявное преоб­
разование выполняется, когда оба типа схожи (то есть являются интегральными
или с плавающей точкой), причем ширина целевого типа такая же, как у исходного
или больше. Это означает, например, что тип f l o a t допустимо неявно преобразо­
вать в d o u b le , а наоборот - уже нет.
Простейшее преобразование состоит в создании нового объекта целевого типа
и конструировании его из исходного. Целевой тип должен иметь конструктор,
принимающий в качестве единственного аргумента объект исходного типа. Такой
метод ничем не отличается от вызова любого другого конструктора. Кроме того, C#
поддерживает определение перегруженных операторов преобразования в классе
и позволяет управлять порядком их применения. В листинге 4.1 демонстрируются
все три метода преобразования.
Листинг 4.1. Примеры преобразований типов
1 public class IntString
2 {
3 int і = 0;
4
5 public IntString( int ival )
6
7 і = ival;
8
9
10 public IntString( String s )
11
12 і = Convert.ToInt32(s);
13
14
15 public static implicit operator IntString( int iVal )
16
17 return new IntString( iVal );
18
19 public static implicit operator IntString( String sVal )
20
21 return new IntString( sVal );
22
23 public static implicit operator int( IntString і )
24
25 return і .і;
26 }
27 public static implicit operator String( IntString і )
28 {
Простые типы данных I I I · · · · 119

29: return і .і .ToString();


ЗО : }
31: }
32 :
33 :
34 :
35: String s = "111";
З 6: int і = 999 ;
37: IntString istr;
38 :
39: // Инициализация на основе значений типа String и int.
40: istr = new IntString(s);
41: istr = new IntString(і);
42 :
43: // Использование неявных операторов IntString
44: // для преобразования из типов String и int.
45 : istr = s;
46: istr = і ;

48: // Использование неявных операторов String и int


49: // для преобразования из типа IntString.
50: i = istr;
51: s = istr ;
Класс IntString объявляет тип, который можно беспрепятственно преоб­
разовывать в строки и целые числа. Преобразования на этапе создания объекта
определены в строках 5-13 с помощью конструкторов, принимающих параметр
типа String или int. В строках 40 и 41 демонстрируется использование такого
метода.
Преобразование типа - это направленный процесс; как вы только что видели,
наличие преобразования из типа А в тип В не означает, что имеется также преоб­
разование из типа В в тип А. Поэтому в классе IntString определены операторы
для преобразования в типы int и String и обратно. В строках 15-22 определе­
ны операторы преобразования, принимающие параметр типа int или String и
возвращающие новый объект типа IntString, сконструированный из исходного
значения. Наличие таких операторов позволяет писать код, представленный в
строках 45 и 46, где значение типа int или String напрямую присваивается пере­
менной типа IntString. Наконец, в строках 23-30 реализовано преобразование
в обратном направлении, а его работа показана в строках 50 и 51.
C# поддерживает также явные приведения типов, как в C++ и Java. Так, в
следующем примере переменная типа float явно округляется, и результат сохра­
няется в переменной типа int.
float f = 3.1415926536F;
і = (int) f; // і = З
ιη ιιι Переменные и типы

В листинге 4.1 все операторы объявлены с модификатором implicit, что­


бы меньше печатать, когда в программе нужно выполнить преобразование в тип
IntString или обратно. Для демонстрации этого достаточно, но на практике
определять неявные преобразования следует лишь тогда, когда они не сопровож­
даются побочными эффектами и не затемняют смысл программы. Например, пре­
образование между сходными числовыми типами вполне может быть неявным,
поскольку такие типы ведут себя практически одинаково. Что касается класса
IntString, который не имеет выраженной семантики и применяется только
для преобразований, то неявные преобразования нельзя счесть удачным реше­
нием: различия типов настолько велики, что преобразование следует сделать яв­
ным, изменив модификатор в объявлениях операторов с implicit на explicit.
В листинге 4.2 показано, как данное действие отразится на коде.
Листинг 4.2. Примеры явного преобразования типов
1 // Изменения в прототипах операторов; теперь они объявлены
2 // с модификатором explicit вместо implicit,
3 public static explicit operator int( IntString і )
4
5
6 public static explicit operator String( IntString і
7
8
9 public static explicit operator IntString( int iVal )
10
11 public static explicit operator IntString( String sVal
12
13
14 // Использование явных операторов IntString
15 // для преобразования из типов String и int.
16 istr = (IntString)s;
17 istr = (IntString)i;
18
19 // Использование явных операторов String и int
20 // для преобразования из типа IntString.
21 і = (int)istr;
22 s = (String)istr;

Предупреждение Невозможно переоценить важность правильной семантики


операторов. У людей сложилось представление о том, что
означает каждый оператор, и определенные вами операторы
не должны идти вразрез с ожиданиями. В частности, неяв­
ные операции, которые ведут себя не так, как ожидается,
могут стать причиной трудно обнаруживаемых ошибок.
Классы І І І Ш И Ш

Классы
Классы - это основные строительные блоки программ на С#. Под классом
понимается определение дискретного типа, в котором описывается внутреннее
состояние и операции, применимые к объектам этого класса. В объектно-ориен-
тированных языках, к каковым относится и С#, класс может наследовать друго­
му классу (базовому) или реализовывать интерфейс. В случае наследования все
члены родительского класса становятся частью производного, который может
по своему желанию расширить родительский класс, определив дополнительное
состояние или поведение. C# позволяет указать, разрешено порождать объекты
данного класса или он является абстрактным, то есть таким, которому можно
только наследовать.
Возможность создавать новые классы позволяет расширять язык и библиотеки
для решения стоящих перед программистом задач. В этом разделе мы для демонс­
трации создадим класс, который будет получать информацию о производительнос­
ти от сервера, работающего под управлением ОС Windows NT или Windows 2000.
В листинге 4.3 представлен код первого варианта такого класса.
Листинг 4.3. Построение класса для получения информации от сервера
1: using System.Diagnostics;
2: using System.Threading;
3:
4: namespace C4Servers
5: {

7: public class Server


8: {
9: // Имя сервера, за которым мы наблюдаем.
10: protected string Host;
11 :
12: // Свойство для получения загрузки процессора.
13: public virtual double CpuUsage
14 : {
15: get
16: {
17: PerformanceCounter pc;
18: CounterSample csl, cs2;
19 :
20: // Прочитать данные о загрузке процессора.
21: pc = new System.Diagnostics.PerformanceCounter(
22: "Processor",
23: "% Processor Time",
24: "_Total",
2 5: Host );
26 :
■ ■ ■ Ill Переменные и типы

27: // Получить два значения показателя и вызвать


28: // функцию Calculate для вычисления среднего.
29: // Отбирать значения следует с интервалом,
// большим интервала таймера.
30: csl = p c .NextSample();
31: Thread.Sleep( 1 + (int)(1.0 / csl.SystemFrequency) );
32: cs2 = p c .NextSample();
33 :
34: // Calculate() вычисляет среднее по результатам
// двух замеров.
35: return CounterSample.Calculate( csl, cs2 );
36: }
37 : }
38 :
39: // Это свойство позволяет получить данные о занятой
40: // памяти (один из многих показателей,
41: // характеризующих состояние памяти машины).
42 : public virtual long MemUsage
43 : {
44: get
45 : {
46: PerformanceCounter pc;
47 :
48: // Прочитать показатель.
49: pc = new System.Diagnostics.PerformanceCounter(
"Memory",
50: "Committed Bytes",
51:
52: Host );
53 :
54: // Поскольку число зафиксированных байтов -
55: // это абсолютное значение, запрашивать его
// нужно только один раз.
56: return p c .NextSample().RawValue;
57: }
58: }
59 :
60: public Server(string Hostname)
61: {
62: Host = Hostname;
63 : }
64: }// КОНЕЦ ОПРЕДЕЛЕНИЯ КЛАССА Server.
65 : }
Здесь класс Server погружен в пространство имен C4Servers. Собственно
объявление класса начинается в строке 7.
Интерфейсы I I I ····
Совет Представленные в примерах классы я обычно проектировал в про­
грамме Microsoft Visio 2002, профессиональная версия которой
содержит средства для рисования диаграмм и генерации кода на
различных языках, включая и С#. Хотя кое-чего мне и недоставало,
в общем и целом это неплохое экономичное решение для создания
не очень больших проектов. К тому же оно интегрировано с Visual
Studio .NET.
В классе Server есть всего одно поле Host, где хранится имя машины, за
производительностью которой мы будем наблюдать. Это поле инициализируется
конструктором в строке 60. Поскольку объект не захватывает ресурсов, деструктора
в нем нет. В классе объявлено также два свойства, оба только для чтения. Изменить
данные о загрузке процессора и занятой памяти нельзя, поэтому свойства CpuUsage
и MemUsage, объявления которых начинаются в строках 13 и 42 соответственно,
имеют только метод get, но не set.
Код методов доступа считывает запрошенные показатели производитель­
ности и возвращает значение вызывающей программы. Использование объектов
класса Server демонстрируется в следующем примере:
double і;
Server s = new Server( "SNAME" );

і = s.CpuUsage;

// Свойство MemUsage возвращает значение типа long,


// но неявное преобразование из long в double разрешено.
і = s.MemUsage;
Этот код получает данные о загрузке процессора и занятости памяти от сервера
с именем SNAME.

Интерфейсы
Класс Server годится, если вы собираетесь работать только с серверами на
платформе Microsoft. Но при использовании аналогичного класса для сбора ста­
тистики о работе базы данных вы столкнетесь с проблемой: хотя стандартизация
продуктов для одной платформы - дело обычное, пользователям часто необхо­
димо поддерживать различные базы данных, поэтому программу следует проек­
тировать так, чтобы ее можно было легко расширить путем подключения других
баз данных. Интерфейсы - это идеальное решение для таких случаев.
Прежде всего, нужно определить интерфейс, который фиксирует способы взаи­
модействия с сервером базы данных. Такое определение показано в листинге 4.4.
124 ■ ■ ■ ■ I I I Переменные и типы

Листинг 4.4. Интерфейс монитора сервера базы данных


1 : namespace C4Servers
2: {

4: public interface IDatabaseServer


5: {

7: // Текущее число работающих пользователей.


8: long UserCount
9: {
10: get;
11 : }
12: }// КОНЕЦ ОПРЕДЕЛЕНИЯ ИНТЕРФЕЙСА IDatabaseServer
13 :
14 : }
Любой класс, реализующий интерфейс IDatabaseServer, должен раскры­
вать свойство UserCount, которое возвращает число пользователей, соединив­
шихся с базой данных.
По определению интерфейс IDatabaseServer не может содержать реализа­
ции объявленных в нем членов. С другой стороны, в листинге 4.5 показан класс,
который реализует этот интерфейс для работы с СУБД Microsoft SQL Server.
Листинг 4.5. Реализация интерфейса IDatabaseServer
1: using System;
2: using System.Data;
3: using System.Data.SqlClient;
4: using System.Text;
5: using System.Diagnostics;
6:
7 : namespace C4Servers
8
9
10 public class MSDbServer: Server,
11 IDatabaseServer
12
13 protected string Instance;
14
15 // Текущее число активных пользователей,
16 public long UserCount
17 {
18 get
19 {
20 PerformanceCounter pc;
21 StringBuilder sb = new StringBuilder();
22
23 // Создать имя показателя.
24 sb.Append("MSSQL");
25 if ( Instance.Length > 0 )
Структуры ■ ■ ■ ■ 125

26: sb.Append("$" + Instance);


27: sb.Append(":General Statistics");
28 :
29: // Прочитать значение показателя.
30: pc = new System.Diagnostics.PerformanceCounter(
31: sb.ToString(),
32: "User Connections",
33:
34: Host );
35 :
36: // Поскольку число зафиксированных байтов
37: // абсолютно, считать показатель нужно
// только один раз.
38: return p c .NextSample().RawValue;
39 : }
40 :
41 : }
42 :
43: public MSDbServer(string Hostname, string
InstanceName)
44: base(Hostname)
45 : {
46: Instance = InstanceName;
47 : }
48 :
49: }// КОНЕЦ ОПРЕДЕЛЕНИЯ КЛАССА MSDbServer
50 :
51: }
В строке 10 начинается определение класса MSDbServer. Он является произ­
водным от класса Server, поэтому наследует всю его функциональность, а также
реализует интерфейс IDatabaseServer для доступа к статистике базы данных. В
этом классе есть защищенное поле Instance, где хранится имя экземпляра SQL
Server, о котором мы хотим получать информацию.
Конструктор класса (строки 43-47) параметризован именами хоста и экзем­
пляра, имя экземпляра сохраняется в поле Instance, а имя хоста передается
конструктору базового класса Server в строке 43.
Реализация метода доступа к свойству практически такая же, как в классе
Server, разница лишь в том, что нужно указать имя категории показателя. Эту
задачу решает код в строках 24-27, после чего значение показателя считывается
так же, как для простых счетчиков.

Структуры
Структуры - это полезные значащие типы, позволяющие организовать взаимо­
связанные элементы в случаях, когда семантика и особенности классов (например,
возможность удаленного доступа) не нужны. Классические примеры - это точки,
цвета и подобные графические объекты. Другое их применение - это хранение
126 ■ ■ ■ ■ I I I Переменные и типы

информации о соединении с базой данных (см. листинг 4.6).


Листинг 4.6. Организация информации о соединении с базой данных
в виде структуры
II Описывает структуру соединения с СУБД SQL Server.
using System.Data.SqlClient;
// Содержит информацию, использованную для установления
// соединения, а также объекты,
// необходимые для работы с этим соединением,
public struct SqlConnectionlnfо
{
public bool trusted; // Доверительное
// соединение?
public string uid, pwd; II Идентификатор
II и пароль пользователя.
public string host, instance; II Имя хоста
// и экземпляра.
public SqlConnectіon conn; // Соединение с сервером.
public SqlCommand cmd; II Объект, представляющий
II команду
// для работы с сервером
public SqlDataReader dr ; // Объект, с помощью
которого читаются
II данные от сервера.
}
Тип struct SqlConnectionlnfо в листинге 4.6 группирует основную ин­
формацию, необходимую для установления соединения с базой данных и для
дальнейшей работы с этим соединением. Вместо того чтобы передавать отдельные
переменные, структуру можно заполнить в некоторой точке, а затем передавать
между различными частями программы.

Перечислимые типы
Перечислимый тип - это способ реализовать в виде значащего типа некоторое
множество значений. Обычно компилятор реализует перечисления в виде значе­
ний типа int, но трактовать их допустимо либо как целые, либо как машинные
типы (с помощью явного приведения типа). В листинге 4.7 перечисление исполь­
зуется для представления возможных состояний документа на различных стадиях
его обработки.
Листинг 4.7. Использование перечисления для представления состояний документа
class Document
{
enum DocStatus { emptyDoc, loadedDoc, newDoc, dirtyDoc };
// emptyDoc - пустой объект, сохранять не нужно.
// loadedDoc - документ загружен из файла
// и не модифицирован.
// newDoc - создан новый документ, нужно сохранить.
Перечислимые типы I I I · · · · 127

// dirtyDoc - модифицирован загруженный документ,


// нужно сохранить.
DocStatus status;
bool LoadDoc()
{
// Загрузить данные из файла,
// затем установить состояние,
status = DocStatus.loadedDoc;

// Вернуть признак успешности,


return true;
}

bool ModifyContent( /* Необходимые параметры. */ )


{
// Выполнить обработку, затем установить состояние,
if ( status == DocStatus.loadedDoc )
status = DocStatus.dirtyDoc;
else
status = DocStatus.newDoc;

return true;

public Document()
{
// В документе пока ничего нет.
status = DocStatus.emptyDoc;
}

public void CheckAndSave()


{
if (status == DocStatus.dirtyDoc II status
== DocStatus.newDoc)
{
// Если необходимо, сохранить данные,
// затем установить состояние,
status = DocStatus.loadedDoc;
}
}
}
Как видите, перечисление делает программу намного понятнее в случаях, когда
есть набор значений, описывающих одну и ту же характеристику. В библиотеке
классов .NET Framework перечисления используются для таких вещей, как
состояние файла, дни недели или стили оконных рамок. Перечисления позволяют
легко уяснить смысл именованных констант и сделать работу с ними более
безопасной.
128 ■ ■ ■ ■ I l l Переменные и типы

Резюме
На платформе .NET есть множество готовых к употреблению типов, но
значительная часть деятельности программиста на этой платформе состоит в
определении собственных и расширении существующих типов. В этой главе
обсуждались приемы работы с типами, а также преобразования из одного типа
в другой. В главе 5 мы подробнее поговорим о понятии композиции классов, с
которым вам придется сталкиваться очень часто.
Часть
Техника
программирования
Гл ава 5. Классы и ко м пон енты
Гл ава 6. У п р а в л е н и е пам ятью и C #
Гл ава 7. У п р а в л е н и е потоком
вы полн ен и я про грам м ы
Гл ава 8. Н еб езо п асн ы й код
Гл ава 9. М етад ан н ы е и о тр аж е н и е
Гл ава 10. К о н ф и гу р и р о в а н и е ко м пон ен то в
и прилож ений
Гл ава 11. И сп о л ьзо ван и е SD K
Знать, как пишется код на языке С#, полезно, но это только первый шаг. В части
I мы говорили о синтаксисе этого языка, инструментальных средствах и библио­
теках. В части II я познакомлю вас с техническими приемами программирования,
существенно углубив ваши представления о применении C# для создания прило­
жений. Материал части II организован так, чтобы легко можно было найти ответы
на вопросы, возникающие в ходе программирования. В каждой главе рассматрива­
ется одна конкретная тема, например конфигурирование или управление памятью,
и объясняется, какими средствами следует пользоваться. Хотя последовательное
чтение этой части нельзя назвать напрасным, это необязательно: вы можете сразу
обратиться к той главе, которая содержит интересующий материал.

Глава 5. Классы и компоненты


В языке C# класс - это фундаментальная конструкция, служащая для поддержки
объектно-ориентированного программирования. C# претендует на звание объект -
но-ориентированного языка, потому что поддерживает концепции инкапсуляции,
наследования и полиморфизма. Более того, C# усовершенствовал многие средства,
имеющиеся в C++, а также добавил кое-что от себя. Однако при этом появились
также и ограничения на способы использования наследования и управление ви­
димостью. Можно сказать, что C# является мощным современным инструментом
объектно-ориентированного программирования.

Определение сущностей и классов


Для демонстрации работы с классами в C# я выбрал сущности Author (Автор)
и Title (Название) из базы данных pubs, поставляемой в качестве примера вместе
с Microsoft SQL Server. Для нас важны следующие атрибуты сущности Author:
□ имя и фамилия;
□ состояние договора с автором;
□ названия книг, написанных автором.
На статической структурной диаграмме (рис. 5.1) показаны некоторые из
основных классов, необходимых для реализации сущности Author. Атрибуты и
операции, показанные в проекте, будут использоваться на протяжении всей главы.
Хотя в окончательной реализации потребуются и другие классы, например
System.Data .Common.DataSet, для простоты диаграмма содержит лишь то,
что имеет непосредственное отношение к задаче. Наличие этих классов обуслов­
лено самим характером сущности и структурой поддерживающих ее таблиц в базе
данных (рис. 5.2).
Определение сущностей и классов ΙΙΙΗ Ι
ЩM icrosoft Visio - [A uthors D es ig n .v s d iA u th o rs D esig n]
|S 1 File Edit View Insert Format Tools Shape UML W indow Help HOW DO I turn off conne - v _ fi1
a Bi ^ | ^ ί :^ тд : Ig096
І Normal T Arial - 12pt. ^ в I U
S h a ..
Author
0 UML Activity!
i-FirstName: string
0 UML Colla.. i-LastName : string
0 UML Com.. S y stem C o llet Mons.SortedList 4-Id: string
HsContract : bool
0 UML Depl.. 4-Titles : System .Data.DataSet
+■Item Qn Key: string): object j 1
0 UML Seq.. tfHost : string
Instance : string
tfAu_id: string
tfFNam e: string
tfLName: string
((Contract: bool
γί : PTitlesSet: System .Data .DataSet
0 UML Use . Basel nfoLoaded : bool
PBaselnloDirty: bool
Mod. Title sSetLoaded : bool
Aitthorpn D bH ost: string, in Dblnstance : string}
UML S y j PLoadBaselnlbQ
□ ^ Static PLoadTitlesQ
GetConnedionQ: Sydem .Data.SqlCIient.SqlConnect!on
□ Tc AulhorCollcclion

HI
Ф EL
#Host : string
#Instance : string
-Instantiates

+Aut horC oil eel ιοηι in H'Ii-HoeL ї Li i ng, i n D Ι:·Ι n it a nee : stri ng)
в Ш #GetConnectionO : S ystem .D ata .SqlC Iient.Sql Connection
+ G e tA u th o r^ ): Syste m.Col lection s.SortedList

Bv
л и ...
► H H ► H \ A u lh o rs D esign"^ | ^ d
Page 1/1

Рис. 5.1. Для реализации сущности Author требуется класс Authors,


контейнер SortedList, в котором будут храниться объекты этого класса,
и класс AuthorsListBuilder, необходимый для заполнения списка

lift S Q L S e rv e r E n te rp ris e M a n a g e r - [2:E dit D ia g ram 'D iag ra m fo r C h a p te r 5, C S harp b o o k 1 in 'p u b ...

Console W indow Help s X


ia m m і а і іШ Ш ір Ш п і сяй ч аіь ^ i
З

title a u t h o r
a u th o rs
title s
9 a u jd
JL aujnam e
au_ld
9 title_ld J L title_Id
au_ord title
a u jn a m e
royaltyper type
phone
pub_id
address
price
city
advance
state
roya Ity
zip
ytd_sales
contract
notes
pubdate

Рис. 5.2. Часть физической модели базы данных pubs,


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

На диаграмме классов представлены классы, их операции и атрибуты. В


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

Методы
Метод - это основное средство программирования поведения объектов. Объяв­
ление метода состоит из комбинации следующих элементов:
□ атрибуты, заключенные в квадратные скобки;
□ модификатор видимости (например, public);
□ модификатор единственности (static);
□ модификатор характера кода (например, unsafe или extern);
□ модификатор наследования (например, sealed, virtual или override);
□ (обязательно) тип возвращаемого значения;
□ (обязательно) имя метода, которое должно быть правильным идентифика­
тором;
□ (обязательно) открывающая и закрывающая круглые скобки, между кото­
рыми заключены нуль или более параметров, разделенных запятыми.
Метод может принимать фиксированные параметры и массивы параметров.
Объявление фиксированного параметра состоит из следующих элементов:
□ атрибуты, заключенные в квадратные скобки;
□ модификаторы способа передачи (ref или out);
□ идентификатор типа;
□ идентификатор параметра.
Для передачи переменного списка параметров предназначен массив парамет­
ров, обозначаемый модификатором params. Его использование продемонстриро­
вано в следующем примере:
void VarParms( params int [] агу )
{
foreach ( int і in агу )
{
// Обработать элемент.

void AMethod()

VarParms(); // Нормально.
VarParms( 1 , 2 , 3 , 4 , 5 ); // И это тоже.

В объявлении одного метода разрешается использовать и фиксированные, и


переменные параметры, но массив параметров должен быть последним в списке:
Методы

static void VarParms(double f, string s, params int [] ary)


{

}
Я взял массив параметров типа int, но в него можно поместить совершенно
произвольные объекты, если воспользоваться классом Syst ern.Ob ject, что видно
из следующего объявления метода Format () класса String:
public sealed class String
{
public static sealed string Format(
string format, params object[] args )
{

}
}
В данном случае первым параметром метода должна быть строка, за которой
может следовать нуль или более объектов произвольных типов.
Реализацию нашего примера лучше начать с класса AuthorsListBuilder,
показанного в листинге 5.1. Здесь используется метод класса для создания списка
объектов Author путем считывания данных из базы.
Листинг 5.1. Метод GetAuthors () создает список объектов класса Author
1 : public class AuthorsListBuilder
2: {
3: III <summary>
4: III Создает набор авторов из базы данных и организует его
5: III так, чтобы воспользоваться классом SortedList.
6: III </summary>
7:
8: // Информация о соединении с базой данных.
9: protected string Host;
10: protected string Instance;
11 :
12: // Сконструировать объект, передав имя хоста и экземпляра
13: // SQL Server, с которым мы хотимсоединиться.
14: public AuthorsListBuilder(string DbHost,
string Dblnstance )
15: {
16: Host = DbHost;
17: Instance = Dblnstance;
18 : }
19 :
20: public SortedList GetAuthors()
Классы и компоненты

21 : {
22: SqlConnection Conn = null;
23: SqlCommand Cmd = null;
24: SqlDataReader Dr = null;
25: SortedList Sl = new SortedList();
26 :
27: // Соединиться с базой данных, выбратьидентификаторы
28: // авторов, затем добавить в списокобъект Author
// для каждого ид.
29: try
30 : {
31: // Соединиться с базой данных.
32: Conn = GetConnection();
33 :
34: // Выбрать данные из базы.
35: Cmd = Conn.CreateCommand();
36: Cmd.CommandText = "SELECT au_id FROM authors";
37: Dr = Cmd.ExecuteReader
( CommandBehavior.SequentialAccess );
38 :
39: // Прочитать данные о каждом авторе
/ / и вставить их в список.
40: while ( Dr.Read() )
41 : {
42: Author Au = new Author(Host, Instance);
43: Au.Id = Dr.GetString(0);
44: Sl.Add(A u .Id, Au);
45 : }

47: // Вернуть окончательный список.


48: return Sl;
49 : }
50: // Гарантировать освобождение ресурсов
51: / / в случае возникновения ошибки.
52: finally
53 : {
54: if ( Cmd != null )
55: Cmd.Dispose();
56 :
57: if ( Conn != null )
58: Conn.Dispose();
59 : }
60 : }
61 :
62: // Вспомогательная функция для соединения с SQL Server.
63: protected SqlConnection GetConnection()
64 : {
65: SqlConnection Conn =null;
66: StringBuilder Sb = new StringBuilder();
Методы I I I ····

68: try
69 : {
70: // Построить строку соединения.
71: if ( Instance.Length > 0 )
72: Sb.AppendFormat("SERVER^{0}\\{1};UID=sa",
73: Host,
74: Instance);
75: else
7 6: Sb.AppendFormat("SERVER={0};Trusted_Connection=Yes",
77: Host );

79: // Соединиться с базой данных.


80: Conn = new SqlConnection( Sb.ToString() );
81: Conn.Open();
82: Conn.ChangeDatabase("pubs");
83 :
84: return Conn;
85: }
86: catch
87 : {
88: if ( Conn != null )
89 : {
90: Conn.Close() ;
91: Conn.Dispose();
92 : }
93 :
94: // Передать исключение вверх по цепочке,
95: // возбудив его повторно.
96: throw;
97 : }
98 : }
99 : }
Конструктору класса AuthorsListBuilder (строки 14-18) передается имя
хоста и имя сервера базы данных, из которой мы собираемся выбирать данные.
Можно было бы передать также имя и пароль пользователя, но простоты ради
мы реализовали защищенный метод GetConnection () (строки 65-98), ориен­
тируясь на конфигурацию по умолчанию, в которой не задан пароль пользовате­
ля sа (администратора). Этот метод устанавливает соединение и обрабатывает
возможные ошибки. Если все хорошо, он возвращает соединенный объект клас­
са System. Data .SqlClient.SqlConnection. Метод объявлен защищенным
(protected), поскольку он не является частью интерфейса класса.
Помимо конструктора, единственным открытым методом является Get
Authors () (строки 20-60). В строке 32 для установления соединения с базой
данных вызывается метод GetConnection (). В строках 35 и 36 мы создаем
объект SqlCommand, который выбирает из базы данных идентификаторы ав­
торов, затем в строке 37 выполняем представленную этим объектом команду,
Η ΙΙΙΙΙ Классы и компоненты

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


бранных идентификаторов. Далее в строках 40-45 мы обходим это множество,
создавая на каждой итерации объект Author и вставляя его в объект класса
SortedList, который и возвращается вызывающей программе. После того как
список сформирован, а также в случае возникновения исключения код в блоке
finally (строки 52-59) очищает объекты SqlCommand и SqlConnection, вы­
зывая их методы Dispose (). В листинге 5.2 показано, как можно использовать
объект AuthorsListBuilder.
Листинг 5.2. Программа для получения данных об авторах от SQL Server
AuthorsListBuilder AL = new AuthorsListBuilder("(local)",
SortedList SL = A L .GetAuthors();

foreach ( Author a in SL.Values )


{
Console.WriteLine("\n\nAuthor id {0}.", a.Id );
Console.WriteLine( "Идентификатор: {0}, Имя: {1} {2}",
a.Id, a.FirstName, a.LastName );
}
Операции над объектами в C# чаще всего реализуются с помощью методов. В
листинге 5.2 я воспользовался классом-построителем, чтобы вычленить метаопе­
рацию создания списка авторов. Для данной цели можно применить статический
метод в классе Author, но я не стал этого делать. Ваше решение зависит от того,
какие способы и стандарты программирования вы предпочитаете.

Свойства
Свойство в C# лежит где-то посередине между полем и методом. Для клиент­
ского кода оно представляется полем, предназначенным только для чтения или
для чтения и записи. В самом же классе это именованный идентификатор, содер­
жащий один метод get, если свойство должно быть доступно только для чтения,
или пару методов get и set, если оно доступно для чтения и записи. Демонс­
трируя описанную конструкцию, я воспользуюсь свойствами, чтобы реализовать
открытые поля данных в классе Author.
При реализации классов, работающих с базой данных, часто оказывается, что
клиентскому коду, обращающемуся к классу, нужна только часть имеющейся ин­
формации. Например, имея список объектов Author, вы можете вывести перечень
имен, где пользователь указывает одного автора и видит, какие книги тот написал.
Для большинства объектов Author, которые пользователь так и не выбрал, загруз­
ка названий книг - пустая трата времени. В C# механизм свойств позволяет эле­
гантно решить эту задачу В последующих листингах иллюстрируется применение
свойств для реализации механизма «отложенной загрузки», когда информации не
считывается из базы данных, пока вызывающая программа явно ее не запросит. В
листинге 5.3 представлена первая часть объекта Author - объявление состояния
и конструктора.
Свойства I I I ····
Листинг 5.3. Начало объявления класса Author - данные о внутреннем
состоянии и конструктор, который их инициализирует
1 public class Author
2 {
3 // Информация о соединении с базой данных.
4 protected string Host;
5 protected string Instance;
fi
и
7 // Основная информация об авторе.
8 protected string Au_id; // Номер социального
// страхования.
9 protected string FName; // Имя.
10 protected string LName; // Фамилия.
11 protected bool Contract; // true, если заключен
// договор.
12 :
13 : protected DataSet TitlesSet; // Названия написанных
// книг.
14
15 // Внутреннее состояние.
16 protected bool BaselnfoLoaded; // Информация из базы
// загружена.
17 protected bool BaselnfoDirty; // Информация изменена
18 protected bool TitlesSetLoaded; // Названия загружены.
19
20 public Author(string DbHost, string Dblnstance )
21 {
-H

22
С
2

II
1

23 TitlesSetLoaded = false;
24 BaselnfoLoaded = false;
25
26 Host = DbHost;
27 Instance = Dblnstance;
28 }

В строках 4 и 5 объявляются имена хоста и сервера, как и в классе Authors­


ListBuilder. В строках 8-11 объявлены основные данные объекта Author,
а в строках 16-18 - переменные состояния, с помощью которых отслеживается,
были данные загружены или модифицированы. Конструктор инициализирует
переменные, существенные для программы.

Совет В листингах 5.3 и 5.4 демонстрируется прямое сохранение дан­


ных в классе, который должен это поддерживать. Другой вари­
ант - воспользоваться классом D a ta S e t. Такой подход сложнее,
поэтому я здесь от него отказался, но зато он открывает новые
возможности, например считывание/запись данных не из базы, а
из X M L -потока.
ιιιιι Классы и компоненты

В следующем фрагменте кода (листинг 5.4) реализована загрузка основных


данных об авторе и названий книг из базы с помощью методов LoadBaselnf о ()
и LoadTitles () и вспомогательного метода GetConnection ().
Листинг 5.4. Загрузка информации в защищенные члены класса Author
1 protected void LoadBaselnfo
2
3 if ( Au_id != "" )
4 {
5 SqlConnection Conn = null;
6 SqlCommand Cmd = null;
7 SqlDataReader Dr = null;
8
9 try
10 {
11 Conn = GetConnection();
12
13 // Выборка основной информации.
14 Cmd = Conn.CreateCommand();
15 Cmd.CommandText =
16 "SELECT au_fname, au_lname, contract " +
17 "FROM authors " +
18 "WHERE au_id = "" + Au_id +
19 Dr = Cmd.ExecuteReader
20 ( CommandBehavior.SequentialAccess );
21
22 // Загрузить данные в поля класса,
23 if ( Dr.Read() )
24 {
25 FName = Dr.GetString(0);
26 LName = Dr.GetString(1);
27 IsContract = Dr.GetBoolean(2);
28 BaselnfoLoaded = true;
29 }
30 }
31 catch
32 {
33 // Если была ошибка, данные не загружены.
34 BaselnfoLoaded = false;
35 throw;
36 }
37 finally
38 {
39 if ( Cmd != null )
40 {
41 Cmd.Dispose();
42 }
43
44 if ( Conn != null )
Свойства I I I ···· 139

45 {
46 Conn.Dispose();
47 }
48 }
49
50 else
51 throw new ArgumentException(
52 "Обращение к свойству, когда идентификатор
не задан.",
53 "Id" );
54
55
56 protected void LoadTitlesO
57 {
58 SqlConnection Conn = null;
59 SqlDataAdapter SqlDa = null;
60 String Select;
61
62 if ( Au_id != ////

63 {
64 try
65 {
66 // Соединиться с базой данных,
67 Conn = GetConnection();
68
69 Select = String.Format(
70 "SELECT t.title_id, title, type, pub_id, " +
71 "price, advance, royalty, ytd_sales " +
72 "FROM pubs..titles t " +
73 "JOIN pubs..titleauthor ta " +
74 " ON ta.title_id = t.title_id " +
75 "WHERE ta.au_id = "{0}"", Au_id );
76
77 SqlDa = new SqlDataAdapter( Select, Conn );
78 TitlesSet = new DataSet("Titles");
79 SqlDa.Fill(TitlesSet);
80
81 Conn.Close();
82 Conn.Dispose();
83
84 TitlesSetLoaded = true;
85 }
86 catch
87 {
88 // Если было исключение, данные не загружены.
89 TitlesSetLoaded = false;
90
91 // Освободить соединение,
92 if ( Conn != null )
Классы и компоненты

93 : {
94: Conn.Dispose();
95 : }
96 :
97: // Передать исключение вверх по цепочке.
98: throw;
99 : }
100 : }
101: else
102: throw new ArgumentException(
103: "Обращение к свойству, когда идентификатор
104: не задан.", "Id" );
105 : }
106 :
107: protected SqlConnection GetConnection()
108 : {
109: SqlConnection Conn = null;
110: StringBuilder Sb = new StringBuilder();
111:
112: try
113 : {
114: // Построить строку соединения.
115: if ( Instance.Length > 0 )
116: Sb.AppendFormat("SERVER={0}\\{1};UID=sa",
117: Host,
118 : Instance) ;
119: else
120 : Sb.AppendFormat("SERVER={0};Trusted_Connection=Yes",
121: Host );
122 :
123: // Соединиться с базой данных.
124: Conn = new SqlConnection( Sb.ToString() );
12 5: Conn.Open();
12 6: Conn.ChangeDatabase("pubs");
127 :
128: return Conn;
129 : }
13 0: catch
131: {
132: if ( Conn != null )
133 : {
134: Conn.Close();
135: Conn.Dispose();
136 : }
137 :
13 8: // Передать исключение вверх по цепочке.
13 9: throw;
140 : }
141 : }
Свойства I I I ···· 141

Метод LoadBaselnfo () (строки 1-54) загружает в объект Author основ­


ную информацию об авторе, а метод LoadTitles () пользуется объектом Sql
DataAdapter для привязки объекта DataSet к подмножеству записей в табли­
це titles, которые соответствуют книгам данного автора. С помощью объекта
DataSet класс Author позволяет своим клиентам обращаться к любой колонке
таблицы titles. Напротив, сам объект Author дает клиенту доступ только к тем
колонкам, которые он предварительно загрузил.
На данный момент класс Author умеет устанавливать свое внутреннее состо­
яние, загружать данные об авторе и написанных им книгах. Остается предоста­
вить доступ к этой информации программе-клиенту. В листинге 5.5 реализованы
свойства, которые по мере необходимости обращаются к соответствующим за­
грузчикам.
Листинг 5.5. Клиенты получают доступ к классу Au thor с помощью свойств,
которые загружают информацию из базы по мере необходимости
1 : public string FirstName
2: {
З: get
4: {
5: // Если базовая информация не загружена,
// загрузить ее.
б: / / В случае ошибки метод возбудит исключение.
7: if ( !BaselnfoLoaded )
8: LoadBaselnfo() ;
9:
10: / / В противном случае необходимая информация
// уже есть.
11: return FName;
12 : }
13: set
14 : {
15: BaselnfoDirty = true;
16: FName = value;
17 : }
18 : }

20: public String LastName


21 : {
22: get
23 : {
24: // Если базовая информация не загружена,
// загрузить ее.
25: / / В случае ошибки метод возбудит исключение.
26: if ( !BaselnfoLoaded )
27: LoadBaselnfo();
28 :
29: / / В противном случае необходимая информация
// уже есть.
Классы и компоненты

30: return LName;


31 : }
32: set
33 : {
34: BaselnfoDirty = true;
35: LName = value;
36 : }
37 : }
38 :
39: public bool IsContract
40 : {
41: get
42 : {
43: // Если базовая информация незагружена,
// загрузить ее.
44: / / В случае ошибки метод возбудит исключение.
45: if ( !BaselnfoLoaded )
46: LoadBaselnfо () ;
47 :
48: / / В противном случаенеобходимая информация
// уже есть.
49: return Contract;
50 : }
51: set
52 : {
53: BaselnfoDirty = true;
54: Contract = value;
55 : }
56 : }

58: public string Id


59 : {
60: get
61: {
62: return Au_id;
63 : }
64: set
65 : {
66: if ( Au_id != value )
67: {
68: Au_id = value;
69: TitlesSet = null;
70 : }
71 : }
72 : }
73 :
74: public DataSet Titles
75: {
76: // Для данного свойства предоставляется
// только метод чтения.
Пространства имен I I I · · · · 143

77: // Установка значения в этом случае не имеет смысла.


7 8: get
79 : {
80: if ( !TitlesSetLoaded )
81: LoadTitles ();
82 :
83: return TitlesSet;
84 : }
85: }
86 : }
В листинге 5.5 показана реализация нескольких свойств: FirstName, Last-
Name и IsContract. Каждое из них имеет тип, соответствующий внутреннему
полю данных, и проверяет значение поля Baselnf oLoaded. Если информация
еще не была загружена, то вызывается нужная функция-загрузчик, а затем возвра­
щается значение, запрошенное клиентом. Свойство Titles делает то же самое по
отношению к объекту TitlesSet класса DataSet.
С помощью свойств вы можете предоставить доступ к данным-членам своего
класса, сохранив при этом полный контроль над ними. В результате обеспечивается
непротиворечивость состояния класса и удается элегантно реализовать отложенную
загрузку данных, что и продемонстрировано на примере класса Author.

Пространства имен
В C# объявления методов и переменных видны только в объемлющей об­
ласти действия, которая может быть классом, методом, свойством или блоком. Но
в программах и особенно в библиотеках (в частности, в самом каркасе .NET Frame­
work) необходимы более широкие области, в которых можно сгруппировать мно­
жества взаимосвязанных типов. Соответствующий механизм называется про­
странством имен.
Пространство имен - это способ концептуальной группировки объявлений ти­
пов. Его необходимо импортировать в программу с помощью предложения using.
Так, System, System. Data и System.Xml представляют собой примеры про­
странств имен, определенных в каркасе .NET Framework. Но вы можете создавать
и собственные пространства имен с помощью ключевого слова namespace. В
листинге 5.6 показано пространство имен Authors, которому принадлежат классы
Author и AuthorsListBuilder.
Л истинг 5.6. Ключевое слово namespace в в о д и т имя для совокупности элементов
кода, которые можно импортировать с помощью предложения using
1 : namespace Authors
2: {
З: public class Author
4: {
5:
6: . Здесь находится реализация
7:
8: }
Illll Классы и компоненты

9
10 public class AuthorsListBuilder
11
12
13 . Здесь находится реализация
14
15
16
Реализованное в листинге 5.6 пространство имен вводит область действия
для библиотеки классов, относящихся к авторам. Порядок использования этих
классов демонстрируется в листинге 5.7.
Листинг 5.7. Клиентский код импортирует пространство имен Authors
для упрощения доступа к содержащимся в нем объявлениям
1 using System;
2 using Authors;
3 using System.Data;
4 using System.Collections;
5
6 class AuthorUserMain
7 {
8 static void Main(string[] args)
9 {
10 double f = 0.0;
11 string s = "это строковое значение.";
12
13 try
14 {
15 AuthorsListBuilder AL = new
AuthorsListBuilder("(local)", "");
16 SortedList SL = A L .GetAuthors();
17
18 foreach ( Author a in SL.Values )
19 {
20 Console.WriteLine("\п\пВыбираем автора с ид {0}.",
а .Id );
21 Console.WriteLine( "Ид: {0}, Имя: {1} {2}",
22 a.Id, a.FirstName, a.LastName );
23
24 foreach ( DataTable Dt in a .Titles.Tables )
25 {
26 foreach( DataRow Dr in Dt.Rows )
27 {
28 Console.WriteLine("Название: " + Dr["title"]);
29 }
30 }
31
32 }
33 catch ( System.Data.SqlClient.SqlException se )
Резюме ш ииида
34 : {
35: Console.WriteLine( se.ToString() );
36: }
37 : }
38 : }
После того как код откомпилирован в сборку, на нее следует явно сослаться
с помощью метаданных программы или поместить ее в глобальный кэш. Код, пока­
занный в листинге 5.7, был откомпилирован в исполняемый файл, ссылающийся
на сборку Authors .dll, которая содержит код класса Author. Предложение
using в строке 2 импортирует типы из пространства имен Authors для исполь­
зования в классе AuthorUserMain. По окончании описанного процесса можно
спокойно обращаться к классу AnchorsListBuilder в строке 15 и к классу
Author в строке 18.
Совет Пространства имен не ограничиваются единственным файлом.
В одно пространство имен можно включать классы, находящиеся
в разных файлах, которые затем компилируются в общую сборку.

Резюме
Для классов - основы объектно-ориентированного программирования -
в языке C# имеется хорошая поддержка. В главе 5 вы познакомились с раз­
личными возможностями, которые C# предоставляет для разработки классов,
а также видели, как организовывать группы взаимосвязанных объявлений в про­
странство имен. По ходу дела была продемонстрирована техника оптимизации
классов, обращающихся к базе данных, за счет отложенной загрузки информации.
В главе 6 мы детально изучим вопрос о том, как каркас .NET Framework манипу­
лирует классами, сохраняет и восстанавливает их.
Глава 6. Управление памятью и C#
Главная цель платформы .NET - сделать программирование более простым и про­
изводительным и, как следствие, повысить надежность продукта. Хотя традицио­
налисты убеждены в том, что надежность управления памятью можно обеспечить
за счет следования стандартам безопасного программирования, за это приходится
платить высокую цену. В большинстве проектов, в которых мне довелось быть ру­
ководителем или участником, именно ошибки, связанные с управлением памятью,
было тяжелее всего отлаживать. В одном проекте на C++ примерно 40% времени,
потраченного на обеспечение надежности, пришлось на устранение утечек памяти.
Чтобы уменьшить эти затраты, среда исполнения .NET предоставляет меха­
низмы управления памятью, включая и асинхронный сборщик мусора. Да, при
написании неконтролируемого (читай, «небезопасного») кода вам по-прежнему
придется убирать за собой самостоятельно, но зато, если вы пишете контролиру­
емый код, то на большую часть проблем, связанных с освобождением ресурсов,
можете просто не обращать внимания. Только в тех случаях, когда объект захва­
тывает ограниченный ресурс (например, открывает файл или соединение с базой
данных), вы должны позаботиться о его явном освобождении.
Сборщик мусора в общем случае способен существенно повысить надежность
с куда меньшими усилиями, но считать его панацеей не следует. Как вы вскоре
увидите, в некоторых особых случаях нужно отчетливо понимать, как он функцио­
нирует, и предпринимать необходимые шаги для того, чтобы программа работала
правильно.

Управление памятью в каркасе .NET Framework


Объекты - экземпляры типов - создаются в контролируемой куче оператором
new. Менеджер памяти распределяет всю память, занятую объектами, в непрерыв­
ном блоке в начале кучи и удовлетворяет запрос на создание нового объекта, вы­
деляя область, непосредственно следующую за уже распределенным участком.
Выделение памяти со следующего свободного адреса выполняется очень эф ­
фективно, но рано или поздно вся доступная память будет исчерпана. Кроме того,
по мере выполнения программы ранее созданные объекты выходят из области
действия, то есть становятся ненужными, в результате чего в куче образуются
дыры. Поэтому, когда дальнейшее выделение памяти становится невозможным,
в игру вступает сборщик мусора, который перемещает активные объекты обратно
в начало кучи, так что они снова занимают непрерывную область.
Код в листинге 6.1 демонстрирует, как работает механизм выделения памяти.
Управление памятью в каркасе .NET Framework ЩШ\

Листинг 6.1. Управление памятью быстро усложняется


1 class MyObj
2 {
3 public void MyMethod()
4 {
5 String [] AnArray = new String[50];
6 // Здесь делаем что-то полезное.
7 }
8
9
10
11 MyObj m = new MyObj();
12 ш .MyMethod();
13
14 MyObj [] MyArray = new MyObj[10];
15 for ( int i = 0; i < 10; i++ )
16 MyArray[i] = new MyObj();
17
18 m = MyArray[5];
В строке 11 создается экземпляр типа MyObj, память для которого выде­
ляется в начале свободной области в куче. Когда в строке 12 вызывается метод
MyMethod (), код в строке 5 выделяет память для массива и 50 объектов String
непосредственно после объекта MyObj. Во время работы метода MyMethod () куча
выглядит так, как показано на рис. 6.1.
После возврата из MyMethod () массив и строки оказываются сиротами -
ссылка на них, находившаяся в кадре стека, вышла из области действия и массив
стал недоступен для программы. В строке 18 то же самое происходит с созданным
ранее экземпляром MyObj, поскольку ссылка на него, хранившаяся в перемен­
ной ш, теперь затерта. В результате создалась ситуация, показанная на рис. 6.2.

10 экземпляров MyObj

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

50 экземпляров строки 50 осиротевших


экземпляров строки

Экземпляр массива Осиротевший


экземпляр массива

Экземпляр MyObj Осиротевший


экземпляр MyObj

Рис. 6.1. Объекты


Рис. 6.2. Со временем в куче
распределяются в куче по
образуются дыры
мере создания
IIIIIL Управление памятью и C#

После того как строка 18 выполнена, остались только корневые ссылки


МуАггау и ш. Ни на исходный объект MyObj, ни на массив строк, ни на сами со­
держащиеся в нем строки ссылок больше не существует. Воспользоваться этими
объектами нет никакой возможности, поэтому их нужно уничтожить и освободить
память.
В этот момент сборщик мусора начнет строить граф ссылок, начиная с каж­
дого корня. Поскольку в каждом типе имеются метаданные, сборщик может по­
местить каждый корень в список, затем добавить те объ­
екты, на которые ссылаются корни и далее рекурсивно
посетить все объекты, до которых он способен добраться.
Свободная память По завершении операции сборщик мусора знает обо всех
объектах, которые программа еще может использовать;
все остальные уничтожаются на следующем шаге, когда
10 экземпляров MyObj сборщик начнет обходить и уплотнять кучу. Память, за­
нятая недостижимыми объектами в начале кучи, переда­
Экземпляр массива
ется активным объектам, расположенным выше, так что
по завершении процедуры все оставшиеся объекты снова
будут занимать непрерывную область (рис. 6.3).
Рис. 6.3. Дыры в куче Для несложных объектов описанной выше проце­
схлопнуты сборщиком дуры достаточно. Увы, не всегда все так просто. Я уже
мусора говорил, что объекты вправе захватывать ограниченные
ресурсы, а, кроме того, программа может манипулировать
данными такими способами, о которых сборщик мусора ничего не знает. Для реше­
ния первой проблемы среда исполнения предоставляет интерфейс IDisposable,
а для решения второй - так называемый чистильщик (finalizer). В совокупности
эти два механизма позволяют разрешить все сложности, связанные с управлением
ограниченными и неконтролируемыми ресурсами.

Интерфейс IDisposable
Класс реализует интерфейс IDisposable, чтобы иметь возможность освобо­
дить ограниченные ресурсы детерминированно, не дожидаясь, пока сборщик му­
сора запустится и уничтожит объект-владельца. Для реализации этого интерфей­
са достаточно предоставить единственный открытый метод с такой сигнатурой:
public void Dispose();
Он должен освободить все ресурсы, включая и данные о состоянии, если для
них была выделена память. В листинге 6.2 показан пример реализация интерфейса
IDisposable.
Листинг 6.2. Использование интерфейса IDisposable
для освобождения ограниченных ресурсов
using System;
using System.Data;
using System.Data.SqlClient;
using System.Text;
Управление памятью в каркасе .NET Framework И Н Н 149

6: namespace GCDisposeFinalize
7: {
8: class MyAuthor : IDisposable
9: {
10: String NameVal =
11: SqlConnection Conn = null;
12: String ConnStr =
13: bool Disposed = false;
14 :
15: public void Dispose()
16: {
17: if ( IDisposed && Conn != null )
18 : {
19: // Conn.CloseO вызывает Conn.Dispose()
2 0: Conn.Close() ;
21: Conn = null;
22: ConnStr =
23 : }
24: else
25: throw new
Obj ectDisposedException("MyAuthor",
26: "Освобожден более одного раза." );
27 : }
28 :
29: public MyAuthor( String MyCStr )
30 : {
31: ConnStr = MyCStr;
32 : }

34: private void GetData(int au_id)


35: {
36: // Реализует взаимодействие с базой данных.
37: SqlCommand cmd = null;
38: SqlDataReader dr =null;
39 :
40: // Соединяемся с базой данных.
41: if ( Conn == null )
42 : {
43: Conn = new SqlConnection(ConnStr);
44: Conn.Open();
45 : }
46 :
47: // Выбираем информацию об авторе.
48: StringBuilder sb = new StringBuilder();
49: sb.Append("SELECT au_fname + " " + au_lname");
50: sb.AppendFormat(" FROM authors where au_id like
51: "{0}%"" , au_i d);
52 :
53: cmd = Conn.CreateCommand();
54: cmd.CommandText = sb.ToString();
■ ■ ■ ■ Ill Управление памятью и C#

55
56 dr = cmd.ExecuteReader
(CommandBehavior.SequentialAccess);
57
58 // Получаем и сохраняем имя.
59 if ( dr.Read() )
60 {
61 NameVal = dr.GetString(0);
62 }
63
64 dr.Close () ;
65 cmd.Dispose();
66 }
67
68 public void Find( int au_id )
69 {
70 if ( IDisposed )
71 {
72 GetData(au_id);
73 }
74 else
75 throw new ObjectDisposedException
( "MyAuthor",
76 "Освобожден до вызова Find()" );
77 }
78
79 public string Name
80 {
81 get
82 {
83 if ( IDisposed )
84 return NameVal;
85 else
86 throw new ObjectDisposedException
( "MyAuthor",
87 "Освобожден до доступа к имени"
00
00

}
89 }
90 }
91
92 // Главный поток.
93 class MainClass
94 {
95 const string ConnStr =
96 "SERVER=(local)\\NETSDK;DATABASE=pubs;UID=sa";
97
98 static void Main(string[] args)
99 {
100 MyAuthor ma = new MyAuthor(ConnStr);
Управление памятью в каркасе .NET Framework И Н Н 151

101 :
102: for ( int і = 1; і < 10; і++ )
103 : {
104: т а .Find(і);
105 :
10 6: // Получили автора, выводим имя.
107: Console.WriteLine("{0}'s ид начинается
с {1}.",т а .Name,і);
108 : }
109 :
110: т а .Dispose();
111 : }
112 : }
113 : }

Класс My Author предоставляет очень простой доступ к информации об авто­


рах, хранящейся в таблице authors в базе данных pubs. В нем применяется ти­
пичная техника оптимизации - создание одного соединения с базой данных, кото­
рое хранится в переменной-члене Conn и используется на протяжении нескольких
обращений. Клиенты класса могут вызывать метод Find () для поиска по автору
(строка 68) и свойство Name для получения имени найденного автора (строка 79).
Проблема, относящаяся к ресурсу, возникает в методе GetData () (строки
34-66). Пока пользователь не запросит запись, обратившись к методу Find (),
ничего дорогостоящего еще не захвачено. Но Find () вызывает метод GetData ()
для выборки данных из базы, а тот создает объект SqlConnection (строки 43
и 44). Чтобы избежать многократного установления соединения с базой, представ­
ленное данным объектом соединение остается открытым в течение всего времени
жизни объекта. В случае, когда некоторый объект создается и многократно исполь­
зуется, это дает заметный выигрыш в производительности. Однако при таком условии
соединение может оставаться открытым, хотя объект уже давно вышел из области
действия, и не закрывается до очередного запуска сборщика мусора.
Для решения этой проблемы в строках 15-27 реализован метод IDispo­
sable .Dispose (), который позволяет клиенту явно сообщить объекту, что он
больше не нужен. Головная программа вызывает этот метод в строке 110, вследствие
чего соединение освобождается сразу после вызова Conn. Close (). Теперь объект
My Author занимает только память, требуемую для него самого и его членов. Хотя
и он, и объект SqlConnection останутся в памяти, пока не будут убраны сборщи­
ком мусора, дорогостоящий ресурс (соединение с базой данных) уже освобожден.

Примечание В классах, описывающих такие ресурсы, как сетевое соединение или


файл, часто присутствует метод C lo s e (). По общепринятому
соглашению его семантика такая же, как у метода D is p o s e ( ).
Комментарий (строка 19) показывает, что метод C lo s e () ведет
себя так, будто он вызвал метод D is p o s e () соединения, хотя
внутренняя реализация может быть иной.
152 ■ ■ ■ ■ III Управление памятью и C#

После того как для объекта был вызван метод Dispose (), его уже нельзя ис­
пользовать. Поэтому в объекте MyAuthor имеется поле Disposed, устанавливаемое
в true внутри Dispose (). Все открытые методы проверяют эту переменную и
возбуждают исключение при попытке что-то сделать после освобождения объекта.

Чистильщики
Для классов, манипулирующих неконтролируемыми данными, в .NET преду­
смотрена концепция чистильщика (finalizer), то есть метода, который вызывается
непосредственно перед уборкой объекта. В других языках, например в V isual
Basic.NET, программист должен реализовать специальны й метод с именем
Finalize, выступающий в роли чистильщика. В C# чистильщики реализуются
в виде деструкторов, как показано в листинге 6.3. Эта программа запрашивает
ресурсы у операционной системы, пользуясь небезопасным кодом, и вызывает
деструктор для очистки в случае, если не было обращения к методу Dispose ().
Л истинг6.3. Использование метода D i s p o s e ( ) и очистки
для освобождения контролируемых и неконтролируемых ресурсов
1 class BinaryFileReader : IDisposable
2 {
3 // Требует, чтобы открываемый файл существовал,
4 const uint OpenExisting = 3;
5
6 // Запрашивает право на чтение,
7 const uint GenericRead = 0x80000000;
8
9 // Возвращается в случае ошибки открытия ресурса,
10 const uint InvalidHandleValue = OxFFFFFFFF;
11
12 // Функции из ядра Win3 2.
13 [DllImport("kernel32", SetLastError=false)]
14 static extern unsafe uint CreateFile
15 (
16 string lpFileName,
17 uint dwDesiredAccess,
18 uint dwShareMode,
19 uint IpSecurityAttributes,
20 uint dwCreationDisposition,
21 uint dwFlagsAndAttributes,
22 uint hTemplateFile
23
24
25 [DllImport("kernel32", SetLastError=false)]
26 static extern unsafe bool CloseHandle
27 (
28 uint hFile
29
30
31 [DllImport("kernel32", SetLastError=false)]
Управление памятью в каркасе .NET Framework И Н Н 153

32 : static extern unsafe bool GetFileSizeEx


33 : (
34: uint hFile,
35: ulong* lpFileSizeHigh
3 6: );

38: [Dlllmport("kernel32", SetLastError=false)]


39: static extern unsafe bool ReadFile
40 : (
41: uint hFile,
42: void* lpBuffer,
43: uint nBytesToRead,
44: uint* nBytesRead,
45: uint overlapped
46: );

48: unsafe byte [] Buffer = null;


49: ulong BufferSize = 0;
50: uint CurrentFileHandle = 0;

52: // Открытые методы для доступа к данным.


53 :
54: // Получить длину прочитанных данных.
55: public ulong Length
56: {
57 : get { returnBufferSize; }
58: }

60: // Индексатор для безопасного доступа кбуферу.


61: public int this[ulong index]
62 : {
63: get
64 : {
65: if ( index >= 0 && index < Length )
66: return Buffer[index];
67 : else
68: throw new IndexOutOfRangeException(
69: "Доступ к BinaryFileReader[]");
70 : }
71 : }
72 :
73: // Считывает данные в память.
74: unsafe public void ReadData( string path )
75: {
76: uint FileHandle =CreateFile( path, GenericRead,
77: 0, 0, OpenExisting,
7 8: 0,0);
79 :
80: if ( FileHandle != InvalidHandleValue )
81: {
154 ■ ■ ■ ■ III Управление памятью и C#

82: ulong NewBufferSize = 0;


83: uint BytesRead = 0;
84 :
85: // Сохраним описатель, чтобы им можно было
86: // пользоваться в последующих операциях.
87: CurrentFileHandle = FileHandle;
88 :
89: if ( GetFileSizeEx(FileHandle, &NewBufferSize) )
90 : {
91: if (BufferSize <NewBufferSize )
92 : {
93: BufferSize = NewBufferSize;
94: Buffer =new byte[BufferSize];
95 : }
96 :
97: fixed ( void* BufferPtr = Buffer )
98: if ( !ReadFile(FileHandle,
99: BufferPtr,
100: (uint)BufferSize,
101: &BytesRead,
102: 0)||
103: BytesRead != BufferSize)
104 : {
105: throw new IOException(
106: "Ошибка при чтении файла.");
107 : }
108 : }
109: else
110: throw new IOException(
111: "Ошибка при получении размера
файла.");
112 : }
113: else
114: throw new FileNotFoundException(
115: "B BinaryFileReader.ReadData()",
116: path);
117 : }
118 :
119: // Файл уже закрыт?
120: bool Disposed = false;
121 :
122: // Освобождает текущий открытый файл.
123: unsafe private void CloseFileO
124 : {
125: if ( CurrentFileHandle != 0 &&
12 6: CurrentFileHandle != InvalidHandleValue )
127 : {
128: CloseHandle( CurrentFileHandle );
129: CurrentFileHandle = 0;
Управление памятью в каркасе .NET Framework И Н Н

130
131
132
133 // Контролируемое освобождение,
134 public void Dispose()
135
136 if ( IDisposed )
137
138 // Освободить описатель файла.
139 CloseFile();
140
141 // Освободить ссылку.
142 Buffer = null;
143
144 // Запретить финальную очистку.
145 G C.SuppressFinalize(this);
146
147 else
148 throw new
Obj ectDisposedException("BinaryFileReader",
149 "Освобожден более одного раза." );
150
151
152 // Очистка.
153 -BinaryFileReader()
154
155 if ( CurrentFileHandle != 0 )
156 CloseFile();
157
158
Сам язык C# не налагает ограничений на поведение чистилыцика-деструкто-
ра, чего нельзя сказать о каркасе .NET. Чистильщик предназначен для освобож­
дения только неконтролируемых ресурсов. Контролируемые объекты, вроде ранее
продемонстрированного SqlConnection, не следует освобождать в деструкторе.
Сборщик мусора не дает абсолютно никаких гарантий относительно того, в каком
порядке он будет убирать объекты. Деструктор вызывается непосредственно перед
уборкой объекта, при этом те контролируемые объекты, на которые он ссылает­
ся, уже могут быть убраны. Поэтому при вызове любого их метода, в том числе
Dispose (), вероятно исключение из-за обращения по нулевой ссылке, и програм­
ма аварийно завершится.
В программе, показанной в листинге 6.3, используется атрибут Dll Import для
статических методов, обращающихся к функциям из библиотеки KERNEL32 .DLL.
В строках 4-10 объявлены константы, применяемые в функциях работы с файлами
и описателями, значения их взяты из файла WINNT .Н. В строках 12-46 объявлены
прототипы функций API операционной системы, которые понадобятся программе.
Все остальное понятно любому человеку, знакомому с программированием на
платформе Win32. Основные действия выполняются в методе ReadData (), кото­
рый считывает содержимое файла в буфер, оставляя описатель файла открытым.
156 ■ ■ ■ ■ III Управление памятью и C#

Когда объект перестает быть нужным, может произойти одно из двух: либо
клиент вызовет метод Dispose () для освобождения ресурсов, либо сборщик
мусора вызовет деструктор -BinaryFileReader (). Первый вариант для клиента
предпочтительнее, поскольку ресурс освобождается, как только в нем отпадает
необходимость. Метод Dispose () вызывает метод CloseFile () для закрытия
описателя файла (строка 139), а затем освобождает и ссылку на буфер (строка 142).
Особый интерес представляет вызов метода GC .SuppressFinalize () в строке
145. В ходе сборки мусора объекты, имеющие чистильщиков, уничтожаются не сра­
зу, а помещаются в очередь на очистку; отдельный поток, работающий внутри CLR,
вызывает чистильщиков объектов, находящихся в очереди. После завершения
очистки объекты убираются сборщиком. Но, после того как в методе Dispose ()
были освобождены неконтролируемые ресурсы, вызывать еще и чистильщика
излишне. Чтобы избежать ненужных расходов, метод Dispose () может вызвать
GC .SuppressFinalize () для подавления очистки данного объекта. Техника,
показанная в листинге 6.3, рекомендуется и в общем случае. Объявите метод, осво­
бождающий все неконтролируемые ресурсы (в листинге 6.3 это CloseFile () ),
а затем объявите метод Dispose (), который освобождает все ресурсы, и чистиль­
щика, отвечающего за освобождение только неконтролируемых ресурсов.

Совет В программе следует избегать феномена, именуемого воскрешени­


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

Слабые ссылки
Часто возникает дилемма: с одной стороны, вам хотелось бы иметь под ру­
ками объект, занимающий много памяти, а с другой стороны, может случиться,
что память потребуется для других целей еще до очередного обращения к этому
объекту. По какой-то причине - скажем, потому что конструирование объекта
обходится дорого - вы не желаете конструировать его при каждом обращении, но
между обращениями к объекту занятая им память все-таки может понадобиться.
Для решения данной проблемы в .NET имеются так называемые слабые ссылки
(weak references). Слабая ссылка позволяет удержать объект в памяти, разрешив
в то же время сборщику мусора при необходимости убрать его. Вам надлежит создать
слабую ссылку на объект, а затем уничтожить другие ссылки на него. Если сборщик
мусора запустится, объект будет убран; если нет, то вы сможете получить сильную
ссылку на объект, воспользовавшись свойством Target класса WeakRef егепсе.
Методика работы с этим классом демонстрируется в листинге 6.4.
Управление памятью в каркасе .NET Framework И Н Н 157

Листинг 6.4. Использование слабых ссылок резко повышает гибкость механизма


управления памятью
1 BinaryFileReader Reader = new BinaryFileReader();
2
3 // Поработаем с объектом.
4
5 WeakReference w = new WeakReference( Reader );
6 Reader = null;
7
8 Программа занимается другим делом, объект может быть убран,
9
10 if ( w.IsAlive )
11 {
12 Reader = (BinaryFileReader)w.Target;
13 }
14 else
15 {
16 // Повторно сконструировать объект
17 Reader = new BinaryFileReader();
18 Reader.ReadData( FileName );
19
20
21 // Снова воспользоваться объектом.
В строке 1 создается объект BinaryFileReader, который потом можно будет
использовать для работы с файлом. В строке 5 мы получаем слабую ссылку на
данный объект, а сильная ссылка на него уничтожается в строке 6 путем присваи­
вания значения null переменной Reader. Затем программа приступает к другим
действиям, как следует из комментария в строке 8. Если в этот момент запустится
сборщик мусора, то объект будет очищен, а занятая им память убрана.
В строке 10 объект может существовать или нет. Поэтому программа опраши­
вает свойство IsAlive объекта класса WeakRef erence, чтобы узнать, жив ли
еще объект. Если это так, то в строке 12 восстанавливается сильная ссылка на него,
в противном случае в строках 17 и 18 объект воссоздается. Затем программа в
состоянии снова пользоваться объектом.
За счет слабых ссылок программа получает в свое распоряжение дополни­
тельную память, коль скоро в ней возникнет необходимость; если же удастся
обойтись без сборки мусора, допустимо вернуться к использованию объекта, не
платя за его повторное создание. В только что продемонстрированном контексте
класс WeakRef erence больше ни на что не годен, такое его применение назы­
вают короткой слабой ссылкой. Но можно попросить объект WeakRef erence
следить за воскрешениями, передав второй булевский параметр, равный true,
его конструктору. В подобном случае будет создана длинная слабая ссылка: если
чистильщик объекта воскрешает его, об этом извещается объект WeakRef erence.
Однако из-за принципиальных недостатков, присущих механизму воскрешения, я
не рекомендую пользоваться и длинными слабыми ссылками.
158 ■ ■ ■ ■ III Управление памятью и C#

Использование памяти в C#
Даже в контролируемой среде использование памяти связано с некоторыми
проблемами. Каркас .NET Framework поддерживает как контролируемые, так
и неконтролируемые ссылки, что ведет к усложнению работы. Простая программа
может слепо доверять сборщику мусора, но более сложные приложения обязаны
принимать это во внимание.

Предложения fixed и using


В языке C# есть два предложения, помогающие в управлении ресурсами. Кон­
тролируемые ссылки - это переменные, которые объявляются в программе и ас­
социируются с объектами посредством ключевого слова new. Среда исполнения
контролирует такие объекты и ссылки на них, обновляя указатели во время сборки
мусора. Но в листинге 6.3 вы видели, что есть возможность использовать пред­
ложение f ix e d , чтобы временно закрепить объект в памяти и не дать сборщику
мусора переместить его.
fixed ( void * BufferPtr = Buffer )
if ( !ReadFile(FileHandle,
BufferPtr,
(uint)BufferSize,
&BytesRead,
0) II
BytesRead != BufferSize)
{
throw new IOException(
"Ошибка при чтении файла.");
}
Предложение f i x e d объявляет указатель, который будет направлен на кон­
кретный объект, но только на время выполнения предложения. Смысл его в том,
чтобы вы могли пользоваться объектами в памяти при обращении к платформен­
ному API, а также в том, чтобы разрешить реализацию алгоритмов, которые на­
иболее естественно записываются с помощью указателей. Предложение f i x e d
закрепляет объект только в ограниченной области действия, так как наличие
глобально закрепленных объектов лишило бы смысла сборщик мусора и при­
вело к падению производительности. Хотя это ограничение нетрудно обойти, я
рекомендую свести применение данной возможности к минимуму.
Еще одно предложение, полезное для управления ресурсами, - u s in g . Объ­
екты, которые захватывают неконтролируемые ресурсы, должны реализовывать
интерфейс I D is p o s a b le ; в C# предложение u s i n g автоматизирует вызов метода
D is p o s e () после того, как программа закончила работу с объектом. Так, в сле­
дующем примере для чтения файла применяется объект B i n a r y F il e R e a d e r :
using ( BinaryFileReader Reader = new BinaryFileReader() )
{
Использование памяти в C # I I I · · · · 159

try
{
// FileName - это строковая переменная, хранящая имя
// читаемого файла.
Reader.ReadData( FileName );

// Сделаем что-нибудь с данными.


}
catch (Exception e)
{
// Обработаем ошибки...
}
}
В списке параметров предложения using имеется одно или несколько предло­
жений создания объектов, разделяемых запятыми. Код внутри блока, следующего
за using, может манипулировать этими объектами, а по выходе из блока для
каждого объекта вызывается метод Di spose (). Такая техника позволяет избежать
утечки ресурсов в случае, если вы забудете вызвать Dispose () сами.

Эффективное управление памятью


Хотя управление памятью и сборка мусора на платформе .NET реализованы
весьма эффективно, можно еще больше ускорить работу программы. На произво­
дительность сборщика мусора влияет степень фрагментации, поэтому, даже когда
проект программы продиктован совсем другими соображениями, если эффектив­
ность работы вас не устраивает, обратите внимание на фрагментацию.
Для начала следует сократить применение закрепленных и неконтролируемых
ресурсов и прибегать к ним лишь в крайнем случае. Я взял для примера работу
с файлами, но в библиотеках каркаса .NET Framework есть собственные классы для
поддержки ввода/вывода и других платформенно-зависимых функций - пользо­
вание ими улучшит взаимодействие вашей программы с внешним миром.
При проектировании программы разумно - с точки зрения удобства сопро­
вождения и производительности - сделать граф объектов максимально плоским.
В ходе работы программа начинает с создания главного класса и всех статических
объектов, объявленных в ней самой или в использованных библиотеках. Потом
она создает другие объекты, которые в свою очередь создают новые. Менеджер
памяти должен следить за всеми ссылками, поэтому чем проще дерево объектов,
тем выше производительность.
Например, если в программе может быть очень много однотипных объектов, луч­
ше заранее выделить для них память большим блоком. Тем самым вы уменьшите чис­
ло ссылок, которые придется просматривать сборщику мусора, так как схлопывать
память, занятую объектами, придется реже. Кроме того, если вам все же пришлось
создать большое число объектов в какой-то части программы, вслед за этим полезно
принудительно вызвать сборщик мусора, обратившись к методу GC.CollectO.
160 ■ ■ ■ ■ III Управление памятью и C#

Резюме
Контролируемое исполнение - основа повышения надежности и произво­
дительности программ на платформе .NET (по крайней мере, Microsoft надеется
на это). Для достижения целей, поставленных перед .NET, важную роль играют
механизмы управления памятью, реализованные в среде исполнения CLR. Хотя
в экстремальных ситуациях сборка мусора может отрицательно сказаться на про­
изводительности программы, такой недостаток с лихвой перекрывается просто­
той создания и сопровождения. В главе 7 мы продолжим изучение того, как вы­
полняется программа, и обратимся к технике управления потоком выполнения.
Глава 7. Управление потоком
выполнения программы
Немногие из современных программ могут позволить себе выполнять в каждый
момент времени только одно действие. Параллельное решение различных задач
немаловажно для персональных приложений и уж совершенно незаменимо для
серверов. В этой главе демонстрируются приемы многопоточного программи­
рования, а также использование делегатов и событий для рассылки извещений
получателям и группового обновления.
Единицей обработки на платформе Win32 традиционно является процесс.
Внутри процесса может существовать несколько трактов выполнения, реализу­
емых потоками. У каждого потока имеется собственный набор регистров, при­
оритет выполнения и счетчик команд. Менеджер виртуальной памяти запрещает
одному процессу напрямую обращаться к памяти другого.
Хотя на платформе .NET по-прежнему поддерживаются процессы и потоки,
границей программы для контролируемого кода является домен приложения.
Среда исполнения CLR разделяет домены, позволяя нескольким изолирован­
ным приложениям существовать в рамках одного процесса. При нормальном
выполнении программы содержащий ее домен приложения стартует в начальном
потоке, а потом приложение может создавать новые потоки и разрушать их. Среда
поддерживает пул дополнительных потоков для выполнения отдельных участ­
ков кода в асинхронных контекстах. При этом для коммуникации между такими
потоками используются таймеры, асинхронный ввод/вывод и другие асинхронные
механизмы.

Потоки
Начнем с основ. Чтобы запустить новый поток, нужно создать делегат Thr ead-
Start, указывающий на метод, с которого начнется исполнение в этом потоке.
Затем создается объект класса Thread, конструктору которого передается делегат,
и вызывается метод Start, запускающий выполнение потока. Названные шаги
показаны в листинге 7.1.
Листинг 7.1. Простой пример многопоточного приложения
1: using System;
2: using System.Threading;
3:
4: namespace cmdThreading
5: {
162 ■ ■ ■ III Управление потоком выполнения программы

6: // <summary>
7: // Пакетная программа, демонстрирующая многопоточность.
8: // </summary>
9: class ThreadClass
10 : {
11: // Номер этогопотока.
12: int id;
13 :
14: public ThreadClass( int і )
15: {
16 : id = і;
17 : }
18 :
19: public voidStart()
20 : {
21: try
22 : {
23: Random rnd = new Random( id );
24: int sleepTime = rnd.Next( 10 0, 2 00 );
25 :
26: Console.WriteLine( "Поток {0}стартовал;
27: задержка {1}.", id, sleepTime );
28: Thread.Sleep( sleepTime );
29: Console.WriteLine( "Поток {0} завершился.", id );
30 : }
31: catch
32 : {
33: Console.WriteLine("Поток {0} завершился
аварийно.", id );
34 : }
35: }
36 : }
37 :
38: class cmdThreading
39 : {
40: public static void Main(string[] args)
41 : {
42: Thread [] t = new Thread[10];
43 : int і = 0 ;
44 :
45: // Используем разные циклы, так как для создания
46: // всех объектов требуется время.
47: for ( і = 0; і < t.Length; i++ )
48 : {
49: ThreadClass tc = new ThreadClass( i );
50: t [i] = new Thread( new ThreadStart
( tc.Start ) );
51: t[i].IsBackground = true;
52 : }
Потоки I I I ···
53
54 Console.WriteLine("Запускаем потоки.");
55
56 // Теперь в цикле быстро запустим все потоки,
57 for ( і = 0; і < t.Length; i++ )
58 t [і] .Start() ;
59
60 Console.WriteLine("Все запущены.");
61
62 t [0].Join();
63
64 Console.WriteLine("Выход из приложения.");
65 for ( і = 0; і < t.Length; i++ )
66 if ( t [і].IsAlive )
67 t [і] .Abort () ;
68
69
70
В строках 9 -3 6 определяется тип ThreadClass с конструктором, который
принимает числовой идентификатор потока, и единственным методом Start (),
служащим точкой входа в класс. Создание отдельного класса для инкапсуляции
работы потока - это типичный прием, который позволяет трактовать поток как
мини-программу внутри приложения.
Строки 38-69 содержат главный класс программы, включающий и функцию
Main (), которая выделяет память для массива объектов Thread, где будут хра­
ниться все созданные программой потоки. В строках 47-52 мы в цикле обходим
данный массив, создавая на каждой итерации объект ThreadClass (строка 49)
и объект Thread (строка 50). В строке 50 создается делегат ThreadStart, ко­
торый тут же передается конструктору объекта Thread. Вероятно, вы обратили
внимание на то, что мы не сохраняем ни ссылку на делегат, ни ссылку на объект
ThreadClass, но, поскольку Thread ссылается на ThreadStart, который в
свою очередь ссылается на ThreadClass, в этом нет необходимости.
В строке 51 свойству IsBackground каждого потока присваивается значение
true. Фоновый поток имеет более низкий приоритет, чем приоритетный; пониже­
ние приоритета полезно, если вы хотите, чтобы фоновая обработка не отражалась
на времени реакции пользовательского интерфейса.
В строках 57 и 58 мы снова обходим массив и быстро запускаем все потоки на
исполнение, вызывая метод Start () каждого из них. Важная особенность такого
метода - его асинхронность: мы лишь просим среду исполнения запустить поток,
когда ей будет удобно. Обычно CLR запускает поток спустя некоторое время пос­
ле получения запроса, но с точки зрения приложения это недетерминированный
процесс.
После запуска всех потоков мы ждем в строке 62, пока завершится первый из
них. Поток «умирает», когда управление покидает стартовую функцию (тот метод,
которым мы инициализировали делегат ThreadStart). Чтобы сделать пример бо-
164 ■ ■ ■ III Управление потоком выполнения программы

лее интересным, я включил в метод ThreadClass .Start () случайную задержку,


вычисляемую на основе поля id экземпляра. Подождав указанное время, метод
возвращает управление. Метод Thread .Join () блокирует программу на время,
пока не завершится указанный поток, поэтому в строке 62 выполнение приоста­
навливается до завершения потока 0. Затем в строках 65-67 мы еще раз обходим
все потоки и для каждого вызываем метод Thread.Abort (), насильственно за­
вершая те из них, которые еще работают. Метод Abort () приводит к возбуж­
дению исключения ThreadAbortedException в прерванном потоке, которое
мы перехватываем в блоке catch, начинающемся в строке 31. Отметим, что этот
блок выполняется в контексте прерванного потока, и после выхода из блока поток
завершается.
После двух прогонов программы были получены следующие результаты:
С :\>cmdthreading
Запускаем потоки.
Все запущены.
Поток 5 стартовал задержка 133
Поток 7 стартовал задержка 138
Поток 9 стартовал задержка 142
Поток 4 стартовал задержка 181
Поток 6 стартовал задержка 186
Поток 8 стартовал задержка 190
Поток 2 стартовал задержка 177
Поток 0 стартовал задержка 172
Поток 1 стартовал задержка 124
Поток 3 стартовал задержка 129
Поток 5 завершился.
Поток 7 завершился.
Поток 1 завершился.
Поток 9 завершился.
Выход из■ приложения.
Поток 3 завершился.
Поток 6 завершился аварийно.
Поток 0 завершился аварийно.
Поток 2 завершился аварийно.
Поток 4 завершился аварийно.
Поток 8 завершился аварийно.

С :\>cmdthreading
Запускаем потоки.
Все запущены.
Поток 3 стартовал задержка 129
Поток 2 стартовал задержка 177
Поток 1 стартовал задержка 124
Поток 9 стартовал задержка 142
Поток 7 стартовал задержка 138
Поток 5 стартовал задержка 133
Поток 4 стартовал задержка 181
Синхронизация ιιιη ι

Поток 6 стартовал; задержка 186.


Поток 0 стартовал; задержка 172.
Поток 8 стартовал; задержка 190.
Поток 3 завершился.
Поток 1 завершился.
Выход из приложения.
Поток 5 завершился аварийно.
Поток 4 завершился аварийно.
Поток 2 завершился аварийно.
Поток 6 завершился аварийно.
Поток 7 завершился аварийно.
Поток 8 завершился аварийно.
Поток 9 завершился аварийно.
Поток 0 завершился аварийно.
В обоих тестах главный поток успевает запустить все потоки до того, как
первый из них начнет исполнение. Однако в первом тесте потоки 5, 7, 1 и 9 завер­
шаются до того, как главный поток прервет их, а во втором успевают завершиться
только потоки 3 и 1. Интересно также отметить, что, хотя запросы на запуск от­
правлялись последовательно, удовлетворялись они совершенно в другом поряд­
ке, определяемом средой исполнения и планировщиком операционной системы.

Синхронизация
Обычно ситуация, когда работа программы зависит от капризов среды ис­
полнения, нетерпима. Каркас .NET Framework предлагает несколько способов
синхронизации. Один из них вы уже видели, это метод Thread.Join (), который
приостанавливает выполнение одного потока до завершения другого. В табл. 7.1
перечислены остальные механизмы синхронизации.
Таблица 7.1. Механизмы синхронизации
М еханизм П рим енение
Предложение lock Синхронизация доступа к участку программы, использует класс
Monitor
Тип interlocked Синхронизация инкремента, декремента и сравнения
Тип Monitor Синхронизация доступа к участку программы, аналогичен
критической секции на платформе Win32
Типы AutoResetEvent, Событие, в ожидании которого поток может быть
ManualRe set Event заблокирован. Имеются варианты с автоматическим и ручным
сбросом после возобновления исполнения
Тип Mutex Аналогичен ManualResetEvent; мьютекс1 может быть именованным
и применяется для синхронизации исполнения различных
приложений или процессов

В листинге 7.2, представляющем собой модифицированный вариант предыду­


щего примера, демонстрируется суть проблемы синхронизации.

1 Mutex (мьютекс) - сокращение от mutually exclusive (взаимно исключающий). - Прим.


перев.
166 ■ ■ ■ III Управление потоком выполнения программы

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


1: using System;
2: using System.Threading;
3:
4: namespace cmdThreading
5: {
6: /// <summary>
7: III Пакетная программа для демонстрации многопоточности
8: III vi синхронизации.
9: III </summary>
10: class ThreadClass
11 : {
12: cmdThreading Target;
13 :
14: public ThreadClass( cmdThreading Tgt )
15: {
16: Target = Tgt;
17 : }
18 :
19: public void Start()
20 : {
21: long Res = Target.Result;
22: Res = Res + 1;
23: Thread.Sleep(1);
24: Target.Result = Res;
25: }
26 : }

28: class cmdThreading


29 : {
30: public long Result = 0;
31 :
32: public void RunThreads()
33 : {
34: Thread [] t = new Thread[100];
35 : int і = 0;
36 :
37: // Используем разные циклы, так как для создания
38: // объектов требуется время.
39 :
40: f o r ( i = 0 ; i < t.Length; i++ )
41 : {
42: ThreadClass tc = new ThreadClass(this);
43: t[i] = new Thread( new ThreadStart( tc.Start
) );
44 : }
45 :
46: // Теперь в цикле быстро запустим все потоки.
47: f o r ( i = 0 ; i < t.Length; i++ )
Синхронизация I I I ···
48 t [і] .Start () ;
49
50 // Подождем завершения потоков,
51 for ( і = 0; і < t.Length; i++ )
52 t [і].Join();
53
54 Console.WriteLine("Результат: {0}.", Result );
55
56
57 public static void Main(string[] args)
58
59 for ( int і = 0; і < 10; i++ )
60
61 cmdThreading ct = new cmdThreading();
62 new Thread
( new ThreadStart(ct.RunThreads)).Start();
63
64
65
66: }
Чтобы усложнить пример, я модифицировал функцию Main () класса cmd­
Threading так, чтобы она создавала 10 копий объекта и запускала их независимо
в отдельных потоках.
В строке 30 объявляется переменная Result типа long, которая обновляется
каждым рабочим потоком в методе ThreadClass . Start (). Поскольку всего есть
100 потоков и каждый прибавляет к Result единицу, интуитивно кажется, что
в результате должно получиться 100. Однако на самом деле результат выглядит
следующим образом:
С :\>cmdthreading
Результат: 11.
Результат: 19.
Результат: 16.
Результат: 12.
Результат: 7.
Результат: 13.
Результат: 3.
Результат: 9.
Результат: 12.
Результат: 15.
Если бы метод Start () завершил исполнение в течение первого временного
кванта, выделенного потоку, то результат совпадал бы с ожидаемым. Однако поток
вытесняется и происходит контекстное переключение, а потому результат ста­
новится непредсказуемым. Чтобы доказать это, в код рабочего потока вставлено
обращение к Sleep () в строке 23, из-за которого среда исполнения может пере­
ключиться на другой поток. В большинстве случаев так и происходит, что влечет
за собой непредсказуемость результата.
168 ■ ■ ■ III Управление потоком выполнения программы

Один из способов осуществить арбитраж доступа к переменной Result - за­


грузить ее обновление в отдельный метод и воспользоваться предложением lock
для синхронизации:
class ThreadClass
{
cmdThreading Target;

public void Start()


{
Target.UpdateResult();
}

class cmdThreading
{
public long Result = 0;

public void UpdateResult()


{
lock(this)
{
long Res = Result;
Res = Res + 1;
Thread.Sleep (1) ;
Result = Res;
}
}

}
Метод UpdateResuits () выполняет те же действия, что и в исходной про­
грамме, но весь его код находится внутри предложения lock (). Это предложе­
ние использует примитив синхронизации Monitor, который блокирует доступ
к охраняемому объекту со стороны любого другого потока. Когда управление
выйдет из блока, охраняемый объект разблокируется, и к нему могут обращаться
другие потоки. Дополнительное преимущество метода заключается в том, что код,
манипулирующий состоянием объекта, адресует именно нужный объект.
В случаях, когда доступ к объекту не синхронизирован внутри самого объ­
екта, например при работе с библиотеками классов .NET, необходим другой под­
ход. Обычно для этой цели применяется объект Mutex, возможно именованный,
который вы захватываете перед началом выполнения операции и освобождаете
после ее завершения. В листинге 7.3 демонстрируется использование мьютекса
для синхронизации доступа.
Синхронизация ιιιη ι

Листинг 7.3. Применение мьютекса для разрешения конфликтов доступа


1 class ThreadClass
2 {
3
4
5 public void Start()
6 {
7 Mutex mut = new Mutex(false, cmdThreading.MutexName);
8
9 // Захватить мьютекс и что-то сделать,
10 if ( mut.WaitOne() )
11 {
12 long Res = Target.Result;
13 Res = Res + 1;
14 Thread.Sleep(1);
15 Target.Result = Res;
16
17 mut.ReleaseMutex();
18 }
19 else
20 throw( new ApplicationException(
21 "He удалось захватить мьютекс!"
22
23 }
24
25 class cmdThreading
26 {
27 public static string MutexName "Result.Mutex.0 817 01";
28
29
30 public void RunThreads()
31 {
32
33
34 // Если системный объект не существует,
35 // он создается.
36 Mutex mut = new Mutex(false, MutexName);
37
38 (Создать и выполнить потоки.)
39
40 }
41
42
43 }
Первая модификация - это добавление строки с именем мьютекса. Строка
может не иметь смысла, но должна быть достаточно своеобразной, чтобы не воз­
никло дублирования. Я включил в имя, объявленное в строке 27, дату. В строке
170 ■ ■ ■ III Управление потоком выполнения программы

36 создается первый экземпляр объекта Mutex в системе. В коде рабочего потока


(строка 7) объявляется другой мьютекс с тем же именем. Поскольку мьютекс при­
надлежит операционной системе, эта переменная ссылается на тот же системный
объект, что и родительский поток.
В строке 10 вызывается метод Mutex.Wait One () для захвата мьютекса. Если
он возвращает true, то поток получает исключительное право на мьютекс и может
выполнять обновление. В строке 17 мьютекс освобождается, что в данном случае
необязательно. Когда объект типа Mutex будет убран сборщиком мусора, мью­
текс освободится автоматически. Однако полагаться на это - пример небрежного
программирования.
Если все, что вам нужно, - безопасно увеличить или уменьшить некоторую
переменную на единицу в многопоточной программе, воспользуйтесь классом
System.Threading.Interlocked:
public void Start()
{
Interlocked.Increment(ref Target.Result);
}
Методы Increment ( ) и Decrement ( ) класса Interlocked синхрони­
зируют увеличение и уменьшение значения типа long на 1. Кроме того, класс
Interlocked содержит методы Exchange и CompareExchange, которые ато­
марно выполняют классические операции обмена значений.

Делегаты
В языках С и C++ есть возможность получить адрес функции и затем вызы­
вать ее косвенно через указатель, приведенный к нужному типу. Эта возможность
необходима в самых разных приложениях, особенно для реализации обратных
вызовов в асинхронных операциях и подобных конструкциях. Вот пример, напи­
санный на C++:
// FuncPtrs.cpp : демонстрация указателей на функции.
#include <stdio.h>

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


typedef int(MathFunc) (int);

// Функция, вычисляющая результат с помощью переданной функции.


MathFunc
void DoMath( MathFunc ptr, int theNumber )
{
// Вызываем функцию, на которую указывает ptr.
int result = (*ptr)(theNumber);

// Что-то делаем с полученным результатом; результат


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

// Функция умножения на два.


int TimesTwo(int in) { return in * 2; }

// Функция умножения на четыре.


int TimesFour(int in) { return in * 4; }

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


void main(int argc , char * argv[])
{
// Вызываем функцию DoMath, передавая адреса
// двух разных функций.
DoMath(&TimesTwo, 15 0);
DoMath(&TimesFour, 150); // Тот же код выполняет
// другое вычисление.
}
Указатели на функции, как, впрочем, и указатели других типов оказались слож­
ны для понимания многих начинающих и даже опытных программистов, к тому же
их очень просто было применить неправильно. Однако язык не предлагал никакой
альтернативы, поэтому приходилось мириться.
В C# многие вопросы решены лучше, чем в предшествующих языках, в том
числе и анонимные обратные вызовы. Для этой цели в язык включены делегаты,
позволяющие передавать ссылки на функции как параметры (листинг 7.4).
Листинг 7.4. Реализация обратных вызовов с помощью делегатов
1 : class MathDemo
2: {
3 // Объявляем тип делегата.
4 public delegate int MathDelegate(int і);
5
6 // Вычисляем результат с помощью переданного MathDelegate.
7 public void DoMath( MathDelegate d, int і )
8
9 int result = d(i);
10
11 // Что-то делаем с полученным результатом.
12
13
14 // Функция умножения на два.
15 public int TimesTwo(int і) { return і * 2; }
16
17 // Функция умножения на четыре.
18 public int TimesFour(int і) { return і * 4; }
19 : }
20 :
21 : class Del
22 : {
23 static void Main(string[] args)
24
172 ■ ■ ■ III Управление потоком выполнения программы

25: MathDemo md = new MathDemo();


26 :
27: m d .DoMath(new MathDemo.MathDelegate(md.TimesTwo),150);
28: md.DoMath(new MathDemo.MathDelegate(md.TimesFour), 150) ;
29 : }
30 : }
В строке 4 объявляется делегат MathDelegate. Объявление напоминает за­
головок метода, но несет другую информацию. Тип возвращаемого делегатом зна­
чения и его список параметров - это на самом деле тип возвращаемого значения
и список параметров функции, которая будет вызываться с помощью делегата.
Функции TimesTwo и TimesFour в строках 15 и 18 возвращают значение типа
int и принимают единственный параметр і типа int в соответствии с объявле­
нием делегата.
Объявленный идентификатор делегата - MathDelegate - употребляется при
использовании делегата в качестве поля, переменной, свойства или параметра.
В объявлении метода DoMath () в строке 7 такой тип имеет первый параметр.
В строках 27 и 28 это тип объекта, созданного оператором new и переданного функ­
ции, причем полное имя типа квалифицировано именем объемлющего класса.
Когда делегат передан методу DoMath (), к нему можно обращаться как к методу
(строка 9). В строке 27 в DoMath () передается делегат функции TimesTwo (), а
в строке 28 - делегат функции TimesFour ().
Такой способ передачи методов как параметров обладает всеми достоинствами
указателя на функцию и абсолютно безопасен по отношению к типам. Кроме того,
указатель функции работает только в адресном пространстве одного процесса,
тогда как делегат может прозрачно передавать управление удаленному объекту,
что характерно для большинства механизмов в .NET.
Делегаты поддерживают также групповую рассылку извещений с помощью
композиции, как показано в листинге 7.5.
Листинг 7.5. Групповая рассылка с помощью композиции делегатов
1: delegate void ClassifyDelegate( int і );
2:
З : class Classifier
4: {
5: public ClassifyDelegate CountFuncs;
6:
7: public void Count( int і )
8: {
9: CountFuncs(і);
10 : }
11 : }
12 :
13: class Del
14 : {
15: static int CountPositive = 0;
Делегаты ιιιη ι

16 static int CountTotal = 0;


17
18 private static void CountPositiveCallback( int і
19 {
20 if ( і >= 0 )
21 CountPositive++;
22
23
24 private static void CountAllCallback( int і )
25 {
26 CountTotal++;
27
28
29 static void Main(string[] args)
30 {
31 Classifier с = new Classifier();
32
33 c.CountFuncs += new
ClassifyDelegate(CountPositiveCallback);
34 c.CountFuncs += new
ClassifyDelegate(CountAllCallback);
35
36 с .Count(-3); / / Подсчитать число положительных
37 с .Count(-2) ; II и всех значений.
38 с .Count(-1);
39 с .Count(0)
40 с .Count(1)
41 с .Count (2)
42 с .Count(3)
43 с .Count(4)
44
45 Console.WriteLine("Положительных, Всего: {0},
46 CountPositive,CountTotal );
47 }
48 }
Чтобы реализовать групповую рассылку, мы объявляем экземпляр класса
делегата, который будет служить источником обратных вызовов; это класс
Classifier в строке 5. Здесь класс, включающий делегат, содержит всего
лишь код для вызова подписчиков делегата - метод Count () в строках 7-10.
В классе Del есть два метода - CountPositiveCallback () и CountAll­
Callback (), которые увеличивают счетчик при передаче положительного или
произвольного числа соответственно. В строках 33 и 34 метод Main () препоручает
обе функции попечению одного и того же делегата. В строках 36-43 мы передаем
функции Count () последовательность чисел, а затем в строке 45 выводим резуль­
тат на консоль. Каждый раз при обращении к делегату вызываются обе подсчиты­
вающие функции, так что мы получаем следующий результат:
174 ■ ■ ■ III Управление потоком выполнения программы

С :\>del
Положительных, Всего: 5, 8
С: \>
Можно также отобрать у делегата ранее порученные ему методы с помощью
оператора -= . Если продолжить предыдущий пример, то вызов делегатом функ­
ций прекращают так:
c.CountFuncs -= new ClassifyDelegate(CountPositiveCallback);
c.CountFuncs -= new ClassifyDelegate(CountAllCallback);
Примерно того же эффекта добиваются с помощью интерфейсов. Однако из-за
различий в реализации интерфейсы и делегаты все же применяются в разных
ситуациях. Интерфейс полезен для передачи ссылки на тип, который должен
раскрывать набор взаимосвязанных методов. Делегаты более примитивны, зато
удобны для применения и обладают высокой гибкостью, особенно если возникает
необходимость организовать групповую рассылку. Кроме того, делегирование мож­
но использовать для вызова статических методов, а интерфейсы этого не позволяют.

События
События в языке C# расширяют концепцию делегатов специально в направ­
лении поддержки механизма оповещения, часто применяемого при программиро­
вании пользовательских интерфейсов и в СОМ. Событие может представлять собой,
например, щелчок мышью по элементу управления, нажатие клавиши, системную
операцию, скажем срабатывание таймера, или завершение некоторого шага об­
работки. Программировать события в C# не сложнее, чем работать с делегатами
(листинг 7.6).
Листинг 7.6. Событие - это специальный вид делегата,
применяемый для доставки оповещения
1: delegate void SourceChangedEventHandler(Object source,
EventArgs e);

3 : class EventSource
4: {
5: String StrVal;
6: public event SourceChangedEventHandler ChangedEvent;
7: public String Val
8: {
9: get { return StrVal; }
10: set
11 : {
12: StrVal = value;
13: ChangedEvent( this, EventArgs.Empty );
14 : }
15: }
16 : }
17 : class Ev
События I II···
18
19 private static void SourceChangedNotifier(Object sender,
20 EventArgs e)
21
22 Console.WriteLine( "Новое значение: {0}",
23 ((EventSource)sender).Val );
24
25 static void Main(string[] args)
26
27 EventSource es = new EventSource() ;
28 es.ChangedEvent +=
29 new SourceChangedEventHandler(SourceChangedNotifier);
30 es.Val = "Это значение строки,
31 es.Val = "Это другое значение.";
32
33
В строках 3 -1 6 объявляется класс со свойством типа String, который воз­
буждает событие SourceChangedEventHandler при изменении значения этого
свойства.
В классе Εν объявлен метод SourceChangedNotif ier (строки 19-24), полу­
чающий извещения об изменениях в переменную типа EventSource, созданную
в строке 27. Этот метод-обработчик соединен с событием изменения с помощью
кода в строках 28 и 29, причем здесь используется тот же синтаксис, что и для де­
легатов. В следующих двух строках присваиваются различные значения строковому
свойству объекта, что приводит к вызову функции оповещения - по одному разу
для каждого присваивания.
События отличаются от обычных делегатов тем, что их разрешается вызывать
только в том классе, в котором событие объявлено. Для интеграции с моделью
событий в каркасе .NET Framework необходимо соблюдать определенные согла­
шения. Например, имя делегата должно заканчиваться строкой Event Handler,
и он должен принимать ровно два параметра, как показано в строке 1 листинга 7.6.
Разумно объявить собственный класс, который передается в качестве парамет­
ра EventArgs и может предоставить обработчикам, слушающим событие, необ­
ходимую информацию. Используемый в таком качестве класс обязан наследовать
типу System.EventArgs, как показано в следующем примере:
class SourceChangedEventArgs : EventArgs

// Открытые поля, в которых будут переданы


// старое и новое значение изменившейся строки,
public String OldValue = ////.
public String NewValue = ////.

Этот класс наследует EventArgs и позволяет дополнительно передать старое


и новое значение изменившейся строки в класс EventSource. Чтобы им восполь­
зоваться, нужно модифицировать код метода set в свойстве Val:
176 ■ ■ ■ III Управление потоком выполнения программы

public String Val


{
get { return StrVal; }
set
{
SourceChangedEventArgs Args = new
SourceChangedEventArgs();

Args.OldValue = StrVal;
StrVal = value;
Args.NewValue = StrVal;
ChangedEvent( this, Args );
}
}
А коль скоро метод доступа к свойству предоставляет дополнительную инфор­
мацию, то обработчик события может ею воспользоваться:
private static void SourceChangedNotifier(Object source,
EventArgs e)
{
Console.WriteLine( "Старое, Новое: <{0}>, <{1}>.",
((SourceChangedEventArgs) e).OldValue,
((SourceChangedEventArgs) e).NewValue );
}
Определенное вами событие способно содержать два специальных метода
с именами add и remove, синтаксис объявления которых аналогичен тому, что
вы видели в функциях доступа к свойствам. Это позволяет предоставить де­
легату, участвующему в событии, специальную область памяти для хранения
данных, а не ту, что обычно выделяет компилятор. Если вы решите прибегнуть
к этому средству, сохраняйте ссылки на делегаты, переданные методам доступа, в
члене value - объявление события разрешается использовать только для добав­
ления или удаления делегата с помощью операторов композиции (+= и -=). В раз­
деле документации по каркасу .NET Framework, посвященном ключевому слову
event, приводятся два примера применения описанной техники для разрешения
конфликтов имен и для реализации редко используемых событий, определенных
в широком контексте.

Совет Ограничение, разрешающее задействовать события с методами


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

Резюме
Язык C# в сочетании с платформой .NET сделали многопоточное и событий-
но-управляемое программирование проще, чем когда бы то ни было, предоставив
модель, учитывающую потребности программистов, создающих промышленные
приложения. Конечно, совсем забыть о проблемах синхронизации и производи­
тельности не удастся, но встроенные в язык механизмы и библиотечные классы
предоставляют все средства для решения возникающих проблем. В главе 8 мы
изучим еще одну уникальную особенность языка С#, позволяющую применять
указатели для прямого доступа к памяти.
Глава 8. Небезопасный код
Языки C# и Java непохожи во многих отношениях, но одна общая черта у них есть -
это контролируемое исполнение. Интерпретируя и верифицируя код во время ис­
полнения, обе среды пытаются защитить программиста от проблем, свойственных
неконтролируемому окружению. Однако иногда возникают ситуации, когда вам
нужно обойти контроль со стороны среды, а методы преодоления ограничений
в C# и Java кардинально различаются. В C# имеется возможность писать небезо­
пасный код, который не затрагивает контролируемую кучу, но позволяет выде­
лять память в стеке и обращаться к ней с помощью указателей. Такое решение,
с одной стороны, способно удовлетворить ваши потребности, а с другой - лока­
лизует последствия ошибок.

Указатели
Вероятно, вам приходилось слышать мнение, что указатели - это причина
всех бед программистов, применяющих традиционные С-подобные языки. Хотя во
многих языках они вообще отсутствуют, большая часть программ, работающих на
платформах Windows и UNIX, написаны на языках С или C++, где указательные
типы используются повсеместно.

Сложности при работе с указателями


Для того чтобы понять, какие проблемы возникают при работе с указателями,
необходимо краткое введение. Большинство компьютеров хранят информацию
в памяти с произвольной выборкой (ЗУПВ, RAM) или в постоянной памяти (ПЗУ,
ROM). Под информацией понимаются данные, которыми манипулирует програм­
ма, и сам код программы. Для организации хранения память обычно структури­
руется в виде 8-разрядных блоков, именуемых байтами. Первый байт имеет адрес
О, второй - 1 и т.д. В большинстве машинных архитектур есть дополнительный
уровень организации - слово. Это основной элемент данных, которым манипули­
рует центральный процессор. Хотя архитектура Intel х86 позволяет работать на
уровне байтов, сам процессор манипулирует двух-, четырех- или восьмибайтовы­
ми величинами (в зависимости от разрядности - 16, 32 или 64 бита).
Проще всего определить указатель как переменную, в которой хранится ад­
рес памяти и которую программа может использовать для доступа к значению,
размещенному по этому адресу. На первый взгляд никаких проблем нет, но так
только кажется. Например, некоторые команды процессора предполагают то
или иное выравнивание данных в памяти; команда, предназначенная для работы
Указатели ιιιη ι

с 32-разрядным значением, выдаст ошибку или станет работать намного медлен­


нее, если значение указателя не делится на четыре (то есть не адресует 32-раз-
рядное слово). Возможно также, что программа по ошибке запишет данные по
неправильному адресу, что рано или поздно приведет к сбою. Записать в указатель
неверный адрес очень легко, а вот гарантировать отсутствие таких ошибок куда
сложнее, чем вы думаете.
Вышеупомянутые проблемы серьезны сами по себе, но наиболее распростра­
ненная ошибка, связанная с указателями, - это утечка ресурсов. Существует огра­
ничение на объем памяти, которую может запросить программа, поэтому вместе
с компилятором всегда поставляется библиотека времени выполнения, дающая
средства для управления памятью. Функционирующая программа выделяет па­
мять, в которой размещается обрабатываемая информация; по завершении об­
работки эту память необходимо освободить, вызвав библиотечную функцию. В
таком примитивном изложении все кажется простым, но многие ошибки связаны
с тем, что менеджер памяти считает память занятой, когда на самом деле она
свободна. Это явление называется утечкой памяти и приводит к исчерпанию до­
ступной памяти и краху программы.

Решение
Несмотря на все свои недостатки, указатели долгое время считались незамени­
мыми для «настоящего» программирования - системных приложений, обработчи­
ков прерываний и т.д. Такие языки, как Java и С#, предоставляют в распоряжение
программиста обширную библиотеку классов, позволяющую отказаться от ука­
зателей при решении прикладных задач общего характера. Тем не менее остается
огромный массив уже написанного кода, который был бы небесполезен для новых
программ, поэтому приложениям на C# нужен способ, поддерживающий старые
методы.
Во многих существующих API и библиотеках указатели применяются для
передачи информации между модулями, поэтому совсем отказаться от них не
представляется возможным. Компания Sun Microsystems решила полностью за­
претить указатели в языке Java, но Microsoft в отношении языка C# не пошла
на столь радикальные меры. В C# участок кода, в котором употребляются ука­
затели, должен быть заключен в специальный блок, помеченный модификато­
ром u n s a f e . Поскольку контроль указателей во время выполнения программы
невозможен, небезопасный код разрешается исполнять только в доверительном
контексте. Это ограничивает использование компонентов, включающих небезо­
пасные фрагменты.
В отличие от принятого в Java подхода «все или ничего», механизм включения
небезопасного кода в C# представляет собой элегантное и не слишком сложное
решение проблемы указателей. В C# вы сами решаете, оставаться в рамках пол­
ностью контролируемого домена или выйти за его пределы. Однако прибегать к
этому следует только в редких случаях, поскольку включение небезопасного кода
в приложение ограничивает область его применения.
180 ■ ■ ■ III Небезопасный код

Память и вызов функций платформенного API


В .NET есть механизм Platform Invoke (его еще называют PInvoke), позволя­
ющий вызывать функции, которые находятся в DLL-библиотеках, написанных с
помощью старых средств разработки. В главе 6 я воспользовался им для импорта
функций работы с файлами из библиотеки KERNEL32 .DLL. Воспроизведу этот
код в листинге 8.1.
Листинг 8.1. Небезопасный код полезен для выполнения низкоуровневых действий
1 class BinaryFileReader : IDisposable
2 {
3 // Требует, чтобы открываемый файл существовал,
4 const uint OpenExisting = 3;
5
6 // Запрашивает право на чтение,
7 const uint GenericRead = 0x80000000;
8
9 // Возвращается в случае ошибки открытия ресурса,
10 const uint InvalidHandleValue = OxFFFFFFFF;
11
12 // Функции из ядра Win3 2.
13 [DllImport("kernel32", SetLastError=false)]
14 static extern unsafe uint CreateFile
15 (
16 string lpFileName,
17 uint dwDesiredAccess,
18 uint dwShareMode,
19 uint IpSecurityAttributes,
20 uint dwCreationDisposition,
21 uint dwFlagsAndAttributes,
22 uint hTemplateFile
23
24
25 [DllImport("kernel32", SetLastError=false)]
26 static extern unsafe bool CloseHandle
27 (
28 uint hFile
29
30
31 [DllImport("kernel32", SetLastError=false)]
32 static extern unsafe bool GetFileSizeEx
33 (
34 uint hFile,
35 ulong* lpFileSizeHigh
36
37
38 [DllImport("kernel32", SetLastError=false)]
39 static extern unsafe bool ReadFile
40
Указатели н и ш

41 uint hFile,
42 void* lpBuffer,
43 uint nBytesToRead
44 uint* nBytesRead,
45 uint overlapped
46
47
48 unsafe byte [] Buffer = null;
49 ulong BufferSize = 0;
50 uint CurrentFileHandle = 0;
51
52 // Открытые методы для доступа к данным.
53
54 // Получить длину прочитанных данных,
55 public ulong Length
56 {
57 get { return BufferSize; }
58 }
59
60 // Индексатор для безопасного доступа к буферу,
61 public int this[ulong index]
62 {
63 get
64
65 if ( index >= 0 && index < Length )
66 return Buffer[index];
67 else
68 throw new IndexOutOfRangeException(
69 "Доступ к BinaryFileReader[]");
70
71
72
73 // Считывает данные в память.
74 unsafe public void ReadData( string path )
75 {
76 uint FileHandle = CreateFile( path, GenericRead,
77 0, 0, OpenExisting,
78 0,0);
79
80 if ( FileHandle != InvalidHandleValue )
81 {
82 ulong NewBufferSize = 0;
83 uint BytesRead = 0;
84
85 // Сохраним описатель, чтобы им можно было
86 // пользоваться в последующих операциях.
87 CurrentFileHandle = FileHandle;
88
89 if ( GetFileSizeEx(FileHandle, &NewBufferSize) )
Ε Π Ξ ···ΙΙΙΙ Небезопасный код

90: {
91: if ( BufferSize < NewBufferSize )
92 : {
93: BufferSize = NewBufferSize;
94: Buffer =new byte[BufferSize];
95 : }
96 :
97: fixed ( void * BufferPtr = Buffer )
98: if ( !ReadFile(FileHandle,
99: BufferPtr,
100: (uint)BufferSize,
101: &BytesRead,
102: 0)11
103: BytesRead != BufferSize)
104 : {
105: throw new IOException(
106: "Ошибка при чтении файла.");
107 : }
108 : }
109: else
110: throw new IOException(
111: "Ошибка при получении размера файла.");
112 : }
113: else
114: throw new FileNotFoundException(
115: "B BinaryFileReader.ReadData()",
116: path);
117 : }
118 :
119: // Файл уже закрыт?
120: bool Disposed = false;
121 :
122: // Освобождает текущий открытый файл.
123: unsafe private void CloseFile()
124 : {
125: if ( CurrentFileHandle != 0 &&
12 6: CurrentFileHandle != InvalidHandleValue )
127 : {
128: CloseHandle( CurrentFileHandle );
129: CurrentFileHandle = 0;
130 : }
131 : }
132 :
133: // Контролируемое освобождение.
134: public void Dispose()
135 : {
136: if ( IDisposed )
137 : {
13 8: // Освободить описатель файла.
139 : CloseFile() ;
Небезопасные контексты I I I · · ·

140
141 // Освободить ссылку.
142 Buffer = null;
143
144 // Запретить финальную очистку.
145 GC.SuppressFinalize(this);
146
147 else
148 throw new
Obj ectDisposedException("BinaryFileReader",
149 "Освобожден более одного раза." );
150
151
152 // Очистка.
153 -BinaryFileReader()
154
155 if ( CurrentFileHandle != 0 )
156 CloseFile();
157
158
Поскольку библиотеки Win32 содержат неконтролируемый код, то я пометил
функции API как небезопасные. Кроме того, функция R e a d F ile () ожидает ука­
зателя на буфер, поэтому в самой вызывающей программе должны использоваться
небезопасные конструкции для фиксации буфера в памяти (строка 97).

Небезопасные контексты
Лексически небезопасный контекст представляет собой участок кода, помечен­
ный ключевым словом u n s a f e . Этот модификатор применим к классам, структу­
рам, интерфейсам, делегатам, полям, методам, свойствам, событиям, индексато­
рам, операторам, конструкторам и деструкторам. Кроме того, им можно помечать
и отдельные блоки внутри метода, как, например, в показанном ниже фрагменте:
public void UseBuffer()

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


// небезопасные конструкции запрещены.
Buffer = new byte[BufferSize];

unsafe // Использование указателя заключено


// в небезопасный блок.

// Сейчас мы находимся в небезопасном контексте,


fixed ( void * BufferPtr = Buffer )
{
// Сделать что-то с указателем BufferPtr.
}
184 ■ ■ ■ III Небезопасный код

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


Модификатор unsafe никак не отражается на генерируемом компилятором коде,
он лишь помечает тот участок, в котором разрешено использовать небезопасные
конструкции.
Доступ к небезопасному контексту из контролируемого ограничен сигнатурой
элемента, к которому вы обращаетесь. Поскольку манипулировать небезопасными
элементами разрешается только в небезопасном коде, обращаться к таким небезо­
пасным элементам, как указатели, нельзя даже косвенно. Листинг 8.2 демонстри­
рует эти ограничения.
Листинг 8.2. Доступ к указателям, даже безобидный,
из контролируемого кода запрещен
public static unsafe char * GetChars()
{
return null;
}

public static unsafe void GoodContext()


{
// Нормально, так как мы находимся в небезопасном контексте.
UnsafeFunc( GetChars(), 0 );
}

public static void BadContextO


{
// He будет работать, хотя контролируемый код
// вообще не обращается к значению указателя.
// Компилятор выдает ошибку "Pointers
// may only be used in unsafe contexts." (Указателями можно
// пользоваться только в небезопасных контекстах).
UnsafeFunc( GetChars(), 0 );
}
Как следует из комментария, хотя в функции BadContext () нет кода, ма­
нипулирующего небезопасными данными (указатель на символ, который возвра­
щает GetChars () ), тем не менее запрещено даже передавать этот указатель в
Unsaf eFunc (). Если все-таки необходимо работать с указательными типами в
контролируемом коде, к вашим услугам структура, куда можно поместить указа­
тель и передать ее другим методам.

Небезопасные конструкции языка


Из небезопасного контекста можно обращаться к контролируемому коду
и данным. Кроме того, он позволяет работать и с дополнительными возможностя­
ми, которые перечислены в табл. 8.1.
Небезопасные конструкции языка И Н !

Таблица 8 .1 Небезопасные конструкции языка C#


.

Конструкция Нотация Назначение


Указатель Т * PtrVar; Содержит адрес переменной типа т
Закрепленный объект fixed ( assign ) stmt Фиксирует размещение объекта
в памяти
Разыменованный *IntPtr = 5; Осуществляет доступ к объекту,
указатель на который ссылается указатель
Выбор члена SPtr->member Осуществляет доступ к члену
структуры, на которую ссылается
указатель
Взятие адреса (&) IntPtr = &IntVar Возвращает адрес объекта
в памяти
Получение размера і = sizeof(IntVar) Возвращает размер типа в байтах
Инкремент/декремент IntPtr++; SPtr— ; Прибавляет или вычитает
из адреса, хранящегося в указателе
на тип т, размер этого типа sizeof (т)
Сравнение Операторы сравнения Указатели сравниваются
по правилам, применяемым
для сравнения беззнаковых целых
Выделение памяти TPtr = stackalloc T[n] Выделяет память для n объектов
в стеке типа т в стеке

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


причина включения в C# небезопасных средств. Предположим, например, что
в библиотеке, написанной на C++, находится функция, текст которой приведен в
листинге 8.3.
Листинг 8.3. Библиотечная функция ROT13, написанная на C++
extern "С"
{
_declspec(dllexport) void Codec(char * buffer, int length = 0) ;
}

declspec(dllexport) void Codec( char * buffer, int length )


{
char bound;

if ( length == 0 )
length = strlen(buffer);
// Приводим к типу byte * для упрощения синтаксиса.
for ( byte * с = (byte *)buffer; с < (byte *)buffer + length;
C++ )
{
// Работаем только с буквами.
M ill Небезопасный код

i f ( isalpha( *c ) )
{
// По-разному обрабатываем большие и маленькие буквы,
if ( islower(*с) )
bound = "z ";
else
bound = "Z";

// Транслируем символ,
if ( *с > bound - 13 )
*с -= 13;
else
*с += 13;
}
}
}
Функция Codec () шифрует текст, пользуясь алгоритмом ROT13 (при этом
каждая буква заменяется другой, отстоящей от нее в алфавите на 13 позиций).
Поскольку алгоритм ROT 13 симметричен1, та же функция одновременно является
и дешифратором. Она экспортируется из соответствующей DLL, поэтому к ней
можно обращаться из программы на С#, пользуясь механизмами вызова платфор­
менного АРІ. В листинге 8.4 показано, как это делается.
Листинг 8.4. Обертка для служебной функции ROT13
1: [DllImport("ROT13.DLL", EntryPoint="Codec",
SetLastError=false,
2 : CallingConvention=CallingConvention.Cdecl)]
3: static extern unsafe void DllCodec( char * buf, int length );
4:
5: public static unsafe void Codec( ref string buffer )
6: {
7: ASCIIEncoding ae = new ASCIIEncoding();
8:
9: byte [] BufArray = ae.GetBytes(buffer);
10 :
11: fixed ( byte * StrParam = BufArray )
12 : {
13: // Вызываем функцию и передаем ей закрепленный буфер.
14: DllCodec( (char *)StrParam, buffer.Length );
15: }

17: buffer = ae.GetString( BufArray );


18 : }
В строках 1-3 используется атрибут System. Runtime .InteropServices .

1 В применении к латинице, в которой 26 букв. Впрочем, представленная версия для кирил­


лицы не будет работать вовсе. - Прим. перев.
Управление памятью в небезопасном коде ШШ\
Dll Import, указывающий, что метод DllCodec () служит для импорта из DLL,
и сообщающий, какую именно функцию он должен вызывать (параметр Entry-
Point). Однако сама функция не видна контролируемому коду, вместо нее рас­
крывается метод Codec (), код которого находится в строках 5-18.
Метод Codec () берет на себя преобразование текста, переданного в пара­
метре buffer, из кодировки Unicode в ASCII, затем вызывает функцию из DLL
и преобразует полученную строку обратно к типу string. Возможно, вы заме­
тили, что в листинге 8.1 я не объявлял параметры типа LPTSTR импортируемых
функций как char *, а описал их как переменные типа string. Для параметров
функций это годится, так как каркас автоматически преобразует строку в подхо­
дящий массив байтов, который передает по значению. Но теперь я сознательно
воспользовался буфером ввода/вывода, чтобы показать, как можно выполнить
такое преобразование самостоятельно.
Стоит отметить, что первый параметр метода DllCodec () я объявил как char
* (в соответствии с тем, как он объявлен в функции на C++), хотя такое действие
несколько усложняет программу. В C# было бы законно объявить его как byte
*, чтобы упростить взаимодействие с классом System.Text.Encoding.ASCII-
Encoding, но это неряшливость как раз того сорта, из-за которого проектиров­
щики языка решили полностью избавиться от указателей.
Небезопасный код часто в шутку называют «встроенным С», но на самом деле
он сохраняет очень много черт С#. Особенно существенным представляется тот
факт, что, даже работая с унаследованными библиотеками, вы лишены возможнос­
ти включать заголовочные файлы, как поступили бы в программе на С или C++.
Поэтому вам придется найти и переобъявить в своей программе все используемые
структуры и константы, что я и сделал в листинге 8.1.

Управление памятью в небезопасном коде


Чтобы небезопасный код мог выделять память для буферов во время выполне­
ния, C# содержит предложение stackalloc. В листинге 8.5 показано, как можно
его использовать с целью выделения памяти для массива структур.
Листинг 8.5. Применение предложения stackalloc для создания массива объектов
struct Tuple
{
public double a, b, c;
}

const int NTUPLES = 10;

unsafe void Evaluate()


{
Tuple * Tuples = stackalloc Tuple[NTUPLES];
for ( Tuple * TPtr = Tuples;
TPtr < Tuples + NTUPLES;
TPtr++ )
188 ■ ■ ■ III Небезопасный код

{
// Сделаем что-нибудь с а, Ь и с.
TPtr->a = 1;
TPtr->b = 2;
TPtr->c = 3;
}
}
Между предложением stackalloc и оператором new есть три важных от­
личия. Во-первых, выделяемая память никак не инициализируется, в ней будут
находиться произвольные значения, оказавшиеся к этому моменту в стеке. Во-
вторых, не существует способа явно освободить выделенную область памяти. Па­
мять, распределенная с помощью stackalloc, освобождается автоматически по
выходе из объемлющей функции. Поскольку память для контролируемых типов
выделяется только из кучи, то с помощью stackalloc нельзя выделять память
для контролируемого типа или для ссылки на таковой. Наконец, когда вы запра­
шиваете с помощью stackalloc память для массива, выделяется память и для
его членов. Напротив, при создании массива с помощью оператора new память для
членов следует запрашивать отдельно.
Между небезопасным кодом и сборщиком мусора все же можно достичь ком­
промисса. Сборщик мусора не знает о том, куда направлены небезопасные ука­
затели, поэтому указатель может ссылаться только на объекты значащих типов
или на массивы таких объектов. Хотя в небезопасном коде допустимо создавать
экземпляры контролируемых типов, объявлять на них указатели запрещено. Так,
при компиляции следующего кода будет выдана ошибка:
String S = "экземпляр строки";

String * SPtr = &S; / / Н е годится: нельзя получать


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

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

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


ях. Однако, если возможностей контролируемой среды недостаточно, допустимо
прибегнуть к небезопасному коду и механизму вызова платформенного API, что­
бы напрямую обращаться к памяти и работать с неконтролируемыми ресурса­
ми. Включив такие средства в язык, Microsoft предоставила простой и понятный
способ доступа к неконтролируемым ресурсам из контролируемых компонентов.
Глава 9. Метаданные и отражение
Метаданными называется информация, описывающая содержимое сборки. Неко­
торые части этой информации жестко определены структурой вашей программы
(например, имя типа или тип значения, возвращаемого методом), но есть также
атрибуты, которые вы можете задавать сами. В каркасе .NET Framework уже
определено 150 атрибутов, и среда исполнения позволяет создавать другие нестан­
дартные атрибуты.
Технология, применяемая в .NET для доступа к метаданным, называется отра­
жением (reflection). Отражение сводит информацию о различных классах в табли­
цу, к которой разрешено обращаться для определения типов во время выполнения,
для исследования сборок и атрибутов и для получения иных сведений. Обладая
достаточными полномочиями, вы даже можете создавать сборки из программы,
хотя в этой книге я не стану приводить примеры такого рода.

Использование атрибутов
Именно атрибутами вы чаще всего будете пользоваться для манипулирова­
ния метаданными. Тип атрибута обычно предназначен для решения узкоспеци­
ализированной задачи и позволяет ассоциировать некоторую информацию или
поведение с элементом программы. Для эффективного программирования на
платформе .NET атрибуты необходимы во многих случаях; не исключено, что вы
найдете применение и для нестандартных атрибутов собственного производства.
Атрибуты можно ассоциировать со следующими элементами:
□ сборками;
□ модулями;
□ типами;
□ событиями;
□ свойствами;
□ интерфейсами;
□ полями;
□ методами, включая конструкторы и деструкторы;
□ возвращаемыми значениями;
□ делегатами;
□ параметрами.
Некоторые атрибуты применимы ко всем этим элементам, но большинство -
только к части.
Атрибуты имеют тип класса и определяются как любой другой класс, то
есть могут включать поля, методы, свойства и другие члены. Атрибуты - это
Использование атрибутов Щ И Н Н Е Е Я

декларативные элементы, которые в основном функционируют как признаки, воз­


действующие на работу компилятора, но они же предоставляют дополнительную
информацию программам, применяющим технологию отражения.
В C# атрибут допустимо присоединить к элементу, расположив его (или их)
перед объявлением и заключив в квадратные скобки. Если несколько атрибутов
помещено внутрь одной пары скобок, то они должны отделяться друг от друга
запятыми. Альтернативно можно заключить каждый атрибут в отдельную пару
скобок. Так, следующие два объявления эквиваленты:
1 // Раздельное объявление.
2 assembly:EnvironmentPermission(SecurityAction.RequestMinimum,
3 Read="OS;SYSTEMROOT")]
4 [assembly:AssemblyTitle("AttributeDemo")]
5
6 // Комбинированное объявление.
7 [
8 assembly:
9 EnvironmentPermission(SecurityAction.RequestMinimum,
10 Read="OS;SYSTEMROOT"),
11 AssemblyTitle("AttributeDemo")
12 ]
Оба объявления требуют наличия определенных полномочий для сборки во вре­
мя выполнения (разрешения на чтение переменных окружения OS и SYSTEMROOT)
и присваивают атрибуту AssemblyTitle сборки значение AttributeDemo во
время построения.
Атрибут EnvironmentPermission имеет позиционные и именованные пара­
метры. Позиционные параметры обязательны и должны быть заданы в строго оп­
ределенном порядке (см. параметр SecurityAction.RequestMinimum в стро­
ках 2 и 9). Вслед за позиционными могут быть указаны именованные параметры,
порядок которых безразличен. Примером служит параметр Read в строках 3 и 10.
В отличие от позиционных параметров, для которых достаточно задать значение,
именованный параметр должен иметь вид < и м я_ п ар ам етр а> = < вы раж ение_
зн ачен и ях
Одним из атрибутов, встроенных в библиотеку базовых классов, является
Serializable. Им помечаются объекты, для которых среда исполнения должна
предоставить механизм сериализации с помощью форматера, позволяющего пере­
давать объекты по сети, записывать их на диск и осуществлять любые операции,
требующие передачи из одного контекста в другой. Для удобства в листинге 9.1
повторен пример использования этой возможности из главы 2.
9.1. С помощью атрибута Serializable и подходящего форматера
Л истинг
можно сериализовать объект, представив его в формате XML
using System;
using System.IO;
using System.Collections;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
192 ■ ■ ■ III Метаданные и отражение

using System.Runtime.Serialization.Formatters.Soap;

[Serializable]
class StreetAddress
{
public int id;
public string name, streetl, street2, city, state, zip;

public StreetAddress()
{
name = streetl = street2 = city = state = zip = "";
id = 0;
}

public StreetAddress(int inld, string inName, string


inStreetl, string inStreet2, string inCity, string instate,
string inZip)
{
id = inld;
name = inName;
streetl = inStreetl;
street2 = inStreet2;
city = inCity;
state = instate;
zip = inZip;
}

class Serializer
{
static void Main(string[] args)
{
ArrayList addresses = new ArrayList (10) ;

// Создать список адресов.


for ( int id = 0; id < 10; id++ )
{
addresses.Add( new StreetAddress(id, "AName",
"12 3 Main St.", "Ste. 800",
"Anywhere", "AK", "123 45") );
}

// Вывести информацию в формате XML.


IFormatter soapFmt = new SoapFormatter();
Stream s = File.Open( "outfile.xml", FileMode.Create );
soapFmt.Serialize( s, addresses );
s .Close();
// Прочитать данные обратно.
Создание нестандартных атрибутов Ш И Н Н Е Е З

s = File.Open( "outfile.bin", FileMode.Open );


addresses = binFmt.Deserialize( s ) as ArrayList;

for ( int і = 0; і < addresses.Count; i++ )


Console.WriteLine(
((StreetAddress)addresses[і]).id.ToString() + " " +
((StreetAddress)addresses[і]).name);
}
}

Создание нестандартных атрибутов


Многие встроенные атрибуты имеют отношение к различным аспектам про­
граммирования на платформе .NET - от базового механизма сериализации до
таких важных параметров, как безопасность, отражение и взаимодействие с СОМ.
Но даже при таком разнообразии встроенных атрибутов Microsoft не может пре­
дусмотреть все ситуации, когда вы захотите что-то включить в состав метаданных.
На самом деле набор встроенных атрибутов чаще всего подвергался изменениям
при разработке каркаса .NET Framework. Из-за широкой распространенности
атрибутов вам предоставляется возможность расширять их состав за счет классов
для нестандартных атрибутов.
Для определения нестандартного атрибута надо создать класс, производный
от System.Attribute (в ранних версиях каркаса такое наследование было ре­
комендуемым, теперь оно обязательно). Параметры конструктора становятся по­
зиционными атрибутами, которые задавать необходимо, а любые поля (открытые
члены, доступные для записи) могут выступать в роли именованных параметров.
Дополнительно такой класс разрешается пометить атрибутом AttributeUsage,
который управляет способом применения атрибута. В листинге 9.2 демонстриру­
ется объявление нестандартного атрибута.
Листинг 9.2. Нестандартный атрибут должен наследовать
классу System. Attribute
1 : namespace CustomAttributes
2: {
3: // Объявить нестандартный атрибут, который можно
// использовать для указания того,
4: // какому требованию заказчика удовлетворяет код.
5: [AttributeUsage( AttributeTargets.Assembly I
6: AttributeTargets.Class I
7: AttributeTargets.Interface)]
8: public class UserRequirementAttribute : Attribute
9: {
10: string RequirementProperty;
11: string RevisionProperty;
12 :
13: public UserRequirementAttribute( string ReqNr )
14 : {
M ill Метаданные и отражение

15 RequirementProperty = ReqNr;
16 RevisionProperty = null;
17
18
19 public string Requirement
20 {
21 get
22 {
23 return RequirementProperty;
24 }
25 }
26
27 // Свойство, описывающее необязательный
// параметр Revision,
28 public string Revision
29 {
30 set
31 {
32 RevisionProperty = value;
33 }
34 get
35 {
36 return RevisionProperty;
37 }
38
39
40

Совет Обратите внимание на последовательность событий в листинге 9.2.


При создании атрибута конструктор завершит исполнение до того,
как будет установлено какое-либо свойство. Поэтому присваивание
строковому свойству R e v i s i o n P r o p e r t y значения n u l l не про­
тиворечит заданию параметра R e v is io n e момент использования
атрибута.
Класс UserRequirementAttribute (требование пользователя) объявляет
нестандартный атрибут, которым можно пометить участки кода, написанные во
исполнение требований заказчика, и при необходимости указать для них номер
версии, в которой было реализовано требование. Имея такой тип, вы в состоянии
написать код, аналогичный представленному в листинге 9.3.
Л истинг 9.3. Класс L i b r a r y C l a s s помечен атрибутом U s e rR e q u ire m e n tA ttrib u te
, в который включена информация о требованиях
[UserRequirement("1.4.6", Revision " 2 .0 " )]
public class LibraryClass
{
public LibraryClass ()
{
// Здесь должен быть код конструктора,
Создание нестандартных атрибутов П Н І 195

public string AProperty


{
get
{
return "Эту строку возвращает класс LibraryClass.";
}
}
}
Здесь показан способ применения нестандартного атрибута. Поскольку ком­
пилятор подставит опущенный суффикс Attribute, то в программе разрешается
ссылаться на атрибут Use rRe qui гement. Конструктор класса UserRequirement -
Attribute должен получить параметр типа string, содержащий номер требова­
ния, поэтому в объявлении класса LibraryClass этот параметр указан первым
(позиционным). Кроме того, в классе UserRequirementAttribute объявлено
свойство Revision, которое в LibraryClass предстает в виде именованного
параметра со значением “2 . 0 ”.
Листинг 9.3 обнажает некую проблему. Да, ассоциирование требований с кодом -
это прекрасно, но предположим, что существует версия 3.0 программы, и ее текст был
модифицирован для поддержки другого требования. Класс LibraryClass по-
прежнему удовлетворяет первому требованию, но атрибут UserRequirement до­
пустимо применить к каждому элементу - сборке, классу или интерфейсу - только
один раз. К счастью, в классе System.AttributeUsage существует свойство
AllowMultiple, и если вы присвоите ему значение true, то сможете задавать
атрибут многократно:
[AttributeUsage( AttributeTargets.Assembly I
AttributeTargets.Class I
AttributeTargets.Interface,
AllowMultiple = true )]
public class UserRequirementAttribute : Attribute
{

}
При таком видоизмененном определении в объявление класса LibraryClass
легко включить информацию обо всех требованиях, которым он удовлетворяет:
[UserRequirement("1.4.6", Revision = "2.0")]
[UserRequirement("З.7.З", Revision = "3.0")]
public class LibraryClass
{

}
196 ■ ■ ■ III Метаданные и отражение

Совет Интересный нестандартный атрибут T ra c e H o o k .N E T предла­


гает компания Razorsoft (его описание находится по адресу h ttp ://
ШШШaШ!^ofШel/^rg£eHooЫlШL)■ Он позволяет протоколировать
вызовы методов, свойств и полей, принадлежащих классу.

Отражение и динамическое связывание


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

Отражение и статически связанные элементы


Чтобы можно было работать с метаданными сборки или типа, эту сборку
надо загрузить в адресное пространство программы. Создавая приложения для
платформы .NET, вы можете выбрать статическое или динамическое связывание.
Чаще применяется статическое связывание, когда вы явно именуете использу­
емые в программе пространства имен и типы. Статическое (раннее) связывание
гораздо проще динамического (позднего), но при этом требуется, чтобы все
сборки, на которые ссылается программа, были доступны на этапе компиляции
и компоновки.
При статическом связывании компилятор включает в выходной файл симво­
лические ссылки на все элементы в других модулях, к которым обращается про­
грамма. Во время исполнения CLR применяет эту информацию для того, чтобы
разрешить ссылку, то есть загрузить нужные элементы и подставить их адреса.
Чтение метаданных для ссылок, которые статически связаны с программой, не
представляет сложностей. В листинге 9.4 показано, как прочитать атрибуты типа
LibraryClass.
Л и с т и н г 9.4. Класс пользуется атрибутом
L ib ra ry C la s s
U s e rR e q u ire m e n tA ttrib u te для получения информации о требованиях
1 LibraryClass 1с = new LibraryClass();
2
3
4 // Для чего-то используем класс...
5
6
7 // Чтение информации о типе: сначала получаем ссылку
8 // на тип.
9 Туре t = 1с.GetType();
10
11 // Извлекаем атрибуты, для чего указываем тип атрибута
12 // и говорим, нужно ли обходить дерево наследования объекта,
Отражение и динамическое связывание I I I · · ·

13: // Так как нас интересует класс LibraryClass,


// то возвращен будет только атрибут
14 : // UserRequirementAttributes.
15: Object [] Reqs = t .GetCustomAttributes( false );
16: foreach ( Object о in Reqs )
17 : {
18: if( о is UserRequirementAttribute ) // Проверить
// все равно надо!
19
20 UserRequirementAttribute ига =
21 (UserRequirementAttribute)о;
22 Console.WriteLine(
23 "К классу применимо требование {0}, версия {1}",
24 ura.Requirement, ига.Revision );
25
26
В строке 9 мы получаем объект класса System.Туре, соответствующий типу
LibraryClass, обращаясь для этой цели к экземпляру типа. В строке 15 запра­
шивается массив атрибутов указанного типа, причем мы ограничиваем поиск
только самим классом LibraryClass. Информация о требованиях и номерах
версий выводится на консоль в строках 16-26. Обратите внимание на строку
18, где проверяется, принадлежит ли атрибут к интересующему нас типу; лишь
убедившись, что это так, программа выполняет приведение типа в строке 21. Хотя
в данном случае я из исходного текста класса LibraryClass знаю, что ничего,
кроме атрибута типа UserRequirementAttribute, получить не могу, но пола­
гаться на такое «знание» не стоит.

Динамическая загрузка и связывание


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

Примечание Если не считать программирования самого каркаса, то кажется,


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

Чтение метаданных из динамически связываемых сборок


Для работы с динамически связываемыми сборками и типами придется при­
бегнуть к помощи отражения. По сравнению с API для работы с модулями и
функциями на платформе Win32 это верх простоты. Отражение напоминает тех­
нологию позднего связывания на основе интерфейса I D i s p a t c h и библиотек
типов, применяемую в СОМ, но все же проще. В листинге 9.5 мы перебираем все
элементы, хранящиеся в сборке.
Листинг 9.5. Отражение раскрывает метаданные программе
1 : using System;
2: using System.Reflection;
3: .
4: .
5: // Читаем сборку.
6: Assembly assy = Assembly.Load( args[0] );
7:
8: // Сборка составлена из модулей.
9: foreach (Module mod in assy.GetLoadedModules() )
10 : {
11: Console.WriteLine( "Модуль:{0}", mod.Name );
12 :
13: // Модуль содержит типы.
14: foreach ( Type t in mod.GetTypes() )
15: {
16: Console.WriteLine( "\tTnn: {0}",t.Name );
17 :
18: // А в каждом типе есть члены.
19: foreach ( Memberlnfo m in t .GetMembers() )
20 : {
21: Console.WriteLine( "\t\t{0}: {1}", m .MemberType,
m .Name );
22: switch ( m.MemberType )
23 : {
24: case MemberTypes.Constructor:
25: Constructorlnfо сі = (Constructorlnfо) m;
26: foreach (Parameterlnfo pi in ci.GetParameters())
27: Console.WriteLine( "\t\t\параметр: {0} {1}",
28: p i .ParameterType,pi.Name );
29 : break;
30: case MemberTypes.Method:
31: Methodlnfo mi =(Methodlnfo) m;
32: foreach(Parameterlnfo piin mi.GetParameters())
33: Console.WriteLine("\t\tXtnapaMeTp:{0} {1}",
34: pi.ParameterType,pi.Name);
35: Console.WriteLine("\t\t^Возвращает: {0}",
36: m i .ReturnType );
37: break;
Отражение и динамическое связывание III· · · !

38 case MemberTypes.Field:
39 Fieldlnfo fi = (Fieldlnfo) m;
40 Console.WriteLine("\t\t\tTMn поля: {0}", fi.Name);
41 break;
42 case MemberTypes.Property:
43 Propertylnfo pri = (Propertylnfо ) m;
44 Console.WriteLine("\t\t\tTnn свойства: {0}",
45 pri.PropertyType);
46 break;
47 }
48 }
49 : }
50 : }
Для использования отражения обычно импортируют пространства имен Sys­
tem и System. Ref lection. Необходимые классы находятся и в том, и в дру­
гом пространстве, поскольку без некоторых из них (например, без класса Туре)
система просто не может работать. Поэтому программа в листинге 9.5 включает
оба пространства имен, а затем загружает сборку (строка 6). Остальная часть про­
граммы (строки 9 -5 0 ) - это несколько вложенных циклов, в которых исследуются
все вложенные друг в друга элементы сборки, и информация о них выводится на
консоль. Если применить нашу программу к библиотеке LibraryCode, получим
следующий результат:
Модуль: librarycode.dll
Тип: LibraryClass
Метод: GetHashCode
Возвращает: System.Int32
Метод: Equals
Параметр: System.Object obj
Возвращает: System.Boolean
Метод: ToString
Возвращает: System.String
Метод: get_AProperty
Возвращает: System.String
Метод: Multiply
Параметр: System.Int32 a
Параметр: System.Int32 b
Возвращает: System.Int32
Метод: GetType
Возвращает: System.Type
Конструктор: .ctor
Свойство: AProperty
Тип свойства: System.String

Пользуясь отражением для чтения атрибутов из примера LibraryClass,


можно заменить строку 9 в листинге 9.4 на следующий фрагмент:
200 ■ ■ ■ III Метаданные и отражение

Assembly Asy = Assembly.Load("LibraryCode");


Type t = Asy.GetType("LibraryCode.LibraryClass");

// Получить атрибут, указав его тип и признак обхода


// дерева наследования объекта.
Object [] Reqs = t .GetCustomAttributes( false );
foreach ( Object о in Reqs )
{
if( о is UserRequirementAttribute )
{
UserRequirementAttribute ura =
(UserRequirementAttribute)o;
Console.WriteLine(
"К классу применимо требование {0}, версия {1}",
ura.Requirement, ura.Revision );
}
}
Вместо того чтобы воспользоваться существующим объектом, этот код создает
объект Assembly, загружая сборку LibraryCode. Она находится в библиотеке
LibraryCode .dll, но мы не указываем расширение имени файла при вызове
метода Assembly. Load ( assy_name ). Когда сборка загружена, для получения
ссылки на тип достаточно вызвать м