1 Особенности среды .NET

Программист пишет программу, компьютер ее выполняет. Программа создается на языке, понятном человек}', а компьютер умеет исполнять только программы, написанные на его языке — в машинных кодах. Совокупность средств, с помо­щью которых программы пишут, корректируют, преобразуют в машинные коды, отлаживают и запускают, называют средой разработки, или оболочкой. Среда разработки обычно содержит:

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

компилятор, с помощью которого программа переводится с языка, на котором она написана, в машинные коды;

средства отладки и запуска программ;

общие библиотеки, содержащие многократно используемые элементы программ;

справочную систему и другие элементы.

Под платформой понимается нечто большее, чем среда разработки для одного язы­ка. Платформа .NET (произносится «дотнет») включает не только среду разработ­ки для нескольких языков программирования, называемую Visual Studio.NET, но и множество других средств, например, механизмы поддержки баз данных, электронной почты и коммерции.

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

□                                            переносимость — возможность выполнения на различных типах компьютеров;
Q безопасность — невозможность несанкционированных действий;

□                                            надежность — способность выполнять необходимые функции в предопреде­
ленных условиях; средний интервал между отказами;

использование готовых компонентов — для ускорения разработки; □ межъязыковое взаимодействие — возможность применять одновременно не­сколько языков программирования. Платформа .NET позволяет успешно решать все эти задачи. Для обеспечения пе­реносимости компиляторы, входящие в состав платформы, переводят программу не в машинные коды, а в промежуточный язык (Microsoft Intermediate Language, MSIL, или просто IL), который не содержит команд, зависящих от языка, опера­ционной системы и типа компьютера. Программа на этом языке выполняется не самостоятельно, а под управлением системы, которая называется общеязыковой средой выполнения (Common Language Runtime, CLR).

Среда CLR может быть реализована для любой операционной системы. При выполнении программы CLR вызывает так называемый JIT-компилятор, пере­водящий код с языка IL в машинные команды конкретного процессора, которые немедленно выполняются. JIT означает «just in time», что можно перевести как «вовремя», то есть компилируются только те части программы, которые требует­ся выполнить в данный момент. Каждая часть программы компилируется один раз и сохраняется в кэше1 для дальнейшего использования.

2 Принципы ООП

Принципы ООП проще всего понять на примере программ моделирования. В ре­альном мире каждый предмет или процесс обладает набором статических и ди­намических характеристик, иными словами, свойствами и поведением. Поведе­ние объекта зависит от его состояния и внешних воздействий. Например, объект «автомобиль» никуда не поедет, если в баке нет бензина, а если повернуть руль, изменится положение колес.

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

При создании объектно-ориентированной программы предметная область пред­ставляется в виде совокупности объектов. Выполнение программы состоит в том, что объекты обмениваются сообщениями. Это позволяет использовать при про­граммировании понятия, более адекватно отражающие предметную область. При представлении реального объекта с помощью программного необходимо выде­лить в первом его существенные особенности. Их список зависит от цели модели­рования. Например, объект «крыса» с точки зрения биолога, изучающего миграции, ветеринара или, скажем, повара будет иметь совершенно разные характеристики. Выделение существенных с той или иной точки зрения свойств называется абст­рагированием. Таким образом, программный объект — это абстракция. Важным свойством объекта является его обособленность. Детали реализации объ­екта, то есть внутренние структуры данных и алгоритмы их обработки, скрыты от пользователя объекта и недоступны для непреднамеренных изменений. Объект используется через его интерфейс — совокупность правил доступа. Скрытие деталей реализации называется инкапсуляцией (от слова «капсула»). Ничего сложного в этом понятии нет: ведь и в обычной жизни мы пользуемся объектами через их интерфейсы. Сколько информации пришлось бы держать в голове, если бы для просмотра новостей надо было знать устройство телевизора! Таким образом, объект является «черным ящиком*, замкнутым по отношению к внешнему миру. Это позволяет представить программу в укрупненном виде -на уровне объектов и их взаимосвязей, а следовательно, управлять большим объ­емом информации и успешно отлаживать сложные программы. Сказанное можно сформулировать более кратко и строго: объект — это инкапсу­лированная абстракция с четко определенным интерфейсом. Инкапсуляция позволяет изменить реализацию объекта без модификации основ­ной части программы, если его интерфейс.остался прежним. Простота модифи­кации является очень важным критерием качества программы, ведь любой про­граммный продукт в течение своего жизненного цикла претерпевает множество изменений и дополнений.

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

 

 

4 Понятие класса

Для представления объектов в языках С#, Java, C++, Delphi и т. п. используется понятие класс, аналогичное обыденному смыслу этого слова в контексте «класс членистоногих», «класс млекопитающих», «класс задач» и т. п. Класс является обобщенным понятием, определяющим характеристики и поведение некоторого множества конкретных объектов этого класса, называемых экземплярами класса. «Классический» класс содержит данные, задающие свойства объектов класса, и функции, определяющие их поведение. В последнее время в класс часто добавля­ется третья составляющая — события, на которые может реагировать объект класса1. Все классы библиотеки .NET, а также все классы, которые создает программист в среде .NET, имеют одного общего предка — класс object и организованы в единую иерархическую структуру. Внутри нее классы логически сгруппированы в так называемые пространства имен, которые служат для упорядочивания имен классов и предотвращения конфликтов имен: в разных пространствах имена мо­гут совпадать. Пространства имен могут быть вложенными, их идея аналогична знакомой вам иерархической структуре каталогов на компьютере. Любая программа, создаваемая в .NET, использует пространство имен System. В нем определены классы, которые обеспечивают базовую функциональность, напри­мер, поддерживают выполнение математических операций, управление памятью и ввод-вывод.

Обычно в одно пространство имен объединяют взаимосвязанные классы. Напри­мер, пространство System.Net содержит классы, относящиеся к передаче данных по сети, System.Windows.Forms — элементы графического интерфейса пользовате­ля, такие как формы, кнопки и т. д. Имя каждого пространства имен представля­ет собой неделимую сущность, однозначно его определяющую. Последнее, о чем необходимо поговорить, прежде чем начать последовательное изучение языка С#, — среда разработки Visual Studio.NET.

 

 

5 Конструкторы и деструкторы

Конструктор предназначен для инициализации объекта. Он вызывается авто­матически при создании объекта класса с помощью операции new. Имя конст­руктора совпадает с именем класса. Ниже перечислены свойства конструкторов: □  Конструктор не возвращает значение, даже типа void.

*                                             Класс может иметь несколько конструкторов с разными параметрами для раз­
ных видов инициализации.

* Если программист не указал ни одного конструктора или какие-то поля не были инициализированы, полям значимых типов присваивается нуль, полям ссылочных типов — значение null.

*                                             Конструктор, вызываемый без параметров, называется конструктором по
умолчанию.

До сих пор мы задавали начальные значения полей класса при описании класса (см., например, листинг 5.1). Это удобно в том случае, когда для всех экземпляров класса начальные значения некоторого поля одинаковы. Если же при создании объектов требуется присваивать полю разные значения, это следует делать в кон­структоре. В листинге 5.6 в класс Demo добавлен конструктор, а поля сделаны закрытыми (ненужные в данный момент элементы опущены). В программе соз­даются два объекта с различными значениями полей.

Деструкторы

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

[ атрибуты ] [ extern ] -иияклассаО тело

Как видно из определения, деструктор не имеет параметров, не возвращает зна­чения и не требует указания спецификаторов доступа. Его имя совпадает с име­нем класса и предваряется тильдой (~), символизирующей обратные по отноше­нию к конструктору действия. Тело деструктора представляет собой блок или просто точку с запятой, если деструктор определен как внешний (extern). Сборщик мусора удаляет объекты, на которые нет ссылок. Он работает в соот­ветствии со своей внутренней стратегией в неизвестные для программиста моменты времени. Поскольку деструктор вызывается сборщиком мусора, не­возможно гарантировать, что деструктор будет обязательно вызван в процессе работы программы. Следовательно, его лучше использовать только для гарантии освобождения ресурсов, а «штатное» освобождение выполнять в другом месте программы. Применение деструкторов замедляет процесс сборки мусора.

 

 

6 NEW.THIS

Ключевое слово this

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

В явном виде параметр this применяется для того, чтобы возвратить из метода ссылку на вызвавший объект, а также для идентификации поля в случае, если его имя совпадает с именем параметра метода, например:

class Demo

{

double у;

public Demo TO      // метод возвращает ссылку на экземпляр

{

return this;

j

public void Sety< double у ) f

this.у - у:      // полю у присваивается значение параметра у } )

 

7 наследование

Класс в С# может иметь произвольное количество потомков и только одного предка. При описании класса имя его предка записывается в заголовке класса

после двоеточия. Если имя предка не указано, предком считается базовый класс всей иерархии System.Object:

[ атрибуты ] [ спецификаторы ] class инякласса [ : предки ] тело класса

Примечание:

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

Конструкторы не наследуются, поэтому производный класс должен иметь соб­ственные конструкторы. Порядок вызова конструкторов определяется приведен­ными далее правилами:

* Если в конструкторе производного класса явный вызов конструктора базового класса отсутствует, автоматически вызывается конструктор базового класса без параметров. Это правило использовано в первом из конструкторов класса Daemon.

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

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

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

 

8 Виртуальные методы

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

Следовательно, надо каким-то образом дать знать компилятору, что эти методы будут обрабатываться по-другому. Для этого в С# существует ключевое слово virtual. Оно записывается в заголовке метода базового класса, например:

virtual public void PassportC)  ...

Слово virtual в переводе с английского значит «фактический». Объявление ме­тода виртуальным означает, что все ссылки на этот метод будут разрешаться по факту его вызова, то есть не на стадии компиляции, а во время выполнения про­граммы. Этот механизм называется поздним связыванием.

Для его реализации необходимо, чтобы адреса виртуальных методов хранились там, где ими можно будет в любой момент воспользоваться, поэтому компилятор формирует для этих методов таблицу виртуальных методов (Virtual Method Table, VMT). В нее записываются адреса виртуальных методов (в том числе унаследо­ванных) в порядке описания в классе. Для каждого класса создается одна таблица. Каждый объект во время выполнения должен иметь доступ к VMT. Обеспечение этой связи нельзя поручить компилятору, так как она должна устанавливаться во время выполнения программы при создании объекта. Поэтому связь экземп­ляра объекта с VMT устанавливается с помощью специального кода, автомати­чески помещаемого компилятором в конструктор объекта. Если в производном классе требуется переопределить виртуальный метод, ис­пользуется ключевое слово override, например:

override public void PassportC)  ...

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

 

 

9 Абстрактные классы

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

ПРИМЕЧАНИЕ           

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

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

Абстрактные классы используются при работе со структурами данных, предна­значенными для хранения объектов одной иерархии, и в качестве параметров методов. Если класс, производный от абстрактного, не переопределяет все абст­рактные методы, он также должен описываться как абстрактный. Можно создать метод, параметром которого является абстрактный класс. На место этого параметра при выполнении программы может передаваться объект любого производного класса. Это позволяет создавать полиморфные методы, работаю­щие с объектом любого типа в пределах одной иерархии. Полиморфизм в раз­личных формах является мощным и широко применяемым инструментом ООП.

 

10-11 типы данных

Целые типы, а также символьный, вещественные и финансовый типы можно объединить иод названием арифметических типов.

Внутреннее представление величины целого типа — целое число в двоичном коде. В знаковых типах старший бит числа интерпретируется как знаковый (0 — положительное число, 1 — отрицательное). Отрицательные числа чаще всего представляются в так называемом дополнительном коде. Для преобразования числа в дополнительный код все разряды числа, за исключением знакового, инвертируются, затем к числу прибавляется единица, и знаковому биту тоже присваивается единица. Беззнаковые типы позволяют представлять только положительные числа, поскольку старший разряд рассматривается как часть кода числа.

Название

 

Логический тип

Ключево) слово

bool

■    Тип .NET

Boolean

Диапазон значений

true, false

Описание

Размер, битов

Целые типы

sbyte

SByte

От -128 до 127

Со знаком

8

 

byte

Byte

От 0 до 255

Без знака

8

 

short

Int 16

От -32 768 до 32 767

Со знаком

16

 

ushort

UInt16

От 0 до 65 535

Без знака

16

 

int

Int32

От -2 ■ 109 до 2   109

Со знаком

32

 

uint

UInt32

От 0 до 4 ■ 109

Без знака

32

 

long

Int64

От-9х Ю,8до 9

· 10й

Со знаком

64

 

ulong

UInt64

ОтОдо 18- 1018

Без знака

64

Символьный

char

Char

От U+0000

Unicode-

16

тип

 

 

дои+ffff

символ

 

Вещественные1

float

Single

От 1.5 - 10 45 до 3.4 • 1038

7 цифр

32

 

double

Double

От 5.0 ■ Ю-324 до 1.7 • 10308

15-16 цифр

64

Финансовый тип

decimal

Decimal

От 1.0 ■ 10'28 до 7.9 ■ 1028

28-29 цифр

128

Строковый тип

string

String

Длина ограничена объемом доступной памяти

Строка из Unicode-символов

 

Тип object

object

Object

Можно хранить все что угодно

Всеобщий предок

 

 

 

 

 

 

 

 

 

 

 

 

 

12 Массивы

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

Массив относится к ссылочным типам данных, то есть располагается в динами­ческой области памяти, поэтому создание массива начинается с выделения памя­ти под его элементы. Элементами массива могут быть величины как значимых, так и ссылочных типов (в том числе массивы). Массив значимых типов хранит значения, массив ссылочных типов — ссылки на элементы. Всем элементам при создании массива присваиваются значения по умолчанию: нули для значимых типов и null — для ссылочных.

На рис. 6.1 представлен массив, состоящий из пяти элементов любого значимого типа, например int или double, а рис. 6.2 иллюстрирует организацию массива из элементов ссылочного типа.

Вот, например, как выглядят операторы создания массива из 10 целых чисел и массива из 100 строк:

Int[ ]       w = new int[10];

Stnng[  ] z - new string[100];

Элементы массива нумеруются с нуля, поэтому максимальный номер элемен­та всегда на единицу меньше размерности (например, в описанном выше мас­сиве w элементы имеют индексы от 0 до 9). Для обращения к элементу массива после имени массива указывается номер элемента в квадратных скобках.

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

При вычислении выражений может возникнуть необходимость в преобразовании типов. Если операнды, входящие в выражение, одного типа и операция для этого типа определена, то результат выражения будет иметь тот же тип. Если операнды разного типа и/или операция для этого типа не определена, пе­ред вычислениями автоматически выполняется преобразование типа по прави­лам, обеспечивающим приведение более коротких типов к более длинным для сохранения значимости и точности. Автоматическое (неявное) преобразование возможно не всегда, а только если при этом не может случиться потеря значимости. Если неявного преобразования из одного типа в другой не существует, програм­мист может задать явное преобразование типа с помощью операции (тип)х. Его результат остается на совести программиста. Явное преобразование рассматри­вается в этой главе немного позже.

ВНИМАНИЕ               

Арифметические операции не определены для более коротких, чем int, типов. Это означает, что если в выражении участвуют только величины типов sbyte, byte, short и ushort, перед выполнением операции они будут преобразованы в int. Таким образом, результат любой арифметической операции имеет тип не менее int.

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

 

18 Операторы цикла

Операторы цикла используются для вычислений, повторяющихся многократно. В С# имеется четыре вида циклов: цикл с предусловием while, цикл с постусло­вием repeat, цикл с параметром for и цикл перебора foreach. Каждый из них со­стоит из определенной последовательности операторов.

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

Проверка условия продолжения цикла выполняется на каждой итерации либо до тела цикла (тогда говорят о цикле с предусловием, схема которого показана на рис. 4.4, а), либо после тела цикла (цикл с постусловием, рис. 4.4. б). Разница между ними состоит в том, что тело цикла с постусловием всегда выполняется хотя бы один раз, после чего проверяется, надо ли его выполнять еще раз. Про­верка необходимости выполнения цикла с предусловием делается до тела цикла, поэтому возможно, что он не выполнится ни разу.

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

Цикл завершается, если условие его продолжения не выполняется. Возможно принудительное завершение как текущей итерации, так и цикла в целом. Для этого служат операторы break, continue, return и goto (см. раздел «Операторы передачи управления», с. 83). Передавать управление извне внутрь цикла запре­щается (при этом возникает ошибка компиляции).

 

 

19 Операторы перехода

Оператор goto

Оператор безусловного перехода goto используется в одной из трех форм:

goto нетка;

goto case константноевыражение;

goto default;

В теле той же функции должна присутствовать ровно одна конструкция вида

метка: оператор;

Оператор goto метка передает управление на помеченный оператор. Метка — это обычный идентификатор, областью видимости которого является функция, в теле которой он задан. Метка должна находиться в той же области видимости, что и оператор перехода. Использование этой формы оператора безусловного пере­хода оправдано в двух случаях: □  принудительный выход вниз по тексту программы из нескольких вложенных

циклов или переключателей; Q  переход из нескольких точек функции вниз по тексту в одну точку (например,

если перед выходом из функции необходимо всегда выполнять какие-либо

действия). В остальных случаях для записи любого алгоритма существуют более подходя­щие средства, а использование оператора goto приводит только к усложнению структуры программы и затруднению отладки. Применение этого оператора на­рушает принципы структурного и модульного программирования, по которым все блоки, образующие программу, должны иметь только один вход и один выход. Вторая и третья формы оператора goto используются в теле оператора выбора switch. Оператор goto case константное_выражение передает управление на соответ­ствующую константному выражению ветвь, а оператор goto default — на ветвь default. Надо отметить, что реализация оператора выбора в С# на редкость не­удачна, и наличие в нем оператора безусловного перехода затрудняет понимание программы, поэтому лучше обходиться без него.

 

21 Свойства

Свойства служат для организации доступа к полям класса. Как правило, свойст­во связано с закрытым полем класса и определяет методы его получения и уста­новки. Синтаксис свойства:

[ атрибуты ] [ спецификаторы ] тип инясвойства

{

[ get коддоступа ]

[ set код_доступа ] }

Значения спецификаторов для свойств и методов аналогичны. Чаще всего свой­ства объявляются как открытые (со спецификатором publ i с), поскольку они вхо­дят в интерфейс объекта.

Код доступа представляет собой блоки операторов, которые выполняются при получении (get) или установке (set) свойства. Может отсутствовать либо часть get, либо set, но не обе одновременно.

Если отсутствует часть set, свойство доступно только для чтения (read-only), если отсутствует часть get, свойство доступно только для записи (write-only). В версии С# 2.0 введена удобная возможность задавать разные уровни доступа для частей get и set. Например, во многих классах возникает потребность обеспе­чить неограниченный доступ для чтения и ограниченный — для записи. Спецификаторы доступа для отдельной части должны задавать либо такой же, либо более ограниченный доступ, чем спецификатор доступа для свойства в целом. На­пример, если свойство описано как publ ic, его части могут иметь любой специфика­тор доступа, а если свойство имеет доступ protected internal, его части могут объяв­ляться как internal, protected или private. Синтаксис свойства в версии 2.0 имеет вид

[ атрибуты ] [ спецификаторы ] тип инясвойства {

[ [ атрибуты ] [ спецификаторы ] get коддоступа ]

[ [ атрибуты ] [ спецификаторы ] set коддоступа ] }

 

22 Класс Object

Корневой класс System.Object всей иерархии объектов .NET, называемый в С# object, обеспечивает всех наследников несколькими важными методами. Про­изводные классы могут использовать эти методы непосредственно или пере­определять их.

Класс object часто используется и непосредственно при описании типа парамет­ров методов для придания им общности, а также для хранения ссылок на объек­ты различного типа — таким образом реализуется полиморфизм. Открытые методы класса System.Object перечислены ниже.

*                                             Метод Equals с одним параметром возвращает значение true, если параметр

и вызывающий объект ссылаются на одну и ту же область памяти. Син­
таксис:

 

public virtual bool  EqualsC object obj  );

*                                             Метод Equals с двумя параметрами возвращает значение true, если оба пара­
метра ссылаются на одну и ту же область памяти. Синтаксис:

public static bool EqualsC object obi. object ob2 );

*                                             Метод GetHashCode формирует хеш-код объекта и возвращает число, однознач­
но идентифицирующее объект. Это число используется в различных структу­
рах и алгоритмах библиотеки. Если переопределяется метод Equals, необходи­
мо перегрузить и метод GetHashCode. Подробнее о хеш-кодах рассказывается
в разделе «Абстрактные структуры данных» (см. с. 291). Синтаксис:

public virtual int GetHashCodeO;

*                                             Метод Get Type возвращает текущий полиморфный тип объекта, то есть не тип
ссылки, а тип объекта, на который она в данный момент указывает. Возвра­
щаемое значение имеет тип Туре. Это абстрактный базовый класс иерархии,
использующийся для получения информации о типах во время выполнения1.
Синтаксис:

public Type GetTypeC):

*                                             Метод ReferenceEquals возвращает значение true, если оба параметра ссыла­
ются на одну и ту же область памяти. Синтаксис:

public static bool( object obi, object ob2 );

* Метод ToString по умолчанию возвращает для ссылочных типов полное имя класса в виде строки, а для значимых — значение величины, преобразованное в строку. Этот метод переопределяют для того, чтобы можно было выводить информацию о состоянии объекта. Синтаксис:

public virtual string ToStringC)

 

24 Структуры

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

Q структура является значимым, а не ссылочным типом данных, то есть экземп­ляр структуры хранит значения своих элементов, а не ссылки на них, и распо­лагается в стеке, а не в хипе; Q структура не может участвовать в иерархиях наследования, она может только реализовывать интерфейсы;

в структуре запрещено определять конструктор по умолчанию, поскольку он определен неявно и присваивает всем ее элементам значения по умолчанию (нули соответствующего типа);

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

ПРИМЕЧАНИЕ           

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

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

ПРИМЕЧАНИЕ           

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

Синтаксис структуры:

[ атрибуты ] [ спецификаторы ] struct иняструктуры [ : интерфейсы ] тело структуры [ ; ]

Спецификаторы структуры имеют такой же смысл, как и для класса, причем из спецификаторов доступа допускаются только public, internal и private (послед­ний — только для вложенных структур).

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

* поскольку структуры не могут участвовать в иерархиях, для их элементов не могут использоваться спецификаторы protected и protected internal;

*                                             структуры не могут быть абстрактными (abstract), к тому же по умолчанию
они бесплодны (sealed);

методы структур не могут быть абстрактными и виртуальными;

переопределяться (то есть описываться со спецификатором override) могут только методы, унаследованные от базового класса object;

параметр this интерпретируется как значение, поэтому его можно использо­вать для ссылок, но не для присваивания;

при описании структуры нельзя задавать значения полей по умолчанию1 — это будет сделано в конструкторе по умолчанию, создаваемом автоматически (конструктор присваивает значимым полям структуры нули, а ссылочным — значение null).

В листинге 9.8 приведен пример описания структуры, представляющей комплекс­ное число. Для экономии места из всех операций приведено только описание сложения. Обратите внимание на перегруженный метод ToString; он позволяет выводить экземпляры структуры на консоль, поскольку неявно вызывается в ме­тоде Console.WriteLine. Использованные в методе спецификаторы формата опи­саны в приложении.

 

25 Перегрузка операций

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

MyObject a. b. с;

с = а + Ь:                           // используется операция сложения для класса MyObject

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

[ атрибуты ] спецификаторы о&ьявительоперации тело

Атрибуты рассматриваются в главе 12, в качестве спецификаторов одновремен­но используются ключевые слова public и static. Кроме того, операцию можно объявить как внешнюю (extern).

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

 

26 Индексаторы

Индексатор представляет собой разновидность свойства. Если у класса есть скрытое поле, представляющее собой массив, то с помощью индексатора можно обратиться к элементу этого массива, используя имя объекта и номер элемента массива в квадратных скобках. Иными словами, индексатор — это такой «ум­ный» индекс для объектов.

Синтаксис индексатора аналогичен синтаксису свойства: атрибуты спецификаторы тип this [ список параметров ]

{

get коддоступа set коддоступа

}

ВНИМАНИЕ               

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

Атрибуты мы рассмотрим позже, в главе 12, а спецификаторы аналогичны спецификаторам свойств и методов. Индексаторы чаще всего объявляются со

спецификатором public, поскольку они входят в интерфейс объекта1. Атрибуты и спецификаторы могут отсутствовать.

Код доступа представляет собой блоки операторов, которые выполняются при получении (get) или установке значения (set) элемента массива. Может отсутст­вовать либо часть get, либо set, но не обе одновременно. Если отсутствует часть set, индексатор доступен только для чтения (read-only), если отсутствует часть get, индексатор доступен только для записи (write-only).

Список параметров содержит одно или несколько описаний индексов, по кото­рым выполняется доступ к элементу. Чаще всего используется один индекс це­лого типа.

Индексаторы в основном применяются для создания специализированных мас­сивов, на работу с которыми накладываются какие-либо ограничения. В листин­ге 7.3 создан класс-массив, элементы которого должны находиться в диапазоне [О, 100]. Кроме того, при доступе к элементу проверяется, не вышел ли индекс за допустимые границы.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

27 Интерфейсы

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

Каждый класс может определять элементы интерфейса по-своему. Так достигав ется полиморфизм: объекты разных классов по-разному реагируют на вызовы одного и того же метода. Синтаксис интерфейса аналогичен синтаксису класса:

[ атрибуты ] [ спецификаторы ] interface иня интерфейса [  ;  предки ] телоинтерфейса [ ; ]

Для интерфейса могут быть указаны спецификаторы new, publ ic, protected, internal и private. Спецификатор new применяется для вложенных интерфейсов и имеет такой же смысл, как и соответствующий модификатор метода класса. Остальные спецификаторы управляют видимостью интерфейса. В разных контекстах опре­деления интерфейса допускаются разные спецификаторы. По умолчанию интер­фейс доступен только из сборки, в которой он описан (internal).

Интерфейс может наследовать свойства нескольких интерфейсов, в этом случае предки перечисляются через запятую. Тело интерфейса составляют абстрактные методы, шаблоны свойств и индексаторов, а также события.

ПРИМЕЧАНИЕ         

Методом исключения можно догадаться, что интерфейс не может содержать кон­станты, поля, операции, конструкторы, деструкторы, типы и любые статические элементы.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

28 Обработка исключительных ситуаций

В языке С# есть операторы, позволяющие обнаруживать и обрабатывать ошибки (исключительные ситуации), возникающие в процессе выполнения программы. Об этом уже упоминалось в разделе «Введение в исключения» (см. с. 46), а сей­час мы рассмотрим механизм обработки исключений более подробно. Исключительная ситуация, или исключение, — это возникновение аварийного со­бытия, которое может порождаться некорректным использованием аппаратуры или неправильной работой программы, например делением на ноль или пере­полнением. Обычно эти события приводят к завершению программы с систем­ным сообщением об ошибке. С# дает программисту возможность восстановить работоспособность программы и продолжить ее выполнение. Исключения С# не поддерживают обработку асинхронных событий, таких как ошибки оборудования или прерывания, например нажатие клавиш Ctrl+C. Меха­низм исключений предназначен только для событий, которые могут произойти в результате работы самой программы и указываются явным образом. Исключе­ния возникают тогда, когда некоторая часть программы не смогла сделать то, что от нее требовалось. При этом другая часть программы может попытаться сделать что-нибудь иное.

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

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

ПРИМЕЧАНИЕ         

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

Исключения генерирует либо среда выполнения, либо программист с помощью оператора throw. В табл. 4.1 приведены наиболее часто используемые стандарт­ные исключения, генерируемые средой. Они определены в пространстве имен System. Все они являются потомками класса Exception, а точнее, потомками его потомка SystemException. Исключения обнаруживаются и обрабатываются в операторе try.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

29 Ключевые слова

Операции преобразования типа обеспечивают возможность явного и неявного преобразования между пользовательскими типами данных. Синтаксис объявите­ля операции преобразования типа:

implicit operator тип ( параметр )                // неявное преобразование

explicit operator тип ( параметр )               // явное преобразование

Эти операции выполняют преобразование из типа параметра в тип, указанный в заголовке операции. Одним из этих типов должен быть класс, для которого определяется операция. Таким образом, операции выполняют преобразование либо типа класса к другому типу, либо наоборот. Преобразуемые типы не долж­ны быть связаны отношениями наследования1. Примеры операций преобразова­ния типа для класса Monster, описанного в главе 5:

public static implicit operator intC Monster m )

{

return m.health:

}

public static explicit operator Monster( int h )

{

return new Monster( h. 100, "Fromlnt" );

}

Ниже приведены примеры использования этих преобразований в программе. Не

надо искать в них смысл, они просто иллюстрируют синтаксис:

Monster Masha - new Monster( 200. 200.  "Masha" );

int i = Masha;                  // неявное преобразование

Masha = (Monster) 500;                                       // явное преобразование

Неявное преобразование выполняется автоматически:

при присваивании объекта переменной целевого типа, как в примере;

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

при передаче объекта в метод на место параметра целевого типа;

при явном приведении типа.

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

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

 

30 Делегаты

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

Описание делегатов

Описание делегата задает сигнатуру методов, которые могут быть вызваны с его
помощью:                        »

[ атрибуты ] [ спецификаторы ] delegate тип имя делегата С [ параметры ] )

Спецификаторы делегата имеют тот же смысл, что и для класса, причем допуска­ются только спецификаторы new, public, protected, internal и private. Тип описывает возвращаемое значение методов, вызываемых с помощью делега­та, а необязательными параметрами делегата являются параметры этих методов. Делегат может хранить ссылки на несколько методов и вызывать их поочередно; естественно, что сигнатуры всех методов должны совпадать.

Пример описания делегата:

public delegate void 0 ( int i ):

Здесь описан тип делегата, который может хранить ссылки на методы, возвра­щающие void и принимающие один параметр целого типа.

ПРИМЕЧАНИЕ     

Делегат, как и всякий класс, представляет собой тип данных. Его базовым классом является класс System. Delegate, снабжающий своего «отпрыска» некоторыми полез­ными элементами, которые мы рассмотрим позже. Наследовать от делегата нельзя, да и нет смысла.

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

Использование делегатов

Для того чтобы воспользоваться делегатом, необходимо создать его экземпляр и задать имена методов, на которые он будет ссылаться. При вызове экземпляра делегата вызываются все заданные в нем методы. Делегаты применяются в основном для следующих целей:

получения возможности определять вызываемый метод не при компиляции, а динамически во время выполнения программы;

обеспечения связи между объектами по типу «источник — наблюдатель»;

□                                            создания универсальных методов, в которые можно передавать другие методы;
J поддержки механизма обратных вызовов.

Все эти варианты подробно обсуждаются далее. Рассмотрим сначала пример реа­лизации первой из этих целей. В листинге 10.1 объявляется делегат, с помощью которого один и тот же оператор используется для вызова двух разных методов (С001 и Hack).

 

31 События

Событие — это элемент класса, позволяющий ему посылать другим объектам уведомления об изменении своего состояния. При этом для объектов, являющих­ся наблюдателями события, активизируются методы-обработчики этого собы­тия. Обработчики должны быть зарегистрированы в объекте-источнике события. Таким образом, механизм событий формализует на языковом уровне паттерн «наблюдатель», который рассматривался в предыдущем разделе. Механизм событий можно также описать с помощью модели «публикация — под­писка»: один класс, являющийся отправителем (sender) сообщения, публикует события, которые он может инициировать, а другие классы, являющиеся получа­телями (receivers) сообщения, подписываются на получение этих событий.

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

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

описание события;

описание метода (методов), инициирующих событие. Синтаксис события похож на синтаксис делегата1:

[ атрибуты ] [ спецификаторы ] event тип имя события

Для событий применяются спецификаторы new, public, protected, internal, private, static, virtual, sealed, override, abstract и extern, которые изучались при рассмот­рении методов классов. Например, так же как и методы, событие может быть ста­тическим (static), тогда оно связано с классом в целом, или обычным — в этом случае оно связано с экземпляром класса. Тип события — это тип делегата, на котором основано событие. Пример описания делегата и соответствующего ему события:

public delegate void Del( object о );              // объявление делегата

class A

(

public event Del Oops;   // объявление события

}

Обработка событий выполняется в классах-получателях сообщения. Для этого в них описываются методы-обработчики событий, сигнатура которых соответст­вует типу делегата. Каждый объект (не класс!), желающий получать сообщение, должен зарегистрировать в объекте-отправителе этот метод. Как видите, это в точности тот же самый механизм, который рассматривался в пре­дыдущем разделе. Единственное отличие состоит в том, что при использовании событий не требуется описывать метод, регистрирующий обработчики, посколь­ку события поддерживают операции += и -=, добавляющие обработчик в список и удаляющие его из списка.

 

32 Директивы препроцессора

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

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

Формат директив:

#if константноевыражение

[ #elif константное_выражение

[ #elif константное выражение …] #else

#endif

Количество директив #elif произвольно. Исключаемые блоки кода могут со­держать как описания, так и исполняемые операторы. Константное выражение может содержать одну или несколько символьных констант, объединенных знаками операций ==, !=, !, && и 11. Также допускаются круглые скобки. Кон­станта считается равной true, если она была ранее определена с помощью ди­рективы #def 1 пе.

Атрибуты

Атрибуты ;гго догюлшггельные сведения об атементах iiporpasiMbi (классах, мето­дах, параметрах и г. х). С помощью атрибутов можно добавлять информацию в ме­таданные сборки и затем извлекать се во время выполнения программы. Атрибут яв­ляется специальным видом класса и происходит от базового класса System Attribute. Атрибуты делятся на стандартные и пользовательские. В библиотеке .NET преду­смотрено множество стандартных атрибутов, которые можно использовать в про­граммах. Если всего разнообразия стандартных атрибутов не хватит, чтобы удовле­творить прихотливые требования программиста, он может описать собственные классы атрибутов, после чего применять их точно так же. как стандартные. При испом>зовании {спецификащш) атрибутов они затаются в секции атрибутов, располагаемой непосредственно перед элементом, для описания которого они предназначены. Секция заключается ь квадратные скобки и может содержать не­сколько атрибутов, перечисляемых через запятую. Порядок следования атрибу­тов произвольный.

Для каждого атрибута задаются имя, а также необязательные параметры и тип элемента сборки, к которому относится атрибут. Простейший пример атрибута:

[Serializable} class Honster {…

[NonSerialized] string папе: int health, агою: }

Атрибут [Serial izable], означающий, что объекты этого класса можно сохранять во внешней памяти, относится ко всему классу Honster. При этом поле паше поме­чено атрибутом [NonSerialized], что говорит о том. что это поле сохраняться не должно. Сохранение объектов рассматривалось в главе 10.

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

 

33  Небезопасный код

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

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

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

public unsafe struct Node

{

public int     Value;

public Node* Left;

public Node* Right; }

Вся структура Node помечается как небезопасная, что делает возможным исполь­зование в ней указателей Left и Right. Можно применить и другой вариант опи­сания, в котором небезопасными объявляются только соответствующие поля структуры:

public struct Node

{

public int Value;

public unsafe Node* Left;

public unsafe Node* Right; }

Оператор unsafe имеет следующий синтаксис: unsafe блок Все операторы, входящие в блок, выполняются в небезопасном контексте.

 

34 Строки

Строки типа string

Тип string, предназначенный для работы со строками символов в кодировке Unicode, является встроенным типом С#. Ему соответствует базовый класс System.String библиотеки .NET. Создать строку можно несколькими способами:

string s;                             //                                             инициализация отложена

string t = "qqq";             //                                             инициализация строковым литералом

string u = new stringC  '. 20);                              //                                             конструктор создает строку из 20 пробелов

char[] а = {  "0'.  '0'.  'О'  };                                        //                                             массив для инициализации строки

string v = new stringC a ):                                    //                                             создание из массива символов

Для строк определены следующие операции:

присваивание (=);

проверка на равенство (=);

проверка на неравенство (!=);

обращение по индексу ([]);

сцепление (конкатенация) строк (+).

Несмотря на то что строки являются ссылочным типом данных, на равенство и неравенство проверяются не ссылки, а значения строк. Строки равны, если име­ют одинаковое количество символов и совпадают посимвольно. Обращаться к отдельному элементу строки по индексу можно только для полу­чения значения, но не для его изменения. Это связано с тем, что строки типа string относятся к так называемым неизменяемым типам данных1. Методы, из­меняющие содержимое строки, на самом деле создают новую копию строки. Не­используемые «старые» копии автоматически удаляются сборщиком мусора. В классе System.String предусмотрено множество методов, полей и свойств, поз­воляющих выполнять со строками практически любые действия.

35 Регулярные выражения

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

эффективный поиск в тексте по заданному шаблону;

редактирование, замену и удаление подстрок;

Q формирование итоговых отчетов по результатам работы с текстом. С помощью регулярных выражений удобно обрабатывать, например, файлы в фор­мате HTML, файлы журналов или длинные текстовые файлы. Для поддержки регулярных выражений в библиотеку .NET включены классы, объединенные в пространство имен System.Text.RegularExpressions.

Метасимволы

Регулярное выражение — это шаблон (образец), по которому выполняется поиск соответствующего ему фрагмента текста. Язык описания регулярных выражений состоит из символов двух видов: обычных и метасимволов. Обычный символ представляет в выражении сам себя, а метасимвол — некоторый класс символов, например любую цифру или букву.

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

ПРИМЕЧАНИЕ           

Синтаксис регулярных выражений .NET в основном позаимствован из языка Perl 5. Неподготовленного человека вид сложного регулярного выражения может привес­ти в замешательство, но при вдумчивом изучении он обязательно почувствует его красоту и очарование. Пожалуй, регулярные выражения более всего напоминают заклинания, по которым волшебным образом преобразуется текст. Ошибка все­го в одном символе делает заклинание бессильным, зато, верно составленное, оно творит чудеса!

В табл. 15.2 описаны наиболее употребительные метасимволы, представляющие собой классы символов.

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

36 Коллекции и словари

В пространстве имен System.Col lections определены наборы стандартных коллек­ций и интерфейсов, которые реализованы в этих коллекциях.

Таблица 13.2. Интерфейсы пространства имен System.Collections
Интерфейс                      Назначение

ICol lection                        Определяет общие характеристики (например, размер)

для набора элементов

IComparer -                      Позволяет сравнивать два объекта

IDictionary                         Позволяет представлять содержимое объекта в виде пар

«имя—значение»

IDictionaryEnumerator     Используется для нумерации содержимого объекта, поддерживающего интерфейс IDictionary

I Enumerable                     Возвращает интерфейс I Enumerator для указанного объекта

I Enumerator                    Обычно используется для поддержки оператора foreach

В отношении объектов

IHashCodeProvider       Возвращает хеш-код для реализации типа с применением

выбранного пользователем алгоритма хеширования

ILi st                                      Поддерживает методы добавления, удаления и индексирования

элементов в списке объектов

I Enumerable                     Возвращает интерфейс I Enumerator для указанного объекта

I Enumerator                    Обычно используется для поддержки оператора foreach

В отношении объектов

IHashCodeProvider       Возвращает хеш-код для реализации типа с применением

выбранного пользователем алгоритма хеширования

ILi st                                      Поддерживает методы добавления, удаления и индексирования

элементов в списке объектов

В табл. 13.3 перечислены основные коллекции, определенные в пространстве

System.Collections1.

Таблица 13.3. Коллекции пространства имен System. Col lections

Класс                                   Назначение                       Важнейшие из реализованных интерфейсов

ArrayList     Массив, динамически                          IList, ICollection, [Enumerable, ICloneable

изменяющий свой размер

BitArray      Компактный массив для                    ICollection, IEnumerable, ICloneable

хранения битовых значений

Hashtable     Хеш-таблица2                               IDictionary, ICollection, IEnumerable,

ICloneable

Queue                                    Очередь                         ICollection, ICloneable, IEnumerable

SortedList   Коллекция, отсортированная    IDictionary, ICollection, IEnumerable,
по ключам. Доступ       ICloneable

к элементам — по ключу или по индексу

Stack                                  Стек                                    ICollection, IEnumerable

Пространство имен System.Collections.Specialized включает специализирован­ные коллекции, например коллекцию строк StringCollection и хеш-таблицу со строковыми ключами StringDictionary. В качестве примера стандартной коллекции рассмотрим класс ArrayLi st.

Хеш-таблица, ассоциативный массив, или словарьэто массив, доступ к эле­ментам которого осуществляется не по номеру, а по некоторому ключу. Можно сказать, что это таблица, состоящая из пар «ключ—значение* (табл. 13.1). Хеш-таблица эффективно реализует операцию поиска значения по ключу. При этом ключ преобразуется в число (хеш-код), которое используется для быстрого нахож­дения нужного значения в хеш-таблице.

Преобразование выполняется с помощью хеш-функции, или функции расстановки. Эта функция обычно производит какие-либо преобразования внутреннего пред­ставления ключа, например, вычисляег среднее арифметическое кодов составляющих его символов (английское слово «hash» означает перемалывать, перемешивать). Если хеш-функция распределяет совокупность возможных ключей равномерно по множеству индексов массива, то доступ к элементу по ключу выполняется почти так же быстро, как в массиве. Если хеш-функция генерирует для разных ключей оди­наковые хеш-коды, время поиска возрастает и становится сравнимым со временем поиска в списке.

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

38 Потоки

Поток (thread1) представляет собой часть испол­няемого кода программы. В каждом процессе есть первичный поток, исполняющий роль точки входа в приложение. Для консольных приложений это метод Main. Многопоточные приложения создают как для многопроцессорных, так и для од­нопроцессорных систем. Основной целью при этом являются повышение общей производительности и сокращение времени реакции приложения. Управление потоками осуществляет операционная система. Каждый поток получает некото­рое количество квантов времени, по истечении которого управление передается другому потоку. Это создает у пользователя однопроцессорной машины впечат­ление одновременной работы нескольких потоков и позволяет, к примеру, вы­полнять ввод текста одновременно с длительной операцией по передаче данных. Недостатки многопоточности:

□ большое количество потоков ведет к увеличению накладных расходов, свя­занных с их переключением, что снижает общую производительность системы; Q в многопоточных приложениях возникают проблемы синхронизации данных, связанные с потенциальной возможностью доступа к одним и тем же данным со стороны нескольких потоков (например, если один поток начинает измене­ние общих данных, а отведенное ему время истекает, доступ к этим же дан­ным может получить другой поток, который, изменяя данные, необратимо их повреждает).

Класс Thread

Поддержка многопоточности осуществляется в .NET в основном с помощью пространства имен System.Threading. Первичный поток создается автоматически. Для запуска вторичных потоков ис­пользуется класс Thread. При создании объекта-потока ему передается делегат, определяющий метод, выполнение которого выделяется в отдельный поток:

Thread t = new Thread ( new ThreadStart( имя_метода ) ):

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

 

 

 

 

 

 

© Grayscaile

Бесплатный конструктор сайтовuCoz