Author: Фролов А.В. Фролов Г.В.
Tags: политика военное дело военная наука программирование самоучитель языки программирования компьютерные технологии язык программирования c#
ISBN: 5-86404-176-9
Year: 2003
А. В. Фролов,
Г. В. Фролов
Язык „
самоучитель
МОСКВА Й\(\Ю?(ШШ 2003
УДК 32.973.1
ББК 681.3
Ф91
Фролов А. В., Фролов Г. В.
Ф91 Язык С#. Самоучитель. - М.: ДИАЛОГ-МИФИ, 2003. - 560 с.
ISBN 5-86404-176-9
Книга представляет собой методическое руководство по изучению современного языка
программирования С#. Рассмотрена платформа Microsoft .NET Framework, в среде которой ра-
работают программы С#, а также все основные возможности языка С#. Это классы, типы данных,
поля, методы, интерфейсы, свойства, делегаты работа с контейнерами, файлами, потоками и др.
Одновременно с изучением языка, читатель получит навыки объектно-ориентированного
и компонентно-ориентированного программирования, познакомится с обширной библиотекой
классов Microsoft .NET Framework. Приводятся многочисленные примеры программ, де-
демонстрирующие различные возможности языка С#.
Книга рассчитана на всех, кто желает самостоятельно изучить новый язык программиро-
программирования С#. Она может использоваться в качестве учебного пособия для студентов и школьников.
ББК 32.973.1
Учебно-справочное издание
Фролов Александр Вячеславович
Фролов Григорий Вячеславович
Язык С#. Самоучитель
Редактор О.А. Голубев
Корректор В. С. Кустов
Макет И. М. Чумаковой
Лицензия ЛР N 071568 от 25.12.97. Подписано в печать 10.11.2002.
Формат 70x100/16. Бум. газетная. Печать офс. Гарнитура Тайме.
Усл. печ. л. 45,15. Уч.-изд. л. 22,1 Тираж 3 000 экз. Заказ 4590.
Акционерное общество "ДИАЛОГ-МИФИ"
115409, Москва, ул. Москворечье, 31, корп. 2. Т.: 320-43-55, 320-43-77
Http://www .bitex.ru/~dialog
E-mail: dialog@bitex.ru
Отпечатано на Ордена Трудового Красного Знамени
ГУП Чеховский полиграфический комбинат
Министерства РФ по делам печати, телевещания и средств массовых коммуникаций
142300, г. Чехов, Московская обл.,
тел.: B72) 71-3-36, факс: B72) 62-5-36
ISBN 5-86404-176-9 © Фролов А. В.. Фролов Г. В., 2003
© Оригинал-макет, оформление обложки.
ЗАО "ДИАЛОГ-МИФИ", 2003
Введение
За всю историю развития компьютеров были созданы десятки (если не сотни) различ-
различных языков программирования. Некоторые из них канули в Лету, другие здравствуют
и поныне. Языки программирования развиваются и трансформируются, на базе одних
возникают другие — словом, все идет своим чередом. В этой книге мы расскажем вам
о новом языке — С# (произносится «си-шарп»), представляющим собой одну из самых
последних разработок компании Microsoft.
Следует сразу отметить, что язык С# — это не просто еще один язык программиро-
программирования. Он является одним из важнейших компонентов новой стратегической платфор-
платформы Microsoft .NET, ориентированной на современные технологии Интернета. Изучая
язык С#, вы закладываете фундамент своей успешной деятельности в области созда-
создания современных приложений с применением технологий компании Microsoft. Уже се-
сегодня вы сможете накапливать багаж знаний, необходимый для разработки программ,
рассчитанных на операционную систему Microsoft Windows .NET, которая еще только
готовится к выпуску.
Для того чтобы точнее определить роль и место языка С# среди других языков
и систем программирования, мы приведем краткий исторический обзор, отразив в нем
свой опыт применения различных языков программирования для решения тех или
иных задач.
Во Введении мы будем употреблять много терминов, возможно, незнакомых читателю.
Некоторые из них разъяснены по ходу изложения материала, для изучения других потре-
потребуется обратиться к дополнительной литературе. Если Вам непонятен какой-то термин,
пропустите его и читайте дальше. После изучения материала можно будет вернуться
и прочитать заново непонятные разделы, вооружившись дополнительными знаниями.
В частности, если вы в ближайшее время не планируете создавать приложения для
Интернета, можете игнорировать непонятную вам терминологию, имеющую отноше-
отношение к созданию Web-приложений. В противном случае для получения дополнительной
информации обратитесь к [2] и [3].
От ассемблера к С#
Имея более чем 15-летний стаж создания программ, программных комплексов и сис-
систем, мы перепробовали самые разные языки программирования. Начиная с составле-
составления программ в машинных кодах для процессора Intel 8080, мы перешли к языку ас-
ассемблера, а затем освоили Basic. Работая над автоматизированной системой исследо-
исследования электромагнитных полей в резонаторах ускорителей заряженных частиц,
мы обсчитывали результаты измерений и представляли их в графическом виде с по-
помощью языка Fortran. Нам также довелось поработать с малоизвестным сейчас макро-
макроязыком PL/M, создав с его помощью систему сбора данных результата физических
экспериментов, файловую систему для хранения данных на цифровом кассетном маг-
магнитофоне, а также отладчик ассемблерных программ процессора Intel 8080.
ДИМОГ/пИФИ 3
Следующий этап нашей деятельности как программистов был связан с компьюте-
компьютерами серии ЕС и разработкой информационных систем с базами данных. Здесь мы то-
тоже начали с ассемблера, а затем перешли на язык PL (не путайте его с упомянутым
выше языком PL/M). Разочаровавшись в языке PL, обладающем, на наш взгляд, мно-
многочисленными недостатками, мы обратили свой взор на Pascal и относительно новый
в то время язык С.
Имевшаяся в нашем распоряжении реализация языка Pascal для ЕС обладала рядом
ограничений и была снабжена недостаточно подробной документацией. Возможно,
поэтому мы больше внимания уделили языку С. С тех пор возможности С и Pascal вы-
равнялись, но наше пристрастие к языкам, подобным С, перешло в привычку и сохра-
сохранилось до сих пор.
Раздобыв транслятор С для компьютеров серии ЕС, мы несколько лет с успехом
использовали его для создания как прикладных, так и системных программ. Например,
для операционной системы (далее — ОС) TKS-432 мы разработали на языке С вирту-
виртуальную файловую систему, напоминающую по своему устройству файловую систему
FAT. В системном программировании мы комбинировали мощь языка С и низкоуров-
низкоуровневые возможности языка ассемблера.
С появлением персональных компьютеров мы обратили свое внимание на объект-
объектно-ориентированный язык C++. Так как мы привыкли к процедурному и структурному
программированию, то нам пришлось затратить определенные усилия для того, чтобы
разобраться в принципах объектно-ориентированного программирования. Тем не ме-
менее впоследствии мы не пожалели о затраченных усилиях, оценив преимущества ново-
нового подхода.
Такие возможности C++, как инкапсуляция, наследование и полиморфизм, оказа-
оказались очень полезными при разработке автономных и сетевых приложений для опера-
операционной системы Microsoft Windows. Готовые библиотеки классов, например уже ус-
устаревшая сейчас Borland OWL и постоянно развивающаяся Microsoft MFC, позволяют
разрабатывать приложения Windows на порядок быстрее, чем это было при использо-
использовании языка С.
Для разработки бухгалтерских программ и автономных справочно-инфор-
мационных систем мы применяли систему FoxPro (впоследствии — Microsoft FoxPro),
в которой использовался собственный язык программирования. В сочетании с визу-
визуальными средствами быстрого проектирования приложений этот язык позволял созда-
создавать программы с базами данных и довольно сложным пользовательским интерфейсом
буквально за считанные дни и недели.
После того как в нашей стране, наконец, появился Интернет, мы с большим энтузи-
энтузиазмом приступили к освоению новой для нас области программирования. И конечно,
мы попытались применить язык C++ для разработки Web-приложений. В частности,
мы создавали расширения серверов Web в виде программ ISAPI и CGI.
Однако оказалось, что, хотя активные компоненты Web-приложений можно полно-
полностью программировать на C++, гораздо удобнее применять для этого другие языки
программирования, специально ориентированные на Интернет.
Активные приложения Интернета представляют собой сложный конгломерат техноло-
технологий, для реализации которых применяются самые разнообразные средства и языки про-
программирования. Некоторые из этих языков и технологий мы использовали в среде ОС Mi-
Microsoft Windows, а некоторые — в среде ОС Linux.
4 А. В. Фролов. Г. В. Фролов. Язык С#. Самоучитель
Мы создавали статические страницы Web-ссрвсров на языке разметки гипертекста
HTML и Dynamic HTML, программы CGI на языке Perl, серверные и клиентские сце-
сценарии JavaScript и VB Script, серверные сценарии РНР, аплеты Java, а также серверные
элементы управления ActiveX с использованием языка C++.
И вот теперь мы получили настоящее удовольствие от работы с языком програм-
программирования С#, создавая на нем обычные приложения Microsoft Windows и Web-
приложения для Интернета. В частности, с его использованием для службы восстанов-
восстановления данных DataRccovcry.Ru (http://www.datarecovery.ru) нами создается программ-
программный комплекс удаленного восстановления данных, пропавших в результате аппарат-
аппаратных сбоев, программных ошибок и ошибок пользователей, а также вредоносного воз-
воздействия компьютерных вирусов.
Классические языки программирования
В начале своей программистской деятельности мы были сильно ограничены в инстру-
инструментах создания программного обеспечения. Сегодня же, наоборот, «ассортимент»
языков программирования и технологий очень велик, поэтому не всегда легко сделать
однозначный выбор. Помимо языков программирования, перечисленных выше, сего-
сегодня существуют такие языки, как Object Pascal, Modula, Visual Basic, Cobol, LISP,
Piton, Natural, RPG, Ada, Oak, SmallTalk и др.
И вот сейчас на арену выходит еще один язык программирования — С#, разрабо-
разработанный компанией Microsoft в рамках современной технологии Microsoft .NET. Этот
язык предназначен для создания как обычных автономных приложений Microsoft Win-
Windows, так и Web-приложений.
Как же выбрать язык программирования для реализации своего проекта?
Этот вопрос, который задают себе многие, правильнее было бы сформулировать
по-другому: «Как выбрать технологию для реализации своего проекта?»
Та или иная технология может предполагать применение одного или нескольких язы-
языков программирования. Совершив ошибку при выборе технологии на начальном этапе
проектирования, можно пойти по ложному пути, потеряв много времени и сил. В то же
время неправильный выбор языка программирования даже в рамках нужной технологии
также может привести к непроизводительным затратам усилий и потере времени.
Рассмотрим области применения нескольких наиболее распространенных языков
программирования. На наш взгляд, это поможет вам правильно оценить место и роль
языка С#.
Ассемблер
Язык ассемблера позволяет составлять программы с применением мнемонических
обозначений машинных команд и символических меток. Получив на входе файл с тек-
текстом программы, ассемблер переводит этот текст в машинный код, пригодный для не-
непосредственного исполнения. Таким образом, используя ассемблер, можно оптималь-
оптимально задействовать все возможности процессора.
Заметим, что сегодня применение языка ассемблера оправдано разве лишь при со-
составлении системных профамм (драйверов) ОС или отдельных фрагментов профамм,
требующих рекордной производительности.
Введение 5
Хотя теоретически на ассемблере можно писать любые программы, реально этот
язык используют только системные программисты, и то далеко не всегда. Например,
те же самые драйверы периферийных устройств для ОС Microsoft Windows и Linux
в большинстве случаев могут быть с успехом написаны на языках С или C++.
Тем не менее мы не считаем изучение ассемблера бесполезным занятием для начи-
начинающего программиста. Так как язык ассемблера в наилучшей степени отражает архи-
архитектурные особенности центрального процессора, знакомство с ассемблером позволит
лучше разобраться в принципах работы компьютерных программ.
Несмотря на появление своего успешного наследника — объектно-ориентированного
языка программирования C++, классический язык С до сих пор широко используется,
например теми, кто создает программы для ОС Linux.
В результате работы транслятора С, а затем редактора связей исходный текст про-
программы С переводится в машинный код, пригодный для исполнения процессором.
Благодаря операторам структурного программирования язык С намного облегчает со-
составление программ по сравнению с языком ассемблера. Библиотека стандартных
функций, поставляющаяся вместе с транслятором и фактически расширяющая язык С,
облегчает решение типовых задач программирования, таких, например, как выполне-
выполнение математических вычислений, работа с текстовыми строками и т. п.
Олнако объектно-ориентированный подход, предлагаемый языком C++, значитель-
значительно облегчает создание программ, в результате чего обоснованность использования
классического языка С при создании новых программ представляется нам весьма со-
сомнительной.
Более того, начиная изучение программирования с процедурного, а не объектно-
ориентированного языка, можно приобрести вредные привычки процедурного прог-
программирования. Эти привычки в дальнейшем затруднят изучение современных объект-
объектно-ориентированных и компонентно-ориентированных технологий.
C++
На сегодняшний день язык C++ представляет собой один из наиболее совершенных
инструментов создания прикладных и особенно системных программ. Применяя объ-
объектно-ориентированные возможности этого языка программирования, а также обшир-
обширные библиотеки классов и шаблонов, программист может создавать весьма и весьма
сложные приложения.
Современные оптимизирующие компиляторы позволяют добиться высокой ско-
скорости исполнения программ, благодаря чему во многих случаях даже критичные
ко времени исполнения фрагменты программы можно составлять без использова-
использования ассемблера.
Вместе с тем необходимо отметить, что для того, чтобы освоить в полной мере все
возможности языка C++, требуется немало времени. Читая и перечитывая основопола-
основополагающий труд, посвященный C++, — книгу Бьерна Страуструпа [1], мы постоянно от-
открываем для себя в этом языке что-то новое. И это несмотря на многолетний опыт ис-
использования C++.
6 А. В. Фролов, Г. В. Фролов. Язык С# Самоучитель
Составляя программы на С и C++, начинающий программист может много раз насту-
наступать на различные «грабли», прежде чем научится составлять программы с минимальным
количеством ошибок. Причина в том, что язык C++ разрешает программисту делать в сво-
своей программе практически все, что угодно. Программа может обратиться к любому дос-
доступному участку памяти с применением указателей, прочитать содержимое неинициали-
неинициализированных переменных, выйти за границы обрабатываемого массива, передать функции
неправильные параметры, вызвать функцию по ошибочному адресу и т. п.
Используя гибкость языка C++, опытный программист сумеет реализовать свои
идеи наилучшим образом, а начинающий рискует допустить трудно обнаруживаемые
ошибки.
Мы рекомендуем применять язык C++ в тех случаях, когда к быстродействию или
к размеру загрузочного модуля создаваемой программы предъявляются особые
требования. Например, выбор C++ будет правильным для системного программи-
программирования, для решения задач моделирования, для создания компактных утилит или
программных модулей, при разработке таких программ, как текстовые процессоры,
графические редакторы и компиляторы, модули расширения серверов Web с высо-
высокой посещаемостью.
С другой стороны, применение C++ в сочетании с классической технологией соз-
создания исходного текста программы и ее последующего компилирования в загрузоч-
загрузочный модуль во многих случаях будет не оправдано.
Например, разработка по этой технологии будет слишком трудоемкой при созда-
создании бухгалтерских программ, систем складского учета и систем управления докумен-
документооборотом предприятия, справочно-информационных систем с базами данных, авто-
автономных приложений для Microsoft Windows со сложным пользовательским интерфей-
интерфейсом, который к тому же время от времени изменяется. Здесь больше подойдет одна
из систем быстрой разработки приложений (Rapid Application Development, RAD),
позволяющих абстрагироваться от многих несущественных деталей и сосредоточиться
на решении прикладной задачи.
Что касается RAD для C++, то наиболее популярными системами сейчас являются
Microsoft Visual C++ в сочетании с библиотекой классов MFC и Borland C++ Builder.
Предоставляя в распоряжение программиста визуальные средства проектирования
приложений, эти системы значительно ускоряют создание диалоговых программ. Не-
Необходимо, однако, отметить, что использование MFC требует глубоких знаний C++
и совершенного владения технологиями объектно-ориентированного программиро-
программирования. Указанные системы RAD не решают проблем C++, связанных с предостав-
предоставлением полного доступа программ к ресурсам процесса и с возможностью совершения
трудно обнаруживаемых ошибок.
Pascal
Изначально разработанный в учебных целях язык программирования Pascal сегодня
превратился в современное средство объектно-ориентированного программирования
и пользуется большой популярностью. Не в последнюю очередь этой популярности он
добился в результате появления мощного и удобного средства ускоренной разработки
приложений Borland Delphi. Фактически именно компания Borland внесла наибольший
вклад в развитие современного языка Pascal.
Введение 7
Заметим, что, обладая всеми достоинствами языка C++, современный объектный
Pascal имеет и большинство его недостатков. Программист, составляющий программу
на языке Pascal, может совершать множество различных ошибок, которые проявятся
только во время выполнения программы и приведут к ее аварийному завершению.
На наш взгляд, область применения языка Pascal практически полностью совпадает
с областью применения языка C++. Язык Pascal в сочетании со средой разработки Bor-
Borland Delphi часто используется и для разработки небольших утилит, и для создания
программ со сложным пользовательским интерфейсом.
Basic
Разработанный компанией Microsoft и доступный вместе с первыми версиями ОС
MS DOS язык программирования Basic прост в изучении, преподавался в школах
и высших учебных заведениях, и потому с него начинали свой трудовой путь многие
программисты.
Между Basic и такими языками программирования, как язык ассемблера, С и C++,
существует одно принципиальное отличие, на которое нужно обратить внимание. Этот
язык не компилируемый, а интерпретируемый.
Чтобы понять, о чем идет речь, рассмотрим процесс компиляции и редактирования
программы, составленной на языке С или C++.
Создавая программу на С или C++, программист вначале готовит ее исходный
текст в каком-либо текстовом редакторе. Исходный текст несложных программ раз-
размещается в одном или нескольких файлах, сложные проекты могут содержать десятки
и сотни файлов исходного текста.
Далее файлы исходного текста передаются компилятору, формирующему объект-
объектный модуль программы (рис. В.1).
Файл
исходного
текста
программы
•=(>
Компилятор
■=>
Объектный
модуль
Рис. В. 1. Компиляция исходного текста программы
При компиляции нескольких исходных файлов может создаваться один или несколько
файлов объектных модулей. В процессе создания объектного модуля компилятор преобра-
преобразует команды исходного текста в машинные команды, предназначенные для исполнения
процессором. Тем не менее объектный модуль еще не готов для исполнения, так как в нем
еще присутствуют символические имена ссылок и переменных.
Для того чтобы собрать все объектные модули программы в один исполнимый мо-
модуль, нужен редактор связей. Эта программа собирает все объектные модули вместе,
заменяя символические ссылки относительными или абсолютными адресами перемен-
переменных и функций. Дополнительно в загрузочный модуль включаются необходимые
функции из библиотек объектных модулей, в частности из стандартной библиотеки
модулей компилятора С или C++ (рис. В.2).
8
А. В Фролов. Г. В. Фролов Язык С#. Самоучитель
Объектный
модуль
Редактор связей i У}
Загрузочный
модуль
Библиотека
объектных
модулей
Рис. В.2. Редактирование связей
В результате этой операции создается исполнимый модуль программы в виде фай-
файла *.сот (программа MS-DOS), *.exe (программа MS-DOS или Microsoft Windows) или
*.dl1 (библиотека динамической загрузки Microsoft Windows). Модуль называется ис-
исполнимым потому, что он содержит машинные команды, непосредственно исполня-
исполняемые центральным процессором компьютера.
Возвращаясь к языку программирования Basic, заметим, что он предполагает при-
применение совершенно другой схемы подготовки и исполнения программ.
В основе классической системы программирования на языке Basic лежит специ-
специальная программа, называемая интерпретатором языка Basic. Интерпретатор Basic
построчно считывает исходный файл программы, и также построчно ее «выполняет»,
а точнее говоря, интерпретирует.
Программа Basic работает не с реальной оперативной памятью компьютера, а с не-
некоторой моделью этой памяти, в которой располагаются переменные и константы. Для
обращения к периферийным устройствам, таким, как консоль ввода-вывода и дисковая
память, программа использует специальные операторы Basic.
При этом, если, например, в строке программы содержится команда вывода тексто-
текстовой строки на консоль, интерпретатор Basic вызывает свой собственный модуль выво-
вывода на консоль, передавая ему текстовую строку.
Программа Basic сильно ограничена в своих возможностях по обращению к физи-
физическим ресурсам компьютера. Фактически она работает в некотором изолированном
пространстве, выполняя только такие операции, которые определены в языке Basic.
Такой подход исключает возникновение критических ошибок, способных нарушить
работу ОС.
С целью упростить процесс программирования Basic берет на себя всю заботу
о преобразовании типов данных, так что программисту не приходится ломать над этим
голову. В результате Basic как нельзя лучше подходит для первых упражнений в про-
программировании.
Однако ценой увеличения надежности и упрощения программирования будет су-
существенное уменьшение скорости работы программы по сравнению с компилируемы-
компилируемыми программами. Причину этого легко понять, так как интерпретация одной строки
Введение 9
исходного текста программы Basic может вылиться в исполнение десятков и сотен
машинных команд. Что же касается операторов языка С или C++, то здесь базовые
операторы транслируются в одну или несколько машинных команд.
Для того чтобы как-то ускорить работу программ Basic, современные системы про-
программирования выполняют предварительное преобразование исходного текста в неко-
некоторый промежуточный код. Этот код не может непосредственно исполняться процес-
процессором, однако на его интерпретацию уходит намного меньше времени.
Современные системы программирования на языке Basic активно развиваются
компанией Microsoft. Пройдя множество трансформаций, язык Basic сегодня превра-
превратился в средство быстрого создания приложений RAD с названием Visual Basic. Но-
Новейшая разработка в этой области — система Microsoft Visual Basic .NET, предназна-
предназначенная для ускоренной разработки приложений для платформы Microsoft .NET.
Но разговор об этой платформе еще впереди.
Java
Язык программирования с «кофейным» названием Java был разработан компанией Sun
Microsystems как объектно-ориентированное средство создания приложений, способ-
способных работать без перетрансляции на различных компьютерных платформах. Прототи-
Прототипом для разработки послужил язык программирования Oak.
С этой целью исходный текст программы преобразовывался в некоторый промежу-
промежуточный байт-код, который затем интерпретировался специальной программой —
виртуальной машиной Java. Для того чтобы программы Java могли работать на раз-
различных компьютерных платформах, необходимо реализовать виртуальную машину
Java для всех платформ. Фактически виртуальная машина Java создана для всех совре-
современных ОС, включая Microsoft Windows, MacOS, Linux и другие Unix-подобные ОС.
С точки зрения защищенности ОС от «беспредела» программ, Java предоставляет
неплохое решение, запуская эти программы в защищенной виртуальной среде, назы-
называемой «песочницей» (sandbox). Программа Java не имеет непосредственного доступа
к физическим ресурсам компьютера и ОС, используя только те средства, что предос-
предоставлены ей в рамках виртуальной машины.
Таким образом, Java является не компилируемым, а интерпретируемым языком.
Вероятно, у вас уже возникли некоторые ассоциации с языком программирования Ba-
Basic, однако между Java и Basic имеются важные различия. Прежде всего, Java разраба-
разрабатывалась для применения на различных компьютерных платформах, a Basic — только
на платформе Microsoft Windows. Далее, Java изначально создавался как объектно-
ориентированный язык программирования, в то время как первые версии языка Basic
были предназначены для процедурного и структурного программирования.
Для Java были созданы многочисленные средства ускоренной разработки про-
1рамм. Что касается платформы Microsoft Windows, то для нее одной из наиболее
удобных систем RAD, на наш взгляд, служит интегрированная система программиро-
программирования Borland Java Builder.
Компания Microsoft создала свою версию виртуальной машины Java, дополнив
язык Java и библиотеки классов собственными расширениями, работоспособными
только на платформе Microsoft Windows. Это послужило одной из причин многочис-
многочисленных судебных разбирательств между компаниями Sun Microsystems и Microsoft.
10 А В Фролов, Г В. Фролов. Язык С# Самоучитель
По мнению компании Sun Microsystems, нестандартные расширения Java нарушали
основной принцип, ради которого создавался язык Java, — обеспечение возможности
работы программ на различных платформах без изменения байт-кода.
Одним из недостатков Java, сдерживающих его распространение, служит относи-
относительно невысокое быстродействие. Этот недостаток возникает из-за того, что Java —
интерпретируемый язык. Возникают проблемы и с точной реализацией виртуальной
машины на различных компьютерных платформах. Из-за принципиальных различий
в архитектуре и принципах работы операционных систем унификация виртуальных
машин представляет собой довольно непростую задачу.
Кроме того, Microsoft Windows практически полностью заполнила рынок ОС для
персональных компьютеров. Поэтому разработчикам настольных приложений нет
смысла вкладывать значительные средства в совместимость с ОС других типов, осо-
особенно учитывая проигрыш в скорости работы приложений.
Другое дело — серверы Интернета. Здесь Microsoft Windows пока еще не добилась
полного господства, разделяя рынок с такими ОС, как Linux, FreeBSD и другими Unix-
подобными ОС. В настоящее время Java пользуется популярностью, например,
как средство создания активных серверных Web-приложений. Работая на сервере Web,
программы Java взаимодействуют с серверами баз данных и почтовыми серверами,
формируя динамические документы HTML, отправляемые затем посетителям Web-
сервера.
Языку программирования Java посвящено очень много книг. Тем, кто интересуется
этим языком программирования, мы можем порекомендовать нашу работу «Программи-
«Программирование на Java: подробное руководство», созданное по заказу московского представи-
представительства компании Sun Microsystems (http://www.sun.ra/wiii/java/books/online/index.htrnl).
Созданная нами большая библиотека примеров приложений Java с подробными описа-
описаниями исходных текстов находится на авторском компакт-диске, который можно при-
приобрести в ЗАО «Диалог-МИФИ» (dialog@bitex.ru).
Языки для создания Интернет-приложений
Как мы уже говорили, создание Web-приложений для Интернета предполагает одно-
одновременное использование многих технологий и языков программирования. Этот про-
процесс мы детально описали в своих книгах [2] и [3].
Здесь мы приведем только краткий список языков программирования, чаще всего
применяемых для создания Web-приложений. Знакомство с этими языками будет по-
полезно (а в некоторых случаях — необходимо) при разработке Web-приложений с при-
применением С#.
HTML
Строго говоря, язык разметки гипертекста (Hyper Text Markup Language, HTML)
не является языком программирования в обычном понимании этого термина. С ис-
использованием этого языка создаются документы HTML, располагаемые в каталогах
Web-сервера и предназначенные для просмотра при помощи программы, называемой
Web-браузером или просто браузером.
Введение 11
Язык HTML выступает в роли связывающего звена, объединяющего текст, иллю-
иллюстрации и активные компоненты, располагающиеся на страницах Web-сервера.
Хотя на первый взгляд язык HTML очень прост, его применение требует высокой
квалификации. Проблема в том, что предоставляемые этим языком средства создания
страниц очень бедны и для того, чтобы страница выглядела красиво, нужно затратить
немало усилий. Кроме того, существует проблема совместимости с браузерами раз-
различных типов, из-за которой одни и те же документы HTML могут выглядеть
у посетителей Web-узла по-разному. Подробнее о языке HTML и его использовании
вы сможете прочитать в [2].
Для достижения наилучшего результата разработчики Web-приложений долж-
должны в совершенстве владеть языком HTML. И хотя существуют многочисленные
системы визуального проектирования документов HTML (такие, например, как
Microsoft FrontPage), в сложных случаях вам не обойтись без прямого редактирова-
редактирования кода HTML.
В рамках платформы Microsoft .NET предлагается технология ускоренного визу-
визуального проектирования Web-приложений. При этом элементы пользовательского ин-
интерфейса Web-приложения создаются с помощью графического редактора (дизайнера
Web-форм). Активные компоненты для обработки событий от элементов управления
форм могут быть написаны с использованием С# или других языков платформы Mi-
Microsoft .NET (например, Visual Basic .NET).
Теоретически этот подход позволяет обойтись без знания HTML, однако на прак-
практике для создания действительно эффективных Web-приложений владение этим язы-
языком просто необходимо. Дело в том, что при использовании любой технологии созда-
создания Web-приложений в качестве интерфейса пользователя применяется браузер, «по-
«понимающий» только HTML. Владея HTML, вы сумеете оптимизировать создаваемые
страницы по времени загрузки, разобраться в проблемах при возникновении ошибок
и реализовать различные нестандартные решения.
И вообще, как мы уже говорили, создание Web-приложений представляет собой
комплексную проблему. Тут не обойтись знанием какого-то одного языка программи-
программирования, даже самого лучшего и современного.
JavaScript
Интерпретируемый объектно-ориентированный язык сценариев JavaScript не имеет
никакого отношения к языку Java, несмотря на свое название. Фактически это совер-
совершенно другой язык, разработанный компанией Netscape Communication Corporation.
Первоначальное название этого языка LiveScript было изменено по коммерческим со-
соображениям, так как в тс времена технология Java считалась очень перспективной.
Его основная область применения — создание активных Web-приложений.
Часто конструкции JavaScript (называемые сценариями JavaScript) встраивают
в исходный текст страниц HTML, после чего они «оживают». Становясь активными,
документы HTML со сценариями JavaScript становятся способными реагировать
на действия пользователя, выполняемые в окне браузера, например проверять данные,
введенные в форму. Сценарии, предназначенные для работы в составе документов
HTML под управлением браузера, называются клиентскими сценариями.
12 А В Фролов. Г. В. Фролов Язык С#. Самоучитель
Другое применение сценариям JavaScript — создание динамических документов
HTML на Web-сервере. В этом случае сценарии JavaScript называются серверными
сценариями. Серверные сценарии JavaScript используются в рамках технологии ак-
активных серверных страниц (Active Server Pages, ASP), разработанной компанией Mi-
Microsoft специально для создания активных Web-приложений.
Язык JavaScript можно использовать и для создания автономно работающих про-
программ. В этом случае программы интерпретируются специальной системой Microsoft
Windows Scripting Host (WSH). Автономные сценарии JavaScript удобно использовать
для выполнения каких-либо пакетных работ, например для анализа содержимого баз
данных, резервного копирования и т. п.
Язык JavaScript применяется так широко, что фактически каждый разработчик ак-
активных Web-приложений должен им владеть. Описание этого языка и многочислен-
многочисленные примеры его применения (в том числе в рамках технологии ASP) вы найдете в [2].
JScript
Язык JScript представляет собой аналог языка JavaScript, разработанный компаний Mi-
Microsoft. Он содержит расширения, которые могут оказаться несовместимыми с браузе-
браузерами, отличными от Microsoft Internet Explorer. Поэтому его применение обычно огра-
ограничивается созданием серверных сценариев для приложений ASP.
VB Script
Язык VB Script является функциональным аналогом только что описанного языка сце-
сценариев JavaScript, однако он был создан компанией Microsoft на базе языка Basic.
Как средство создания клиентских сценариев для документов HTML этот язык
имеет ограниченное применение. Причина этого в том, что сценарии VB Script рабо-
работают только с браузером Microsoft Internet Explorer. Другие браузеры, например широ-
широко распространенный Netscape Navigator, его игнорируют.
Поэтому основная область применения языка VB Script -•- создание серверных
сценариев для приложений ASP и автономных программ, работающих под управлени-
управлением Microsoft WSH.
С точки зрения возможностей VB Script не имеет никаких преимуществ перед
JavaScript. Поэтому, если вы уже владеете JavaScript, тратить время на изучение
VB Script ни к чему. С другой стороны, если у вас есть большой опыт написания про-
программ на языке Microsoft Visual Basic, то вы сможете создавать серверные сценарии
ASP без изучения JavaScript.
Perl
Это язык интерпретируемого типа, кому-то напоминающий Basic, а кому-то — язык С.
Вероятно, истина находится где-то посередине. Основное преимущество языка Perl
как средства разработки Web-приложсний в том, что для него созданы обширные биб-
библиотеки модулей. Эти библиотеки решают практически все задачи, встающие перед
разработчиками программ CGI — ядра активных Web-серверов.
Такие операции, как обработка текстовых строк и форм ввода данных, отправка
и получение электронной почты, взаимодействие с файлами и базами данных различных
Введение 13
типов, решаются с применением Perl легко и элегантно. Программы Perl занимают не-
небольшой объем и относительно быстро интерпретируются сервером Web, обеспечивая
приемлемую производительность при средней посещаемости.
Языку Perl посвящено множество книг. Что же касается примеров использования Perl
для создания реальных Web-приложений, то вы найдете их в написанной нами книге [3].
РНР
Технология серверных сценариев РНР сильно напоминает упомянутую выше техноло-
технологию Microsoft ASP. Аббревиатура РНР расшифровывается рекурсивно как РНР Hyper-
Hypertext Preprocessor, что означает «препроцессор гипертекста РНР».
Интерпретируемые серверные сценарии РНР способны динамически создавать до-
документы HTML, обращаясь при этом к базам данных, почтовым серверам и другим
программным системам и комплексам.
В настоящее время интерпретаторы РНР доступны для различных компьютерных
платформ, в том числе для Microsoft Windows, Linux и других Unix-подобных ОС. Что
же касается ASP, то эта технология работает только в среде Microsoft Windows. При-
Причина такого ограничения заключается в том, что ASP базируется на модели компо-
компонентного объекта (Component Object Model, COM) и технологии элементов управле-
управления ActiveX, доступных в полной мере только в среде Microsoft Windows. Подробно
об этом мы рассказали в [2].
Новые технологии Microsoft .NET
Как видите, к настоящему моменту разработано огромное множество языков и техно-
технологий программирования, несовместимых между собой или совместимых лишь час-
частично. В то время как разработку автономных приложений можно выполнять на одном
каком-то языке программирования, создание приложений для Интернета требует зна-
знаний множества языков и технологий.
Новые технологии Microsoft .NET, ориентированные на разработку автономных
и распределенных приложений Интернета, призваны облегчить создание сложных со-
современных приложений, их документирование и внедрение. В рамках Microsoft .NET
разработчикам программ предоставляется новый интерфейс программирования (Ap-
(Application Program Interface, API), пригодный для создания обычных настольных про-
программ Microsoft Windows, системных сервисов Microsoft Windows, а также Wcb-
приложений и Web-сервисов.
В рамках Microsoft .NET доступны следующие языки программирования:
• Microsoft C#.
• Microsoft Visual Basic .NET.
• Managed C++.
• Microsoft Visual J# .NET.
• JScript.NET.
Кроме того, в рамках Microsoft .NET предоставляется чрезвычайно удобная интег-
интегрированная среда разработки приложений Microsoft Visual Studio .NET, а также среда
выполнения программ Microsoft .NET Framework.
14 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
В составе Microsoft .NET имеется набор сетевых служб и серверов серии .NET En-
Enterprise Server, предназначенных для решения задач аутентификации, для создания
систем хранения данных, обработки электронной почты и создания бизнес-систем,
а также средства для программирования и встраиваемых вычислительных систем, на-
например для мобильных телефонов, игровых приставок и т. п.
Планируется выпуск ОС Microsoft Windows .NET, в полной мере реализующей пре-
преимущества технологии Microsoft .NET.
Таким образом, овладев языком С# и другими технологиями Microsoft .NET, вы смо-
сможете создавать программы и программные системы самого разного уровня, от простейших
утилит и сервисов до сложных распределенных корпоративных информационно-справоч-
информационно-справочных систем с базами данных, взаимодействующими через Интернет.
Платформа Microsoft .NET Framework
Платформа Microsoft .NET Framework, предназначенная для работы приложений Mi-
Microsoft .NET, дает большие преимущества разработчикам программ. В частности, она
способна преодолеть барьеры языковой несовместимости, допуская создание отдель-
отдельных компонентов создаваемой системы на различных языках программирования.
Среди других преимуществ Microsoft .NET Framework заслуживает упоминания
наличие обширной библиотеки классов, существенно облегчающей решение задач,
наиболее часто возникающих при создании автономных программ и Web-приложений.
Эта библиотека, насчитывающая десятки тысяч (!) классов, готовых к употреблению,
позволит вам использовать в своих разработках готовые и отлаженные модули.
Платформа Microsoft .NET Framework обеспечивает возможность использова-
использования модулей, разработанных вами ранее, а также возможность обращения к новым
компонентам из разработанного ранее программного кода. В результате после от-
относительно небольших переделок старые программы смогут приобрести новую
функциональность.
Приложения Microsoft .NET работают в среде Microsoft .NET Framework в рамках
системы исполнения программ Common Language Runtime (CLR). Примененная в Mi-
Microsoft .NET Framework концепция управляемого кода обеспечит надежное и безопас-
безопасное выполнение программ, а также значительно уменьшит вероятность допущения
ошибок в процессе программирования. Этому же способствует система обработки ис-
исключений и система автоматического освобождения неиспользуемой оперативной па-
памяти, называемой системой сборки мусора (garbage collection).
Встроенные в язык С# и рассчитанные на среду Microsoft .NET Framework
средства документирования, такие, как атрибуты и операторы комментариев спе-
специального вида, позволят существенно упростить создание конструкторской доку-
документации на программный код. Это особенно ценно при разработке больших про-
проектов, когда из-за сложности и объемности задачи сопровождение разработки пре-
превращается в непосильную задачу и становится настоящим кошмаром для менед-
менеджера проекта.
В сочетании с мощным средством ускоренной разработки приложений Microsoft
Visual Studio .NET набор языков платформы Microsoft .NET послужит отличным под-
подспорьем при создании программ самого разного типа, как автономных, так и рассчи-
рассчитанных на использование в Интернете.
Введение 15
Для ОС Microsoft Windows 9x/NT/2000/XP программу установки Microsoft .NET
Framework можно бесплатно загрузить с сервера Microsoft (размер дистрибутивного
файла около 20 Мбайт). Новые версии Microsoft Windows должны содержать в своем
составе готовую для использования среду .NET Framework.
Совмещение разных языков программирования
Разработчик приложений для Microsoft .NET Framework более не стоит перед мучи-
мучительным выбором языка программирования — на этой платформе доступны трансля-
трансляторы многих языков программирования. Это, например, Microsoft C#, Microsoft Visual
Basic .NET, Managed C++, JScript.NET, Visual Perl и др.
Подробности относительно языка Perl вы найдете на сайте компании ActiveState
по адресу http://www.activestate.com. Мы также знаем о существовании планов компа-
компании Borland (hUp".//www.borland.com) насчет поддержки платформы Microsoft .NET
Framework в новой версии своей системы разработки приложений Borland Delphi 7.
Хотя теперь проблема выбора языка программирования стоит не так остро, из-за
ограничений, присущих некоторым языкам программирования, наиболее полно воз-
возможности платформы Microsoft .NET Framework можно реализовать только с приме-
применением С#. Что же касается языка Microsoft Visual Basic .NET, то для достижения
максимальной совместимости с Microsoft .NET Framework он был значительно перера-
переработан. Не исключено, что вам будет легче освоить С#, нежели изучать все нововведе-
нововведения языка Microsoft Visual Basic .NET.
Для достижения совместимости между различными языками программирования
компиляторы языков платформы Microsoft .NET Framework переводят исходный текст
программы в промежуточный язык, называемый Microsoft Intermediate Language
(MSIL). Таким образом, на каком бы вы языке платформы Microsoft .NET Framework
не писали свою программу— на С#, Visual Basic .NET или каком-либо другом, эта
программа всегда будет транслироваться в MSIL.
Здесь у читателя может сразу возникнуть аналогия с байт-кодом Java. Мы должны
заметить, что возможности С#, в частности и платформы Microsoft .NET Framework,
в целом простираются намного дальше простой интерпретации байт-кода виртуальной
машиной Java.
Чтобы у всех разработчиков языков программирования была возможность созда-
создавать свои компиляторы совместимыми со средой выполнения Microsoft .NET Frame-
Framework, была создана спецификация Common Language Specification (CLS). Придержива-
Придерживаясь этой спецификации, разработчики языков программирования могут быть уверены
в том, что создаваемые с применением этих языков программы будут удовлетворять
минимальным требованиям платформы Microsoft .NET Framework. В частности, эти
программы смогут взаимодействовать с программами, разработанными с использова-
использованием других языков платформы Microsoft .NET.
Интегрирование с ранее созданными проектами
Разумеется, платформа Microsoft .NET Framework и язык С# вряд ли были бы встрече-
встречены программистами доброжелательно, если бы в них не была предусмотрена возмож-
возможность использования ранее разработанных программных модулей: библиотек динами-
16 А В Фролов, Г. В. Фролов Язык С# Самоучитель
ческой компоновки (Dynamic Load Libraries, DLL), объектов COM и ActiveX. К сча-
счастью, такие средства в С# имеются, и пользоваться ими достаточно просто.
Кроме того, предусмотрена возможность обращения к объектам С# из проектов,
разработанных ранее, например на языке C++. Для этого необходимо использовать
расширение Managed Extensions, создав классы-оболочки специального вида.
Библиотека классов Microsoft .NET Framework
Традиционно вместе с языками программирования, такими, как C++ и Pascal, постав-
поставляются библиотеки функций, предназначенные для решения рутинных задач, вроде
обработки текстовых строк, файловый ввод-вывод, сортировка, математические вы-
вычисления и пр.
Интегрированные системы создания программного обеспечения дополняются биб-
библиотеками классов типа Microsoft MFC и шаблонов.
Например, в состав языка C++ включена стандартная библиотека шаблонов (Stan-
(Standard Template Library, STL), упрощающая работу с текстовыми строками и контейне-
контейнерами данных, такими как массивы переменного размера, словари, стеки и т. д.
Понимая, что сложные программные системы невозможно создавать на пустом месте,
компания Microsoft включила в состав платформы богатейшую библиотеку классов.
Эта библиотека насчитывает, как мы уже говорили, десятки тысяч классов. Она со-
содержит классы для работы со строками и датами, с массивами и коллекциями различ-
различных типов, с потоками и файлами, с графическими изображениями самых разных фор-
форматов, с сетевыми протоколами и протоколами Интернета, сделанными в форма-
формате XML, классы для создания приложений Windows с графическим интерфейсом
и т. д. — всего не перечислить.
Наличие подобной библиотеки позволяет разработчику сосредоточить усилия
на решении своей прикладной задачи, а не тратить их, например, на организацию ас-
ассоциативных массивов, алгоритмов сортировки, передачу данных с помощью прото-
протокола TCP/IP или на решение других аналогичных задач.
Важно, что средства библиотеки классов Microsoft .NET Framework доступны
в программах, написанных на любых языках программирования, ориентированных
на платформу Microsoft .NET Framework. Теперь действительно не имеет особого зна-
значения, на каком языке вы будете писать программу, — ваши возможности в любом
случае будут примерно одинаковыми.
Заметим, что традиционные системы программирования, основанные на таких язы-
языках, как C++ и Pascal, обеспечивают очень плохую совместимость. Фактически каждая
из таких систем представляет собой свой изолированный мир с собственными библио-
библиотеками функций, классов и шаблонов, использовать которые вместе в рамках одного
проекта либо чрезвычайно сложно, либо вообще невозможно. Платформа Micro-
Microsoft .NET Framework позволяет покончить с этой проблемой раз и навсегда.
Хотя библиотека классов Microsoft .NET Framework очень объемна, все же может слу-
случиться так, что ее возможностей окажется недостаточно для решения какой-либо специ-
специфической задачи. Ничего страшного — в рамках платформы Microsoft .NET Framework
предусмотрены средства для обращения к низкоуровневым функциям ОС, к функциям
библиотек динамической компоновки DLL, методам и интерфейсам объектов СОМ
и ActiveX и т. п. Это делает платформу Microsoft .NET Framework пригодной для решения
задач, связанных, например, с системным программированием, автоматизацией производ-
производственных процессов или исследовательских физических установок.
Введение 17
Виртуальная машина CLR
Как мы уже говорили, исходный текст программы, написанной на языке программи-
программирования С# или другом языке платформы Microsoft .NET Framework, перед исполне-
исполнением транслируется в промежуточный язык MSIL.
С целью обеспечения безопасности исполнения код MSIL интерпретируется специ-
специальной виртуальной машиной в рамках системы исполнения программ Common Lan-
Language Runtime (CLR). Заметим, что теоретически интерпретатор MSIL может быть
создан не только для Microsoft Windows, но и для других ОС, например для Linux.
Об одном таком проекте с названием DotGNU мы расскажем чуть позже.
Что такое виртуальная машина и как она обеспечивает безопасность выполнения
программы MSIL?
Концепция виртуальной машины возникла очень давно, еще на заре компьютерной
техники. Эта концепция была реализована в ОС IBM VM, созданной для «больших»
вычислительных машин (мейнфреймов) серии IBM 360/370.
ОС IBM VM разделяла физические ресурсы одного дорогостоящего компьютера
между несколькими ОС. Каждая ОС получала в пользование виртуальные ресурсы
компьютера — виртуальный диск, виртуальный процессор, виртуальную оперативную
память и виртуальные устройства ввода-вывода. Получалось, что каждая ОС работала
на своем виртуальном компьютере (виртуальной машине).
Некоторые ресурсы (например, дисковое пространство и участки оперативной па-
памяти) выделялись виртуальным машинам в монопольное пользование, некоторые ис-
использовались по очереди. Например, ресурсы центрального процессора выделялись
на какое-то время сначала одной виртуальной машине, затем другой и т. д. При этом
создавалось впечатление, что все ОС работают одновременно.
Современные системы виртуальных машин, такие, как, например, VMWare, позво-
позволяют запускать на одном компьютере сразу несколько ОС — Microsoft Windows раз-
различных версий, MS-DOS, Linux и др. Лишь бы хватило мощности процессора, объема
дисковой и оперативной памяти.
Для нас сейчас принципиально важным является то, что программы, работающие
в рамках одной виртуальной машины, не имеют никакого доступа к ресурсам другой
виртуальной машины. Если такая программа выполнит недопустимую операцию
и «повесит» ОС своей виртуальной машины, это никак не отразится на работоспособ-
работоспособности других виртуальных машин.
Так вот, виртуальная машина CLR, обеспечивающая работу программ платформы
Microsoft .NET Framework, закрывает доступ этим программам к ресурсам других про-
процессов, работающих на том же компьютере. В результате программа никоим образом
не сможет нарушить работоспособность остальных программ или ОС.
Код MSIL, получающийся в результате трансляции программы, составленной
на языке С# или другом языке платформы Microsoft .NET Framework, выполняется под
полным контролем виртуальной машины CLR. Такой код, в отличие от обычного ис-
исполняемого кода, получающегося после трансляции программ С и Pascal, называется
управляемым кодом (managed code). Правила управляемого кода обеспечивают кор-
корректную работу программ, написанных на любом языке платформы Microsoft .NET
Framework.
18 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Домены приложений
Для обеспечения безопасности и надежности работы приложений Microsoft .NET
Framework в рамках виртуальной машины CLR реализованы так называемые домены
приложений (application domains).
Каждая программа исполняется в рамках своего домена и не имеет непосредствен-
непосредственного доступа к ресурсам остальных доменов.
В то же время с точки зрения ОС несколько доменов могут работать в рамках одно-
одного процесса, что дает повышение общей производительности работы виртуальной ма-
машины CLR. Для обеспечения одновременной работы нескольких приложений Micro-
Microsoft .NET Framework нет необходимости выполнять переключение процессов, отни-
отнимающее немало вычислительных ресурсов системы.
Компилятор JIT
Каким же образом происходит интерпретация кода MSIL, передаваемого для исполне-
исполнения только что упомянутой виртуальной машине CLR?
Центральный процессор компьютера может выполнять только машинные команды,
поэтому необходимо обеспечить преобразование кода MSIL в коды машинных ко-
команд. Для того чтобы обеспечить высокую скорость такого преобразования, виртуаль-
виртуальная машина CLR использует специальный компилятор, называемый компилятором
just-in-time (JIT).
Преобразование может выполняться однократно во время установки приложения
на диск компьютера либо каждый раз при запуске приложения. Первый способ, оче-
очевидно, обеспечивает более высокую скорость выполнения приложения по сравнению
со вторым, но более требователен к дисковой памяти. Впрочем, сегодня, когда в обыч-
обычный настольный компьютер можно установить один — два недорогих диска объемом
60-120 Гбайт, этот фактор не играет существенной роли.
Заметим, что похожая система исполнения байт-кода не является изобретением компа-
компании Microsoft и уже использовалась раньше, например виртуальной машиной Java.
Сборки
Еще одним преимуществом технологии, предоставляемой в рамках платформы Micro-
Microsoft .NET Framework, перед традиционными технологиями программного обеспечения
является наличие так называемых сборок. Сборка представляет собой один или не-
несколько файлов, содержащих все необходимое не только для работы приложения,
но и для ее самодокументирования.
Чтобы оценить преимущества данного подхода, достаточно вспомнить, как проис-
происходит процесс развертывания обычных программ.
Как правило, программы поставляются в виде загрузочного файла типа *.ехе, к ко-
которому может прилагаться набор файлов библиотек динамической компоновки DLL,
а также набор элементов управления СОМ и ActiveX.
Помимо этого для работы загрузочного модуля программы могут потребоваться
файлы библиотек DLL среды исполнения компилятора, а также библиотек DLL, реа-
реализующих функциональность библиотек классов, таких, как MFC.
Введение 19
Чтобы развернуть подобную программу на компьютере, необходимо скопировать
на диск этого компьютера все файлы, полученные в результате трансляции и редакти-
редактирования файлов исходных текстов программы. Кроме того, необходимо скопировать
в системный каталог ОС Microsoft Windows файлы библиотек DLL среды исполнения
компилятора и файлы библиотек DLL дополнительных классов, использованных
в проекте. Если в состав программного комплекса входят объекты СОМ и ActiveX, не-
необходимо их установить, зарегистрировав в системном реестре Microsoft Windows.
Копируя файлы стандартных библиотек DLL компилятора и библиотек классов
(например, библиотеки MFC), необходимо учитывать номера версий. При копирова-
копировании библиотеки новых версий должны заменять библиотеки старых версий,
но ни в коем случае не наоборот.
В результате развертывание сложного программного комплекса может превратить-
превратиться в весьма нетривиальную процедуру, для реализации которой потребуется создание
специальной инсталляционной программы. Существуют даже специальные системы
автоматизированного создания таких инсталляционных программ, например Install-
Shield. Освоение подобных систем может отнять немало времени.
Что же касается приложений, создаваемых для платформы Microsoft .NET Frame-
Framework, то благодаря использованию сборок их развертывание сводится к простому ко-
копированию файлов сборки на диск целевого компьютера.
Входящий в сборку перечень содержимого — файл манифеста (manifest) содержит
всю информацию, необходимую для правильной загрузки и работы приложения. Если
приложение сложное и состоит из нескольких сборок, то в манифесте перечисляются
все необходимые дополнительные сборки.
Упрощение отладки программ С#
Известно, что на отладку сложной программы можно потратить намного больше вре-
времени и сил, чем на ее разработку и написание. Объем отладочного кода, создаваемого
специально для тестирования модулей программы, может многократно превышать
объем кода самих отлаживаемых модулей.
Исполнимый код, получающийся в результате трансляции исходного текста про-
программы, написанной на таких языках программирования, как C++ и Pascal, имеет
практически полный доступ к ресурсам своего процесса. Такие ошибки, как чтение
неинициализированных переменных, неправильное преобразование типов указателей
или их неправильная инициализация, забывчивость при освобождении динамически
полученных блоков памяти, ошибки при выполнении числовых операций, могут при-
привести к аварийному завершению программы или к другим плачевным результатам.
Многие ошибки обычно остаются незамеченными на этапе компиляции и сказыва-
сказываются только при работе программы, причем, как это обычно бывает, в самый неподхо-
неподходящий момент.
Компилятор языка С# исключает возникновение перечисленных выше и многих
других ошибок еще на этапе компиляции, что существенно облегчает и ускоряет про-
процесс отладки сложных программ. Необходимые для этого средства встроены непо-
непосредственно в язык С#.
20 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Среди других средств, встроенных в язык С# и упрощающих отладку программ,
следует упомянуть объектно-ориентированную обработку исключений и систему
сборки мусора.
Применение исключений для обработки ошибок позволяет сократить объем исход-
исходного текста и дополнительно избавляет от необходимости при каждом вызове функ-
функции проверять ее код возврата. Проверка ошибок может производиться для целого
фрагмента программы, содержащего множество обращений к различным методам
и интерфейсам, что приводит к лучшей читаемости исходного текста. Это также спа-
спасет, если вы случайно забудете проверить код возврата какого-либо метода.
Заметим, что объектно-ориентированная обработка исключений не является досто-
достоинством одного только языка С#. Сходные возможности имеются, например, в языках
программирования C++ и Java.
Что же касается системы сборки мусора, то она позволяет автоматизировать осво-
освобождение ненужных более блоков оперативной памяти. Как только заказанный про-
программой блок памяти становится ненужным, он отмечается как подлежащий уничто-
уничтожению. Специальный фоновый процесс сборки мусора удалит такой блок при первой
же возможности.
Существует также возможность освобождения ресурсов явным образом на том или
ином этапе работы программы. Такая возможность может пригодиться при освобож-
освобождении таких критичных ресурсов, как, например, открытых файлов или соединений
с базой данных.
Аналогичная система сборки мусора с успехом используется и в других языках
программирования, например в языке Java.
Программирование на С#
для Microsoft Windows
Когда ОС Microsoft Windows только появилась на свет, мы, как и множество других про-
программистов, создавали для нее приложения на языках С и C++. При этом нам приходилось
напрямую обращаться к программному интерфейсу (API) этой ОС, обрабатывая сообще-
сообщения при помощи оператора swi tch.
Пока приложения состояли из нескольких окон и диалоговых панелей, такая техно-
технология программирования не вызывала особых проблем. Когда же перед нами встала
задача создания действительно сложных приложений Windows, имеющих очень боль-
большое количество окон и диалоговых панелей, а также нетривиальный пользовательский
интерфейс, пришлось примейить библиотеку классов MFC.
Несмотря на то что MFC сильно упрощает создание приложений Microsoft Win-
Windows, даже в комплекте с этой библиотекой инструментарий Microsoft Visual C++ едва
ли можно назвать настоящим визуальным средством проектирования приложений.
Во-первых, все еще приходится писать слишком много программного кода,
не имеющего непосредственного отношения к прикладной задаче, для решения кото-
которой создастся приложение.
Во-вторых, визуальные средства проектирования пользовательского интерфей-
интерфейса не позволяют выйти дизайну этого интерфейса за рамки стандартных «серых»
Введение 21
диалоговых панелей и надоевших уже элементов управления — кнопок, меню, спи-
списков и т. п. Сегодня, когда пользователь компьютера привык видеть на экране красоч-
красочно оформленные страницы Web-сайтов, едва ли стоит предлагать ему программы
с блеклым интерфейсом, разработанным еще на заре создания Microsoft Windows.
Намного более удобные средства проектирования диалоговых приложений предо-
предоставляет такое средство, как Microsoft Visual Basic. Однако разные версии этой среды
разработки приложений предлагали разные и порой несовместимые между собой ме-
механизмы компонентного программирования, что затрудняло миграцию разработанных
ранее проектов в новую среду разработки. Кроме того, скорость работы интерпрети-
интерпретируемых приложений, созданных с применением Microsoft Visual Basic, была не слиш-
слишком высока. В языке Microsoft Visual Basic не было полной реализации объектно-
ориентированного и, тем более, компонентно-ориентированного программирования.
Инструментальная среда Borland Delphi совмещала в себе преимущества визуаль-
визуальных средств проектирования и высокой скорости программ, загрузочный код которых
получался в результате компиляции исходных текстов, написанных на языке Pascal.
Через некоторое время появилось аналогичное решение и для языка C++ с названием
Borland C++ Builder.
В этих инструментальных средствах весьма скромные по своим возможностям, об-
обладающие незамысловатым дизайном стандартные диалоговые панели Microsoft Win-
Windows были заменены формами. Пользуясь визуальными средствами проектирования,
программист может разместить на такой форме различные элементы управления, про-
просто перетащив их из инструментальной панели. Так же легко он может задать цвет фо-
фона формы или графическое изображение подложки. Если форма должна содержать
статические или динамические строки текста, инструментарий разработчика позволяет
легко выбрать шрифт и цвет этого текста.
Но самое главное, появилась возможность разрабатывать собственные компонен-
компоненты, реализующие элементы пользовательского интерфейса, и добавлять их в палитру
инструментальной панели. С применением этих компонентов становится возможным
визуальное проектирование достаточно сложных интерактивных программ.
Визуальные средства проектирования приложений Microsoft Visual Studio .NET
унаследовали самое лучшее от предыдущих систем визуального проектирования, до-
добавив преимущества платформы Microsoft .NET Framework. Вероятно, своим успехом
система Microsoft Visual Studio .NET и язык программирования С# не в последнюю
очередь обязаны тому, что в их разработке принимали участие ведущие сотрудники
компании Microsoft Anders Hejlsberg и Scott Wiltamuth. Первый из них известен как
создатель популярной системы программирования Borland Turbo Pascal, а также упо-
упоминавшейся выше системы визуальной разработки приложений Borland Delphi.
Важно отметить, что приемы создания автономных приложений, реализованные
в Microsoft Visual Studio .NET, можно с успехом применить и для создания Web-
приложений Интернета. Более того, существует возможность переноса автономных при-
приложений Microsoft .NET Framework с оконным пользовательским интерфейсом вереду
Web-сервера. Таким образом, изучая язык С# и систему Microsoft Visual Studio .NET
для создания автономных приложений Microsoft Windows, Вы, возможно, сделаете свой
первый шаг на пути к освоению наиболее современных технологий Интернета.
22 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Проект DotGNU, или С# для Linux
Ну хорошо, скажете вы. Пусть технологии Microsoft .NET действительно облегчают
создание автономных приложений Microsoft Windows, а также приложений Интернета,
ориентированных на использование серверных версий ОС Microsoft Windows. Но дают
ли что-нибудь идеи, положенные в основу Microsoft .NET, создателям программ
для других ОС, в частности широко распространенной ОС Linux?
Оказывается, да. Прежде всего, следует упомянуть новейшие разработки ком-
компании Borland, с применением которых можно создавать современные Web-прило-
жения не только для Microsoft Windows, но и для Linux, а также для других Unix-
подобных ОС.
Далее, по адресу http://www.southern-stonn.com.au/portable_net.htm] находится Web-сайт
проекта DotGNU Portable.NET. В рамках этого проекта разрабатывается свободно доступ-
доступный инструментарий, с помощью которого можно создавать и выполнять приложения,
созданные по технологии Microsoft .NET в среде ОС Linux.
В настоящий момент на сайте проекта DotGNU Portable.NET вы можете бесплатно
загрузить транслятор языка С#, библиотеку классов PNETLIB, среду выполнения про-
программ, дизассемблер промежуточного языка и другие утилиты.
Сам по себе проект DotGNU Portable .NET является составной частью общего проекта
DotGNU. Сайт этого проекта вы найдете по адресу http://www.dotgnu.org. Участники про-
проекта DotGNU поставили перед собой цель создать полную замену проекту Microsoft .NET,
обеспечив при этом свободный и бесплатный доступ всем желающим к любым исходным
текстам, компонентам и технологиям проекта. Заметим, что Microsoft не поставляет ис-
исходные тексты среды исполнения и библиотеки классов Microsoft .NET Framework.
В новой среде Portable .NET планируется обеспечить возможность выполнения
байт-кода программ Java, реализовать безопасную среду исполнения приложений,
как автономных, так и распределенных.
Пока проект DotGNU находится в начальной стадии своего развития, поэтому рано
еще делать заключения о результатах его реализации. Тем не менее сам факт его воз-
возникновения подчеркивает особое значение языка программирования С# и платформы
Microsoft .NET, для которой он был разработан.
Отличия С# от C++
Если вы ранее программировали на языке C++, вам будет интересно, чем отличается
С# от этого языка. Мы будем делать замечания по этому поводу на протяжении всей
книги по мере изложения материала, но здесь вы найдете краткий перечень наиболее
важных отличий.
Классы и наследование
Классы С#, подобно классам Java, могут наследоваться только от одного базового
класса. Таким образом, в С# нет множественного наследования, допускаемого в C++.
В языке С# объекты любого типа (даже bool и int) являются объектами соответ-
соответствующих классов.
Введение 23
Изменены правила определения методов производного класса, замещающих (пере-
(перегружающих) методы базового класса. Теперь для них необходимо явным образом ука-
указывать ключевые слова, такие, как new или overridden.
Изменен способ вызова из производного класса перегруженного метода базового
класса.
Если в классе не определен конструктор, то создается конструктор по умолчанию.
Он инициализирует все поля класса значениями по умолчанию, которые зависят
от типов инициализируемых полей. В программах, написанных на C++, конструктор
по умолчанию никогда не создается.
Добавлен альтернативный способ инициализации списком в конструкторе базового
класса.
Отсутствуют деструкторы классов. Для освобождения ненужных блоков памяти
применяется система сборки мусора (garbage collection).
Параметры методов класса не могут иметь значения по умолчанию. Для достиже-
достижения аналогичной функциональности в С# нужно применять перегрузку методов.
В программах, написанных на С#, невозможно определить глобальные переменные
или методы. Определения переменных и методов должны находиться внутри классов
или структур.
Что же касается локальных переменных, определенных в теле методов, то перед
использованием они должны быть проинициализированы. Ошибочное обращение
к неинициализированным переменным обнаруживается еще на этапе компиляции про-
программы, что упрощает процесс отладки.
Интерфейсы
Схожая с множественным наследованием функциональность достигается в С#, так же
как и в Java, с помощью реализации множественных интерфейсов.
Типы данных
Все данные делятся на ссылочные и размерные. Ссылочные данные хранятся в общем
пуле памяти, а размерные — в стеке метода. Для ускорения работы с размерными ти-
типами при представлении их в виде классов применяется механизм упаковки, а для об-
обратного представления — распаковки. Операции упаковки и распаковки полностью
прозрачны для программиста.
Данные типа bool могут принимать только два значения: true и false.
При этом не допускается преобразование этого типа данных в другие, такие, как int.
Разрядность, т. е. количество битов, отведенных для хранения данных определен-
определенного типа, зависит только от этого типа, но не от разрядности ОС. Таким образом,
данные типа int всегда будут занимать в памяти 32 разряда независимо от того, уста-
установлен в компьютере 32-разрядный процессор или 64-разрядный. В языке C++ для
хранения данных типа long отводится 32 бита (т. е. 4 байта), а в языке С# — 64 бита.
Имеются важные отличия в представлении текстовых строк. Язык C++ допускает
хранение строк в виде массивов символов ANSI или UNICODE, причем последним
элементом этого массива должен быть двоичный нуль. Соответственно в библиотеке
компилятора имеется набор функций для работы с этими строками.
24 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Что же касается С#, то в нем строки являются объектами класса (как, впрочем,
и данные всех других типов). В качестве внутреннего представления строк и отдель-
отдельных символов строки применяется только кодировка UNICODE.
В языке С# не используются битовые поля.
Указатели
Использование указателей ограничено специально отмеченными областями небезо-
небезопасного кода (unsafe code). Таким образом, в обычных программах исключаются
ошибки, связанные с неправильным применением указателей. Эти ошибки часто до-
допускаются в программах, составленных на языках С и C++.
Указатели на функции, применяемые в C++, потенциально опасны тем, что прог-
программист по ошибке может их неправильно проинициализировать. В языке С# вместо них
применяется аналогичный по функциональности, но менее чувствительный к ошибкам
программистов и объектно-ориентированный механизм специальных методов— делега-
делегатов (delegates). Делегаты чаще всего применяются для обработки событий (events).
Массивы
В языке С# для обозначения массивов применен другой синтаксис, чем в языке C++.
При объявлении массива квадратные скобки должны располагаться сразу после обо-
обозначения типа данных, а не после имени массива, как это было в C++.
Структуры
В языке С# структуры являются размерными типами данных, а классы — ссылочны-
ссылочными. В языке C++ такого разделения между структурами и классами нет.
Операторы и ключевые слова
Применяя оператор switch в языке программирования C++, вы могли опускать
в блоках case оператор break. Если в блоке case отсутствовал оператор break,
то после обработки этого блока программа автоматически переходила к обработке сле-
следующего блока, case или default.
Потенциально такое поведение программы могло приводить к ошибкам, если про-
программист по забывчивости не завершал блок case оператором break. Чтобы изба-
избавиться от ошибок подобного рода, в языке С# описанная выше ситуация исключается
на стадии компилирования.
Таким образом, автоматический переход к обработке следующего блока case
в языке С# запрещен. Если же вам все же необходимо организовать подобный пере-
переход, то его можно сделать явным образом при помощи оператора goto.
В операторе обработки исключений try-catch добавлен блок finally, выпол-
выполняющийся вне зависимости от того, произошло исключение или нет.
По сравнению с языком C++ в язык С# добавлены новые операторы и ключевые
слова is, as, typeof, ref, out, f oreach и др. По-другому, чем в C++, трактуются
операторы static и extern.
Для использования понятия пространства имен в язык С# добавлено ключевое сло-
слово namespace.
Введение 25
Директивы препроцессора
В языке С# не используются заголовочные include-файлы, знакомые программистам
Си C++. Соответственно не применяется и директива ttinclude, предназначенная
для включения содержимого таких файлов в исходный текст программы.
Благодаря отказу от использования заголовочных файлов описание и определение
класса, а также его полей и методов размещается в одном месте — в файле исходного
текста программы. Безусловно, это упрощает работу с файлами проектов, так как те-
теперь нет необходимости при редактировании исходного текста класса переключаться
от файла исходного текста к заголовочному файлу и обратно.
Для ссылок на типы в заданных пространствах имен без указания полного имени
применяется команда using.
Хотя в языке С# разрешено использование директив препроцессора #define,
#ifdef и аналогичных, с их помощью можно только определять макрокоманды,
но не задавать для них значения. Таким образом, область применения директив пре-
препроцессора сужается до условной компиляции.
Причина запрета использования директив препроцессора для создания макро-
макрокоманд, заменяющих константы или фрагменты кода, положительно сказывается
на читабельности исходных текстов программ и снижает вероятность допущения
ошибок.
Чем транслировать программы С#
Чтобы транслировать исходные тексты программ, приведенные в книге, можно ис-
использовать либо визуальную среду проектирования программ Microsoft Visual Studio
.NET, либо пакетный транслятор, входящий в комплект Microsoft .NET Framework
SDK. В то время как Microsoft Visual Studio .NET нужно покупать, инструментарий
Microsoft .NET Framework SDK доступен для бесплатной загрузки из Интернета.
Приобретая Microsoft Visual Studio .NET, учтите, что это средство разработки по-
поставляется в разных редакциях: Professional, Enterprise Developer, Enterprise Architect,
Academic и Visual C# .NET Standard. Для работы с нашей книгой подойдет любая вер-
версия, даже самая недорогая. В частности, годится академическая версия Microsoft Vis-
Visual Studio .NET Academic, в которой есть дополнительные примеры программ, а также
другие утилиты и средства, помогающие студентам в процессе изучения возможно-
возможностей платформы Microsoft.NET.
Недорогая редакция Visual C# .NET Standard вполне пригодна для работы с про-
программами, приведенными в нашей книге, однако в ней имеются серьезные ограниче-
ограничения, например с ее помощью нельзя создавать библиотеки компонентов. Такие биб-
библиотеки заметно упрощают реализацию сложных программных проектов.
Версии Microsoft Visual Studio .NET в редакциях Enterprise Developer и Enterprise
Architect предназначены для разработки корпоративных приложений с базами данных
и различными Интернет-службами.
Что же касается бесплатного набора инструментов Microsoft .NET Framework SDK,
то его можно загрузить с Web-узла компании Microsoft по адресу http://msdn.microsoft.com.
26 А В Фролов, Г. В. Фролов. Язык С# Самоучитель
Для этого откройте раздел Downloads и в разделе Software Development Kits щелкните
строку Microsoft .NET Framework SDK. Вы окажетесь на странице загрузки, откуда
можно скачать нужный SDK либо в виде одного файла размером 131 Мбайт, либо частя-
частями размером примерно по 13 Мбайт.
Ввиду значительного объема информации для загрузки следует использовать ско-
скоростные линии передачи данных. Через обычный модем загрузка будет выполняться
слишком долго. В качестве альтернативы компания Microsoft предлагает приобрести
по очень небольшой цене компакт-диск с Microsoft .NET Framework SDK.
Вне зависимости от того, будете вы транслировать свои программы при помощи
Microsoft Visual Studio .NET или при помощи Microsoft .NET Framework SDK, необхо-
необходимо загрузить и установить пакет обновлений .NET Framework Service Pack. Когда
мы работали над книгой, была доступна только первая версия пакета исправлений.
Объем пакета исправлений невелик (он может поместиться на одну дискету), а бес-
бесплатно получить его можно, например, на упомянутой выше странице загрузки Micro-
Microsoft .NET Framework SDK.
Условные обозначения в книге
В нашей книге вы найдете много фрагментов исходных текстов программ. Для выде-
выделения программ и их отдельных фрагментов мы использовали специальный шрифт,
например: оператор switch, тип данных double.
Элементы пользовательского интерфейса и ссылки, расположенные на Web-стра-
Web-страницах, выделяются полужирным шрифтом, например меню File, ссылка Download.
Курсивом выделяются новые понятия и термины.
Угловыми скобками (< >) мы выделяем символы или строки, вместо которых
в программе должен быть подставлен конкретный идентификатор. Например, вместо
строк <Выражение> и <Оператор> в условном операторе, задающих выражение
и оператор в общем виде, необходимо подставить конкретное выражение и конкрет-
конкретный оператор:
if (<Выражение>)
<Оператор>
При помощи квадратных скобок ([]) выделяются необязательные фрагменты опе-
операторов, выражений или других элементов программ. Например, в операторе if кон-
конструкция else может отсутствовать:
if(<Выражение>)
«Эператор 1>
[else
«Эператор 2>]
Выражения вида ]10, 100] означают интервал значений. Если квадратная скобка
направлена внутрь, то расположенное рядом с ней значение принадлежит данному ин-
интервалу, а если нет — то не принадлежит. Приведенное выше выражение описывает
значения, большие 10, но меньшие или равные 100.
Введение 27
Многоточие (...) используется в листингах программ для обозначения пропущен-
пропущенных строк повторяющихся синтаксических конструкций:
namespace Hello
Благодарности
Мы благодарим Сергея Ноженко, который натолкнул нас на идею использования язы-
языка С# в системе удаленного восстановления данных через Интернет для службы Da-
taRecovery.Ru, а также на идею создания этой книги.
Мы также благодарим всех сотрудников издательства «Диалог-МИФИ», благодаря
труду которых стало возможно появление этой книги.
Как связаться с авторами книги
Свои замечания по этой книге и предложения присылайте авторам по адресу
alexandre@frolov.pp.ru. Информацию о других наших книгах и проектах можно найти
по адресам: http://www.frolov.pp.ru, http://www.datarecovery.ru, http://www.zerohops.ru.
28 А. В Фролов, Г. В. Фролов Язык С# Самоучитель
Глава 1. Базовые понятия
и определения
Как мы отметили во Введении, язык С# унаследовал многое от других языков про-
программирования, таких, как С, C++ и Java. Поэтому, если вы уже владеете одним
из этих языков, освоить С# будет намного легче.
Наша книга предназначена не только для опытных, но и для начинающих програм-
программистов, поэтому для работы с ней вам не потребуется предварительного изучения ка-
каких-либо еще языков программирования.
В гл. 1 мы рассмотрим основы — базовые понятия и определения, элементарные
типы данных, основные операторы и выражения языка С#. Мы приведем несколько
примеров простейших программ, демонстрирующих приемы работы с элементарными
типами данных и операторами С#.
Многие из важнейших понятий языка С# будут только упомянуты в этой главе,
а их детальное изучение мы отложим до следующих глав нашей книги.
Первая программа на языке С#
Когда мы только обдумывали содержание этой книги, то перед нами встала проблема,
с чего начать изложение материала — с теории или практики.
Теоретический материал нужно иллюстрировать примерами программ. В то же
время для написания даже простейшей программы нужны элементарные теоретиче-
теоретические знания.
Практически все учебники по языкам программирования начинаются с изучения
исходного текста программы, отображающей на консоли текстовую строку «Hello,
world!». Эта традиция пошла еще с самых первых учебников по языку программи-
программирования С.
Мы считаем, что при изучении С# такой подход вполне оправдан, так как для луч-
лучшего понимания базовых понятий необходимо приводить исходные тексты реальных
работающих программ. Даже в самой простейшей программе С# используется слиш-
слишком много особенностей языка, детальное изучение которых при первом знакомстве
нецелесообразно. Однако мы не будем отступать от испытанной временем практики
и постараемся сделать все необходимые пояснения.
Исходный текст простейшей программы
Наша первая программа С# сразу после запуска выводит на консоль строку «Hello,
С# world!», а затем ждет, когда вы нажмете клавишу Enter на клавиатуре компью-
компьютера. Исходный текст программы представлен в листинге 1.1.
~ШШ 29
Листинг 1.1. Файл ch01\Hello\HelloApp.cs
using System;
namespace Hello
class HelloApp
static void Main()
System.Console.WriteLine("Hello, C# world!");
System.Console.ReadLine();
Как видите, исходный текст программы состоит всего из нескольких строк, причем
некоторые из них используются только для описаний программы и ее объектов, а не-
некоторые непосредственно исполняются и приводят к появлению видимых результатов.
На данном этапе вам нужно представлять себе только общую структуру этой програм-
программы, не вникая в подробности. Мы пока отложим детальный разговор о многих поняти-
понятиях, упомянутых здесь.
Пространство имен System
Первая строка нашей программы содержит ключевое слово using и предписывает
компилятору просматривать в процессе своей работы так называемое пространство
имен System:
using System;
В состав среды выполнения программ Microsoft Framework .NET входит обширная
библиотека, насчитывающая десятки тысяч классов. Сильно упрощая, скажем, что
классы представляют собой описания некоторых данных и методов работы с этими
данными. Пользуясь классами как кирпичиками (или как прототипами), можно созда-
создавать весьма и весьма сложные программы, не затрачивая на это колоссальных усилий.
Чтобы компилятор мог ориентироваться в названиях классов, а также определен-
определенных в рамках этих классов символических именах, в языке С# используются простран-
пространства имен.
Указывая при помощи ключевого слова using пространство имен System,
мы открываем компилятору доступ к классам, необходимым, в частности, для ввода
текстовых строк с клавиатуры и вывода их на консоль. В своих примерах программ мы
постоянно будем использовать пространство имен System и другие пространства
имен.
Определение собственного пространства имен
Любая, даже простейшая программа С# создает свои классы. Она также может опре-
определять для этих классов собственные пространства имен. Такое определение делается
при помощи ключевого слова namespace:
30 А В. Фролов, Г. В Фролов. Язык С# Самоучитель
namespace Hello
После ключевого слова namespace указывается параметр— имя определяемого
пространства имен. В данном случае наше пространство имен будет называться
Hello. С помощью фигурных скобок мы ограничиваем строки программы, имеющие
отношение к определяемому пространству имен.
Класс HelloApp
Как вы скоро узнаете, все данные в языке С# представляются в виде объектов некото-
некоторых классов. Наша программа тоже создает класс HelloApp, в котором определен
единственный метод Main:
class HelloApp
{
static void Main()
{
System.Console.WriteLine("Hello, C# world!");
System.Console.ReadLine();
Название класса задается параметром оператора class, а содержимое класса рас-
располагается внутри фигурных скобок:
class HelloApp
Для своей первой программы мы выбрали произвольное название содержащего
ее класса— HelloApp. Это сокращение от Hello, Application. В названии класса
мы отразили назначение класса и всего приложения — отображение приветствен-
приветственного сообщения. Но, строго говоря, здесь мы могли бы использовать любое допус-
допустимое название.
Метод Main
Как мы уже говорили, классы представляют собой некоторые данные и методы для
работы с ними. В нашем приложении определен класс HelloApp, а в этом классе —
метод Main:
static void Main()
{
System.Console.WriteLine("Hello, C# world!");
System.Console.ReadLine() ;
Глава 1 Базовые понятия и определения 31
Отвлечемся пока от ключевых слов static и void, а также от круглых скобок,
расположенных после имени метода Main.
Чтобы система Microsoft .NET Framework могла запустить приложение, в одном
из классов приложения необходимо определить метод с именем Main. Этот метод
нужно сделать статическим, снабдив ключевым словом static, иначе ничего
не получится.
Запомните пока просто как аксиому, что в программе обязательно должен быть
статический метод Main, определенный подобным образом. Именно этот метод полу-
получает управление при запуске приложения. Позже мы проясним ситуацию с ключевыми
словами static и void.
Тело метода Main ограничено фигурными скобками, внутри которых находятся
два оператора:
System.Console.WriteLine("Hello, C# world!");
System.Console.ReadLineО;
Первый из них выводит строку «Hello, С# world!» на консоль, а второй ожи-
ожидает, пока кто-нибудь не введет с клавиатуры произвольную строку и не нажмет кла-
клавишу Enter.
В первой строке нашего метода мы обращаемся к методу WriteLine, предназна-
предназначенному для вывода данных на консоль. Этот метод определен в классе Console, ко-
который принадлежит упоминавшемуся ранее пространству имен System.
В круглых скобках методу WriteLine передаются параметры, определяющие, что
собственно нужно выводить на консоль. В данном случае мы выводим текстовую
строку «Hello, C# world!», ограничив ее двойными кавычками.
Метод ReadLine тоже определен в классе Console из пространства имен
System. Он предназначен для получения текстовой строки, введенной с консоли.
Мы не передаем методу ReadLine никаких параметров.
Единственное назначение метода ReadLine в нашей программе — приостановить
ее работу после вывода на консоль строки сообщения «Hello, C# world!». Если
этого не сделать, то при запуске программы в среде ОС Microsoft Windows консольное
окно с сообщением появится на очень короткое время, а затем будет автоматически
уничтожено. В результате вы не успеете ничего рассмотреть. Поэтому многие приме-
примеры консольных программ, приведенные в нашей книге, будут завершаться вызовом
метода ReadLine.
Трансляция программы при помощи
.NET Framework SDK
Если вы решили транслировать примеры программ, приведенных в нашей книге,
при помощи Microsoft .NET Framework SDK, то этот раздел для вас. Процедура созда-
создания простейших консольных приложений и их трансляции с применением Microsoft
Visual Studio .NET описана в следующем разделе.
Прежде всего вам нужно создать файл исходного текста программы, воспользо-
воспользовавшись для этого любым текстовым редактором, например программой Notepad или
32 А. В Фролов, Г. В. Фролов. Язык С#. Самоучитель
встроенным редактором файлового менеджера FAR. Вы должны получить простой
текстовый файл без элементов стилевого и шрифтового оформления. Поэтому для соз-
создания и редактирования исходных текстов программ не стоит использовать текстовые
процессоры вроде Microsoft Word.
Трансляция исходного текста программы запускается командой следующего вида:
D:\Microsoft.NET\Frainework\csc.ехе HelloApp.cs
Эту команду нужно ввести в консольном приглашении или в приглашении про-
программы FAR.
В приведенной выше строке команды вам, вероятно, придется изменить путь
к файлу программы csc.exe транслятора С#, указав тот каталог, куда вы установили
утилиты Microsoft .NET Framework SDK. Чтобы найти каталог с файлом csc.exe,
можно использовать, например, стандартные средства поиска файлов ОС Microsoft
Windows.
Прежде чем запускать команду трансляции, сделайте текущим каталог, в котором
расположен файл исходного текста программы.
После завершения работы транслятора в текущем каталоге будет создан исполни-
исполнимый файл HelloApp.exe. Запустив его, вы увидите на консоли сообщение
Hello, C# world!
Посмотрев на сообщение, нажмите клавишу Enter. Программа завершит свою рабо-
работу, после чего консольное окно исчезнет с экрана.
Если сообщение «Hello, C# world!» появилось, все в порядке. В том случае,
когда вместо него вы получили сообщение о невозможности запуска программы
csc.exe, убедитесь, что вы правильно установили Microsoft .NET Framework SDK
и правильно указали в команде запуска транслятора путь к файлу csc.exe.
При появлении других ошибок проверьте, правильно ли был набран исходный
текст программы.
Использование Microsoft Visual Studio .NET
Интегрированная система разработки Microsoft Visual Studio .NET значительно облег-
облегчает создание программ С#. В ее составе имеются мощные отладочные средства, спра-
справочная библиотека MSDN Library, содержащая невероятно огромное количество ин-
информации (на английском языке), визуальные средства проектирования приложений
Microsoft Windows и многое другое.
Подробный рассказ об использовании Microsoft Visual Studio .NET может соста-
составить предмет отдельной книги. В нашей книге мы опишем только некоторые приемы
использования этого замечательного инструмента.
Создание нового проекта
Чтобы создать простейшую консольную программу с использованием Microsoft Visual
Studio .NET, запустите эту среду разработки приложений и выберите из меню File
строку New. Затем из меню второго уровня выберите строку Project.
Глава 1. Базовые понятия и определения 33
2 Язык С# Самоучитель
В результате на экране появится диалоговое окно New Project, показанное на рис. 1.1.
Project Туры:
Templetes:
[И Щ
:_J Visual Basic Projects
>'_3 Visual C# Projects
LJ Visual C++ Prelects
. £j Se'up and Deployment Projects
* LJ Other Projects
Cl Visual Studio Solutions
^d
Console Windows Empty Project
Appketlon Service
Empty Web New Protect In
Protect Existing Folder
IA project for creating a command-he applcerjon
Name:
Location:
C# Book]\src\ch01
Project wl to cruttd at H:\£B«gjnnar C* Book]\5rc\ch01\He(lo.
Browse.,
Puc. 1.1. Создание нового проекта
В списке Project Types укажите тип проекта, щелкнув мышью строку Visual C#
Projects. Далее в списке шаблонов Templates выберите шаблон проекта консольного
приложения Console Application, щелкнув левой клавишей мыши соответствующий
значок в правой части окна New Project.
В поле Name введите имя проекта, а в поле Location — путь к каталогу проектов,
в котором будет создан каталог создаваемого проекта. Для выбора пути к каталогу
проектов вы можете воспользоваться кнопкой Browse.
Проделав все описанные выше действия, щелкните кнопку ОК. В результате будет
запущен мастер проектов, который автоматически создаст и загрузит в окно редакти-
редактирования исходный текст программы:
using System;
namespace Hello
<summary>
/// Summary description for Classl.
/// </summary>
class Classl
<summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
С
// TODO: Add code to start application here
34
А. В. Фролов. Г. В. Фролов. Язык С#. Самоучитель
Сравнив этот текст с текстом, приведенным в листинге 1.1, вы сможете обнаружить
некоторые отличия.
Во-первых, мастер проектов вставил в текст программы строки комментариев, на-
начинающиеся с двух и трех слешей (/). Комментарии вида / / / предназначены для ав-
автоматического создания документации для программного проекта. Во время своей ра-
работы транслятор С# игнорирует строки комментария, и вы пока можете поступать та-
таким же образом. Позже мы расскажем о комментариях подробнее.
Во-вторых, мастер проектов выбрал для класса имя classl, в то время как мы на-
назвали этот класс HelloApp. Название данного класса не имеет никакого значения для
работоспособности программы, однако лучше использовать семантически значимые
имена. Поэтому мы скоро изменим это имя.
В-третьих, перед объявлением метода Main появилась конструкция [STAThread].
Это так называемый атрибут. Атрибуты С# определяют различные параметры объектов,
перед которыми они находятся.
В данном случае атрибут [STAThread] означает, что по умолчанию будет использо-
использована так называемая однозадачная модель разделенных потоков single-threaded apartment
(STA). Разговор о моделях потоков мы отложим до гл. 10, посвященной многопоточным
программам и компонентному программированию. Почти во всех примерах программ,
приведенных в нашей книге, вы можете не указывать данный атрибут.
И наконец, в-четвертых, в программе, созданной мастером проектов, метод Main
имеет параметр args:
static void Main(string[] args)
Через этот параметр передаются аргументы запуска программы, указываемые в ко-
командной строке.
В своих примерах программ мы обычно не используем параметр метода Main,
но позже вы узнаете, что с ним можно делать. Пока же его существование можно про-
просто игнорировать.
Проекты и решения
При создании программ и комплексов программ в интегрированной системе разработки
Microsoft Visual Studio .NET используются понятия решение (solution) и проект (project).
Проект представляет собой набор файлов исходных текстов, файлов графических
изображений и других файлов, необходимых для создания программы. Например,
для нашего проекта создается файл исходного текста HelloApp.cs и ряд других файлов,
например файл значка (пиктограммы).
Обычно сложные программные комплексы содержат в себе много программ и компо-
компонентов, причем, возможно, созданных с использованием разных языков программирова-
программирования и даже рассчитанных для использования на различных компьютерных платформах.
Для облегчения разработки Microsoft Visual Studio .NET позволяет представить все
создаваемые компоненты такого комплекса в виде набора проектов, объединенных
общим решением.
В нашем случае при создании проекта было также автоматически создано решение
Hello. Информация об этом решении была сохранена в файле с именем Hello.sln. По-
После того как вы закончите работу с Microsoft Visual Studio .NET, для того, чтобы снова
Глава 1. Базовые понятия и определения 35
вернуться к работе над проектом, достаточно просто щелкнуть дважды пиктограмму
этого файла в папке проекта.
Компоненты, образующие решение, можно посмотреть в виде иерархического де-
дерева на вкладке Solution Explorer (рис. 1.2).
!j3 Solution "Hello1 A project)
- ^ Hello
-: -^ References
*_J System
; <_i System .Data
• _ISystem.XML
'0 App.ico
jfj Assemblylnfo.es
^Solution... |ggctoVie... ! g] Inder. »jSea:h риС. /2. Вкладка Solution Explorer
Окно вкладки Solution Explorer находится в правом верхнем углу главного окна
Microsoft Visual Studio .NET.
Как видно на рис. 1.2, в решение Hello входит один проект, который тоже называ-
называется Hello. Проект Hello, в свою очередь, содержит определение ссылок Reference, ко-
которые нам пока не интересны, файл значка App.ico, файл сборки (assembly) с именем
Assemblylnfo.cs, а также файл с исходным текстом нашей программы Classl.cs.
Изменение проекта
Теперь, чтобы заставить программу делать то, что нам нужно, внесем некоторые изме-
изменения в исходный текст, созданный мастером проекта.
Прежде всего, щелкните мышью имя файла Classl.cs на вкладке Solution Explorer
(рис. 1.2). После этого в нижнем правом углу главного окна Microsoft Visual Studio
.NET откроется окно с вкладкой Properties, предназначенной для редактирования
свойств файла (рис. 1.3).
J HefloApp.es File Properties
РЖ
,Q iviv.mo-d
Build Action Compile
■ Custom Tool
; Custom Tool Namespace
I HelloApplcs
File Name
: Name of the file or folder.
Рис. 1.З. Вкладка Properties
36 А В Фролов, Г В. Фролов Язык С#. Самоучитель
Щелкните левой клавишей мыши название файла Classl.cs в поле File Name
и подождите некоторое время, не двигая курсор мыши. Вы сможете отредактировать
имя файла. Замените его именем HelloApp.cs (рис. 1.3).
Далее внесите изменения в исходный текст программы. Замените название класса
Classl на HelloApp и добавьте в исходный текст метода Main две строки, одна
из которых предназначена для вывода на консоль сообщения «Hello, C# world!»,
а вторая ожидает нажатия на клавишу Enter:
System.Console.WriteLine("Hello, C# world!");
System.Console.ReadLine();
Ниже мы привели новый вариант исходного текста, который должен получиться
у вас после выполнения всех перечисленных действий:
using System;
namespace Hello
{
/// <summary>
/// Summary description for HelloApp.
/// </summary>
class HelloApp
{
/// <summary>
/// The main entry point for the application.
/// </suramary>
[STAThread]
static void Main(string[] args)
// TODO: Add code to start application here
//
System.Console.WriteLine("Hello, C# world!");
System.Console.ReadLine();
Строки комментариев можно удалить, точно так же как и строку с атрибутом
[STAThread].
Теперь мы готовы оттранслировать программу и запустить ее на выполнение. Что-
Чтобы это сделать, просто нажмите клавишу F5.
Если все было сделано правильно, через некоторое время на экране компьютера
появится консольное окно с сообщением «Hello, C# world!». Нажмите в этом
окне клавишу Enter — и окно исчезнет.
В том случае, если вы ошиблись при наборе кода, в нижней части главного окна
Microsoft Visual Studio .NET появится сообщение об ошибке. Кроме того, ошибочные
строки выделяются в окне редактирования волнистой линией синего цвета. Если под-
подвести курсор мыши к такой линии, около курсора появится текст соответствующего
сообщения об ошибке.
Глава 1 Базовые понятия и определения 37
Элементарные типы данных
Известно, что компьютеры обрабатывают данные. Устройство компьютера и принцип
его работы очень хорошо описаны в книге Чарльза Пстцольда [4]. Мы настоятельно
рекомендуем ознакомиться с этой книгой всех новичков в области программирования.
Что же касается данных и обрабатывающих эти данные команд, то ими мы займемся
в оставшейся части этой главы.
За обработку данных отвечает центральный процессор компьютера. Внутри про-
процессора имеется управляющее устройство, арифметико-логическое устройство, набор
регистров для временного хранения данных, схемы адресации внешнего устройства
оперативной памяти и устройств ввода-вывода, а также другие схемы и устройства.
В оперативную память тем или иным способом записывается программа — набор
инструкций (машинных команд), предписывающих центральному процессору выпол-
выполнить заданные действия в определенной последовательности. Процессор извлекает
машинные команды из оперативной памяти и выполняет их.
Что могут делать машинные команды?
Эти команды могут писать данные в ячейки оперативной памяти компьютера или
во внутренние регистры процессора, а также читать их оттуда, читать регистры ввода
и писать в регистры вывода, обмениваясь данными с периферийными устройствами
(клавиатурой, принтером и т. д.).
Помимо команд чтения и записи данных, существуют команды, предназначенные
для изменения нормального линейного хода чтения и выполнения машинных команд.
Это так называемые команды условного и безусловного перехода.
Команды безусловного перехода предписывают процессору выбирать вместо следую-
следующей команды другую команду, адрес которой задается как параметр команды. Что же ка-
касается команд условного перехода, то помимо адреса перехода им передается условие,
при выполнении которого нужно изменить линейный порядок исполнения программы.
Программирование в машинных командах предполагает ввод числовых кодов ко-
команд и параметров в оперативную память компьютера. В первом компьютере, который
мы собрали самостоятельно на базе процессора Intel 8080, для этого использовалась
небольшая кнопочная клавиатура.
Мы убедились на собственном опыте, что программирование в машинных кодах
и ручной ввод команд в оперативную память компьютера — ужасно утомительное за-
занятие. Чтобы избавить программистов от рутинной работы с числами, на заре компь-
компьютерной техники использовали язык ассемблера.
В языке ассемблера числовые коды машинных команд были заменены легко запо-
запоминающимися символическими обозначениями. Например, команда сложения чисел
обозначается как add (от слова addition, означающего «сложение»), а команда вычи-
вычитания — как sub (subtraction — «вычитание»).
Кроме команд, символические обозначения можно использовать для ссылок на ре-
регистры процессора, области оперативной памяти, выделяемые для хранения данных,
а также для ссылок на адреса расположения машинных команд.
Любые программы, составленные в машинных кодах или написанные на таких
языках, как С, C++ или С#, имею! дело с элементарными типами данных, такими, как
числа и текстовые строки, а также с командами и выражениями. Прежде чем мы
38 А. В Фролов, Г. В. Фролов Язык С#. Самоучитель
сможем продвинуться дальше в изучении технологаи программирования, нам необхо-
необходимо со всем этим познакомиться.
Далее мы рассмотрим элементарные типы данных. Элементарными типами данных
мы будем считать биты, байты, типы данных, предназначенные для хранения чисел
и текстовых строк. Они будут вам встречаться при использовании практически любого
языка программирования, в том числе и языка С#.
Бит
Известно, что наименьшей единицей хранения информации является бит. Бит может
принимать только два состояния — 1 или 0. Про состояние 1 часто говорят, что бит
установлен, а про состояние 0 — что бит сброшен.
Байт
Так как в 1 бите данных может храниться очень мало информации, биты часто объе-
объединяют в байты (рис. 1.4), комбинируя их по 8 штук. Самый левый бит с номером 7
называют старшим битом, а самый правый с номером 0—младшим битом.
1
1 | 0
0
1
1
1
0
Рис. 1.4. Внутреннее устройство байта
Биты в байте часто называют разрядами. Старший разряд байта — это его старший
бит, а младший разряд — младший бит. Всего в байте 8 разрядов, поэтому говорят,
что байт представляет собой 8-разрядный тип данных. Очевидно, бит является одно-
одноразрядным типом данных.
Каждый из восьми разрядов 1 байта (т. е. каждый бит байта) может принимать состоя-
состояние 0 или 1. Всего же с помощью 1 байта можно закодировать 2s=256 состояний.
Состояние байта, в котором все разряды сброшены, соответствует нулевому значе-
значению байта, а состояние, в котором все разряды установлены, — значению 255. Таким
образом, с помощью байта можно представить целые числа в диапазоне от 0 до 255.
Для примера в табл. 1.1 мы показали несколько состояний разрядов байта и соответст-
соответствующие им числовые значения.
Таблица 1.1. Числовые значения
Биты
00000000
00000001
00000010
00000011
00000100
11111101
11111110
11111111
Числовое значенея
0
1
2
3
4
253
254
255
Глава 1. Базовые понятия и определения
39
Хотя с помощью 1 байта можно представить заметно больше информации, чем
при помощи бита (например, можно закодировать все символы русского или латин-
латинского алфавита), реально в программах работают и с более емкими структурами дан-
данных. Сейчас нас интересуют данные, хранящиеся в оперативной памяти.
Оперативную память можно представить себе как набор (массив) ячеек, каждая из
которых хранит 1 байт данных (рис. 1.5).
0000
0001
0002
0003
0004
1А
08
В7
25
30
4FFF
56
Рис. 1.5. Адресация байтов оперативной
памяти
Размер оперативной памяти в современных компьютерах может быть очень боль-
большим. Для измерения больших объемов памяти используют такие единицы, как кило-
килобайт (Кбайт), мегабайт (Мбайт), гигабайт (Гбайт) и терабайт (Тбайт). В одном ки-
килобайте содержится 1024 байт, в одном мегабайте — 1024 Кбайт и т. д.
В нашем первом самодельном компьютере была установлена оперативная память
объемом всего 8 Кбайт, что позволяло разместить в ней только простейшие програм-
программы или всего несколько страниц текста. Сегодня мы пользуемся компьютерами с объ-
объемом оперативной памяти, равным 512 Мбайт и даже больше. В память такого объема
можно поместить целую библиотеку книг.
Для того чтобы записать или прочитать содержимое байта оперативной памяти,
в соответствующей машинной команде тем или иным способом нужно указать номер
соответствующей ячейки. Например, на рис. 1.1 видно, что в ячейке 0000 хранится
значение 0x1 А, а в ячейке 0004 — 0x30.
Здесь мы использовали шестнадцатеричное представление чисел, снабдив их префик-
префиксом Ох, как это принято в языках программирования С, C++ и С#. Подробнее о различных
представлениях чисел (двоичном, десятичном и шестнадцатеричном) читайте в [4].
Числовые типы данных
Первые компьютеры создавались главным образом для ускорения математических вы-
вычислений и потому работали с числами. Это обстоятельство подчеркивалось и в отече-
отечественном переводе зарубежного термина компьютер (computer) — «вычислительная
машина». Буквальный перевод этого термина — «вычислитель».
Данные элементарных типов (бит и байт) позволяют хранить числа в весьма огра-
ограниченном диапазоне значений. С помощью 1 бита можно представить всего два чис-
числа — 0 и 1, а с помощью 1 байта — 256 целых чисел в диапазоне от 0 до 255.
Комбинируя несколько байтов, можно создавать типы данных, предназначенные
для хранения намного большего количества значений. Например, комбинируя вместе
2 байта, можно определить 16-разрядный тип данных, способный хранить целые зна-
значения от 0 до 2К - 1 = 65 535. При необходимости можно комбинировать вместе 4, 8
пли больше байт, создавая типы данных, пригодные для реальных вычислений.
40 А. В Фролов, Г. В. Фролов Язык С# Самоучитель
Для того чтобы представлять в памяти не только положительные, но и отрицатель-
отрицательные числа, старший разряд байта (или комбинации нескольких расположенных рядом
байтов) используют для хранения знака числа. Когда этот разряд сброшен, то число
имеет положительный знак, а когда установлен — отрицательный. При этом в осталь-
остальных разрядах хранится абсолютное значение числа.
Отдельные байты или объединения смежных байтов оперативной памяти можно
использовать для создания так называемых переменных — объектов, предназначенных
для хранения числовых данных.
Переменная имеет свой адрес и разрядность. Чем больше разрядность, тем большее
число можно записать в переменную. Кстати, само слово «переменная» подчеркивает тот
факт, что программа может изменять содержимое отведенной для нее области памяти.
На рис. 1.6 мы разместили в оперативной памяти 3 переменные, одна из которых
занимает 2 байта, вторая — 4 и третья — 1 байт. Области памяти, отведенные для пе-
переменных, выделены полужирной рамкой. Мы также снабдили переменные именами
(идентификаторами), чтобы их было легче различать.
— MaxWidth
MaxHeight
0000
0001
0002
0003
0004
0005
0006
1А
08
В7
25
В7
25
30
.
4FFE
4FFF
34
56
Count
Рис. 1.6. Именованные переменные
Переменная с именем MaxWidth занимает в памяти 2 байта, первый из которых имеет
адрес 0x0000, а второй— 0x0001. Для переменной MaxHeight выделены 4 байта с адре-
адресами от 0x0003 до 0x0006, а для переменной Count — 1 байт с адресом 0x4FFF.
Программы, составляемые непосредственно в машинных кодах, вынуждены опе-
оперировать числовыми значениями адресов. Что же касается программ, составленных
на языке ассемблера, С или на других языках более высокого уровня, то в них вы мо-
можете обращаться к переменным по именам.
Компьютеру проще обрабатывать числа, человеку же понятнее символические
имена. Обычно имена переменных выбираются так, чтобы они указывали назначение
переменных. Например, нетрудно догадаться, что в переменной MaxWidth хранится
максимальное значение ширины, в переменной MaxHeight— максимальное значе-
значение высоты, а переменная Count используется для хранения значения счетчика. Пока
для нас не важно, высота и ширина какого объекта хранится в переменных Max-
MaxHeight и MaxWidth, а также что именно подсчитывает счетчик Count.
Имя переменной может состоять из произвольной последовательности букв
и цифр, а также из символа подчеркивания (_), считающегося буквой. Имя не должно
начинаться с цифры, содержать пробелы или совпадать с зарезервированными ключе-
ключевыми словами С#. Список зарезервированных ключевых слов С# вы найдете в прило-
приложении 1 к этой книге.
Глава 1. Базовые понятия и определения
41
Вот примеры правильных имен:
xCoord
TimeMachineLoader
time_machine_loader
sMyString
NULL_POINTER
pay_date
number?
Расположенные ниже последовательности символов недопустимо использовать
при определении имен переменных:
88
73 box
Time Machine
int
$total
my.name
Первые две последовательности начинаются с цифры, третья содержит пробел,
четвертая совпадает с зарезервированным именем int, а пятая и шестая содержат не-
недопустимые символы.
Учтите, что транслятор С# (так же как и трансляторы языков С и C++) делает раз-
различие между строчными и прописными буквами. Таким образом, ниже представлены
разные имена:
TimeMachineLoader
TimemachineLoader
Timemachineloader
В языке Pascal, напротив, все эти имена будут означать одно и то же, так как транс-
транслятор Pascal игнорирует различия между строчными и прописными буквами. На наш
взгляд, допустимость различного обозначения одних и тех же вещей является недос-
недостатком и в конечном счете может привести к возникновению трудно обнаруживаемых
ошибок.
Текстовые символы и строки
Помимо «перемалывания» чисел в процессе математических вычислений, компьютер
можно использовать и для решения многих других задач, например для обработки текста.
Выше мы уже говорили, что 1 байта достаточно для представления всех символов
русского или латинского алфавита. На самом деле задача представления символов раз-
различных алфавитов в компьютере не так проста, как может показаться на первый
взгляд.
Вот одна из проблем: в одном документе иногда используются символы сразу не-
нескольких алфавитов, например кириллица, латиница и символы греческого алфавита.
С учетом того, что символы текста могут быть строчными и прописными, 1 байта бу-
будет недостаточно для представления кодов символов нескольких алфавитов.
42 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Другая проблема связана с тем, что существует великое множество способов коди-
кодирования символов. Придумана, например, почти дюжина кодировок символов кирил-
кириллицы для компьютеров и ОС разных типов.
Кодировку символов в ОС Microsoft Windows удобно изучать при помощи про-
программы Character Map, которую можно найти в группе программ Accessories.
На рис. 1.7 мы показали один из возможных вариантов кодировки кириллицы — так
называемую кодировку ANSI.
<*Ch,ir.ut<>rMap
Font:
1
5
1
]
q
T
>
1
Г
4
0 Ariel
"
6
J
л
r
X
о
Д
Ш
#
7
К
_
s
€
к
±
E
Щ
$
8
L
*
t
%c
ri
1
Ж
ъ
Character to copy
17 Advanced view
Character sat:
Group by:
Search foi-.
%
9
M
a
u
Jb
M
i
3
Ы
"P
I
&
N
b
V
<
f
И
Ь
1
.
0
с
w
hb
У
Й
Э
'-
|windows
I
J!
(
<
p
d
X
К
У
H
К
ю
)
-
Q
e
У
Ъ
J
л
Я
Cynic
{U+0M1 (№411 Latin Capital Lettef*'•
>
R
f
z
□
ё
M
а
g
{
Г)
NQ
н
б
Z
v:
■. ■'■■.
А
h
1
1
!
с
0
в
i
}
§
»
п
г
»
d
d
*
■"■■■■
j
~
■
Ё
j
Р
Д
По*
/
к
Ъ
©
S
С
е
0
D
X
1
Г
•
е
S
Т
ж
Seta
• !
Search
W
1
Е
Y
m
,
-
«
1
У
3
'I
*
2
F
Z
п
г
—
—1
А
Ф
и
н
3
G
[
0
тм
-
Б
X
й
*1
4
Н
\
Р
®
В
Ц
К
Сору
1
d
I
<
Рис. 1.7. ANSI-кодировка символов кириллицы
Чтобы узнать код символа, щелкните его мышью в окне программы. Код будет по-
показан в нижней части окна (в круглых скобках). Например, если щелкнуть прописную
латинскую букву А, внизу отобразится ее код 0x41.
Как видите, верхнюю половину таблицы кодов занимают символы пунктуации,
цифры и латинские символы. Дальше идут различные специальные символы и симво-
символы кириллицы.
На рис. 1.8 мы показали одну из кодировок символов кириллицы, используемых
в ОС MS-DOS. Здесь символы пунктуации, цифры и латинские символы кодируются
таким же образом, как и в предыдущем случае, но символы кириллицы расположены
в другом месте кодовой таблицы.
Глава 1. Базовые понятия и определения
43
ft thararter Map
Fon
0
I
5
I
]
q
e
«
T
0
6
J
л
r
e
U
»
f-
0
Arial
#
7
К
_
s
s
Ю
—
П
$
8
L
t
S
Ю
Ш
¥
j
Characters to copy
17 AoVancedview
Charade«s*:
Group by:
Search lor:
%
9
M
a
u
i
ъ
1
к
г
V
&
N
b
V
1
Ъ
1
К
1
|dos
1
л
•
О
с
w
i
а
-1
IL
■
■*
(
<
Р
d
X
1
А
X
[г
П
Cvrftc
т
)
-
Q
е
У
J
б
X
JL
Я
| U+0041 @к4П Latin Capital Letter A
> '
R i
f
2
J /
Б i
и t
тг
■ i
1111
А
3 h
{ 1
ьГЬ
4 Ц
L _
Ц р
i
}
rfa
д
II
JL
Р
J
~
д
□
с
1
*>
/
-
к
Г)
h
е
=*•
Л
С
0
D
X
1
ъ
ъ
Е
й
Л
т
Select
оУ
ii:
тс4п
J
1
Е
Y
m
г
К
Ф
И
м
Т
|
2
F
2
п
Г
К
Ф
1
м
У
i
Help
3
G
[
0
e
У
г
L
н
У
4
Н
\
Р
Ё
У
Г
1
н
ж
Copy
1
Рис. 1.8. DOS-кодировка символов кириллицы
Чтобы закодировать символы сразу всех алфавитов, была разработана так называе-
называемая кодировка UNICODE. В ней каждый символ представляется не одним, а двумя
байтами. С помощью 2 байтов можно представить максимально 65 535 символов, что,
по всей видимости, достаточно во всех случаях. С использованием UNICODE можно
составлять текстовые документы, в которых одновременно применяются символы
любых существующих алфавитов.
В первых компьютерах и ОС каждый символ текста кодировался 1 байтом,
т. с. применялась 1-байтовая кодировка символов. Наряду с другими кодировками
такая кодировка применяется и до сих пор. При записи текстовой строки с 1-байто-
1-байтовой кодировкой в оперативную память компьютера коды символов располагаются
в смежных ячейках памяти, как это показано на рис. 1.9.
н ч HelloMessage
е
I
I
о
1000
1001
1002 "
1003
1004
1005
1006
4ff"F" "
48
65
6С
6С
6F
00
30
5~6 "
Рис. 1.9. Хранение в памяти строки
"Hello»
44
А В Фролов, Г. В Фролов Язык С# Самоучитель
Здесь символы строки «Hello» с именем HelloMessage занимают ячейки памя-
памяти с адресами от 1000 до 1004 включительно. При этом адресом строки является адрес
ее 1 -го байта. В нашем случае это адрес 1000.
Текстовые строки могут иметь любую длину, поэтому нужно как-то обозначить
конец строки в памяти. В языках программирования С и C++ текстовая строка всегда
завершается нулевым байтом. На рис. 1.6 это байт с адресом 1005. В Pascal перед на-
началом строки в памяти располагается байт, хранящий длину строки, а завершающий,
нулевой символ не используется. Из-за того что в 1 байте можно хранить числа от 0
до 255, длина закодированной этим способом текстовой строки в языке Pascal не мо-
может превышать 255 символов.
Если для хранения символов использовать кодировку UNICODE, то строка будет
занимать в оперативной памяти вдвое больше места, так как каждый символ будет
кодироваться не одним, а 2 байтами. Такова цена универсальности.
В языке С# для хранения текстовых символов и строк применяется кодировка
UNICODE, допускающая представление символов всех существующих алфавитов.
Обозначение типов данных в С#
Итак, теперь мы знаем, что программа может обращаться к переменным, расположен-
расположенным в оперативной памяти компьютера, не только по адресам, но и по символическим
именам. Однако одного только имени недостаточно для того, чтобы правильно рабо-
работать с переменной. Необходимо еще знать, какой тип данных хранит переменная,
и сколько байтов памяти она занимает.
Во всех языках программирования для обозначения типа данных используются
специальные ключевые слова, и язык С# не является исключением.
Заметим, что в отличие от других языков программирования все переменные в С#
рассматриваются как объекты тех или иных классов. Пока мы еще не приступили
к изучению объектно-ориентированного программирования, не будем принимать дан-
данное обстоятельство во внимание. Позже мы вернемся к этому вопросу и рассмотрим
его во всех подробностях.
В языке С# можно использовать числовые типы данных со знаком, числовые пере-
переменные без знака, символьные переменные, логические (булевы) переменные и стро-
строки, а также перечисления. Рассмотрим элементарные типы данных С# и их обозначе-
обозначение подробнее.
Числа без знака
Вы уже знаете, что если все разряды байта используются для представления абсолют-
абсолютного значения числа, то байт хранит только положительные числа и нуль. В табл. 1.2
мы перечислили имена числовых типов данных без знака, используемые в языке про-
программирования С#.
Глава 1. Базовые понятия и определения 45
Таблица 1.2. Числовые типы данных без знака
Тип
byte
ushort
uint
ulong
Возможные значения
От 0 до 255
От 0 до 65 535
От 0 до 4 294 967 295
ОтОдо
18 446 744 073 709 551 615
Описание
8-разрядное значение без знака,
занимает 1 байт памяти
16-разрядное значение без знака,
занимает 2 байта памяти
32-разрядное значение без знака,
занимает 4 байта памяти
64-разрядное значение без знака,
занимает 8 байт памяти
Чтобы использовать переменные в программе С#, их необходимо объявить, указав
имя и тип переменной, например:
byte myByte;
ushort myShort;
uint mylnt;
ulong myLong;
Здесь объявлены 4 переменные с именами myByte, myShort, mylnt и myLong.
Обратите внимание, что мы записали объявление каждой переменной на отдельной
строке, завершив его символом «точка с запятой». Можно было бы расположить все
объявления на двух или даже на одной строке, однако такая запись выглядит менее на-
наглядно:
byte myByte; ushort myShort; uint mylnt; ulong myLong;
Если объявляются несколько переменных одного типа, можно использовать список
имен переменных, разделенных запятыми, например:
ushort xCoord, yCoord, zCoord;
uint height, width;
Здесь мы объявили 3 переменные xCoord, yCoord и zCoord типа ushort,
а также две переменные height и width типа uint.
Транслятор С#, преобразуя исходный текст программы в промежуточный код
MSIL, о котором мы упоминали во Введении, игнорирует незначащие пробелы, сим-
символы табуляции, символы возврата каретки и перехода на другую строку. Однако если
не ставить пробел между обозначением типа переменной и ее именем, компилятор бу-
будет считать такую строку ошибочной:
bytemyByte; // Ошибка!
Обратите внимание, что в этой строке после символов / / мы записали так назы-
называемую строку комментария. Комментарии позволяют вставлять в исходный текст
программы произвольные поясняющие текстовые строки. В языке С# существуют
комментарии трех типов. На данный момент вам следует знать, что компилятор игно-
игнорирует символы / / и все символы, следующие за ними до конца текущей строки:
byte myByte; // Это правильное определение переменной типа byte
46
А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Если нужно вставить комментарий в середину строки, используется конструкция
/*...*/. Текст комментария помещается между парой символов /*, открывающей
комментарий, и парой символов */, закрывающей комментарий. Например:
byte /* это комментарий*/ myByte;
С помощью конструкции /*...*/ можно создавать многострочные комментарии,
включающие в себя при необходимости однострочные комментарии. В частности,
данный фрагмент исходного текста программы будет рассматриваться компилятором
как многострочный комментарий:
/*
Это переменные для хранения чисел без знака
byte myByte; // это байт
ushort myShort;
uint mylnt;
ulong myLong;
*/
Использование комментариев делает программу более понятной. Не жалейте вре-
времени на комментирование программ, особенно сложных. Впоследствии, если вам при-
придется дорабатывать программу, комментарии помогут вспомнить назначение фрагмен-
фрагментов ее исходного текста.
Числа со знаком
Числовые типы данных со знаком, доступные программисту С#, представлены
в табл. 1.3. Как видите, при помощи этих типов данных можно представить числа
в довольно широком диапазоне значений.
Таблица 1.3. Числовые типы данных со знаком
Тип
sbyte
short
int
long
Возможные значения
От-128 до 127
От-32 768 до 32 767
От-2 147 483 648
до 2 147 483 647
От -9 223 372 036 854 775 808
до 9 223 372 036 854 775 807
Описание
8-разрядное значение без знака,
занимает 1 байт памяти
16-разрядное значение без знака,
занимает 2 байта памяти
32-разрядное значение без знака,
занимает 4 байта памяти
64-разрядное значение без знака,
занимает 8 байт памяти
Вот примеры объявления переменных для перечисленных в этой таблице типов
данных со знаком:
sbyte distanceX;
short distanceY;
int height;
long width;
Глава 1. Базовые понятия и определения
47
Сделаем небольшое замечание относительно выбора имен переменных.
Прежде всего, имена нужно задавать таким образом, чтобы в контексте программы
было понятно, для чего эти переменные используются и что они хранят.
Компилятор С# не запрещает указывать такие имена переменных, как х, у, ab, d
и т. п. Однако смысл имени переменной будет намного прозрачнее, если составлять
его из слов английского языка, обозначающих связанное с этой переменной понятие.
Например, по имени distanceFromHome сразу можно догадаться, что в соответст-
соответствующей переменной хранится расстояние от дома.
Кроме того, рекомендуется (хотя это и не обязательно) начинать имя переменной
со строчной буквы и применять прописные буквы для выделения слов, входящих в со-
составное имя переменной.
Числа с плавающей точкой
При проведении научных вычислений часто используются числа в формате с плаваю-
плавающей точкой, такие, например, как 2,1x10"' .
Для представления данных в формате с плавающей точкой в языке С# используется
два типа данных— float и double (табл. 1.4).
Таблица 1.4. Числовые типы данных с плавающей точкой
Тип
float
double
Возможные значения
OTl,5xl(r%o3,4xl03i!
От 5,0x1024 до 1,7x10308
Описание
32-разрядное число с плавающей точкой,
максимальная точность представления
чисел — 7 десятичных цифр
64-разрядное число с плавающей точкой,
максимальная точность представления
чисел — 16 десятичных цифр
Вот примеры объявления переменных, предназначенных для хранения чисел с пла-
плавающей точкой, в программе С#:
float piNumber;
double eNumber;
Как видите, диапазон значений, представляемых с помощью типов данных float
и double, чрезвычайно широк и вполне подходит для научных расчетов.
Числа для финансистов
Деньги, как известно, любят счет. Если речь идет о компьютерных системах банка,
оперирующих с астрономическими денежными суммами, необходимо обеспечить мак-
максимально возможную точность вычислений.
Многократное возникновение ошибки округления может привести к бесследному
исчезновению (или, наоборот, к появлению из ниоткуда) довольно заметных денеж-
денежных сумм.
48
А В Фролов, Г. В Фролов Язык С# Самоучитель
Из прессы нам известен случай, когда недобросовестный программист пользо-
пользовался данным обстоятельством с выгодой для себя. Мизерные денежные суммы,
отбрасываемые при округлении результатов вычислений, он переводил на свой
счет, проводя в жизнь известное утверждение о том, что копейка рубль бережет.
Когда махинации вскрылись, оказалось, что на счету злоумышленника скопилась
изрядная сумма денег.
В языке С# предусмотрен один тип данных специально для работы с деньгами —
decimal (табл. 1.5). Он обеспечивает колоссальную точность представления денеж-
денежных сумм — до 29 десятичных цифр!
Таблица 1.5. Числовые типы данных для работы с деньгами
Тип
decimal
Возможные значения
От1,0х1(Г28до7,9х1028
Описание
128-разрядное число с плавающей точкой,
максимальная точность представления
чисел — 29 десятичных цифр
Вот пример объявления переменной типа decimal:
decimal totalAccountValue;
Текстовые символы
Для хранения отдельных символов текста (таких, как буквы, знаки пунктуации
и управляющие символы) в языке С# предусмотрен специальный тип данных
char.
Как мы уже говорили, для этого типа данных используется кодировка UNICODE,
поэтому символы char занимают в памяти 2 байта. Заметим, что переменные типа
char в языках программирования С и C++ занимают в памяти только 1 байт.
Вот как можно объявить переменную типа char в программе, написанной на С#:
char tokenDelemiter ;
Текстовые строки
Многие программы работают с текстовыми строками. Язык С# содержит мощные
и удобные объектно-ориентированные средства, позволяющие выполнять со строками
все необходимые операции.
Про строки мы пока скажем лишь то, что они определяются с помощью ключевого
слова string и хранятся в кодировке UNICODE. Детальное описание приемов рабо-
работы со строками мы приведем после того, как рассмотрим технологию объектно-
ориентированного программирования.
Вот так можно объявить в программе С# переменную типа string:
string firsLName;
Глава 1 Базовые понятия и определения 49
Логический тип данных
Логические переменные в С# объявляются с помощью ключевого слова bool и могут
принимать одно из двух значений — true (истина) или false (ложь).
В этом логические переменные напоминают отдельные разряды байта. Дейст-
Действительно, каждый бит может хранить только одно из двух значений — 1 или 0.
Во многих языках программирования (в частности, в языках С и C++) значение 1
сопоставляется с истиной, а значение 0 — с ложью. В языке С# такое сопоставле-
сопоставление не используется. Подробнее об этом мы поговорим позже при описании услов-
условных операторов.
Перечисления
Для определения некоторых типов данных вместо чисел, текстовых символов и строк
больше подходят перечисления. Например, мы можем пронумеровать дни недели
и ссылаться в программах на номера, но проще использовать привычные для нас на-
названия — понедельник, вторник, среда и т. д.
В языке С# перечислимый тип данных задается при помощи ключевого слова enum
и фигурных скобок, например:
enum Days
{
Monday, // понедельник
Tuesday, // вторник
Wednesday, // среда
Thursday, // четверг
Friday, // пятница
Saturday, // суббота
Sunday // воскресенье
}
Здесь с помощью ключевого слова enum мы создали собственный перечислимый
тип данных Days и определили составляющие его элементы перечисления, располо-
расположив их внутри фигурных скобок.
Так как компилятор С# игнорирует незначащие пробелы и символы новой строки,
в тексте программы все элементы перечисления могут находиться и на одной строке.
Например, ниже определен перечислимый тип для представления основных компо-
компонентов цвета — красного, зеленого и голубого:
enum BaseColors { Red, Green, Blue }
Компилятор С# автоматически ставит в соответствие элементам перечисления це-
целочисленные значения, однако программист может оперировать именами переменных,
что намного удобнее.
Обратите внимание, что оператор объявления переменной нужно завершать точкой
с запятой. При объявлении перечисления этот символ не используется.
По умолчанию перечисления создаются на базе типа данных int. Однако вы мо-
можете создавать перечисления и других типов. Вот полный список типов, которые
50 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
могут служить базовыми для перечислений: byte, ushort, uint, ulong, sbyte,
short, int, long.
Вот пример перечисления, созданного на базе типа byte:
enum Days : byte
Monday, // понедельник
Tuesday, // вторник
Wednesday, // среда
Thursday, // четверг
Friday, // пятница
Saturday, // суббота
Sunday // воскресенье
Так как переменные типа byte занимают в памяти вчетверо меньше места, чем пе-
переменные типа int, новый вариант перечисления Days будет компактнее.
Такие типы данных, как long и ulong, используют в качестве базовых для созда-
создания перечислений с очень большим количеством элементов.
Литералы
Литералы применяются в исходном тексте программы для обозначения числовых или
логических значений, текстовых символов и строк. С помощью литералов програм-
программист может присвоить начальные значения переменным.
Целочисленные литералы
Для обозначения целых десятичных чисел в языке С# используются цифры от 0 до 9,
а также (при необходимости) дополнительные суффиксы.
Вот примеры простейших целочисленных литералов:
25 0 767 6786867867 1233
Целочисленные литералы без дополнительных суффиксов могут представлять зна-
значения типов со знаком или без знака: byte, sbyte, int, uint, long, ulong.
Чтобы указать, что литерал представляет собой число без знака, он снабжается
суффиксом и или U, например:
256u 1U 0U
В этом случае литерал соответствует типам byte, uint, ulong.
Суффиксы 1 или L применяются для создания литералов типа long и ulong:
2564124739362741 11 0L
Комбинируя в произвольном порядке суффиксы и, и, 1 и L, можно создавать лите-
литералы типа ulong:
25UL 145637365UL 76787234711u 7678723471LU 6410193764LU
Глава 1. Базовые понятия и определения 51
На экране букву 1 очень легко перепутать с цифрой 1, поэтому в суффиксах лите-
литералов рекомендуется применять вместо нее прописную букву L.
Целочисленные литералы могут обозначать и шестнадцатеричные числа. В этом
случае они составляются из цифр от 0 до 9 и букв А, В, С, D, E, F. Перед шестнадцате-
ричным числом помещается префикс Ох, например:
Oxl 0x23BF OxFFFF 0xFE67BCA0001
Для обозначения знака числа допускается использование символов + и -.
Литералы с плавающей точкой
Литералы с плавающей точкой бывают типа float, double и decimal. Если в ли-
литерале используется суффикс f или F, то это литерал типа float, а если суффикс d
или D — типа double. В том случае, когда суффикс не указан, литерал принимает тип
double.
Для обозначения литерала типа decimal используется суффикс m или М.
Экспоненциальная часть литерала отделяется символом е или Е. Для обозначения
знака числа используются символы + и -.
Вот примеры литералов с плавающей точкой типа float:
3F 6.52f 2elOF 1356.4560f -3.7E-10F
Ниже мы привели примеры литералов типа double:
.25 3.3 6.52d 2elO 1356.4560D -3.7E-10D
Вот несколько примеров литералов типа decimal, применяемых обычно для обо-
обозначения денежных сумм:
.25т З.ЗМ 6.52ш 2е10М 13573847420983746.45М
Символьные литералы
Обычно с помощью символьных литералов представляют одиночные символы, заклю-
заключая их в одинарные кавычки, например:
'С 'А' ЧЦ'
С помощью символа обратного слеша (\) можно указать в качестве символьного
литерала некоторые специальные и управляющие символы. Например, литерал ' \п '
задает символ новой строки.
Последовательность символа \ и идущего вслед за ним символа называют escape-
посяедоватеяьностью. В табл. 1.6 мы привели escape-последовательности, допусти-
допустимые в языке С#, а также соответствующие им 16-разрядные значения в кодировке
UNICODE.
52 А. В Фролов, Г. В Фролов Язык С#. Самоучитель
Таблица 1.6. Escape-последовательности
Escape-
последовательность
\ '
\"
\\
\0
\а
\Ь
\f
\п
\г
\t
\v
\х
СиМвО/7
Одинарная кавычка(' )
Двойная кавычка (")
Обратный слеш (\)
Нулевое значение
Звуковой сигнал
Возврат каретки
Новая страница
Новая строка
Возврат каретки
Горизонтальная табуляция
Вертикальная табуляция
Произвольное значение
в шестнадцатеричной кодировке
Код, UNICODE
0x0027
0x0022
ОхОО5С
0x0000
0x0007
0x0008
ОхОООС
ОхОООА
OxOOOD
0x0009
ОхОООВ
От 0x0000 до OxFFFF
В наших программах мы часто будем пользоваться литералом ' \п' для перевода
строки при выдаче данных на консоль.
С помощью конструкции вида ' \ul23 4 ' или ' \U1234 ' можно задать так назы-
называемый символьный литерал UNICODE. Цифры, следующие за escape-последо-
escape-последовательностью \и, задают шестнадцатеричный код UNICODE определяемого символа.
Так как символы UNICODE занимают в памяти 2 байта, этот код должен находиться
в интервале от 0x0000 до OxFFFF.
Строковые литералы
Для представления текстовых строк в языке программирования С# используют стро-
строковые литералы. Строковый литерал представляет собой простую строку символов,
заключенную в двойные кавычки, например:
"Hello, C# world!"
Строковые литералы могут включать в себя escape-последовательности, описанные
в предыдущем разделе. Например, для разделения слов в строковом литерале может
применяться символ табуляции \t:
"Случайные числа:\t657\t3\4585t\t54545"
Для того чтобы включить в литерал символ двойной кавычки, его нужно предста-
представить в виде escape-последовательности \ ", например:
"Гостиница \"Москва\""
Глава 1 Базовые понятия и определения
53
Если же нужно включить в литерал символ обратного слеша (\), то его нужно пов-
повторить дважды. Вот, например, как с помощью текстового литерала можно задать путь
к каталогу c:\games\tetris\doc:
"с:\\games\\tetris\\doc"
В языке С# строковый литерал может иметь префикс @. Он задает так называемый
буквальный или дословный (verbatim) литерал. В таком литерале все символы интер-
интерпретируются буквально, поэтому, например, нет необходимости повторять дважды
символ обратного слеша:
@"с:\games\tetris\doc"
Если же нужно включить в буквальный строковый литерал символ двойной кавыч-
кавычки, его все же придется повторить дважды:
@"Гостиница ""Москва"""
Обычные строковые литералы C++ должны полностью располагаться на одной
строке. Что же касается буквальных строковых литералов, то они могут быть не толь-
только однострочными, но и многострочными, например:
@"Это
многострочный
литерал"
Такой литерал включает в себя все пробелы, символы новой строки и любые другие
символы, располагающиеся между двойными кавычками, ограничивающими литерал.
Логические литералы
Логические литералы предназначены для задания значений логическим перемен-
переменным. В языке С# используются два логических литерала — true (истина) и fal-
false (ложь).
Напомним, что в языке С# логические литералы не сопоставляются с целочислен-
целочисленными значениями, такими, например, как 1 и 0.
Литерал null
В языках программирования С и C++ переменные всегда содержат какие-либо значе-
значения. Программист может задать эти значения явно с помощью оператора присваива-
присваивания (который мы скоро рассмотрим), а может и не задавать. В последнем случае в пе-
переменной будет храниться либо нуль, либо случайное значение.
Что же касается языка С#, то в нем с помощью специального литерала null про-
программист может указать, что переменная вообще не содержит никакого значения.
Заметим, что литерал пи 11 не эквивалентен нулевому значению. Он предназначен
для использования в тех ситуациях, когда в переменной не хранится ни нулевое,
ни какое-либо другое значение.
54 А. В. Фролов, Г. В. Фролов. Язык С# Самоучитель
Базовые выражения и операторы С#
Итак, мы познакомились с типами данных, переменными и литералами. Теперь нам
нужно научиться инициализировать переменные при помощи литералов, а также изу-
изучить основные операторы языка С#.
Операторы представляют собой символы, с помощью которых программа может
выполнять те или иные действия над переменными, литералами и другими объектами.
В результате выполнения оператора всегда получается какое-то значение, представ-
представляющее собой результат выполнения оператора.
Объекты программы, над которыми выполняются действия, называются операнда-
операндами. Например, оператор сложения чисел принимает два операнда. Результатом сложе-
сложения будет число, равное сумме значений операндов, т. е. сумме складываемых чисел.
Операторы и операнды формируют выражения.
Изучение операторов и выражений языка С# мы начнем с фундаментального опе-
оператора присваивания. Далее мы рассмотрим арифметические, логические, условные
и другие операторы, а также выражения, составляемые с их применением.
Инициализация переменных и оператор присваивания
Прежде чем использовать переменную, программа должна задать для нее начальное
значение. В языке С# это обязательное условие. Начальное значение может быть зада-
задано с помощью оператора присваивания при объявлении переменной, а также в процес-
процессе выполнения программы.
Вот несколько выражений, в которых присваиваются начальные значения пере-
переменным различных типов при объявлении этих переменных:
// Числовые переменные без знака
byte myByte = 25;
ushort myShort = 0;
uint mylnt = 0x1233;
ulong myLong = 6786867867;
// Числовые переменные со знаком
sbyte distanceX = -25;
short distanceY = 356;
int height = -0x1234;
long width = 567743;
// Переменные с плавающей точкой
float piNumber = 3.1415926F;
double eNumber = 2.71;
decimal totalAccountValue = 9000000000m;
// Текстовый символ
char tokenDelemiter = 'с' ;
// Текстовая строка
string firstName = "Ivan";
string message = "Гостиница \"Москва\"";
Глава 1. Базовые понятия и определения 55
// Перечисление
enum BaseColors
{
Red, Green, Blue
}
Инициализаторы переменных всех типов данных, кроме перечисления, выглядят
одинаково. Составляя выражение для инициализации, вначале вы указываете тип дан-
данных, затем имя переменной, потом оператор присваивания в виде знака равенства (=)
и, наконец, литерал соответствующего типа.
После инициализации программа может присвоить переменной новое значение
с помощью все того же оператора присваивания:
int х = 0;
х = 1;
Здесь мы вначале присваиваем переменной х значение 0, а затем — значение 1.
Для инициализации одной переменной может использоваться другая, уже инициа-
инициализированная ранее переменная:
int х = 0;
int у = х;
int z ,-
z = у;
Если нескольким переменным нужно присвоить одно и то же значение, это можно сде-
сделать за один прием при помощи многократного использования оператора присваивания:
int х, у, z;
х = у = z = 1 ;
Такое выражение вычисляется справа налево. Вначале значение 1 присваивается
переменной z, затем значение переменной z присваивается переменной у, и, наконец,
значение переменной у присваивается переменной х. В результате все 3 переменные
будут проинициализированы значением 1.
Инициализация перечислений
По умолчанию компилятор автоматически присваивает значения элементам перечис-
перечисления начиная со значения 0.
Если вам нужно определить конкретные значения для элементов перечисления, это
можно сделать с помощью оператора присваивания (=).
Вот пример инициализированного перечисления:
enum Days
Monday = 1,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
//
11
//
II
II
11
II
понедельник
вторник
среда
четверг
пятница
суббота
воскресенье
56 А В. Фролов, Г. В Фролов. Язык С# Самоучитель
Здесь нумерация элементов начинается не с нуля, как это принято по умолчанию,
а с единицы. Таким образом, понедельнику будет соответствовать значение 1, вторни-
вторнику — 2 и т. д.
Ниже мы применили инициализацию всех элементов перечисления шестнадцате-
ричными числами (для обозначения шестнадцатеричных чисел используется префикс
Ох), а также изменили базовый тип перечисления:
enum BaseColors : uint
{
Red = OxFFOOOO,
Green = OxOOFFOO,
Blue = OxOOOOFF
}
При инициализации перечислений можно использовать выражения, зависящие
от других элементов перечисления, например:
enum BaseColors : uint
{
Red = OxFFOOOO,
Green = OxOOFFOO,
Blue = OxOOOOFF,
White = Red | Green | Blue
}
Здесь значение элемента White, означающего белый цвет, получается путем по-
поразрядного сложения красного, зеленого и голубого компонентов цвета.
Проверка результата инициализации
Теперь, когда мы познакомились с операторами инициализации, давайте напишем
простую программу, инициализирующую переменные нескольких типов и отобра-
отображающую результаты инициализации в консольном окне.
Исходный текст такой программы представлен в листинге 1.2.
Листинг 1.2. Файл chO1\DataTypes\DataTypesApp.cs
using System;
namespace DataTypes
{
class DataTypesApp
{
static void Main(string[] args)
{
string helloMessage = "Hello, C# world!";
System.Console.WriteLine(helloMessage);
floatpiNumber = 3.1415926F;
System.Console.WriteLine(@"Число ""ПИ"" = {0}",
piNumber);
Глава 1. Базовые понятия и определения 57
bool itlsTrueValue = true;
bool itlsFalseValue = false;
System.Console.WriteLine("Правда = {0}, ложь = {1}
itlsTrueValue, itlsFalseValue);
char tokenDelemiter = 'c1;
System.Console.WriteLine{"Символ-разделитель = {О}
tokenDelemiter);
System.Console.ReadLine() ;
Прежде всего программа инициализирует текстовую строку helloMessage
при помощи текстового литерала «Hello, C# world!»:
string helloMessage = "Hello, C# world!";
Далее при помощи метода WriteLine наша программа отображает содержимое
переменной helloMessage на консоли:
System.Console.WriteLine(helloMessage);
Обратите внимание: теперь мы передаем методу WriteLine не литерал, как это
было в самой первой программе (листинг 1.1), а заранее проинициализированную пе-
переменную helloMessage. Но результат будет тот же самый— текст сообщения
с приветствием появится в консольном окне.
В следующих двух строках мы инициализируем числовую переменную в формате
с плавающей точкой приблизительным значением числа л, а затем выводим содержи-
содержимое этой переменной на консоль:
floatpiNumber= 3.1415926F;
System.Console.WriteLine (©"Число ""ПИ11" = {0}", piNumber) ;
Здесь мы применили новый формат вызова метода WriteLine. Теперь этому ме-
методу передаются два параметра, разделенные запятой. Первый из них представляет со-
собой буквальный строковый литерал @"Число ""ПИ"" = {0} ", а второй— число-
числовую переменную piNumber.
Метод WriteLine обладает большими возможностями, но мы будем изучать
их постепенно по мере изложения материала. В данном случае мы передали методу
в качестве первого параметра так называемую строку формата, определяющую, что
и как нужно выводить на консоль. Второй параметр представляет собой переменную,
содержимое которой нужно вывести.
Во время работы программы метод WriteLine вначале выведет на консоль стро-
строку «Число "ПИ" = », а затем вместо конструкции «{0}» подставит значение второ-
второго параметра метода, т. е. содержимое переменной piNumber.
В следующем фрагменте программы, отображающем содержимое двух логических
переменных, мы тоже указали методу WriteLine строку формата:
58 А В Фролов, Г. В. Фролов Язык С#. Самоучитель
bool itlsTrueValue = true;
bool itlsFalseValue = false;
System.Console.WriteLine("Правда = {0}, ложь = {1}",
itlsTrueValue, itlsFalseValue);
При обработке строки формата конструкции «{0}» ставится в соответствие со-
содержимое переменной itlsTrueValue, передаваемое методу WriteLine в ка-
качестве второго параметра. Конструкция «{1}» заменяется содержимым перемен-
переменной itlsFalseValue, передаваемой методу WriteLine через третий параметр.
С помощью одного обращения к методу WriteLine можно вывести на консоль
несколько значений, указывая их в параметрах метода после строки формата. Этим
удобным приемом мы будем часто пользоваться в наших примерах программ.
В завершающем фрагменте кода программы мы выводим содержимое переменной,
проинициализированной символьным литералом:
char tokenDelemiter = 'с';
System.Console.WriteLine("Символ-разделитель = {0}",
tokenDelemiter);
Вот что появится на консоли в результате работы программы:
Hello, C# world!
Число "ПИ" = 3,141593
Правда = True, ложь = False
Символ-разделитель = с
Обратите внимание, что в качестве значений логических переменных отобразились
строки True и False, а не 1 и 0. Как мы уже говорили, в языке С# логическим пере-
переменным не ставятся в соответствие никакие числа или символы.
А что будет, если попытаться вывести на консоль содержимое переменной, которая
еще не была проинициализирована?
Это у вас не получится.
Попробуйте, например, добавить к программе следующие две строки:
int notlnitialized;
System.Console.WriteLine(notlnitialized); // Ошибка!
Здесь мы объявили числовую переменную с именем notlnitialized типа int
и, не присвоив ей никакого значения, пытаемся отобразить содержимое этой перемен-
переменной на консоли методом WriteLine.
В процессе компиляции вы получите следующее сообщение об ошибке:
Use of unassigned local variable 'notlnitialized'
Как мы уже говорили во Введении, язык С# не допускает использования неинициа-
неинициализированных переменных. Как правило, это происходит по ошибке. Язык С#, в отли-
отличие от языков С и C++, позволяет обнаруживать ошибки обращения к неинициа-
неинициализированным переменным еще на этапе компиляции, что значительно упрощает от-
отладку программ.
Глава 1. Базовые понятия и определения 59
Значение в левой части
Как вы теперь знаете, с помощью оператора присваивания можно задать начальное
значение переменной при се объявлении или изменить это значение в процессе вы-
выполнения программы. При этом изменяемое значение должно находиться слева от опе-
оператора присваивания, а литерал или другое выражение, применяемые для инициализа-
инициализации или изменения значения, — справа:
int х;
X = 1;
Программа, однако, не может изменить значение литерала, поэтому следующие
строки будут ошибочными:
1 = 2;
"Text" = "New text";
int x;
2 = x = 1;
Для объекта, который может быть использован для присваивания (например, пере-
переменная), придуман специальный термин — lvalue. Этот термин образован как сокра-
сокращение от left value (значение в левой части).
Таким образом, в левой части оператора присваивания всегда должен располагать-
располагаться объект типа lvalue.
Математические операторы
В языке С# имеются математические операторы, предназначенные для сложения, вы-
вычитания, умножения, деления и вычисления остатка от деления. Эти операторы, а так-
также используемые для их обозначения символы приведены в табл. 1.7.
Таблица 1.7. Математические операторы
Символ
+
-
*
/
%
Оператор
Сложение
Вычитание
Умножение
Деление
Вычисление остатка при целочисленном делении
Рассмотрим использование этих операторов в программах С#.
Сложение
При помощи оператора сложения можно складывать содержимое числовых перемен-
переменных и числовых литералов. Результат операции должен быть записан в переменную,
т. е. в объект типа lvalue.
Результат операции сложения получается путем вычисления суммы двух склады-
складываемых величин. Так как в операции принимают участие два операнда, операция сло-
сложения называется бинарной (binary). Помимо бинарных операторов имеются еще и так
60 А В Фролов, Г. В. Фролов Язык С# Самоучитель
называемые унарные (unary) операторы, имеющие дело только с одним операндом.
Унарными операторами мы займемся позже в этой главе.
Для демонстрации применений оператора сложения мы подготовили программу,
исходный текст которой представлен в листинге 1.3.
Листинг 1.3. Файл ch01\MathOperators\MathOperatorsApp.cs
using System;
namespace MathOperators
{
class MathOperatorsApp
{
static void Main(string[] args)
{
int x, y, z;
x = 1;
У = 1+2;
z=x+y+10;
System.Console.WriteLine("x = {0}, у = {1}, z = {2}",
x, у, z) ;
string hello = "Hello,
string cSharp = "C#";
string message = hello + cSharp + " world!";
System.Console.WriteLine(message);
System.Console.ReadLine();
В первой строке метода Main, получающего управление при запуске программы,
мы объявили 3 переменные типа int с именами х, у и z:
int x, у, z;
Для инициализации переменной х мы использовали числовой литерал 1:
х = 1;
Здесь пока нет для нас ничего нового. Но вот в следующей строке для инициализа-
инициализации переменной у мы использовали сумму двух числовых литералов:
у = 1 + 2;
В результате переменная у будет содержать значение 3.
Далее мы инициализируем переменную z:
z = х + у + 10 ;
В нее записывается сумма значений, хранящихся в переменных х и у, к которой
прибавляется число 10.
Глава 1. Базовые понятия и определения 61
После выполнения всех этих действий содержимое наших числовых переменных
выводится на консоль при помоши известного вам метода WriteLine:
System.Console.WriteLine("x = {0}, у = {1}, z = {2}", x, y, z) ;
Забегая вперед, скажем, что оператор сложения можно применять не только к чи-
числовым, но и к строковым данным. При этом сложение заменяется операцией конка-
конкатенации, т. е. слиянием строк.
В следующем фрагменте кода мы объявляем две текстовые переменные, инициали-
инициализируя их строковыми литералами. Затем мы выводим на консоль результат «сложе-
«сложения» этих текстовых переменных, добавляя к ним еще один текстовый литерал:
string hello = "Hello, ";
string cSharp = "C#";
string message= hello + cSharp + " world!";
System.Console.WriteLine(message);
Вот что появится в консольном окне после запуска программы:
х = 1, у = 3, z = 14
Hello, C# world!
Как видите, получился ожидаемый результат. Числа сложились, а 3 текстовые
строки слились в одну.
Опять же, забегая вперед, скажем, что в языке С#, так же как и в языке C++, допус-
допускается переопределение большинства операций. Что касается текстовых строк,
то по умолчанию операция сложения для них заменена конкатенацией. Как вы узнаете,
в языке С# можно конкатенировать текстовые строки с любыми объектами. Позже вы
научитесь создавать свои собственные типы данных и определять для них смысл опе-
операторов.
Вычитание
В операции вычитания, так же как и в только что рассмотренной операции сложения,
принимают участие два операнда, например:
int х = 4;
int у = 2;
int z = х-у;
Здесь в переменную z записывается результат вычитания значения переменной у
из значения переменной х.
В операции вычитания могут принимать участие и числовые литералы, например:
int х = 4;
int z = х-5;
После выполнения этой операции в переменную z будет записано отрицательное
значение -1.
Чтобы продемонстрировать применение оператора вычитания, мы приготовили от-
отдельную программу (листинг 1.4).
62 А. В Фролов, Г В. Фролов. Язык С# Самоучитель
Листинг 1.4. Файл ch01\MathOperators1\MathOperatorsApp1.cs
using System;
namespace MathOperatorsl
{
class MathOperatorsAppl
{
static void Main(string[] args)
{
int x, y, z;
X = 1;
У = -1-5;
z=x-y+10;
System.Console.WriteLine("x = {0}, у = {1}, z = {2}",
x, y, z) ;
uint uX = 2;
System.Console.WriteLine( - 3 = {0}", uX - 3);
System.Console.ReadLine();
В первых строках метода Main мы в результате вычислений получаем, как и ожи-
ожидается, значение 17. Здесь мы также применили знак вычитания для изменения знака
числового литерала, что вполне допустимо:
У = -1-5;
Теперь обратите внимание на следующий фрагмент кода:
uint uX = 2;
System.Console.WriteLine( - 3 = {0}", uX - 3);
Мы объявили переменную без знака типа uint с именем uX, записав в нее число 2.
Затем мы вывели на консоль результат вычитания из содержимого этой переменной
значения 3.
Вот что появилось на консоли после запуска программы:
х = 1, у = -б, z = 17
2 - 3 = 4294967295
В первой строке все правильно — здесь показан результат выполнения арифмети-
арифметических действий. Что же касается второй строки, то в результате вычитания из числа 2
числа 3 мы получили ни много ни мало... 4 294 967 295!
В чем причина ошибки?
Все дело в том, что мы попытались использовать в операции вычитания перемен-
переменную без знака uX. Вспомните, что для представления знака числа используется
его старший разряд. Если число отрицательное, этот разряд установлен.
Глава 1. Базовые понятия и определения 63
При вычитании из 2 числа 3 должно получиться число -1. В шестнадцатеричном
представлении 32-разрядное число -1 записывается как OxFFFFFFFF. Выполнив пре-
преобразование десятичного числа 4 294 967 295 в шестнадцатеричное, например
при помощи стандартного калькулятора Microsoft Windows, вы сможете убедиться,
что что число как раз равно OxFFFFFFFF.
Но так как тип первого операнда (переменная иХ) — целое число без знака, то отрица-
отрицательный результат вычитания OxFFFFFFFF трактуется как большое положительное число.
Составляя выражения для выполнения математических вычислений, очень важно
правильно выбирать типы переменных, иначе появятся ошибки. Компилятор С#
не сможет обнаружить ошибки подобного рода, потому что ему не известно, с какими
числами (со знаком или без знака) вы собираетесь работать.
Умножение
Для обозначения операции умножения используется символ *. Эта операция выполняется
очевидным образом — результат вычисляется путем перемножения обоих операндов.
Вот пример использования операции умножения:
int first = 2;
int second = 3;
int result = first * second * 2;
Как видите, в операции умножения могут быть задействованы числовые перемен-
переменные и числовые литералы.
Хотя смысл операции умножения и способ применения соответствующего опера-
оператора достаточно понятен, здесь есть свои подводные камни.
Давайте рассмотрим следующую программу (листинг 1.5).
Листинг 1.5. Файл ch01\MathOperators2\MathOperatorsApp2.cs
using System;
namespace Math0perators2
{
class Math0peratorsApp2
f
static void Main(string[] args)
{
int x, y, z;
x = 2;
у = x * 2;
z=x*y*10;
System.Console.WriteLine("x = {0}, у = {1}, z = {2}",
x, y, z) ;
System.Console.WriteLine(27 * 0 = {0}", 127 * 0);
uint uX = 400000000;
System.Console.WriteLine(00000000 * 3 = {0}", uX * 3) ;
64 А В Фролов, Г. В. Фролов. Язык С# Самоучитель
uint uXl = 4000000000;
System.Console.WriteLine(000000000 * 3 = {0}", uXl * 3);
System.Console.ReadLine{);
Получив управление, метод Main выполняет операции умножения, в которых за-
задействованы переменные х, у и z, а также числовые литералы. Смысл выполняемых
действий очевиден, а результат соответствует ожиданиям.
Далее мы выводим на консоль результат умножения числа 127 на 0:
System.Console.WriteLine(27 * 0 = {0}", 127 * 0);
Здесь тоже все понятно — на консоль будет выведено нулевое значение, так как ре-
результат умножения любого числа на 0 будет равен нулю.
А вот теперь мы попробуем умножить на 3 два довольно больших целых числа:
uint uX = 400000000;
System.Console.WriteLine(00000000 * 3 = {0}", uX * 3);
uint uXl = 4000000000;
System.Console.WriteLine(000000000 * 3 = {0}", uXl * 3);
Результат всех вычислений показан ниже:
х = 2, у = 4, z = 80
127 * 0 = 0
400000000 * 3 = 1200000000
4000000000 * 3 = 3410065408
В результате умножения числа 400 000 000 на 3 получилось, как мы и ожидали, значе-
значение 1 200 000 000. Однако та же самая операция над числом 4 000 000 000 привела к не-
неправильному результату — вместо 12 000 000 000 у нас получилось 3 410 065 408!
Где же ошибка?
Ошибка была допущена при выборе типа данных для переменной uXl. Если вы по-
посмотрите на табл. 1.2, то увидите, что в переменной типа uint можно хранить значения,
не превосходящие 4 294 967 295. Мы же попытались записать туда число 12 000 000 000,
почти втрое превосходящее максимально допустимое. В результате произошло перепол-
переполнение и старшие разряды результата умножения оказались потеряны.
Чтоб исправить ошибку, нужно заменить тип uint на ulong, специально предна-
предназначенный для хранения больших чисел. После этого операция умножения будет вы-
выполнена правильно.
Деление
Для обозначения операции деления используется слеш (/ ). В операции участвуют два
операнда — делимое и делитель:
int х = 4 ;
л n t у = 2 ;
int z = х/у;
Глава 1. Базовые понятия и определения 65
3 Язык С# Самоучитель
Если делятся целые числа, то остаток от деления теряется. В результате деления
чисел с плавающей точкой получается число с плавающей точкой. Если один из опе-
операндов является целым числом, а другой — числом с плавающей точкой, то в резуль-
результате получится число с плавающей точкой.
Как и во многах других языках профаммирования, деление на нуль не допускается.
Все вышеперечисленные особенности операции деления демонстрирует програм-
программа, исходный текст которой приведен в листинге 1.6.
Листинг 1.6. Файл ch01\MathOperators3\MathOperatorsApp3.cs
using System;
namespace MathOperatorsApp3
{
class MathOperatorsApp3
{
static void Main(string[] args)
{
System.Console.WriteLineC'6 / 3 = {0}", 6 / 3);
System.Console.WriteLine( / 4 = {0}", 6 / 4);
System. Console. WriteLine ( .0 / 4.0 = {0}1', 6.0 / 4.0);
System.Console.WriteLinef.0 / 4 = {0}", 6.0 / 4);
/*
int x = 5;
int у = 0;
int z = x/y;
*/
System.Console.ReadLine();
Если запустить эту программу на выполнение, она выведет на консоль следующие
строки:
6/3 = 2
6/4 = 1
6.0 / 4.0 = 1,5
6.0 / 4 = 1,5
Обратите внимание, что при целочисленном делении остаток действительно теря-
теряется. Об этом нам говорит вторая строка с результатами деления числа 6 на число 4.
Третья строка представляет результат деления таких же чисел, но в формате с пла-
плавающей запятой. Как видите, теперь в результате вычислений мы получили ожидае-
ожидаемый результат — значение 1,5.
Последняя строка отражает результат деления числа с плавающей точкой на целое
число. Из нее видно, что результат такого деления представляется в виде числа с пла-
плавающей запятой.
66 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Теперь займемся фрагментом программы, расположенным между операторами
комментариев / * и * /:
/*
int х = 5;
int у = 0;
int z = х/у;
*/
Напомним, что компилятор игнорирует все, что находится между этими операторами.
Если убрать операторы комментариев, оттранслировать, а затем запустить про-
программу на выполнение, то на экране появится сообщение об ошибке следующего вида:
System.DivideByZeroException has occurred in MathOperatorsApp3.exe
В сообщении говорится, что при выполнении программы MathOperatorsApp3.exe
произошло так называемое исключение класса System. DivideByZeroException.
Механизм исключений повсеместно применяется в языке С# для обработки оши-
ошибок, и мы рассмотрим его позже в гл. 9. Сейчас же мы только скажем, что состояние
исключения, которое привело к аварийному завершению программы, произошло в ре-
результате деления на нуль.
Механизм обработки исключений, предусмотренный в С#, позволяет перехватить
из программы ошибочную ситуацию и обработать ее соответствующим образом. Об-
Обработчик исключения может, например, выдать осмысленное сообщение на экран,
проигнорировать ошибку или скорректировать поведение программы.
Вычисление остатка при целочисленном делении
Как мы говорили в предыдущем разделе, при целочисленном делении остаток отбра-
отбрасывается. В некоторых случаях, однако, программисту нужен именно остаток от деле-
деления двух целых чисел, например чтобы узнать, делятся эти числа нацело или нет.
Для вычисления остатка при целочисленном делении в языке С# используется опе-
оператор деления по модулю %. Он применяется таким же образом, что и оператор деле-
деления, но в результате вычисляется не частное, а остаток.
В листинге 1.7 мы представили исходный текст программы, демонстрирующей вы-
выполнение операций целочисленного деления и деления по модулю.
Листинг 1.7. Файл ch01\MathOperators4\MathOperatorsApp4.cs
using System;
namespace MathOperators4
{
class MathOperatorsApp4
{
static void Main(string[] args)
{
System.Console.WriteLine{ / 2 = {0}, 5 % 2 = {1}",
5/2, 5%2);
System.Console.ReadLine();
Глава 1. Базовые понятия и определения 67
Вот что будет введено на консоль после запуска этой программы:
5/2 = 2, 5 % 2 = 1
Если разделить 5 на 2, то частное будет равно двум, а остаток — единице.
Деление по модулю на нуль, так же как и обычное целочисленное деление на нуль,
в языке С# запрещено. Попытка выполнить такое деление приведет к исключению
класса System. DivideByZeroException.
Унарные операторы
До сих пор мы изучали бинарные (binary) операторы, работающие с двумя операнда-
операндами. Однако в языке С# имеется также набор унарных (unary) операторов, имеющих де-
дело только с одним операндом. Набор унарных операторов языка С# аналогичен набору
унарных операторов языков С и C++ (табл. 1.8).
Таблица 1.8. Унарные операторы
Символ
+
-
++
--
i
~
(...)
Оператор
Унарный плюс
Унарный минус
Инкремент
Декремент
Унарное логическое отрицание
Унарная поразрядная операция дополнения
Преобразование типа выражения
Рассмотрим эти операторы.
Унарный плюс
Результатом выполнения унарного оператора плюс над числовым операндом является
значение операнда, т. е. фактически унарный плюс не выполняет никаких действий
над числовыми значениями.
Унарный минус
Унарный минус изменяет знак операнда на противоположный:
int х = -5;
int у = -х;
Здесь в первой строке унарный минус добавлен в качестве префикса к числово-
числовому литералу 5, в результате чего в переменную х будет записано отрицательное
значение -5.
Во второй строке оператор унарного минуса используется для того, чтобы записать
в переменную у значение, равное по модулю значению переменной х, но противопо-
противоположное по знаку.
68 А В. Фролов. Г. В. Фролов. Язык С#. Самоучитель
Инкремент и декремент
Операторы инкремента ( + +) и декремента (--) могут использоваться либо в пре-
префиксной форме, либо в постфиксной форме. Префиксная форма предполагает разме-
размещение оператора перед операндом, а постфиксная — после операнда.
Вот примеры использования постфиксного оператора + + :
int х = 5;
В результате выполнения этого фрагмента кода переменная х будет содержать зна-
значение 6.
По выполняемым результатам унарный оператор ++, использованный во второй
строке, эквивалентен следующему бинарному оператору:
X = X + 1;
Аналогично вместо унарного оператора х— можно использовать бинарный оператор:
х = х - 1;
Если используется префиксная форма унарного оператора инкремента или декре-
декремента, то содержимое переменной изменяется до ее чтения, а если постфиксная — по-
после. Поясним это на примере.
Ниже мы использовали префиксный оператор инкремента, поэтому вначале содер-
содержимое переменной х увеличивается на единицу, а затем ее новое значение, равное
шести, используется для инициализации переменной у:
int х = 5;
int у = ++х;
При использовании постфиксного оператора инкремента все будет по-другому:
int х = 5;
int у = х++;
Вначале переменная у будет проинициализирована исходным значением, храня-
хранящимся в переменной х, равным пяти. После этого содержимое переменной х увели-
увеличится на единицу.
Таким образом, выполнение любого из приведенных выше фрагментов кода приве-
приведет к тому, что в переменную х будет записано значение 6. Однако содержимое пере-
переменной у будет различным. В первом случае оно будет равно шести, а во втором —
пяти.
Унарное логическое отрицание
Оператор унарного логического отрицания вычисляет значение своего логического
операнда, а затем изменяет его на противоположное значение. Вот пример использо-
использования этого оператора:
bool isBlackColor = true;
bool check = !isBlackColor;
Глава 1. Базовые понятия и определения 69
Здесь мы вначале записали в логическую переменную isBlackColor значение
true, а затем использовали это значение и оператор отрицания для инициализации
логической переменной check. В результате переменная check будет содержать зна-
значение false.
Оператор унарного логического отрицания может использоваться с любым логиче-
логическим выражением, а также с логическими литералами:
bool isBlackColor = !true;
Подробнее о логических выражениях мы поговорим позже, в разделе «Логические
операторы» этой главы.
Унарная поразрядная операция дополнения
Этот оператор вычисляет поразрядное дополнение своего операнда. В результате вы-
выполнения этой операции состояние всех разрядов операнда будет изменено на проти-
противоположное.
Вот пример использования унарной поразрядной операции дополнения:
uint op = 0x5a5a;
uint compl = -op;
Операции с отдельными разрядами мы рассмотрим подробнее позже, в разделе
«Поразрядные операторы».
Преобразование типа выражения
В процессе обработки программы транслятор выполняет явные и неявные преобразо-
преобразования типов данных. Например, в следующей строке перед сложением целочисленное
значение 5 неявным образом преобразуется в тип с плавающей точкой:
long х = 1.4 + 5;
В языке С# имеется унарный оператор, позволяющий выполнять явное преобразо-
преобразование типов данных. Он используется следующим образом:
long х = 1.4 + (longM;
Здесь при помощи конструкции (long) мы указали, что перед сложением необхо-
необходимо преобразовать числовой литерал 5 в литерал с плавающей точкой типа long.
В гл. 5 мы расскажем о преобразованиях типов в С# более подробно.
Пример использования унарных операторов
Исходный текст программы, демонстрирующей использование некоторых унарных
операторов, представлен в листинге 1.8.
Листинг 1.8. Файл ch01\UnaryOp\UnaryOpApp.cs
using System;
namespace UnaryOp
{
class UnaryOpApp
{
static void Main(string[] args>
70 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
int x = 5;
int у = ++Х;
System.Console.WriteLine("x = {0},\ty = {1}", x, y);
int xl = 5;
int yl = xl + +;
System.Console.WriteLine("xl = {0},\tyl = {1}", xl, yl) ;
uint op = 0x5a5a;
uint со = ~op;
System.Console.WriteLine("op = {0:X}, со = {1:X}", op, со);
System.console.ReadLine();
Первые два фрагмента показывают отличия между префиксной и постфиксной
унарной операцией инкремента. Третий фрагмент демонстрирует выполнение унарной
поразрядной операции дополнения. Обратите внимание, что значение операнда (пере-
(переменной ор) задано в шестнадцатеричном виде и равно 0х5а5а. После выполнения
преобразования состояние всех разрядов операнда изменяется на противоположное.
Вот результат работы программы:
х = 6, у =6
xl = б, yl = 5
op = 5А5А, со = FFFFA5A5
Как видите, префиксная и постфиксная операции инкремента не эквивалентны.
Что же касается унарной поразрядной операции дополнения, то она выполнилась так,
как мы ожидали — состояние всех разрядов изменилось на противоположное (или, как
еще говорят, оказалось инвертированным).
Чтобы отобразить результат выполнения операции дополнения в шестнадцатерич-
шестнадцатеричном виде, мы указали в строке формата методу WriteLine спецификатор формата X,
отделив его от номера параметра двоеточием:
System.Console.WriteLine("op = {0:X}, со = {1:X}", op, со);
В табл. 1.9 мы перечислили стандартные символы, которые можно использовать
для определения формата вывода при помощи метода WriteLine.
Таблица 1.9. Стандартные символы формата
Символы
с,
d,
е,
Hi
Я,
С
D
Е
F
G
Тип данных, соответствующий формату
Деньги
Десятичный
Научный (экспоненциальный)
Формат с фиксированной точкой
Общий формат
Глава 1. Базовые понятия и определения 71
Символы
n, N
г, R
X, X
Тип данных, соответствующий формату
Числовой формат
Формат для представления чисел с округлением. Число, преобразованное
в текстовую строку с использованием этого формата, сохранит свое значе-
значение при обратном преобразовании из текстовой строки в число
Шестнадцатеричный формат
Постепенно мы расскажем вам и о других возможностях форматирования тексто-
текстовых строк, отображаемых на консоли методом WriteLine.
Составные операторы
В разделе «Инкремент и декремент» мы рассказывали вам об унарных операторах ин-
инкремента и декремента, позволяющих соответственно увеличивать или уменьшать
на единицу значение операнда.
Использование этих операторов позволяет сделать исходный текст программы бо-
более компактным и легко читаемым. Кроме того, такой код будет выполняться быстрее
из-за оптимизации, выполняемой компилятором.
В языке С# имеется еще целый ряд так называемых составных операторов, при-
применение которых позволяет улучшить читаемость исходного текста программы и уве-
увеличить скорость ее работы.
Составные операторы получаются комбинированием бинарного оператора и опера-
оператора присваивания (табл. 1.10).
Таблица 1.10. Составные операторы
Символ
+=
- =
* =
/ =
% =
&=
| =
<< =
>> =
Оператор
Сложение и присваивание
Вычитание и присваивание
Умножение и присваивание
Деление и присваивание
Вычисление остатка от деления и присваивание
Логическое И и присваивание
Логическое ИЛИ и присваивание
Логическое ИСКЛЮЧАЮЩЕЕ ИЛИ и присваивание
Сдвиг влево и присваивание
Сдвиг вправо и присваивание
Как пользоваться составными операторами?
Предположим, нам нужно увеличить значение, хранящееся в переменной х, на 2.
Это можно сделать при помощи обычного бинарного оператора сложения, например,
так:
int х = 1;
X = х +■ 2 ;
72
А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Если же использовать составной оператор сложения и присваивания, то соответст-
соответствующий код будет выглядеть проще:
х += 2;
Как видите, здесь переменная х упоминается только один раз.
Аналогичным образом можно использовать и другие числовые составные операторы:
X -= 3;
х *= 5;
х \= 2;
х %= 2;
Пока речь идет об изменении значений обычных числовых переменных, преиму-
преимущество использования составных операторов, возможно, не столь очевидно. Однако
если необходимо увеличить или уменьшить поле структуры или элемент массива,
то оно станет намного заметнее. О структурах и массивах мы поговорим позже. Тогда
же мы и вернемся к рассказу о преимуществах составных операторов.
Логические составные операторы и операторы сдвига используются аналогичным
образом. Соответствующие им бинарные поразрядные операторы описаны в следую-
следующем разделе.
Если вы ранее программировали на языке Pascal или другом языке, не допускаю-
допускающем использования рассмотренных выше операторов инкремента, декремента, а также
составных операторов, мы рекомендуем изучить новые возможности.
Хотя на первый взгляд обозначение этих операторов могут выглядеть для вас непонят-
непонятными и нелогичными, время, потраченное на их изучение, не пропадет даром. Эти опера-
операторы действительно помогают создавать более эффективный программный код.
Что же касается непонятного и нелогичного, то взгляните на внешний вид операто-
оператора присваивания языка Pascal, представляющего комбинацию знака равенства и двое-
двоеточия (= :). С точки зрения программиста, работающего с языками С и C++, назначе-
назначение комбинации символов - -. тоже может показаться непонятным и нелогичным —
при чем тут двоеточие?
Поразрядные операторы
Ранее в разделе «Элементарные типы данных» этой главы мы рассказывали вам о бай-
байтах и разрядах. В языке С# имеются так называемые поразрядные операторы, с помо-
помощью которых можно изменять состояние отдельных или всех разрядов значений, хра-
хранящихся в переменных.
Поразрядные операторы предназначены для выполнения операций логическое
И (AND), логическое ИЛИ (OR), логическое ИСКЛЮЧАЮЩЕЕ ИЛИ (XOR) и логи-
логическое отрицание НЕ (NOT), а также операций поразрядного сдвига влево и вправо.
Соответствующие им символы перечислены в табл. 1.11.
Таблица 1.11. Поразрядные операторы
I
Символ
Логическое
Логическое
Логическое
Оператор
И
ИЛИ
ИСКЛЮЧАЮЩЕЕ ИЛИ
Глава 1. Базовые понятия и определения 73
Символ
~
<<
>>
Оператор
Унарная поразрядная операция дополнения
Сдвиг влево
Сдвиг вправо
Расскажем подробнее об этих операциях и соответствующих поразрядных операторах.
Поразрядное логическое И
Операция поразрядное логическое И обозначается символом & и выполняется над
двумя операндами. При этом состояние каждого разряда результата операции зависит
от состояний исходных разрядов обоих операндов.
Если разряды с одинаковыми номерами установлены, то разряд результата с тем же
номером тоже будет установлен. В противном случае разряд результата будет сброшен.
Для наглядности мы показали результат выполнения операции логическое И над
двумя одноразрядными операндами в табл. 1.12.
Таблица 1.12. Выполнение операции логическое И
Первый операнд
0
0
1
1
Второй операнд
0
1
0
1
Результат
0
0
0
1
Как видите, чтобы разряд результата был установлен, разряды первого и второго
операнда должны быть установлены.
На рис. 1.10 мы показали результат выполнения поразрядной операции логическое
И для 2 байтов данных с произвольно установленными разрядами.
о
0 | Первый операнд
Второй операнд
Результат операции
логическое И
Рис. 1.10. Выполнение поразрядной операции И
В байте результата установлены только те разряды, которые установлены и в пер-
первом и во втором операнде.
Поразрядное логическое ИЛИ
Операция поразрядное логическое ИЛИ, как и только что рассмотренная операция И,
выполняется над двумя операндами. Она обозначается символом |. Состояние каждо-
каждого разряда результата операции зависит от состояний исходных разрядов обоих опе-
операндов, но эта зависимость другая.
7
1
1
1
6
1
о
0
ь
1
о
0
4
0
1
0
3
0
1
0
2
1
1
1
1
1
0
0
0
о
0
0
74
А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Если разряды с одинаковыми номерами сброшены, то разряд результата с тем же
номером тоже будет сброшен. В противном случае разряд результата будет установлен
(табл. 1.13).
Таблица 1.13. Выполнение операции логическое ИЛИ
Первый операнд
0
0
1
1
Второй операнд
0
1
0
1
Результат
0
1
1
1
Как видите, чтобы разряд результата был установлен, разряды первого или второго
операнда должны быть установлены. Это же иллюстрирует и рис. 1.11.
7
1
В
1
ь
1
4
0
3
0
2
1
1
1
0
0
1
0
0
1
1
1
0
0
Первый операнд
Второй операнд
Результат операции
логическое ИЛИ
Рис. 1.11. Выполнение поразрядной операции логическое ИЛИ
Поразрядное логическое ИСКЛЮЧАЮЩЕЕ ИЛИ
Операция поразрядное логическое ИСКЛЮЧАЮЩЕЕ ИЛИ работает следующим об-
образом. Если разряды имеют одинаковое состояние (оба сброшены или оба установле-
установлены), то разряд результата с тем же номером будет установлен. В противном случае
разряд результата будет сброшен (табл. 1.14).
Таблица 1.14. Выполнение логической операции ИСКЛЮЧАЮЩЕЕ ИЛИ
Первый операнд
0
0
1
1
Второй операнд
0
1
0
1
Результат
1
0
0
1
Чтобы разряд результата был установлен, разряды первого или второго операнда
должны находиться в одинаковом состоянии (рис. 1.12).
Первый операнд
Второй операнд
Результат операцм логическое
ИСКЛЮЧАЮЩЕЕ ИЛИ
Рис, 1.12. Выполнение поразрядной операции логическое ИСКЛЮЧАЮЩЕЕ ИЛИ
7
1
б
1
ь
л
4
0
3
0
2
1
1
1
0
0
1
0
0
1
1
1
0
°
1
0
0
0
0
1
0
1
Глава 1. Базовые понятия и определения
75
Операцию ИСКЛЮЧАЮЩЕЕ ИЛИ удобно применять в тех случаях, когда нужно
определить, находятся ли разряды двух числовых значений в одинаковых состояниях,
причем не имеет значения, какое это состояние A или 0).
Унарная поразрядная операция дополнения
Унарная поразрядная операция дополнения обозначается символом ~. Она заменя-
заменяет состояние каждого разряда противоположным. Эта операция выполняется только
над одним операндом (табл. 1.15), т. е. она унарная.
Таблица 1.15. Выполнение унарной поразрядной операции дополнения
Операнд
0
1
Результат
1
0
Сказанное иллюстрирует рис. 1.13.
7 6 5 4 3 2 10
|1|1|1|O|O|1|1|O| Операнд
Г 0 I 0 I 0~| 1 I 1 I 0 I 0 I 1 I Результат логического
I 1 1 J 1 1 1 1 1 отрицания НЕ
Рис. 1.13. Выполнение поразрядной операции логическое
отрицание НЕ
Как видно на этом рисунке, после выполнения операции логическое отрицание НЕ
все разряды байта изменили свое состояние.
Поразрядный сдвиг
В языке С# имеются два оператора поразрядного сдвига: << и >>. Первый из них вы-
выполняет поразрядный сдвиг влево, а второй — вправо.
При сдвиге влево старшие разряды теряются (отбрасываются), а на место младших
разрядов записываются нули.
Способ выполнения сдвига вправо зависит от того, над данными какого типа вы-
выполняется операция.
Если операция сдвига вправо выполняется над целыми числами без знака
(byte, uint, ulong), младшие разряды теряются, а на место старших разрядов запи-
записываются нули.
Когда операция сдвига вправо выполняется над целыми числами со знаком
(sbyte, int, long), то для положительных чисел старшие разряды заполняются
нулями, а для отрицательных — единицами. Младшие разряды теряются в любом
случае.
Пример выполнения операции поразрядного сдвига для чисел со знаком и без знака
показан на рис. 1.14.
76 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
1 О
1 Т 0~| Операнд
1
1
-
Г"
)
о
' 6
"У 1
1
' 6
1
о
5
1
1
5
1
1
4
0
1
4
0
1
3
о
1
3
о
1
2
1
1
2
1
0
1
1
^-
0
1
1
0
0
0
0
0
0
0
0
1
1
0
0
0
0
Сдвиг вправо на 3 разряда
(число без знака)
Операнд
Сдвиг вправо на 3 разряда
(число со знаком)
Операнд
Сдвиг влево на 3 разряда
(число без знака)
Рис. 1.14. Выполнение поразрядной операции сдвига
Пример использования поразрядных операторов
Выполнение поразрядных операторов демонстрируется в программе, исходный текст
которой приведен в листинге 1.9. Здесь мы последовательно применяем несколько
различных поразрядных операторов к числу 0х5А5А. В двоичном представлении это
число записывается как 0101101001011010.
Листинг 1.9. Файл ch01\BitWise\BitWiseApp.cs
using System;
namespace Bitwise
{
class BitWiseApp
{
static void Main(string!] args)
uint op = 0x5A5A;
uint result = op & OxA;
System.Console.WriteLine("op
op, result);
result = op | OxF;
System.Console.WriteLine("op
op, result);
result = op Л OxF;
System.Console.Wri teLine("op
op, result);
= {0:X}, op & OxA = {1:X}'
= {0:X}, op | OxF = {1:X}'
= @:X), op Л OxF = {1:X}'
Глава 1. Базовые понятия и определения
77
result = ~op;
System.Console.WriteLine("op = {0:X}, -op = {1:X}",
op, result);
result = op << 3;
System.Console.WriteLine("op = {0:X}, op << 3 = {1:X}",
op, result);
result = op >> 3 ;
System.Console.WriteLine("op = {0:X}, op >> 3 = {1:X}",
op, result);
System.Console.ReadLine();
Результат работы нашей программы представлен ниже:
ор =
ор =
ор =
ор =
ор =
ор =
5А5А,
5А5А,
5А5А,
5А5А,
5А5А,
5А5А,
ор
ор
ор
-ор
ор
ор
& ОхА
| OxF
Л OxF
= А
= 5A5F
= 5А55
= FFFFA5A5
« 3 =
» 3 =
2D2D0
В4В
Мы оставляем вам эту программу для самостоятельного изучения. Представьте ре-
результаты работы поразрядных операторов в двоичном виде и убедитесь, что все дейст-
действия над разрядами исходного числа выполнены правильно.
Логические операторы
Логические операторы (табл. 1.16) предназначены для выполнения логических опера-
операций над логическими данными, объявленными в программе при помощи ключевого
слова boo 1.
Напомним, что логические переменные могут принимать одно из двух значений —
true (истина) или false (ложь). В языке С# эти значения никак не соотносятся
с числами 1 и 0. Результатом выполнения логического оператора всегда является ло-
логическое значение true или false.
Таблица 1.16. Логические операторы
Символ
&&
II
i
Оператор
Логический оператор И
Логический оператор ИЛИ
Унарное логическое отрицание
Если оба операнда логического оператора И равны true, то результатом выполне-
выполнения этой операции будет true. В противном случае результат будет false.
78 А В Фролов, Г. В Фролов. Язык С#. Самоучитель
Если один из операндов логического оператора ИЛИ равен true, то результатом
выполнения этой операции будет true. Если же оба операнда равны false, то и ре-
результат будет тоже равен false.
Вот пример использования логических операторов:
bool op = true;
bool resultl = op && false;
bool result2 = op || false;
bool result3 = lop;
В результате выполнения этих операторов в переменных resultl и result3 бу-
будет храниться значение false, а в переменной result2 — значение true.
Логические операторы широко используются в так называемых условных операто-
операторах, о которых мы расскажем в следующей главе нашей книги.
Операторы отношения
Результат выполнения бинарных операторов отношения (табл. 1.17) представляется
в виде логических значений true или false.
Таблица 1.17. Операторы отношения
Символ
==
! =
<
< =
>
> =
Оператор
Равно
Не равно
Меньше
Меньше или равно
Больше
Больше или равно
Операторы отношения позволяют сравнивать значения переменных и выражений
между собой. Например, результат вычисления выражения 2 < 4 будет true, а ре-
результат вычисления выражения 1 == 2 будет равен false.
Операторы отношения, так же как и рассмотренные в предыдущем разделе логиче-
логические операторы, используются в условных операторах, рассказ о которых мы отложим
до следующей главы.
Приоритеты операторов
В своих программах вы часто будете составлять выражения из нескольких операторов.
Не исключено, что в этих выражениях будут встречаться унарные и бинарные операторы.
Вот пример составного выражения:
int х = 1;
int у = 2;
int result= 2+х*3/5 + у++ / 2;
Глава 1. Базовые понятия и определения 79
Рассматривая такое выражение, трудно сразу сказать, какое значение получит пере-
переменная result после его вычисления. Проблема состоит в том, что далеко не всегда оче-
очевиден порядок выполнения отдельных операторов в составном выражении. Однако в зави-
зависимости от порядка выполнения операторов может получиться разный результат.
Приведем более простой пример.
Пусть нам нужно вычислить значение переменной х после выполнения следующей
строки кода:
int х = 2 + 3 * 4;
Если вычислять это выражение слева направо, то сначала нужно сложить числа 2
и 3, а затем результат умножить на 4. После подобных вычислений значение перемен-
переменной х будет равно 20.
Если же вычислять это выражение справа налево, то сначала нужно умножить 3 на 4,
а затем к результату добавить 2. При этом в переменную х будет записано значение 14.
Чтобы избежать неоднозначности, все операторы в языке С# (как, впрочем,
и в других языках программирования) имеют свой приоритет. Более приоритетные
операторы выполняются в первую очередь.
В приведенном выше примере оператор умножения имеет высший приоритет
по сравнению с оператором сложения, поэтому в результате вычисления получится
значение 14.
При необходимости мы можем задать порядок вычислений в составных выражени-
выражениях подобного рода при помощи скобок, например:
int х = B + 3) * 4;
Здесь мы сообщаем компилятору, что вначале нужно сложить числа 2 и 3, а затем
умножить полученный результат на 4.
Для наглядности мы рекомендуем в сложных выражениях всегда задавать порядок
выполнения операций явным образом. Это поможет избежать ошибок, связанных
с неправильной оценкой приоритетов операторов.
В табл. 1.18 мы привели полную таблицу приоритетов операторов С#. С некоторы-
некоторыми операторами, представленными в ней, вы уже знакомы, а с некоторыми познакоми-
познакомитесь позже.
Таблица 1.18. Приоритеты операторов
Операторы
х.у f (x) а[х] х+ +
new typeof checked
checked
+ - ! ~ ++x --x
* / %
+
<< >>
<><=>= is as
x--
un-
(T)x
Категория
Простые операторы
Унарные операторы
Операторы умножения и деления
Аддитивные операторы
Операторы сдвига
Операторы отношения и проверки типа
80
А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Операторы
= = i =
&
л.
1
&&
11
■р .
= *= /= %= += -=
>>= & = л= =
<< =
Категория
Операторы равенства и неравенства
Поразрядный оператор И
Поразрядный оператор ИСКЛЮЧАЮЩЕЕ
ИЛИ
Поразрядный оператор ИЛИ
Логический оператор И
Логический оператор ИЛИ
Оператор условия
Операторы присваивания
Как видите, по приоритету вначале выполняются простые операторы, затем унар-
унарные, после них — операторы умножения и деления, затем — сложения и сдвига. Опе-
Операторы отношения, равенства и неравенства, а также логические операторы имеют
низкий приоритет. Но самый низкий приоритет отдается операторам присваивания
(простым и составным).
Глава 1. Базовые понятия и определения
81
Глава 2. Управляющие операторы
В языке С# имеется специальный набор операторов, управляющих ходом выполнения
программы. С некоторыми из этих операторов мы уже сталкивались в предыдущей
главе, а с некоторыми нам еще только предстоит познакомиться.
От программ было бы мало проку, если бы они представляли собой набор команд,
выполняющихся только в строго линейной последовательности. Именно так ведут се-
себя все программы, рассмотренные нами в предыдущей главе. Чтобы программы могли
проверять какие-либо условия и на основании результатов этих проверок выполнять
те или иные действия, в любом языке программирования имеются условные операто-
операторы и операторы выбора.
Для выполнения каких-либо повторяющихся действий применяются специальные
итерационные (циклические) операторы.
И наконец, операторы безусловного перехода позволяют программе прервать ис-
исполнение текущей последовательности команд, переключившись на исполнение дру-
другой последовательности команд.
Все эти команды позволяют управлять ходом исполнения строк программы, поэто-
поэтому они и называются управляющими.
В табл. 2.1 мы перечислили некоторые управляющие операторы С# и кратко опи-
описали их применение.
Таблица 2.1. Управляющие операторы С#
Оператор
if
switch
goto
for
while
foreach
do
continue
break
return
Применение
Выполнение строк программы в зависимости от значения логиче-
логического выражения
Оператор выбора. Используется для исполнения того или иного
фрагмента программы в зависимости от значения переменной или
выражения
Оператор безусловного перехода к выполнению новой последова-
последовательности команд
Оператор цикла. Проверка выполнения условия завершения,
а также итерация выполняются в начале цикла
Оператор цикла с проверкой условия завершения, выполняемой
в начале цикла
Оператор цикла для просмотра всех элементов массива или
коллекции
Оператор цикла с проверкой условия завершения, выполняемой
в конце цикла
Выполнение цикла с начала
Прерывание выполнения цикла
Возврат управления из метода
После изучения условных и циклических операторов, а также операторов передачи
управления мы расскажем вам о пустом и составном операторе (блоке).
/ягмлогшиаяп
82
Условный оператор
С помощью условного оператора i f программа может проверить выполнение некото-
некоторого условия и на основании результатов проверки принять решение о выполнении то-
того или иного фрагмента кода.
Мы будем делить условные операторы на простые и вложенные. Кроме того,
мы рассмотрим сокращенную запись условного оператора.
Простой условный оператор
Исходный текст практически любой реальной программы содержит простые
и вложенные условные операторы. Вот внешний вид простого условного оператора i f
в наиболее общем виде:
if(<Выражение>)
«Эператор 1>
[else
<Оператор 2>]
Оператор if может дополнительно содержать необязательную конструкцию else.
Так как эта конструкция необязательная, мы заключили ее в квадратные скобки.
Действует этот оператор следующим образом.
Если логическое <Выражение> равно true, выполняется «Эператор 1>.
В противном случае управление передается оператору <Оператор 2>. Если же кон-
конструкция else не определена, программа продолжает свою работу со строки, распо-
расположенной после оператора if.
Приведем простой пример:
int i = 2 ;
int j = 3;
System.Console.WriteLine("{0} > {1}", i, j);
else
System.Console.WriteLineC{0} < {1}", i, j);
В этом примере для выражения <Выражение> мы использовали оператор отно-
отношения >. Операторы отношения были описаны в гл. 1 в разделе «Операторы отноше-
отношения».
Если содержимое переменной i больше содержимого переменной j, то в действие
вступит строка, расположенная сразу после оператора if, а если меньше, то строка,
расположенная сразу после оператора else.
В результате работы этого фрагмента программы на консоли будет отображено
правильное неравенство 2 < 3.
В предыдущем примере в зависимости от результата проверки условия мы выполняли
одно из двух выражений, ограниченных символом точка с запятой. Если нужно выполнять
не одно, а несколько таких выражений, следует использовать фигурные скобки:
Глава 2. Управляющие операторы 83
int i = 2;
int j = 3;
if(i != 0)
{
float x = (float)j / i;
System.Console.WriteLine("{0} / {1} = {2}", j, i, x) ;
}
Здесь мы заключили в фигурные скобки две строки программы с выражениями, ко-
которые должны выполняться, если переменная i не равна нулю.
В первой строке нашего фрагмента программа делит переменную j на перемен-
переменную i, записывая результат в переменную х. Обратите внимание, что перед делением
мы явным образом преобразуем тип переменной j из целого числа в число с плаваю-
плавающей запятой. Если этого не сделать, в результате целочисленного деления пропадет
остаток от деления.
Вложенный условный оператор
Условные операторы допускается вкладывать друг в друга без ограничений. В резуль-
результате можно проверять довольно сложные условия.
Рассмотрим, например, следующий фрагмент программы:
int i = 2;
int j = 3;
if((i - j) * 2 > 0)
System.Console.WriteLine("({0} - {1}) * 2 > 0", i, j);
else
{
System.Console.WriteLine("({0} - {1}) * 2 <= 0", i, j);
bool f;
f = (i == i) ;
if (f)
{
System.Console.WriteLine("i == i");
Здесь мы вначале проверяем значение выражения (i-j ) *2 > 0. Учитывая ини-
инициализацию переменных i и j, это выражение должно быть ложно, т. е. равно false.
Далее в дело включается конструкция else.
В зависимости от знака выражения на консоли отображается одна из двух строк.
В нашем случае выражение имеет отрицательный знак (оно равно -2), поэтому на кон-
консоль будет выведено.
B - 3) * 2 <= 0
Второй оператор i f выполняется только в том случае, если выражение ( i - j ) * 2
меньше или равно нулю. В этом операторе анализируется содержимое логической пе-
переменной f.
84 А. В Фролов, Г. В. Фролов. Язык С# Самоучитель
Если эта переменная равна true, то на консоль выводится текстовая строка, пока-
показанная ниже:
i == i
Переменная f вычисляется следующим образом:
f = (i == i);
Здесь в переменную f записывается результат операции сравнения переменной i
с самой собой. Он всегда равен true.
Вы можете испытать описанные выше приемы использования простых и вложенных
условных операторов на программе, исходный текст которой приведен в листинге 2.1.
Листинг 2.1. Файл chO2\lfElse\lfElseApp.cs
using System;
namespace IfElse
{
class IfElseApp
{
static void Main(string[] args)
{
int i = 2;
int j = 3;
if(i > j)
System.Console.WriteLine("{0} > {1}", i, j ) ;
else
System.Console.WriteLine("{0} < {1}", i, j);
if (i != 0)
{
float x = (float)j / i;
System.Console.WriteLine("{0} / {1} = {2}", i, j, x);
if((i - j) * 2 > 0)
System.Console.WriteLine("({0} - {1}) * 2 > 0", i, j);
else
{
System.Console.WriteLine("({0} - {1}) * 2 <= 0", i, j)
bool f;
f = (i == i) ;
if (f)
{
System.Console.WriteLine("i == i");
System.Console.ReadLine();
Глава 2. Управляющие операторы 85
Проведите над ней ряд экспериментов. Попытайтесь, например, изменить началь-
начальные значения переменных, а также логические операторы, заданные в качестве выра-
выражений для оператора i f.
Оператор проверки
В языке С# существует упрощенный вариант условного оператора, называемый опе-
оператором проверки или тернарным оператором.
В общем виде этот оператор выглядит так:
(<Выражение>) ? <Оператор 1> : <Оператор 2>
Если значение выражения <Выражение> истинно, вычисляется оператор
«Эператор 1>, а если ложно — оператор <Оператор 2>.
Вот простой пример использования оператора проверки:
int i = (х > 100): 8 ? 9;
Здесь если значение переменной х больше 100, переменной i присваивается значе-
значение 8, а если меньше, то 9.
Множественный выбор
Очень часто перед программистом встает задача последовательной проверки несколь-
нескольких условий, причем при выполнении каждого условия нужно предпринимать какие-
то действия. Кроме того, отдельно нужно обрабатывать ситуацию, когда ни одно
из проверяемых условий не выполнено.
Для решения этой задачи можно использовать вложенные условные операторы i f
следующего вида:
if(<Выражение1>)
«Эператор 1>
else if(<Выражение2>)
«Элератор 2>
else if(<ВыражениеЗ>)
«Эператор 3>
else
<Оператор N>
Здесь вначале вычисляется <Выражение1>. Если оно истинно (т. е. равно true),
то выполняется «Эператор 1>. В противном случае вычисляется <Выражение2>.
Если оно истинно, выполняется <Оператор 2>, и т. д. В том случае, когда все про-
проверяемые выражения ложны, выполняется <Оператор N>.
Использование вложенных операторов i f для множественного выбора демонстри-
демонстрируется в программе, исходный текст которой приведен в листинге 2.2.
86 А. В. Фролов, Г В Фролов. Язык С#. Самоучитель
Листинг 2.2. Файл chO2\SelectNumbers\SelectNumbersApp.cs
using System;
namespace SelectNumbers
{
class SelectNumbersApp
{
static void Main(string[] args)
{
System.Console.Write("Введите произвольное число: ");
string inputString;
inputString = System.Console.ReadLine();
int inputNumber;
try
{
inputNumber = System.Convert.Tolnt32(inputString);
}
catch (System.ArgumentNullException)
{
System.Console.WriteLine("ОШИБКА : пустая строка");
inputNumber = 0;
}
catch (System.FormatException)
{
System.Console.WriteLine("ОШИБКА : неправильный формат")
inputNumber = 0;
}
catch (System.OverflowException)
{
System.Console.WriteLine("ОШИБКА : переполнение");
inputNumber = 0;
System.Console.WriteLine("Вы ввели число {0}", inputNumber)
if(inputNumber > 10 && inputNumber <= 100)
System.Console.Write(
"Это число больше 10, но меньше или равно 100");
else if (inputNumber > 100)
System.Console.Write("Это число больше 100");
else if (inputNumber == 0)
System.Console.Write("Это число равно нулю");
else
System.Console.Write("Это число меньше нуля");
System.Console.ReadLine();
Глава 2. Управляющие операторы 87
В эту программу мы внесли много нововведений, из-за чего ее исходный текст может
показаться вам довольно сложным. Однако мы рассмотрим все новшества достаточно
подробно, чтобы у вас не оставалось неясностей относительно назначения всех ее строк.
Главное отличие этой программы от всех предыдущих, описанных в нашей книге,
в том, что она не только выводит данные на консоль, но и вводит числа с клавиатуры.
Прежде всего с помощью метода Write наша программа сообщает пользователю
о том, что он должен ввести число:
System.Console.Write("Введите произвольное число: ");
Здесь вместо метода WriteLine мы использовали метод Write. Отличие между
ними заключается в том, что метод WriteLine после вывод строки на консоль авто-
автоматически переходит на новую строку, а метод Wr i te этого не делает.
Для ввода мы использовали известный вам метод ReadLine, однако теперь мы
сохраняем полученное от него значение в строковой переменной inputString:
string inputString;
inputString = System.Console.ReadLine();
Как это работает?
Получив управление, метод ReadLine приостанавливает работу программы до тех пор,
пока пользователь не введет с клавиатуры текстовую строку, завершив ввода при помощи
клавиши Enter. Далее символы введенной строки сохраняются в переменной inputString.
Нам, однако, нужно вводить не любые текстовые строки, а только такие, которые пред-
представляют целые числа. Чтобы преобразовать текстовую строку в число, нужно вызвать
один из методов класса Convert, определенного в пространстве имен System.
Для преобразования текстовой строки в 32-разрядное целое число со знаком ис-
используется метод Tolnt32:
int inputNumber;
inputNumber = System.Convert.Tolnt32(inputString);
В качестве параметра мы передаем преобразуемую текстовую строку этому методу,
а назад получаем искомое число.
Преобразование текстовой строки в числа и данные других типов можно выпол-
выполнить при помощи методов класса Convert, перечисленных в табл. 2.2.
Таблица 2.2. Методы класса Convert для преобразования строк в числа
Метод
ToBoolean
ToChar
ToSbyte
ToByte
Tolntl6
Tolnt32
Tolnt:64
Тип числовых
данных
bool
char
sbyte
byte
short
int
long
Метод
ToUintl6
ToUInt32
ToUInt64
ToSingle
ToDouble
ToDeciraal
Tun числовых
данных
ushort
uint
ulong
float
double
decimal
88
А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Применяя эти методы, вы сможете вводить с клавиатуры числовые, логические
и символьные значения. В частности, для преобразования строки в число типа int
нужно использовать метод Tolnt3 2, что мы и сделали в своей программе.
Однако зададим себе вопрос, что будет, если ввести такую строку, которая
не может быть преобразована в число, а затем передать ее методу Tolnt32.
При этом возникнет ошибочная ситуация. Метод Tolnt3 2 не сможет выполнить
преобразование и передаст (или, как еще говорят, возбудит) исключение.
Исключениям мы посвятим отдельную главу нашей книги. Сейчас мы только заме-
заметим, что при возникновении исключения нормальный ход выполнения программы на-
нарушается. Если в программе не предусмотрен обработчик исключения, она завершит
свою работу с сообщением об ошибке.
В процессе преобразования текстовой строки в целое число могут возникать ошиб-
ошибки разных типов. Например, пользователь может ввести вместе с цифрами (или вместо
цифр) буквы, ввести слишком большое число или число с ошибочным форматом (на-
(например, вместо целого числа ввести число с плавающей запятой). Наконец, пользова-
пользователь может вообще не ввести ничего, просто нажав в ответ на приглашение клавишу
Enter.
Для того чтобы различить ошибочные ситуации, метод Tolnt32 в каждом случае
возбуждает исключения разных типов (классов). В своей программе мы предусмотре-
предусмотрели обработку таких исключений:
try
{
inputNumber = System.Convert.Tolnt32(inputstring);
}
catch (System.ArgumentNullException)
{
System.Console.WriteLine("ОШИБКА : пустая строка");
inputNumber = 0;
}
catch (System.FormatException)
{
System.Console.WriteLine("ОШИБКА : неправильный формат");
inputNumber = 0;
}
catch (System.OverflowException)
(
System.Console.WriteLine("ОШИБКА : переполнение");
inputNumber = 0;
}
Вызов метода Tolnt32, во время работы которого может произойти исключение,
мы заключили в блок try.
Вслед за блоком try мы расположили несколько блоков catch, каждый из кото-
которых обрабатывает ошибку своего класса и выводит сообщение об ошибке на консоль.
Кроме того, все обработчики исключений принудительно записывают в переменную
inputNumber нулевое значение.
Глава 2 Управляющие операторы 89
Конечно, представленный здесь способ обработки ошибок далек от совершенства,
однако мы только продемонстрировали общий принцип. Детальный разговор о том,
как обрабатывать ошибки в С#, мы отложим до гл. 9, посвященной исключениям.
Но вот, наконец, мы можем вернуться к нашему вложенному оператору if, ис-
использованному для множественного выбора:
if(inputNumber > 10 && inputNumber <= 100)
System.Console.Write(
"Это число больше 10, но меньше или равно 100");
else if (inputNumber > 100)
System.Console.Write("Это число больше 100");
else if (inputNumber == 0)
System.Console.Write("Это число равно нулю");
else
System.Console.Write("Это число меньше нуля");
Ниже мы привели выражение, которое вычисляется в самом начале работы вло-
вложенного оператора i f:
inputNumber > 10 && inputNumber <= 100
Как видите, в этом выражении есть два оператора отношения: > и <=, а также один
логический оператор И, обозначенный символами &&. Выражение проверяет, лежит ли
введенное значение в интервале ] 10, 100].
При обработке оператора && в соответствии с приоритетами операторов вначале
вычисляется значение выражения inputNumber > 10.
Если оно ложно, то все выражение будет ложно. В этом случае выражение
inputNumber <= 100 не вычисляется, так как в этом нет никакого смысла.
Если же выражение, расположенное слева от оператора &&, истинно, то вычисляет-
вычисляется выражение inputNumber <= 100. В том случае, когда оба эти выражения ис-
истинны, наша программа выводит на консоль сообщение о том, что введенное число
больше 10, но меньше или равно 100.
В том случае, когда введенное число не принадлежит интервалу ]10, 100], срабаты-
срабатывает первый блок else. Внутри ее находится еще один, вложенный оператор if, про-
проверяющий условие inputNumber > 100. Далее аналогичным образом с помощью
оператора == мы сравниваем введенное число с нулем.
В том случае, если ни одно из наших условий не выполнено, в дело вступает по-
последний блок else. Внутри его находится строка программы, отображающая на кон-
консоли сообщение о том, что введенное число меньше нуля.
Применение логических операций
В предыдущей программе для определения принадлежности значения переменной
inputNumber интервалу ]10, 100] мы использовали следующую конструкцию с ло-
логическим оператором И:
if(inputNumber > 10 && inputNumber <= 100)
System.Console.Write(
"Это число больше 10, но меньше кли равно 100");
90 А В Фролов, Г В Фролов. Язык С#. Самоучитель
Аналогичного результата можно было бы добиться при помощи вложенного опера-
оператора if:
if(inputNumber > 10)
if(inputNumber <= 100)
{
System.Console.Write(
"Это число больше 10, но меньше или равно 100");
}
Если проверяется сложное условие, логические операторы позволят сократить ис-
исходный текст программы и сделать его нагляднее. Список логических операторов мы
привели в гл. 1 (табл. 1.16).
Интерпретация выражений, содержащих большое количество операторов отноше-
отношений и логических операторов, может вызывать определенные трудности. Чтобы не за-
запутаться с приоритетами операций, используйте круглые скобки, например:
if(((inputNumber > 10) && (inputNumber <= 100)) ||
!((х > 20) && (х <= 200)))
Здесь тело оператора if будет выполнено в том случае, если число inputNumber
принадлежит интервалу ]10, 100] или число х не принадлежит интервалу ]20, 200].
Наличие скобок никак не скажется на скорости компиляции или исполнения про-
программы, но позволит улучшить ее читаемость и в конечном счете облегчит отладку.
Оператор выбора
Выше в разделе «Множественный выбор» мы приводили пример программы, которая вы-
выполняла те или иные действия в зависимости от значения числа, введенного с клавиатуры.
Задача выбора одного действия из нескольких по результатам вычисления какого-
либо выражения возникает очень часто, и в языке С# предусмотрен специальный опе-
оператор выбора switch, предназначенный для ее решения. Этот оператор используется
для исполнения того или иного фрагмента программы в зависимости от значения пе-
переменной или выражения.
Ниже мы показали оператор switch в общем виде (квадратными скобками отме-
отмечены необязательные конструкции оператора):
switch(<Переключающее выражение>)
{
case <Выражение-константа1>:
<Оператор 1>
[<Оператор перехода>]
[case <Выражение-константа2>:
<Оператор 2>
[<Оператор перехода>]]
Глава 2. Управляющие операторы 91
[default:]
<Оператор N>]
}
Оператор switch выполняется следующим образом.
Вначале вычисляется значение выражения <Переключающее выражение>, сле-
следующего за ключевым словом switch. Это выражение должно быть целочисленным
(со знаком или без знака), символьным char либо строковым string. Оно также
может быть перечислением enum, созданным на базе значений указанных типов.
Результат вычислений сравнивается с выражениями-константами блоков case (на-
(называемых также метками case). В случае совпадения значения управление передает-
передается оператору соответствующего блока case. Если совпадений не обнаружено, управ-
управление передается оператору «Эператор N> блока default.
Операторы, расположенные в блоках case и default, можно заключать в фигур-
фигурные скобки, хотя это и не обязательно.
По сравнению с вложенным оператором if оператор выбора позволяет создавать
более эффективные программы, так как компилятор соответствующим образом опти-
оптимизирует создаваемый код.
В частности, для ускорения работы оператора создается специальная таблица пере-
переходов, каждый из которых соответствует своему блоку case. Вычислив условие, про-
программа сразу получает из таблицы информацию, необходимую для исполнения соот-
соответствующего блока case или блока default. Что же касается вложенного операто-
оператора if, то он последовательно сравнивает полученный результат со всеми констант-
константными выражениями, а на это требуется больше времени.
Примеры применения оператора выбора
Вот простейший пример использования оператора switch для выбора одной из не-
нескольких строк, отображаемых на консоли. Выбор делается в зависимости от значения
переменной i:
int i = 2;
switch(i)
{
case 1:
System.Console.WriteLine("case 1") ;
break;
case 2:
{
System.Console.WriteLine("case 2") ;
break;
}
case 3:
f
System.Console.WriteLine("case 3");
break;
)
92 А В Фролов, Г В. Фролов Язык С# Самоучитель
default:
{
System.Console.WriteLine("default");
break;
Так как содержимое переменной i равно двум, на консоли всегда будет отобра-
отображаться строка «case 2».
Обратите внимание на оператор перехода break, расположенный в конце каждого
блока case, а также в конце блока default. Этот оператор завершает выполнение строк
программы текущего блока и передает управление вниз, за пределы оператора switch.
В листинге 2.3 мы привели исходный текст программы, которая вводит текстовую
строку с клавиатуры и сравнивает ее с одной из трех строк. При совпадении програм-
программа выводит на консоль соответствующее сообщение. Если совпадений нет, выводится
сообщение об ошибке.
Листинг 2.3. Файл ch02\SwitchOp\SwitchOpApp.cs
using System;
namespace SwitchOp
{
class SwitchOpApp
{
static void Main(string[] args)
{
System.Console.Write(
^"Введите одну из строк ""мама"", ""мыла"", ""раму"" : ");
string inputString;
inputString = System.Console.ReadLine();
switch{inputString)
{
case "мама":
System.Console.WriteLine(@"Вы ввели строку ""мама""");
break;
case "мыла":
System.Console.WriteLine(@"Вы ввели строку ""мыла""");
break;
case "раму":
System.Console.WriteLine(@"Вы ввели строку ""раму""");
break;
default:
System.Console.WriteLine("Ошибка при вводе");
break;
System.Console.ReadLine(
Глава 2. Управляющие операторы 93
В отличие от предыдущего примера здесь в качестве переключающего выражения
используется не число, а текстовая строка.
Объединение меток case
В некоторых случаях требуется выполнять одну и ту же обработку для разных значе-
значений выражения, расположенного после ключевого слова switch. В языке С# это
можно сделать следующим образом:
int i = 2;
switch(i)
{
case 1:
case 2:
{
System.Console.WriteLine("case 1 или 2") ;
break;
}
case 3:
{
System.Console.WriteLine("case 3");
break;
}
default:
{
System.Console.WriteLine("default");
break;
Здесь мы объединили вместе метки case 1 и case 2, определив одинаковую об-
обработку для значений переменной i, равных единице или двум. В любом из этих слу-
случаев на консоли будет отображаться строка «case 1 или 2».
Пропущенный break
Тем из вас, кто раньше составлял программы на языках С и C++, может показаться,
что единственное отличие оператора switch в языке С# заключается в возможности
использовать в качестве переключающего выражения текстовые строки. Однако есть
и еще одно важное отличие, на которое нам хотелось бы обратить внимание.
Рассмотрим следующий фрагмент программы:
// Эта программа транслироваться не будет!
int i = 1;
switch (i)
{
case 1:
System.Console.WriteLine("case 1"); // Ошибка!
case 2:
{
System. Console .WriteLine ( "case 2"),- // Ошибка!
}
94 А. В. Фролов, Г В. Фролов. Язык С# Самоучитель
case 3:
{
System.Console.WriteLine("case 3") ;
break;
}
default:
{
System.Console.WriteLine("default");
break;
Как видите, здесь в два первых блока case мы намеренно «забыли» вставить опе-
операторы break. К чему это приведет?
Если бы программа была написана на языке С или C++, на консоль были бы после-
последовательно выведены 3 строки:
case 1
case 2
case 3
При отсутствии оператора break в программе С или C++ управление передается
на следующий блок case или default, а не за пределы оператора switch. Эта осо-
особенность применялась многими программистами в тех случаях, когда нужно было со-
совместить обработку, выполняемую в разных блоках case.
В языке С# такое совмещение не допускается. Вы можете объединять метки,
но конструкция, приведенная выше, компилироваться не будет.
Причина этого заключается в том, что разработчики программ С или C++ часто
пропускали оператор break по ошибке, а вовсе не потому, что это было необходимо
для реализации логики работы программы. Компилятор С# пресекает такие ошибки
еще до того, как они смогут себя проявить во время работы программы.
Итерационные операторы
Итерационные операторы применяются в программах С# для выполнения каких-либо
повторяющихся действий, т. е. для организации циклов. Иногда эти операторы назы-
называются циклическими.
В этом разделе мы расскажем об использовании итерационных операторов for,
while, do и f oreach.
Оператор for
Оператор for предназначен для повторного выполнения оператора или группы опера-
операторов заданное количество раз. Вот как выглядит этот оператор в общем виде:
for ( [Инициализация]; [Условие];[Приращение])
<Оператор>
Оператор [Инициализация] выполняется один раз перед началом цикла.
Глава 2. Управляющие операторы 95
Перед каждой итерацией (т.е. перед каждым выполнением тела цикла «Эпера-
тор>) проверяется [Условие]. И наконец, после каждой итерации выполняется опе-
оператор [Прирашение].
Как правило, в цикле имеется переменная, играющая роль так называемой пере-
переменной цикла. При каждой итерации переменная цикла изменяет свое значение в за-
заданных пределах.
Начальное значение переменной цикла задается в программе до оператора for или
в операторе [Инициализация]. Предельное значение переменной цикла определяется
оператором приращения, а проверка ее текущего значения — в блоке [ Условие ].
Поясним сказанное на простом примере.
int i ;
forfi = 0; i < 10; i++)
{
System.Console.Write("{0} ",i ) ;
}
Здесь переменная i используется в качестве переменной цикла. Перед началом
цикла ей присваивается нулевое значение. Перед каждой итерацией содержимое пере-
переменной i сравнивается с числом 10. Если i меньше 10, тело цикла выполняется один
раз. В тело цикла мы поместили вызов метода Write, отображающий текущее значе-
значение переменной цикла на консоли.
После выполнения тела цикла значение i увеличивается на единицу в блоке при-
приращения. Далее переменная цикла вновь сравнивается с числом 10. Когда значение i
превысит 10, цикл завершится.
Таким образом, параметр цикла анализируется перед выполнением тела цикла,
а модифицируется после его выполнения.
Вот что выведет на консоль приведенный выше фрагмент программы:
0123456789
Прерывание цикла
С помощью оператора break можно в любой момент прервать выполнение цикла.
Например, в следующем фрагменте программы мы прерываем работу цикла, когда
значение переменной i становится больше пяти:
for(i = 0; i < 10;
if(i > 5)
break;
System. Console. Write (" {0} "',i );
В результате на консоль будут выведены цифры от 0 до 5:
12 3 4 5
96 А В. Фролов, Г В. Фролов ЯзыкС# Самоучитель
Оператор break в паре с условным оператором может заменить блок проверки ус-
условия в операторе for:
for (i = 0;; i + +)
{
if(i > 10)
break;
System.Console.Write(0} ",i ) ;
}
Как видите, здесь пропущена проверка условия.
Далее можно опустить все блоки оператора for, выполняя инициализацию, про-
проверку условия, а также итерацию самостоятельно:
int i = 0;
for(;;)
{
if(i > 10)
break;
System.Console.Write("{0} ", i );
Это позволяет реализовывать любую необходимую логику циклической обработки.
Например, можно изменять значение переменной цикла перед итерацией, а не после
нее:
for(i = 0; i > 10;)
{
i + + ;
System.Console.Write("{0} ",i );
}
При создании цикла вам обязательно нужно предусмотреть условие его заверше-
завершения. Если же этого не сделать, цикл будет выполняться бесконечно. Программа при
этом будет работать вхолостую на одном месте, или, как еще говорят, «зациклится».
Вот пример цикла, из которого нет выхода:
for{i =0;;) // Зацикливание!
{
i++;
System.Console.Write("{0} ",i );
}
Здесь мы не предусмотрели проверку значения переменной цикла, поэтому про-
программа будет постоянно выводить на консоль возрастающие значения, пока вы не пре-
прервете ее работу. Кстати, это можно сделать, нажав комбинацию клавиш Control-C или
закрыв консольное окно.
Возобновление цикла
В отличие от оператора break, прерывающего цикл, оператор continue позволяет
возобновить выполнение цикла с самого начала.
Глава 2. Управляющие операторы 97
4 ЯзыкС» Самоучитель
Вот как он используется:
for(i = 0;;
System.Console.Write("{0} ",i ) ;
if(i < 9)
continue;
else
break;
}
Если в ходе выполнения цикла значение переменной i не достигло девяти, цикл
возобновляет свою работу с самого, начала (т. е. с вывода значения переменной цикла
на консоль). Когда указанное значение будет достигнуто, выполнение цикла прервется
оператором break.
Оператор while
Оператор while проверяет условие завершения цикла перед выполнением тела цикла:
i = 0;
while(i < 10)
{
System.Console.Write("{0} ",i ) ;
В отличие от оператора for оператор while никак не изменяет значения пере-
переменной цикла, поэтому мы должны позаботиться об этом сами.
Перед тем как приступить к выполнению цикла, мы устанавливаем начальное зна-
значение параметра цикла i, равное нулю. После выполнения тела цикла мы сами изме-
изменяем значение параметра цикла, увеличивая его на единицу.
Цикл будет прерван, как только значение переменной i превысит 10.
В цикле while можно использовать описанные ранее операторы прерывания цик-
цикла break и возобновления цикла continue.
Следующий цикл будет выполняться бесконечно:
while(true)
{
System.Console.Write("{0} ",i );
Еще раз напоминаем вам, что нужно всегда предусматривать возможность выхода
из цикла.
Оператор do
Оператор do используется вместе с ключевым словом while. При этом условие за-
завершения цикла проверяется после выполнения его тела:
98 А В. Фролов. Г. В. Фролов. Язык С#. Самоучитель
i = 0;
do
{
System.Console.Write("{0} ",i );
i + +;
} while(i < 10) ;
В этом цикле мы сами устанавливаем начальное значение параметра цикла i и са-
сами его изменяем, увеличивая на единицу. Как только это значение достигнет 10, цикл
будет прерван.
Аналогично циклу while цикл do допускает прерывание оператором break
и возобновление оператором continue.
Оператор foreach
Для обработки таких типов данных, как массивы и контейнеры, язык С# предлагает
очень удобный оператор foreach, для которого нет аналога в языках программиро-
программирования С и C++.
Так как изучение этих типов данных еще впереди, в этой главе мы дадим упрощен-
упрощенное определение массива и приведем простейший пример программы, работающей
с массивами при помощи операторов for и foreach.
Итак, мы будем считать массив набором упорядоченных объектов, каждый из ко-
которых имеет свой номер, или индекс. Первый элемент массива имеет индекс 0, вто-
второй — 1 и т. д.
Массив целых чисел со знаком объявляется следующим образом:
int[] nums;
Пара квадратных скобок указывает на то, что переменная nums является массивом.
Прежде чем пользоваться массивом, его необходимо создать, указав максимальное
количество объектов, которые могут в нем храниться. Вот как объявляется и создается
массив переменных типа int:
int[] nums;
nums = new int[10];
Созданный массив инициализируется значениями по умолчанию. Для числовых
массивов в качестве такого значения используется 0.
Чтобы записать в элемент массива с заданным номером какое-либо значение, необ-
необходимо указать индекс этого элемента в квадратных скобках.
Ниже мы в цикле инициализируем массив, записывая в его элементы числа от 0
до 9, причем в нулевой элемент массива записывается значение 0, в первый — значе-
значение 1 и т. д.:
for(i =0; i < 10; i++)
nums[i] = i;
Глава 2. Управляющие операторы 99
Чтобы отобразить содержимое всех ячеек массива, можно использовать обычный
цикл for:
for(i = 0; i < 10;
System.Console.Write("{0} ", nums(i]);
Здесь мы последовательно выводим на консоль все значения, хранящиеся в массиве.
Хотя на первый взгляд этот способ обработки всех элементов массива достаточно
прост, ему присущи некоторые недостатки. Например, нам нужно объявлять и ини-
инициализировать переменную цикла (применяемую в роли индекса массива), а затем
увеличивать ее значение при каждой итерации. При этом нужно следить, чтобы значе-
значение переменной цикла не превысило размер массива, иначе возникнет исключение.
Оператором f oreach пользоваться намного проще:
foreach(int current in nums)
System.Console.Write("{0} ", current);
В скобках после ключевого слова f oreach мы объявляем переменную current
типа int, которой при каждой итерации будут последовательно присваиваться все
значения массива nums. Имя этого массива указывается после ключевого слова in.
Таким образом, нам не нужна переменная цикла и, следовательно, не нужно ее
инициализировать, инкрементировать и проверять, не вышло ли значение индекса
массива за допустимые пределы.
Оператор foreach сделает все за нас. Он последовательно присвоит значение
всех элементов массива переменной current, а нам останется только выводить при
каждой итерации значение этой переменной на консоль.
Пример использования итерационных операторов
Описанные выше приемы использования итерационных операторов демонстрирует
программа, исходный текст которой представлен в листинге 2.4.
Листинг 2.4. Файл ch02Wteration4terationApp.cs
using System;
namespace Iteration
{
class IterationApp
{
static void Main(string[] args)
{
System.Console.Write("Цикл for\n");
int i ;
for(i = 0; i < 10; i++)
{
System. Console. Write (" {0} ",i ); ■•
100 А В Фролов, Г. В. Фролов. Язык С# Самоучитель
System.Console.WriteLine("\п\пЦикл for (вариант 2)")
for(i = 0; i < 10;
if(i > 5)
break;
System.Console.Write(" {0} " , i ) ;
System.Console.WriteLine("\п\пЦикл for (вариант 3)")
for (i = 0;; i++)
{
System.Console.Write("{0} ", i );
if(i < 9)
continue;
else
break;
System.Console.WriteLine("\п\пЦикл while");
i = 0;
while(i < 10)
{
System.Console.Write("{0} ",i );
System.Console.WriteLine("\п\пЦикл do");
i = 0;
do
{
System.Console.Write("{0} ",i );
i + +;
} while(i < 10);
System.Console.WriteLine("\п\пЦикл foreach");
int[] nums;
nums = new int[10];
for(i = 0; i < 10; i++)
nums [ i] = i;
foreach(int current in nums)
System.Console.Write("{0} ", current);
System.Console.ReadLine();
Глава 2 Управляющие операторы 101
Так как в ней используются описанные ранее фрагменты кода, то мы оставляем вам
эту программу для самостоятельного изучения. Экспериментируя с программой, по-
попытайтесь изменять параметры цикла, условия выхода и т. д.
При исследовании приемов работы с массивом попытайтесь заставить программу
выйти за его пределы, указав недопустимое значение индекса. Посмотрите, какого ти-
типа исключение при этом произойдет.
На протяжении нашей книги мы будем приводить и другие примеры использования
итерационных операторов. А сейчас займемся операторами безусловного перехода.
Операторы безусловного перехода
Как мы уже говорили, операторы безусловного перехода предписывают программе
прервать нормальный ход своей работы и перейти к исполнению другой последова-
последовательности команд.
Операторы break и continue
Рассказывая об операторе выбора switch, мы описали один такой оператор безус-
безусловного перехода, а именно оператор break. Напомним, что этот оператор завершает
выполнение команд текущего блока case или default, после чего передает управ-
управление вниз за пределы оператора switch.
В итерационных операторах for, while и do оператор break применяется вме-
вместе с другим оператором безусловного перехода continue. В то время как оператор
break прерывает цикл и передает управление вниз за пределы цикла, оператор
continue вызывает выполнение новой итерации цикла.
Оператор return
Еще одна команда безусловного перехода return будет описана позже. Она прерыва-
прерывает выполнение текущего метода и может дополнительно вернуть в вызывающий метод
определенное значение.
Оператор goto
Среди разработчиков языков программирования и программистов, наверное, наи-
наибольшее количество споров вызывает команда безусловной передачи управления
на метку goto. Эта команда присутствует практически во всех языках программиро-
программирования, кроме тех, которые пропагандируют так называемый «чистый» структурный
подход к программированию.
О чем здесь идет речь и почему команда goto вызывает споры?
Организация цикла с помощью goto
Чтобы понять, для чего в первых языках программирования был создан оператор
goto, представим себе, что нам нужно создать цикл, но в нашем распоряжении нет
ни одного итерационного оператора, такого, как for, while или do, а есть только ус-
ловный оператор if.
102 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Данная задача решена в программе, исходный текст которой представлен в лис-
листинге 2.5.
Листинг 2.5. Файл ch02\GoToOp\GoToOpApp.cs
using System;
namespace GoToOp
{
class GoToOpApp
{
static void Main(string(] args)
{
int i ;
i = 0;
LoopLabel:
System.Console.Write("{0} ",i ) ;
if(i < 10)
goto LoopLabel;
System.Console.ReadLine();
Обратите внимание на строки, вьаделенные в этом тексте полужирным шрифтом.
Первая из них представляет собой так называемую метку:
LoopLabel:
Метка — это идентификатор, с помощью которого можно отметить какое-то место
в программе. Метка не является исполняемым оператором, т. е. сама по себе она
не выполняет никаких действий.
Другая пара строк представляет собой условный оператор, в теле которого распо-
расположен оператор goto:
if(i < 10)
goto LoopLabel;
Если условие выполняется (т. е. если в переменной i находится значение, мень-
меньшее 10), то управление передается в строку программы, расположенную сразу после
метки LoopLabel.
Переменная i применяется здесь в качестве переменной цикла. Вначале мы записыва-
записываем в нее нулевое значение. Это действие не что иное, как инициализация цикла.
Далее при помощи метода Write мы выводим на консоль текущее значение пере-
переменной i, а затем увеличиваем это значение на единицу. Таким образом, переменная
цикла получает приращение.
И наконец, наша программа проверяет условие выхода из цикла. Если значение пе-
переменной цикла не достигло 10, управление передается снова методу Write. В про-
противном случае цикл завершает свою работу.
Глава 2. Управляющие операторы 103
Очевидно, при использовании любого из итерационных операторов, описанных ра-
ранее, исходный текст программы выглядел бы намного понятнее. Итерационные и ус-
условные операторы, а также оператор выбора позволяют подчеркнуть в исходном тек-
тексте программы ее структуру. Одного взгляда на исходный текст достаточно, чтобы
найти циклы, операторы обработки условий, а также проследить вложенность циклов
и условных операторов.
Что же касается оператора goto, то при таком использовании, как в только что
рассмотренной программе (листинг 2.5), он только скрывает структуру программы.
Пока у нас только одна метка и только один оператор перехода на эту метку, но пред-
представьте, что получится, если их будет много — десяток или больше. Изучая исходный
текст программы, будет очень трудно проследить, при каких условиях и куда переда-
передается управление, а это приведет к тому, что программу будет трудно отлаживать.
Тем не менее разработчики языка С# решили оставить оператор goto в этом су-
суперсовременном языке программирования, несмотря на его недостатки. Более того,
они расширили возможность его применения.
Почему?
Дело в том, что при грамотном использовании оператор goto все же позволяет пи-
писать эффективные программы с понятным исходным текстом.
Применение goto в операторе выбора switch
Рассказывая об операторе выбора switch, мы обращали ваше внимание на то,
что внутри этого оператора не может быть пропущенных операторов break — ком-
компилятор С# рассматривает данную ситуацию как ошибочную.
Тем не менее если вам нужно выполнить последовательно несколько блоков case,
то в языке С# это можно сделать при помощи оператора goto.
Рассмотрим следующую учебную задачу.
Мы будем выводить на консоль фрагменты фразы «мама мыла раму». Программа
должна вводить слова этой фразы с клавиатуры. Если введено любое из слов «мама»,
«мыла» или «раму», нужно показать на консоли это слово, а также все слова фразы,
расположенные после введенного.
Например, если пользователь ввел слово «мама», программа должна вывести всю фра-
фразу целиком. Если введено слово «мыла», программа должна вывести слово «мыла раму».
И наконец, если введено слово «раму», программа должна также вывести слово «раму».
В листинге 2.6 мы привели исходный текст программы, выполняющей эти незамы-
незамысловатые действия.
Листинг 2.6. Файл ch02\GotolnSwitch\GotolnSwitchApp.cs
using System;
namespace GotoInSwitch
class GotoInSwitchApp
static void Main(string[] args)
string i.nputString ;
while(true)
104 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
System.Console.Write(@"Введите одну из строк ""мама"",
""мыла"", ""раму"" или ""exi t"": " ) ;
inputString = System.Console.ReadLine();
if(inputstring == "exit")
break;
switch(inputString)
{
case "мама":
System.Console.Write("мама ");
goto case "мыла";
break;
case "мыла":
System.Console.Write("мыла ");
goto case "раму";
break;
case "раму":
System.Console.Write("раму\п");
break;
default:
System.Console.WriteLine("Ошибка при вводе");
break;
Прежде всего обратите внимание, что наша программа вводит и обрабатывает
строки в цикле:
while(true)
{
System.Console.Write(@"Введите одну из строк ""мама"", ""мыла"",
""раму"" или ""exit"": ");
inputstring = System. Console .ReadLine О;
if(inputString == "exit")
break;
Условие завершения цикла — ввод пользователем слова exit. Теперь, когда у нас
есть цикл ввода строк, мы можем вводить разные слова, не перезапуская программу
после ввода каждого слова.
В теле первых двух операторах case после вызова метода Write, отображаю-
отображающего соответствующее слово на консоли, мы расположили оператор goto case
"мыла":
Глава 2 Управляющие операторы 105
case "мама":
System.Console.Write("мама ");
goto case "мыла";
break;
case "мыла":
System.Console.Write("мыла ");
goto case "раму";
break;
Таким образом, после вывода на консоль слова «мама» программа переходит с помо-
помощью оператора goto к обработке следующего блока case. В результате достигается
нужный нам эффект — программа может начинать вывод известной фразы школьного бу-
букваря начиная с любого слова и затем продолжит вывод оставшейся части фразы.
После обработки тела любого блока case программа может перейти к исполнению
строк блока default. Для этого она должна использовать оператор goto следующе-
следующего вида:
goto default;
Заметим, что описанным выше образом нельзя передать управление за пределы
оператора switch. Такая ситуация будет рассматриваться компилятором как оши-
ошибочная.
Другие применения оператора goto
Оператор goto можно использовать вместо оператора break для передачи управле-
управления за пределы цикла (однако нельзя передавать таким способом управление внутрь
цикла).
Заметим, однако, что единственное преимущество, получаемое при этом по срав-
сравнению с использованием оператора break, — возможность передать управление поч-
почти на любую строку, расположенную выше или ниже цикла.
Мы считаем, что по возможности следует избегать использования goto, так как
он запутывает исходный текст программы. В большинстве случаев без него вполне
можно обойтись. Единственным исключением здесь является, пожалуй, применение
goto в операторе выбора switch.
Пустой оператор
Наверное, самый простой оператор в языке С# — это пустой оператор. Он состоит
из точки с запятой и может располагаться в любом месте исходного текста программы,
где по правилам языка требуется наличие оператора.
В частности, итерационные операторы требуют обязательного присутствия в своем
теле какого-либо оператора, хотя бы и пустого. Ниже мы привели пример использова-
использования пустого оператора в теле цикла for:
for(i = 0; i < 10; nums[i++] = 0)
106 А. В. Фролов, Г. В. Фролов. Язык С# Самоучитель
Единственный результат выполнения этого цикла— запись нулевого значения
во все ячейки массива nums. Больше от цикла ничего не требуется, поэтому в теле
цикла мы использовали пустой оператор.
Пустой оператор может быть помечен, например, для использования в операторе
goto.
Составной оператор
Несколько операторов можно объединить в блок, заключив их в фигурные скобки.
В результате получится оператор, называемый составным. Вот пример составного
оператора:
int i;
{
int[1 nums;
nums = new int[10];
for(i =0; i < 10; i++)
nums[i] = i;
}
Исполнение составного оператора сводится к последовательному исполнению всех
содержащихся в нем операторов (если, конечно, среди них нет операторов условной
или безусловной передачи управления).
Внутри составного оператора можно объявлять переменные, причем они будут
доступны только внутри блока составного оператора. Говорят, что это локальные пе-
переменные блока составного оператора.
Ранее мы уже сталкивались с составными операторами. Эти операторы обычно
применяются в составе итерационных и условных операторов, например:
i = 0;
while(i < 10)
{
System.Console.Write("{0} ",i );
Здесь в составной оператор входит вызов метода Write, а также инкремент значе-
значения переменной i.
Глава 2. Управляющие операторы 107
Глава 3. Объектно-ориентированное
программирование
В этой главе мы рассмотрим важнейшее понятие— концепцию объектно-
ориентированного программирования (ООП). Мы уже говорили во Введении, что язык
С# представляет собой средство объектно-ориентированного и компонентно-ориенти-
компонентно-ориентированного программирования.
И действительно, в языке С# все программы и данные представляют собой объек-
объекты, а все обрабатывающие их алгоритмы являются методами. Оба этих понятия име-
имеют самое непосредственное отношение к ООП и будут предметом изучения в этой
главе.
Владение методикой ООП абсолютно необходимо для успешного программирова-
программирования на языке С#. Фактически, не разобравшись в этом, вы не сможете создавать на С#
хоть сколько-нибудь сложные программы и системы.
Первые шаги к ООП
Объектно-ориентированное программирование тесно связано с нашим обычным жи-
житейским опытом. В повседневной жизни мы встречаемся со многими понятиями ООП,
даже не задумываясь над этим.
Прежде всего это такое понятие, как объект.
Описания объектов, с которыми мы имеем дело в жизни, можно представить себе
как совокупность некоторых данных об объекте и набора действий, которые можно
выполнять над данным объектом.
Возьмем, например, обычный телевизор. Ниже мы перечислили некоторые пара-
параметры, которые могут характеризовать телевизор как объект:
• марка телевизора;
• название компании-изготовителя;
• габаритные размеры;
• вес;
• количество принимаемых каналов;
• возможность работы с пультом дистанционного управления;
• наличие выхода для подключения видеомагнитофона.
Если телевизор включен, то приобретают значение и другие параметры, например:
• номер принимаемого канала;
• громкость звука;
• стандарт видеосигнала (PAL, SEC AM, NTSC).
"liorS 108
Можно сказать, что набор перечисленных выше характеристик представляет собой
данные объекта-телевизора. Такие данные полностью характеризуют объект сам по се-
себе, а также его текущее состояние.
Помимо данных существуют еще и методы работы с телевизором, т. е. действия,
выполняемые в процессе эксплуатации телевизора:
• включение электропитания;
• отключение электропитания;
• включение канала с заданным номером с панели телевизора;
• включение канала с заданным номером с пульта дистанционного управления;
• увеличение громкости;
• уменьшение громкости;
• временное отключение звука во время рекламных вставок;
• включение звука после временного отключения.
Возможность использования тех или иных методов работы с телевизором зависит
от его характеристик, а также от текущего состояния. Например, если телевизор
не может работать с пультом дистанционного управления, вы не сможете использо-
использовать часть методов, например включение канала с помощью пульта. Если телевизор
выключен, его нельзя выключить еще раз, так как эта операция не имеет никакого
смысла. Если телевизор в состоянии принимать только 6 каналов, никаким образом
не удастся включить канал с номером 40 и т. д.
Таким образом, возможность и способы выполнения тех или иных операций с те-
телевизором определяются его характеристиками и текущим состоянием, т. е. данными
о телевизоре.
Программная модель телевизора
Изучать ООП лучше всего на каком-то конкретном примере. В этом разделе мы поста-
поставим перед собой задачу создания сильно упрощенной программной модели телевизо-
телевизора. Вначале эта модель будет реализована с применением средств, уже знакомых вам
по предыдущим главам нашей книги, а затем мы применим для ее решения объектно-
ориентированный подход.
Данные
Характеристики и текущее состояние телевизора мы будем хранить в наборе перемен-
переменных различных типов. Ниже мы перечислили эти переменные и привели их краткое
описание:
bool isPowerOn; // включен или выключен
byte maxChannel; // максимальный номер канала
byte currentChannel; // текущий номер канала
byte currentVolume; // текущая громкость звука
В переменной isPowerOn типа bool мы будем хранить состояние выключателя
электропитания телевизора. Если телевизор включен, в переменной isPowerOn бу-
будет находиться значение true, а если выключен — значение false.
Глава 3. Объектно-ориентированное программирование 109
Максимальное количество каналов, принимаемых телевизором, будет храниться
в переменной maxChannel, а номер текущего канала, принимаемого в настоящий
момент времени, — в переменной currentChannel.
Кроме того, в переменной currentVolurae мы будем хранить уровень громкости,
выражаемый в процентах. При этом, если звук выключен, уровень громкости будет
равен 0 %, а если включен на максимальную громкость — 100 %.
Методы
Наша модель телевизора сможет выполнять несколько операций. Это операция инициали-
инициализации, включения и выключения телевизора, операции установки номера принимаемого
канала и уровня громкости, а также определение состояния телевизора (включен или вы-
выключен), определение текущего номера канала и текущего уровня громкости.
Для выполнения перечисленных операций в нашей программной модели преду-
предусмотрено несколько конструкций, называемых методами.
Метод объединяет в одном блоке программные строки, имеющие отношение к вы-
выполнению той или иной операции. Метод может получать параметры и возвращать
значения.
Вот, например, как выглядит метод, включающий электропитание телевизора:
void SetPowerStateOn()
{
isPowerOn = true;
}
Ключевое слово void означает, что метод не возвращает никакого значения.
Мы считаем, что включение телевизора обязательно заканчивается успехом (хотя
на практике это не всегда так), поэтому результат выполнения данной операции нас
не интересует.
После ключевого слова void идет название метода SetPowerStateOn. Мы вы-
выбрали его произвольно, однако с тем условием, чтобы это название отражало действие,
выполняемое методом.
Далее в фигурных скобках располагаются операторы метода. Включение телевизо-
телевизора сводится к записи в переменную isPowerOn значения true. Пока мы отвлечемся
от способа определения этой переменной и будем считать, что методы имеют доступ
ко всем переменным, описывающим состояние нашего виртуального телевизора.
Чтобы выключить телевизор, мы определили метод SetPowerStateOf f:
void SetPowerStateOffО
{
isPowerOn = false;
}
Выключение телевизора сводится к записи в переменную isPowerOn значения
false.
Чтобы определить, включен или выключен обычный телевизор, нам достаточно
взглянуть на его экран. Для определения состояния виртуального телевизора придется
создать несколько методов.
110 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Метод GetPowerState позволяет получить текущее значение, хранящееся в пе-
переменной isPowerOn:
bool GetPowerState()
{
return isPowerOn;
}
Обратите внимание, что объявление метода начинается с ключевого слова bool.
Это означает, что метод должен вернуть логическое значение типа bool.
Для возвращения значения используется оператор return, упоминавшийся в кон-
конце предыдущей главы. В данном случае этот оператор возвращает значение перемен-
переменной isPowerOn. Если телевизор включен, метод GetPowerState возвратит значе-
значение true, в противном случае — значение false.
Итак, мы научились включать и выключать телевизор. Теперь займемся каналами.
Мы определим методы, позволяющие установить заданный канал и определить
номер установленного канала.
Переключение виртуального телевизора на заданный канал можно сделать с помо-
помощью метода SetChannel:
bool SetChannel(byte channel)
{
if(channel <= maxChannel && channel > 0)
{
currentChannel = channel;
return true;
}
else
return false;
}
В качестве параметра этому методу передается номер канала channel. Метод
проверяет номер канала и, если он больше нуля и не превышает максимально допус-
допустимого, записывает новый номер канала в переменную currentChannel.
При успешном переключении канала метод SetChannel возвращает значение
true, а в случае ошибки — значение false. Это значение можно использовать для
выявления ошибочной попытки установить недопустимый номер канала. В дальней-
дальнейшем мы расскажем вам о более совершенном методе обработки ошибок, основанном
на обработке исключений.
Получить номер текущего канала можно с помощью метода GetChannel:
byte GetChannel()
{
return currentChannel;
}
Этот метод просто возвращает текущее содержимое переменной current-
Channel.
Глава 3. Объектно-ориентированное программирование 111
Для управления громкостью мы предусмотрели метод SetVolume:
void SetVolume{byte volume)
{
if(volume > 0 && volume <= 100)
currentVolume = volume;
else
currentVolume = 0;
}
Он устанавливает новый уровень громкости, записывая значение volume в пере-
переменную currentVolume. Перед этим, однако, наш метод проверяет, находится
ли новое значение громкости в диапазоне от 0 до 100 %. Если задан неправильный
уровень громкости, звук выключается.
Получить текущий уровень громкости можно с помощью метода GetVolume:
byte GetVolume()
{
return currentVolume;
)
Он просто возвращает текущее значение уровня громкости, хранящееся в перемен-
переменной currentVolume.
В завершение мы определим метод, выполняющий инициализацию всех
переменных:
TelevisionSet()
{
// Устанавливаем исходное состояние телевизора
isPowerOn = false; // выключен
maxChannel = 40; // всего можно смотреть 4 0 каналов
currentChannel = 1; // при включении показывать канал 1
currentVolume = 10; // громкость при включении - 10%
}
Сразу после создания объекта (виртуального телевизора) этот метод выключает
электропитание телевизора, определяет максимальное количество каналов D0), уста-
устанавливает текущий номер канала, равный единице, а также задает начальный уровень
громкости — 10 % от максимального уровня.
Мы намеренно не снабдили этот метод ключевым словом void, хотя он и не воз-
возвращает никаких значений. Об этом, а также о выборе имени для метода инициализа-
инициализации мы поговорим в следующем разделе.
Объединяем все вместе
Итак, мы создали данные — набор переменных, хранящих текущие параметры вирту-
виртуального телевизора, а также набор методов, позволяющий получать и изменять эти па-
параметры. Теперь нам нужно объединить данные и методы вместе, образовав описание
нашего объекта — виртуального телевизора.
112 А. В Фролов, Г. В. Фролов. Язык С#. Самоучитель
В языке С# описание объекта представляется в виде нового типа данных и называ-
называется классом.
Вы создаете новый тип данных (класс), определяя для него набор переменных, на-
называемых в этом случае полями класса, а также набор методов, предназначенных
для работы с этими полями.
В общем виде определение класса выглядит достаточно просто:
class <Имя класса>
{
[<Поле класса 1>]
[<Поле класса 2>]
[<Поле класса N>]
[<Конструктор>]
[<Метод класса 1>]
[<Метод класса 2>]
[<Метод класса М>]
}
После ключевого слова class идет название класса. Например, для нашего теле-
телевизора мы выберем название класса TelevisionSet:
class TelevisionSet
Объявление полей и методов класса находится внутри блока операторов, ограни-
ограниченного фигурными скобками. Вы можете размещать поля и методы внутри этого бло-
блока в произвольном порядке, однако предпочтительнее придерживаться какой-либо
стратегии.
Мы, например, размещаем внутри объявления класса вначале поля класса, а потом
методы, но можно поступить и наоборот. Вот сокращенный пример объявления класса
TelevisionSet (полный пример будет рассмотрен позже):
class TelevisionSet
{
// включен или выключен
// максимальный номер канала
// текущий номер канала
// текущая громкость звука
public TelevisionSet()
{
isPowerOn = false; // выключен
maxChannel =40; // всего можно смотреть 40 каналов
currentChannel =1; // при включении показывать канал 1
currentVolume =10; // громкость при включении - 10%
Глава 3. Объектно-ориентированное программирование 113
bool
public
byte
byte
byte
isPowerOn;
maxChannel;
currentChannel;
currentVolume;
public void SetPowerStateOn{)
{
isPowerOn = true;
}
public void SetPowerStateOff()
{
isPowerOn = false;
Как видите, в верхней части объявления класса находятся переменные isPow-
erOn, maxChannel, currentChannel и currentVolume, описанные нами ранее
и отражающие текущее состояние телевизора. В контексте определения класса эти
переменные обычно и называют полями класса.
Поле maxChannel снабжено так называемым классификатором доступа
public, о котором мы расскажем позже.
Ниже полей объявлен метод, имя которого совпадает с именем нашего класса, —
TelevisionSet:
public TelevisionSet(byte numberOfChannels)
{
isPowerOn = false; // выключен
maxChannel = numberOfChannels; // макс, количество каналов
currentChannel= 1; // при включении показывать канал 1
currentVolume =10; // громкость при включении - 10%
}
Метод с таким именем называется конструктором класса. На конструктор класса
возлагается задача инициализации полей класса, поэтому мы поместили сюда не-
несколько операторов, определяющих начальное состояние телевизора.
Мы передаем конструктору один параметр numberOf Channels, определяющий
максимальное количество телевизионных каналов, которое способен показывать наш
телевизор.
По определению конструктор не может возвращать никакого значения. В отличие
от других методов класса в конструкторе не допускается использование ключевого
слова vo id для обозначения этого обстоятельства.
Классификатор доступа public определяет один из видов доступа к полю класса
или методу. Здесь оно разрешает вызов конструктора из внешней программы.
Управление доступом к полям и методам класса мы рассмотрим позже во всех под-
подробностях. Сейчас вы должны запомнить, что если в классе определен метод-
конструктор, то он вызывается при создании объекта, описанного данным классом,
с целью его инициализации.
Вслед за конструктором мы разместили в классе несколько описанных ранее мето-
методов, снабдив их ключевым словом public.
Поля и методы, а также другие объекты, объявленные в классе, часто называют
членами класса (class members). Мы тоже будем пользоваться этой терминологией.
114 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Создание объектов класса TelevisionSet
Теперь у нас есть класс TelevisionSet — описание телевизора, но нет самого теле-
телевизора. Пользуясь нашим классом, мы сможем создать необходимое нам количество
телевизоров.
Допустим, нам нужны два телевизора, большой, способный принимать до 40 кана-
каналов, и маленький, рассчитанный на прием шести каналов.
Прежде всего для создания объектов-телевизоров необходимо объявить в програм-
программе две переменные типа TelevisionSet:
TelevisionSet tvSmall;
TelevisionSet tvLarge;
Как видите, объявление выглядит аналогично объявлению числовых или строчных
переменных, с той лишь разницей, что вместо одного из стандартных типов данных С#
мы использовали здесь определенный нами тип данных TelevisionSet.
На самом деле есть еще одно отличие.
В то время как стандартные типы данных можно использовать в программе сразу
после объявления, объекты других классов нужно создавать явным образом при по-
помощи ключевого слова new:
tvSmall = new TelevisionSetF) ;
tvLarge = new TelevisionSetD0) ;
Здесь мы создали два объекта, вызвав их конструкторы. При первом вызове мы пе-
передали конструктору значение 6, а при втором — значение 40.
В результате всех этих действий программа записала в переменные tvSmall
и tvLarge так называемые ссылки на объекты. Переменная tvSmall ссылается
на объект-телевизор, способный принимать 6 каналов, а переменная tvLarge —
на 40-канальный телевизор.
Важно, что для создания этих разных телевизоров мы использовали один и тот же
класс, т. е. одно и то же описание объекта.
Отсюда наглядно видно различие между классом и объектом. Класс представляет
собой описание объекта, а объект — это реально существующая в памяти компьютера
сущность, с которой программа может выполнять какие-либо действия.
Вызов методов класса
После того как программа создала объекты, она может вызывать методы, определен-
определенные в классе этих объектов.
Вызвать метод очень просто:
tvSmall.SetPowerStateOn();
tvSmall.SetChannelE);
tvSmall.SetVolumeE0);
tvLarge.SetPowerStateOn();
tvLarge.SetChannelB7);
tvLarge.SetVolumeC0);
Глава З. Объектно-ориентированное программирование 115
Здесь мы указали имя переменной, содержащей ссылку на объект, а затем через
точку — имя вызываемого метода. Если метод принимает параметры, эти параметры
следует указать в скобках.
Вызывая одни и те же методы для разных объектов, мы выполняем с этими объек-
объектами одни и те же действия. Например, метод SetPowerStateOn включает наши те-
телевизоры:
tvSmall.SetPowerStateOn();
tvLarge.SetPowerStateOn{);
Чтобы переключить телевизоры на разные каналы, достаточно при вызове метода
SetChannel передать ему разные параметры:
tvSmall.SetChannelE);
tvLarge.SetChannelB7);
Первый телевизор будет переключен на 6-й канал, а второй — на 27-й канал.
Аналогичным образом вы можете изменять громкость каждого телевизора, вызы-
вызывая для соответствующих объектов метод SetVolume:
tvSmall.SetVolumeE0);
tvLarge.SetVolumeC0);
Если метод возвращает значение, оно может быть присвоено переменной или ис-
использовано в выражении. Вот, например, как можно узнать текущий номер канала
и текущий уровень громкости наших телевизоров:
byte chSmall = tvSmall. GetChannel О;
byte volSmall= tvSmall.GetVolume())■
byte chLarge = tvLarge . GetCl4annel () ;
byte volLarge = tvLarge.GetVolume()) ;
После выполнения этих строк в переменных chSmall и volSmall будут записа-
записаны текущие значения номера канала и громкости первого телевизора, а в переменных
chLarge и volLarge — второго.
Заметим, что программа, создавшая объект, может обращаться только к таким
методам, которые объявлены в описании класса со спецификатором доступа
public.
Обращение к полям класса
С помощью ссылки на объект программа может не только вызывать методы класса, но
и обращаться напрямую к его полям (если, конечно, они определены со спецификато-
спецификатором доступа public, разрешающим такое обращение).
Вот, например, как можно узнать максимальное количество каналов, предусмот-
предусмотренное в наших телевизорах:
byte maxChannelSmall = tvSmall.maxChannel;
byte maxChannelLarge - tvLarge.maxChannel;
116 А. В. Фролов, Г. В Фролов. Язык С#. Самоучитель
При необходимости программа сможет изменить максимальное количество кана-
каналов уже после создания телевизора:
tvSmall.maxChannel = 10;
tvLarge.maxChannel = 150;
Заметим, что максимальное количество каналов можно безболезненно увеличивать,
в то время как уменьшение следует выполнять осторожно. Например, если телевизор
переключен на 10-й канал, а вы устанавливаете максимальный номер канала, равный,
скажем, пяти, то возникнет ошибочная ситуация — телевизор должен показывать ка-
канал с недопустимым номером.
Для избежания логических ошибок подобного рода лучше возложить задачу изме-
изменения полей класса на методы, определенные в классе. Эти методы будут учитывать
вес особенности работы с объегтом. В случае нашего телевизора перед уменьшением
максимального количества каналов такой метод мог бы, например, установить номер
текущего канала равным единице.
Лучше всего, если методы класса будут реализовывать всю внутреннюю логику
поведения объекта. При этом программист, составляющий программу для работы
с объектом, может не отвлекаться на особенности внутреннего устройства объекта
и его реализации. Это позволит сконцентрировать внимание на других, более важных
задачах и снизить количество ошибок, допускаемых в процессе программирования.
Способность объекта скрывать детали своей внутренней реализации и внутренней
логики работы от вызывающих программ называется инкапсуляцией. Инкапсуляция —
один из китов, на которых базируется ООП.
Пример программы
В листинге 3.1 мы привели полный исходный текст программы, в которой определен
рассмотренный выше класс TelevisionSet. Эта программа демонстрирует проце-
процедуру создания объектов на базе определенных нами классов, обращение к методам
и полям класса.
Листинг 3.1. Файл chO3\TvSet\TvSetApp.cs
using System;
namespace TvSet
{
class TelevisionSet
{
bool isPowerOn,- // включен или выключен
byte maxChannel; // максимальный номер канала
byte currentchannel; // текущий номер канала
byte currentVolume; // текущая громкость звука
// Конструктор класса TelevisionSet
public TelevisionSet(byte numberOfChannels)
{
// Устанавливаем исходное состояние телевизора
isPowerOn = false; // выключен
maxChannel = numberOfChannels; // макс, количество каналов
currentchannel= 1; // при включении показывать канал 1
currentVolume = 10; // громкость при включении - 10%
}
Глава 3. Объектно-ориентированное программирование 117
// Включить телевизор
public void SetPowerStateOn()
{
isPowerOn = true;
// Выключить телевизор
public void SetPowerStateOff()
{
isPowerOn = false;
// Определить состояние телевизора - включен или выключен
public bool GetPowerState()
{
return isPowerOn;
// Переключиться на прием заданного канала
public bool SetChannel(byte channel)
{
if(channel <= maxChannel && channel > 0)
{
currentChannel = channel;
return true;
}
else
return false;
// Получить номер текущего канала
public byte GetChanneK)
{
return currentChannel;
// Установить громкость
public void SetVolumefbyte volume)
{
if(volume > 0 && volume <= 100)
currentVolume = volume;
else
currentVolume = 0;
// Получить текущий уровень громкости
public byte GetVolumeO
{
return currentVolume;
118 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
class TvSetApp
{
static void Main(string[] args)
{
TelevisionSet tvSmall;
TelevisionSet tvLarge ,-
tvSmall = new TelevisionSetF);
tvLarge = new TelevisionSetD0);
tvSmall.SetPowerStateOn();
tvSmall.SetChannelE);
tvSmall.SetVoluroeE0);
tvLarge.SetPowerStateOn();
tvLarge.SetChannelB7);
tvLarge.SetVolumeC0);
Console.Write("Телевизор tvSmall: ");
Console.WriteLine("{0}, канал {1} из {2}, громкость {3}",
tvSmall.GetPowerState() ? "Включен" : "Выключен",
tvSmall.GetChannel(), tvSmall.maxChannel,
tvSmall.GetVolume());
Console.Write("Телевизор tvLarge: ");
Console.WriteLine("{0}, канал {1} из {2}, громкость {3}",
tvLarge.GetPowerState() ? "Включен" : "Выключен",
tvLarge.GetChannel(), tvLarge.maxChannel,
tvLarge.GetVolume());
tvSmall.SetChannelC);
tvSmall.SetVolume(80);
tvLarge.SetChannelC9);
tvLarge.SetVolumeF0);
tvSmall.SetPowerStateOff();
tvLarge.SetPowerStateOff();
Console.Write("ХпТелевизор tvSmall: ");
Console.WriteLine("{0}, канал {1} из {2}, громкость {3}",
tvSmall.GetPowerState() ? "Включен" : "Выключен",
tvSmall.GetChannel(), tvSmall.maxChannel,
tvSmall.GetVolume());
Console.Write("Телевизор tvLarge: ");
Глава З. Объектно-ориентированное программирование 119
Console.WriteLine("{0), канал {1} из {2}, громкость {3}
tvLarge.GetPowerState() ? "Включен" : "Выключен",
tvLarge.GetChannel(), tvLarge.maxChannel,
tvLarge.GetVolume());
Console.ReadLine();
В самом начале листинга 3.1 находится объявление класса TelevisionSet, о ко-
котором мы уже рассказывали раньше. Назначение полей и методов этого класса было
подробно описано, поэтому мы не будем повторяться.
Нам хотелось бы обратить ваше внимание на то, как в этой программе мы вызыва-
вызываем методы Write и WriteLine:
Console.Write("Телевизор tvSmall: ");
Console.WriteLine("{0}, канал {1} из {2}, громкость {3}",
tvSmall.GetPowerState() ? "Включен" : "Выключен",
tvSmall.GetChannel(), tvSmall.maxChannel,
tvSmall.GetVolume());
Обратите внимание, что при вызове этих методов мы опустили пространство имен
System. Это вполне допустимо, так как в начале нашей программы имеется строка
с оператором using, предписывающая компилятору С# просматривать это прост-
пространство имен:
using System;
В дальнейшем при частом обращении к методам, определенным в рамках какого-
либо пространства имен, мы будем опускать имя пространства имен при вызове мето-
метода, одновременно указывая его с помощью оператора using. Так наши программы
будут короче и проще для понимания. В своих проектах вы можете поступать как
угодно в зависимости от собственных предпочтений.
Теперь о том, что делает наша программа.
Получив управление, метод Main создает два виртуальных телевизора, один из ко-
которых (маленький) способен принимать 6 каналов, а другой (большой) — 40 каналов:
TelevisionSet tvSmall;
TelevisionSet tvLarge;
tvSmall = new TelevisionSet(б);
tvLarge = new TelevisionSetD0) ;
Далее наша программа включает по очереди оба телевизора, устанавливая на пер-
первом из них 5-й канал, а на втором — 27-й канал. Уровень громкости тоже устанавли-
устанавливается разный:
tvSmall.SetPowerStateOn();
tvSmall.SetChannelE ) ;
tvSmall.SetVolumeE0);
120 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
tvLarge.SetPowerStateOn();
tvLarge.SetChannelB7);
tvLarge.SetVolumeC0);
После этого программа отображает текущее состояние обоих телевизоров на консоли.
На следующем этапа программа вновь изменяет номера текущих каналов и уровень
громкости обоих телевизоров, а затем выключает их вовсе:
tvSmal1.SetChannelC);
tvSmall.SetVolume(80);
tvLarge.SetChannelC9);
tvLarge.SetVolumeF0);
tvSmall.SetPowerStateOff();
tvLarge.SetPowerStateOff();
И наконец, перед завершением своей работы программа отображает новое состоя-
состояние телевизоров на консоли:
Телевизор tvSmall: Включен, канал 5 из б, громкость 50
Телевизор tvLarge: Включен, канал 27 из 40, громкость 30
Телевизор tvSmall: Выключен, канал 3 из 6, громкость 80
Телевизор tvLarge: Выключен, канал 39 из 40, громкость 60
Таким образом, разработав класс, описывающий поведение телевизора, мы создали
два объекта этого класса, а затем управляли этими объектами совершенно независимо.
Этот принцип можно обобщить на данные любого типа. Пользуясь объектно-
ориентированными средствами С#, мы можем описать с помощью классов данные лю-
любых типов, а затем на базе этих классов создавать объекты.
Наследование
После того как мы сделали первые шаги в освоении ООП и разобрались с первой объ-
объектно-ориентированной программой, настало время дать более строгие определения
терминов и понятий ООП. В этом разделе мы займемся классами.
Теперь вы знаете, что классы позволяют объединить вместе данные и методы,
предназначенные для их обработки. Создав объекты того или иного класса, программа
может вызывать методы и обращаться напрямую к полям объекта (если это разрешено
при объявлении класса). Работая с объектами, созданными на базе классов, програм-
программисту нет нужды вникать во внутренние детали устройства класса— достаточно
знать, как и какие методы можно вызывать, какие передавать им параметры и какие
значения эти методы возвращают.
Механизм сокрытия, или, как говорят, инкапсуляции, данных и методов класса об-
облегчает создание программ, особенно сложных, так как позволяет программисту
не вникать во все детали реализации всех программных компонентов.
Глава 3. Объектно-ориентированное программирование 121
В этом разделе мы рассмотрим другое базовое понятие ООП, а именно наследова-
наследование. С этим понятием неразрывно связаны два других понятия — базовый класс
и производный класс.
Базовый класс
Наследование позволяет создавать новые классы на базе уже существующих классов.
Создавая новый тип данных на базе типа данных, определенного ранее, можно упро-
упростить работу за счет использования разработанных ранее методов базового класса.
Базовым (base), или родительским (parent), классом называется класс, на основе
которого создаются другие классы.
Чтобы это определение было понятнее, рассмотрим следующий пример.
Пусть нам нужно работать в программе с такими объектами, как прямоугольники.
Для этого мы создаем класс Rectangle, инкапсулирующий в себе данные и методы,
необходимые для размещения прямоугольника на плоскости:
class Rectangle
{
int xPos;
int yPos;
int width;
int height;
public Rectangle()
{
xPos = yPos = width = height = 0;
public void SetPosition(int x, int y)
{
xPos = x;
yPos = y;
public void SetSize(int w, int h)
{
width = w;
height = h;
Поля класса xPos и yPos хранят координаты левого нижнего угла прямоугольни-
прямоугольника (в прямоугольной системе координат), а поля width и height — соответственно
ширину и высоту прямоугольника. Таким образом, содержимое полей класса Rec-
Rectangle однозначно определяет расположение прямоугольника на плоскости и его
размеры (рис. 3.1).
122 А. В. Фролов, Г. В. Фролов. Язык С# Самоучитель
Ось Y
ОсьХ
Рис. 3.1. Расположение прямоугольника на плоскости
Конструктор класса Rectangle записывает в поля класса нулевые значения, в ре-
результате чего все новые прямоугольники создаются в центре системы координат
с размерами, равными нулю.
В классе Rectangle мы также определили методы SetPosition и SetSize,
позволяющие установить соответственно расположение прямоугольника на плоскости
и его размеры.
Работать с этим классом очень просто — нужно сначала создать прямоугольник,
а затем установить его расположение и размеры:
Rectangle r;
г = new ColorRectangle();
г.SetPosition@, 10);
r.SetSizeB0, 30);
Производный класс
Итак, мы создали базовый класс, описывающий прямоугольники. А теперь представьте
себе, что вам понадобились не простые прямоугольники, а раскрашенные в разные цвета.
Вы, конечно, можете изменить класс Rectangle, добавив в него поля и методы
для работы с цветом. Однако более красивое решение заключается в создании на базе
класса Rectangle нового класса ColorRectangle, дополненного средствами
представления цвета:
class ColorRectangle : Rectangle
{
byte colorR;
byte colorG;
byte colorB;
Глава З. Объектно-ориентированное программирование
123
public void SetColor(byte r, byte g, byte b)
С
colorR = r;
colorG = g;
colorB = b;
Обратите внимание, что при объявлении класса ColorRectangle после двоеточия
мы указали имя базового класса Rectangle. В результате класс ColorRectangle на-
наследует все поля и методы своего базового класса, а также добавляет к ним еще 3 поля:
colorR, colorG, colorB — и один метод — SetColor. Новые поля предназначены
для хранения трех основных компонентов цвета (красный, голубой и зеленый), а метод
SetColor позволяет установить значения этих полей, раскрасив наш прямоугольник.
Таким образом, мы очень легко добавили новую функциональность — теперь наши
прямоугольники стали цветными. При добавлении новой сущности (цвета) нам были
несущественны детали реализации базового класса Rectangle, такие, как способ
размещения прямоугольника на плоскости и способ установки его размеров.
Более того, создавая свой собственный класс на базе готового класса, программист
может даже и не подозревать о существовании в этом классе каких-то еще полей и ме-
методов. Например, в базовом классе Rectangle могло существовать поле для хране-
хранения запаха прямоугольника, а также метод для установки этого запаха.
Пока мы имеем дело с простыми классами и простыми программами, эффект
от использования наследования может показаться небольшим. Однако в реальности
наследование открывает перед программистом возможность создания своих классов
на базе огромной библиотеки классов С#.
Взяв за основу один из десятков тысяч классов библиотеки, вы можете создать
на его базе собственный класс, наделив его необходимыми вам свойствами. При этом
вам не потребуется реализовывать полную функциональность, так как она уже реали-
реализована в базовом классе. Достаточно только добавить свои поля и методы (а также,
возможно, переопределить существующие поля и методы базового класса, но об этом
мы поговорим позже).
Класс ColorRectangle, созданный на базе класса Rectangle, называется про-
производным или дочерним (derived, child). В свою очередь, класс Rectangle играет
роль базового (base), или родительского (parent), класса.
Множественное наследование
Заметим, что в языке С# дочерний класс может наследовать свойства только одного
базового класса. В других языках программирования (например, в C++) допускается
так называемое множественное наследование.
Множественное наследование позволяет создавать один производный класс на ос-
основе нескольких базовых классов, что бывает удобно в некоторых случаях, когда про-
производный класс нужно наделить свойствами сразу нескольких базовых классов.
Язык С# не допускает множественного наследования, однако аналогичная функ-
функциональность может быть достигнута при использовании механизма так называемых
интерфейсов. Интерфейсы С# мы рассмотрим в гл. 8.
124 А В. Фролов, Г. В. Фролов Язык С# Самоучитель
Представление иерархии классов
Взаимосвязи между родительскими и дочерними (т. е. между базовыми и производ-
производными) классами могут быть достаточно сложными. Чтобы сделать эти взаимосвязи на-
нагляднее, их часто иллюстрируют графическими диаграммами.
На рис. 3.2 мы показали схему взаимосвязей базового класса Rectangle и произ-
производного класса ColorRec tangle.
Rectangle
ColorRectangle
Рис. 3.2. Базовый и производный
классы
Обратите внимание, что базовый класс располагается в верхней части диаграммы,
а производный — в нижней части. Стрелка, обозначающая наследование, идет в на-
направлении от производного класса к базовому классу.
Диаграммы зависимостей классов могут быть достаточно сложными. На рис. 3.3
мы показали пример более сложной схемы, построенной для случая, когда производ-
производные классы базового класса Rectangle становятся базовыми для других классов.
В этом случае наша схема превращается в дерево иерархии классов.
Rectangle
CoiorRectangle
Card
SmellRectangle
ThickRectangle
Puc. 3.3. Дерево иерархии классов
Как видно на рис. 3.3, на базе класса Rectangle созданы два класса —
CoiorRectangle и Card. О первом из них мы уже рассказывали — это класс для
представления прямоугольников. Второй класс с именем Card может быть иСпользо-
ван, например, для создания игральных карт. Помимо координат и размеров играль-
игральные карты имеют и другие атрибуты, такие, например, как масть.
Глава 3 Объектно-ориентированное программирование
125
Производный класс ColorRec tangle служит базовым для классов Smell-
Rectangle и ThickRectangle.
Класс SmellRectangle предназначен для представления прямоугольников с за-
запахом, а класс ThickRectangle— прямоугольников, имеющих не только высоту
и ширину, но и толщину.
Кстати, если вы думаете, что для программистов работа с запахами — дело весьма
отдаленной перспективы, то знайте, что в прессе уже появляются сообщения о разра-
разработке устройств, генерирующих запахи. Подключив такое устройство к компьютеру,
имеющему соединение с Интернетом, вы сможете не только посмотреть на Web-
сайты, но и понюхать их.
Заметим, что не допускается рекурсивное или циклическое наследование классов,
когда, например, второй класс наследуется от первого, а первый — от второго. Поэто-
Поэтому такая конструкция будет ошибочной:
class MyBaseClass : MyDerivedClass // Ошибка!
class MyDerivedClass : MyBaseClass
Важной особенностью библиотеки классов С# является то, что все классы этой
библиотеки происходят от одного общего класса с названием Object. Как мы
увидим дальше, это имеет далеко идущие последствия. В частности, такое насле-
наследование позволяет выполнять некоторые действия с объектами любых классов,
созданных на базе Object. Программист может, например, помещать эти объекты
в контейнеры (массивы, списки, словари), получать текстовое описание любого
объекта и т. д.
Пример программы
В листинге 3.2 мы привели исходный текст программы, в которой используется произ-
производный класс ColorRectangle. Базовым для него служит уже знакомый вам класс
Rectangle.
Листинг 3.2. Файл chO3\Rectangle\RectangleApp.cs
using System;
namespace Rectangle
{
class Rectangle
{
int xPos;
int yPos;
int width;
int height;
126 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
public Rectangle()
{
xPos = yPos = width = height = 0;
}
public void SetPosition(int x, int y)
{
xPos = x;
yPos = y;
}
public void SetSize(int w, int h)
{
width = w;
height = h;
}
}
class ColorRectangle : Rectangle
{
byte colorR;
byte colorG;
byte colorB;
public void SetColor(byte r, byte g, byte b)
{
colorR = r;
colorG = g;
colorB = b;
}
}
class RectangleApp
{
static void Main(string[] args)
{
ColorRectangle cr;
cr = new ColorRectangle();
cr.SetPosition@, 10);
cr.SetSizeB0, 30);
cr.SetColor@, 0, OxFF);
Console.ReadLine();
Получив управление, метод Main создает объект производного класса сг:
ColorRectangle cr;
cr = new ColorRectangle();
Глава 3. Объектно-ориентированное программирование 127
Далее этот метод вызывает два метода базового класса для изменения расположе-
расположения и размеров прямоугольника:
cr.SetPosition@, 10);
cr.SetSizeB0, 30) ;
Обратите внимание: в классе ColorRectangle нет определения методов
SetPosition и SetSize, так как это методы базового класса. Тем не менее мы вы-
вызываем их для объекта сг производного класса. Модификатор доступа public, при-
примененный при объявлении методов SetPosition и SetSize в базовом классе, до-
допускает такой вызов.
Аналогичным образом мы вызываем метод SetColor производного класса:
cr.SetColor @, 0, OxFF) ;
Заметим, что; создав объект базового класса Rectangle, вы не сможете вызвать
для него метод SetColor:
Rectangle rect;
rect = new Rectangle ();
rect.SetColor @, 0, OxFF); // Ошибка!
Причина очевидна — в классе Rectangle нет объявления метода SetColor.
Маскирование методов базового класса
Программисты очень часто создают свои классы на базе классов библиотеки Microsoft
.NET Framework, а также используют эти классы в неизменном виде, создавая объекты
классов. При наследовании классов, созданных другими программистами, вы можете
по ошибке определить в производном классе такой метод, который уже имеется в ба-
базовом классе.
Вернемся снова к нашему базовому классу Rectangle. Теперь мы опять создадим
на его основе производный класс ColorRectangle, но добавим в него помимо всего
прочего метод SetSize, объявленный в базовом классе Rectangle:
class ColorRectangle : Rectangle
{
byte colorR;
byte colorG;
byte colorB;
int width;
int. height;
public void SetColor(byte r, byte g, byte b)
{
colorR = r;
colorG = g ;
colorB = b;
}
128 А. В Фролов, Г. В. Фролов. Язык С# Самоучитель
public new void SetSize(int w, int h)
{
width = w;
height = h;
public int GetWidthO
{
return width;
}
public int GetHeightO
{
return height;
Обратите внимание, что в объявлении метода SetSize мы использовали ключевое
слово new. Если убрать это слово, то компилятор выведет на экран следующее преду-
предупреждающее сообщение:
warning CS0108: The keyword new is required on
1 Redefinition.ColorRectangle.SetSize(int, int)' because it hides
inherited member 'Redefinition.Rectangle.SetSize(int, int)'
Здесь говорится о том, что метод SetSize, определенный в классе Color-
Rectangle, маскирует, или скрывает, одноименный метод из класса Rectangle.
В результате такой маскировки метод SetSize базового класса Rectangle стано-
становится недоступным программе.
Компилятор не знает, возникла такая ли ситуация по ошибке, или же вы намеренно
заменили в дочернем классе метод базового класса. Ключевое слово new проясняет
ситуацию, в результате чего предупреждающее сообщение не появляется.
Заметим, что в языке C++ описанная выше ситуация не вызывает появления преду-
предупреждающих сообщений. Поэтому если вы по ошибке переопределите подобным об-
образом метод базового класса в программе C++, то так и не узнаете об этом. Язык С#
исключает возникновение подобной ошибки.
Модификаторы доступа
Теперь настала очередь разобраться, наконец-то, в модификаторах доступа, таких,
как public.
Что это такое и для чего они нужны?
Модификаторы доступа определяют права доступа к полям и методам класса. Ра-
Разумеется, тут речь не идет о том, что программист или пользователь программы будет
вводить свой идентификатор и пароль для работы с объектом класса. Механизм моди-
модификаторов доступа призван облегчить создание программ, вводя дополнительный кон-
контроль на этапе компиляции исходного текста программы.
Глава 3. Объектно-ориентированное программирование 129
5 Язык С# Самоучитель
Когда мы создаем производные классы, методы этих классов могут вызывать мето-
методы базовых классов или обращаться к полям, объявленным в базовом классе. Мы так-
также можем обращаться к полям и методам класса, создавая объекты этих классов.
Модификаторы доступа позволяют контролировать этот процесс, разрешая или за-
запрещая подобное обращение к полям и методам класса в тех или иных ситуациях.
Модификатор private
Если при объявлении класса вы не укажете модификаторы доступа для полей и мето-
методов класса, то по умолчанию компилятор будет полагать, что для них нужно использо-
использовать модификатор доступа private.
Модификатор private запрещает доступ к полям и методам класса извне самого
класса. Поля и методы, объявленные с модификатором private, будут доступны
только в методах самого класса, т. е. это «личные» поля класса.
Приведем пример.
Вспомним про наших старых знакомых — базовый класс Rectangle и производ-
производный от него класс ColorRectangle. Ранее поля базового класса xPos, yPos,
width, height были объявлены без модификатора доступа. Это равносильно тому,
как если бы для них был указан модификатор доступа private:
class Rectangle
{
private int xPos;
private int yPos;
private int width;
private int height;
Теперь попытаемся определить в производном классе метод GetXPos, возвра-
возвращающий текущее значение поля xPos:
class ColorRectangle : Rectangle
{
public int GetXPos()
{
return xPos;
}
}
В результате при компиляции мы получим следующее сообщение об ошибке:
'Redefinition.Rectangle.xPos' is inaccessible due to its protection level
В этом сообщении говорится, что установленный уровень защиты запрещает дос-
доступ к методу xPos, определенному в классе Rectangle.
Аналогичное сообщение появится и при попытке обращения из производного клас-
класса к методам базового класса, определенного с модификатором доступа private или,
что равносильно, вообще без какого-либо модификатора доступа.
130 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Рассмотрим пример базового класса, в котором метод SetPosition определен
с модификатором доступа private:
class Rectangle
int xPos;
int yPos;
private void SetPosition(int x, int y)
{
xPos = x;
yPos = y;
Попытаемся вызвать метод SetPosition из метода ColorSetPosition, кото-
который объявлен в производном классе ColorRectangle:
class ColorRectangle : Rectangle
{
public void ColorSetPosition()
{
SetPosition@, 0);
Так как в базовом классе метод SetPosition объявлен как private, в ре-
результате этой попытки мы получим следующее сообщение на этапе компиляции
программы:
'Redefinition.Rectangle.SetPosition(int, int)' is inaccessible due to
its protection level
Аналогичная ошибка произойдет и при попытке вызвать метод SetPosition для
объекта, созданного на базе класса ColorRectangle:
ColorRectangle cr;
cr = new ColorRectangle();
cr.SetPosition @, 10); // Ошибка! Доступ к методу запрещен
Таким образом, с помощью модификатора доступа private мы можем полностью
запретить доступ методам производных классов к полям и методам базового класса.
Заметим, что в языке C++ отсутствие модификатора доступа у поля или метода оз-
означает, что к этому полю или методу имеется полный доступ со стороны методов про-
производных классов или других методов, внешних по отношению к самому классу.
В этом смысле язык С# более строг — чтобы предоставить доступ к полю или методу
класса, необходимо указать модификатор доступа явным образом.
Глава 3. Объектно-ориентированное программирование 131
Модификатор public
Если поле или метод класса определены с модификатором доступа public, они дос-
доступны вне объявления базового класса или производных классов. Это, в частности, оз-
означает, что методы, объявленные вне класса, могут свободно обращаться к таким по-
полям и методам.
Ранее в наших примерах мы часто использовали модификатор доступа public
при объявлении методов класса. Приведем пример использования этого модификатора
для полей класса.
Изменим определение базового класса Rectangle, разрешив доступ к полям
класса:
class Rectangle
{
public int xPos ;
public int yPos;
int width;
int height;
Теперь определим в производном классе два метода с именами GetXPos
HGetYPos:
class ColorRectangle : Rectangle
public int GetXPos()
{
return xPos;
}
public int GetYPos()
{
return yPos;
Первый из этих методов будет возвращать содержимое поля xPos, а второй — по-
поля yPos. Теперь у нас есть способ не только установить текущее расположение пря-
прямоугольника на плоскости, но и получить текущие координаты его левого нижнего уг-
угла (рис. 3.1).
Так как поля xPos и yPos определены с модификатором доступа public,
мы можем в своей программе вывести расположение прямоугольника двумя спо-
способами:
ColorRectangle cr;
cr = new ColorRectangle();
cr.SetPosition@, 10);
132 А. В. Фролов, Г. В Фролов. Язык С#. Самоучитель
Console.WriteLine("Координаты прямоугольника = ({0}, {1})",
cr.GetXPos () , cr.GetYPos {));
Console.WriteLine("Координаты прямоугольника = ({0}, {1})",
cr.xPos, cr.yPos};
Первый способ предполагает получение текущих координат с помощью методов
GetXPos и GetYPos, а второй — прямое обращение к полям xPos и yPos.
Модификатор protected
Применение модификатора доступа public при объявлении полей класса до некото-
некоторой степени нарушает принцип инкапсуляции. Действительно, если программа рабо-
работает с полями класса напрямую, она зависит от типа и назначения этих полей. Между
тем разработчик класса (а это не всегда вы) может пожелать внести во внутреннюю
реализацию класса какие-либо изменения, затрагивающие поля класса, и оставить не-
нетронутым только набор методов класса.
Другое дело — производные классы. Связанные узами «родства», производным
классам для увеличения эффективности программы часто приходится обращаться
к полям базового класса напрямую.
Модификатор доступа protected позволяет найти некий компромисс. С помощью
этого модификатора вы можете предоставить доступ к полям и методам базового класса
только для производных классов, но не для внешних по отношению к классу методов.
Снова внесем небольшое изменение в класс Rectangle:
class Rectangle
{
public int xPos;
public int yPos;
protected int width;
protected int height;
Теперь мы добавили спецификатор доступа protected к полям width и height.
В производном классе Со lor Rectangle мы определили методы GetWidth и Get
Height, с помощью которых можно определить текущие размеры прямоугольника:
class ColorRectangle : Rectangle
public int GetWidth()
{
return width;
}
public int GetHeight ()
{
return height;
}
Глава З. Объектно-ориентированное программирование 133
Так как поля базового класса width и height объявлены как protected, мы
можем обращаться к ним из методов GetWidth и GetHeight производного класса.
Для вывода текущих размеров прямоугольника на консоль нам необходимо ис-
использовать эти методы:
ColorRectangle cr;
cr = new ColorRectangle ();
cr.SetSizeB0, 30);
Console.WriteLine("Ширина прямоугольника = @), высота = A}",
cr.GetWidth(), cr.GetHeight());
Попытка прямого обращения к полям width и height для объекта сг окончится
неудачей:
Console.WriteLine("Ширина прямоугольника = {0}, высота = {1}",
cr.width, cr.height); // Ошибка! Доступ запрещен
В результате с помощью модификатора доступа protected мы скрыли от пользо-
пользователя классов Rectangle и ColorRectangle внутренние поля width и height.
Доступ к этим полям возможен только при посредстве методов — метод Set Size из-
изменяет содержимое полей, а методы GetWidth и GetHeight позволяют программе
узнать это содержимое.
При необходимости разработчик базового класса Rectangle может изменить на-
названия полей width и height или полностью изменить способ хранения размеров
прямоугольника. Это никак не отразится на пользователях производного класса
ColorRectangle, так как они задают и определяют размеры прямоугольника с по-
помощью соответствующих методов.
Модификатор internal
Модификатор internal представляет собой комбинацию модификаторов public
и protected. Он ограничивает доступность отмеченных им полей и методов преде-
пределами одной сборки (assembly). При этом внешние классы не имеют к таким полям ни-
никакого доступа. Подробнее о сборках мы поговорим позже.
Все модификаторы, кроме internal, должны использоваться по отдельности.
Вы не можете, например, указывать доступ public protected или priva-
private protected. Тем не менее модификатор internal допускается использовать
вместе с модификатором protected.
В этом случае одновременно предоставляются виды доступа protected (для ис-
использования производных классах) и public (для использования в пределах текущей
сборки).
Вот пример использования модификатора internal:
class Rectangle
{
public int xPos;
public int yPos;
internal int width;
internal int height;
134 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Пример программы
В листинге 3.3 мы привели исходный текст программы, демонстрирующей использо-
использование модификаторов доступа. В этой программе мы работаем с немного модифици-
модифицированными классами Rectangle и ColorRectangle.
Листинг 3.3. Файл ch03\Redefinition\RedefinitionApp.cs
using System;
namespace Redefinition
class Rectangle
public int xPos;
public int yPos;
protected int width;
protected int height;
public Rectangle()
xPos = yPos = width = height = 0;
public void SetPosition(int x, int y)
xPos = x;
yPos = y;
public void SetSize(int w, int h)
width = w;
height = h;
class ColorRectangle : Rectangle
{
byte colorR;
byte colorG;
byte colorB;
public void SetColor(byte r, byte g, byte b)
{
colorR = r;
colorG = g;
colorB = b;
Глава З. Объектно-ориентированное профаммирование 135
public new void SetSize(int w, int h)
{
width = w;
height = h;
public int GetXPosО
return xPos;
public int GetYPos()
return yPos;
public int GetWidthO
return width;
public int GetHeight()
return height;
class Redefinition
static void Main(string[] args)
ColorRectangle cr;
cr = new ColorRectangle();
cr.SetPositionfO, 10);
cr.SetSizeB0, 30);
cr.SetColor @, 0, OxFF);
Console.WriteLine("Координаты прямоугольника = ({0}, {1})",
cr.GetXPosО, cr.GetYPos());
Console.WriteLine("Координаты прямоугольника = ({0}, {1})",
cr.xPos, cr.yPos);
Console.WriteLine("Ширина прямоугольника = {0}, высота = {1}",
cr .GetWidthO , cr .GetHeight Ob-
Console .ReadLine() ;
}
136 А. В. Фролов, Г. В. Фролов. Язык С# Самоучитель
Прежде всего мы указали спецификаторы доступа для полей базового класса
Rectangle:
public int xPos;
public int yPos;
protected int width;
protected int height;
Модификатор public позволил нам напрямую обращаться к этим полям при вы-
выводе текущих координат прямоугольника на консоль. Что же касается модификатора
protected, то он открывает доступ к полям width и height только для методов
производных классов. В нашем случае это класс ColorRectangle.
В производном классе мы добавили методы, с помощью которых можно извлекать
значения полей, объявленных в базовом классе:
public int GetXPosO
{
return xPos;
}
public int GetYPosO
{
return yPos;
}
public int GetWidthO
{
return width;
}
public int GetHeight()
{
return height;
}
Метод Main нашей программы демонстрирует способы прямого обращения
к полям базового класса, объявленным со спецификатором доступа public, а так-
также способы обращения к полям типа protected. В последнем случае доступ
осуществляется с применением специально предназначенных для этого методов
производного класса.
Статические члены класса
Создавая объекты классов в наших программах, мы до сих пор создавали для каждого
такого объекта члены экземпляра класса (instance member).
Чтобы объяснить, о чем здесь идет речь, вернемся к объявлению класса Tele-
vis ionSet, описанного нами в начале этой главы. Вот сокращенный исходный текст
этого класса:
Глава 3. Объектно-ориентированное программирование 137
class TelevisionSet
{
bool isPowerOn; // включен или выключен
byte maxchannel; // максимальный номер канала
byte currentchannel; // текущий номер канала
byte currentVolume; // текущая громкость звука
public void SetPowerStateOn()
{
isPowerOn = true;
}
public void SetPowerStateOff()
{
isPowerOn = false;
Как видите, в классе определены 4 поля и несколько методов.
При создании объектов на базе класса TelevisionSet (т. е., иными словами,
объектов класса TelevisionSet) мы фактически создаем в оперативной памяти
компьютера два набора таких полей и методов:
TelevisionSet tvSmall;
TelevisionSet tvLarge;
tvSmall = new TelevisionSetF);
tvLarge = new TelevisionSetD0);
Для объекта tvSmall создаются свои поля и методы, а для объекта tvLarge —
свои. Это позволяет управлять нашими двумя виртуальными телевизорами по отдель-
отдельности, так как их параметры хранятся в различных областях оперативной памяти ком-
компьютера:
tvSmall.SetPowerStateOn();
tvSmall.SetChannelE) ;
tvSmall.SetVolumeE0);
tvLarge.SetPowerStateOn();
tvLarge.SetChannelB7);
tvLarge.SetVolumeC0);
В данном случае такая особенность важна, так как речь идет о совершенно независи-
независимых друг от друга объектах. В самом деле, каждый телевизор может быть переключен
на свою программу и для каждого телевизора можно установить свой уровень громкости.
Однако встречаются ситуации, в которых желательно было бы иметь нечто общее,
объединяющее все объекты, созданные на базе одного и того же класса. В языке С# такая
возможность реализуется с помощью так называемых статических членов класса.
138 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Статические поля класса
Статическое поле класса— это такое поле, которое существует в оперативной памяти
компьютера в единственном экземпляре, сколько бы объектов класса ни создавалось.
Наиболее очевидное применение таких полей — подсчет общего количества соз-
созданных объектов класса.
Рассмотрим это на простейшем примере (листинг 3.4).
Листинг 3.4. Файл chO3\StaticFields\StaticFieldsApp.cs
using System;
namespace StaticFields
{
class Point
{
int xPos;
int yPos;
static public uint totalPoints;
public Point(int x, int y)
{
xPos = x;
yPos = y;
totalPoints++;
class StaticFieldsApp
{
static void Main(string[] args)
{
Point pt;
for(int i=0; i < 10; i++)
{
pt = new Point@, i);
}
Console.WriteLineCВсего точек: {0}", Point.totalPoints);
Console.ReadLine();
Здесь мы объявили класс Point, представляющий обыкновенную точку на плос-
плоскости. Поля класса xPos и yPos хранят координаты точки на плоскости.
Помимо этих полей в классе Point объявлено статическое поле totalPoints,
предназначенное для подсчета общего количества точек, размещенных на плоскости:
static public uint totalPoints;
Глава 3. Объектно-ориентированное программирование 139
Ключевое слово static указывает на то, что поле totalPoints является
статическим. Числовые статические поля инициализируются нулевым значением.
В нашей программе мы воспользуемся этим фактом. Заметим также, что при необ-
необходимости вы можете проинициализировать статические поля класса при их объ-
объявлении, например:
static public uint totalPoints = 5;
Метод Main нашей программы, получающий управление после запуска, создает
в цикле 10 объектов класса Point:
Point pt;
for(int i=0; i < 10; i++)
{
pt = new Point@, i) ;
}
Здесь в теле метода Main мы определили локальную переменную pt класса
Point. Эта переменная называется локальной, потому что доступ к ней возможен
только в пределах метода Main.
Далее в цикле программа создает объекты, по очереди сохраняя ссылки на них
в переменной расами по себе эти объекты нам не нужны, нам интересно только их
количество).
При создании каждого объекта управление получает конструктор класса:
public Point(int x, int у)
{
xPos = x;
yPos = y;
totalPoints++;
}
Он сохраняет координаты создаваемой точки в соответствующих полях, а также
увеличивает содержимое статического поля totalPoints.
В то время как для хранения координат каждой создаваемой точки в оперативной
памяти компьютера резервируются разные области памяти, статическое поле total-
Points хранится в единственном экземпляре. В результате при создании каждого но-
нового объекта конструктор увеличивает на единицу число, хранящееся в этой общей
для всех объектов памяти.
После завершения цикла мы можем вывести на консоль количество созданных
объектов, взяв его из статической переменной totalPoints:
Console.WriteLine("Всего точек: {0}", Point.totalPoints);
Обратите внимание, что для ссылки на это поле мы использовали имя класса Point, a
не имя объекта pt, созданного на базе этого класса. Это замечание важно. Для адресации
статических членов класса нужно использовать имя класса, а не имя поля или локальной
переменной, хранящих экземпляр объекта. Следующая строка будет неверна:
140 А В Фролов, Г. В Фролов. Язык С#. Самоучитель
Console.WriteLine("Всего точек: @)", pt.totalPoints); // Ошибка!
Для чего еще можно использовать статические поля класса?
Часто их используют для хранения каких-либо общих данных, имеющих отноше-
отношение ко всей программе в целом. Другие языки программирования, например С и C++,
допускают создание с этой целью так называемых глобальных переменных. Глобаль-
Глобальные переменные доступны из любого места программы, однако их применение запу-
запутывает программу и усложняет поиск ошибок. Язык С# не допускает использования
глобальных переменных.
Еще одно полезное применение статических полей класса — создание статических
констант — будет рассмотрено в следующем разделе.
Статические константы
Многие программы работают с константами — числами, текстовыми символами
и строками, содержимое которых на протяжении работы программы никогда не из-
изменяется. Обычно такие константы содержат значения параметров работы про-
программы.
Для того чтобы определить константу в программе С#, необходимо использовать
ключевое слово const. Приведем пример использования этого ключевого слова для
создания константы в области локальных переменных метода Main.
В гл. 1 мы рассказывали об инициализации переменных. Вот фрагмент про-
программы, в которой в переменную piNumber записывается приблизительное зна-
значение числа я:
float piNumber = 3.1415926F;
System. Console .WriteLine (@"Число п|1ПИ"" = {0}", piNumber) ;
Хотя переменная piNumber и может быть использована в роли константы, напри-
например при вычислении длины окружности или при других операциях с числом л, этот
способ не очень-то хорош. Программист по ошибке может изменить значение, храня-
хранящееся в переменной piNumber, из-за чего все вычисления с использованием этой пе-
переменной будут выполнены неверно.
Определив переменную piNumber как константу, вы гарантируете неизменность
ее значения:
const float piNumber = 3.1415926F;
Попытка выполнить, например, следующую операцию присваивания будет пресе-
пресечена еще на этапе компиляции программы:
const float piNumber = 3.1415926F;
piNumber =10; // Ошибка, т. к. переменная piNumber — константа
Если в программе имеются группы логически связанных между собой констант,
удобно оформить их как классы, содержащие только константные статические поля.
Рассмотрим программу, исходный текст которой приведен в листинге 3.5.
Глава 3. Объектно-ориентированное программирование 141
Листинг 3.5. Файл ch03\StaticConstant\StaticConstantApp.cs
using System;
namespace StaticConstant
{
class GlobalParams
{
public const string url = @"http://www.datarecovery.ru";
public const int port = 80;
class StaticConstant
{
static void Main(string[] args)
{
Console.WriteLine(
"Адрес сайта службы восстановления данных: {0}:{1}",
GlobalParams.url, GlobalParams.port);
Console.ReadLine();
В этой программе мы объявили класс GlobalParams, содержащий только два
члена — константные поля url и port. Первое из них содержит универсальный адрес
ресурса (Universal Resource Locator, URL) Web-сайта службы восстановления данных
DataRecpvery.Ru, созданной одним :*з авторов этой книги, а второе— номер порта
протокола ТСРЛР для доступа к данному Web-сайту.
Задача программы — отображение на консоли полного адреса сайта, состоящего
из адреса URL и номера порта ТСРЛР, разделенного двоеточием:
Console.WriteLine(
"Адрес сайта службы восстановления данных: {0}:{1}",
GlobalParams.ur1, GlobalParams.рогt);
Обратите внимание, что, хотя при объявлении полей url и port мы не использо-
использовали ключевое слово static, обращение к полям класса GlobalParams выполняет-
выполняется как к статическим членам класса, т. е. с использованием имени класса.
Это возможно потому, что в контексте объявления полей класса ключевое слово
const автоматически делает описанные с его помощью поля статическими. При этом
дополнительное использование ключевого слова static не только излишне,
но и не допускается компилятором.
Статические методы класса
В предыдущем разделе мы создавали класс, состоящий из одних только полей. Такие
классы удобны, например, для объединения логически связанных между собой констант.
Существует и другая возможность — можно создать классы, состоящие из одних
только методов. Такие классы могут объединять методы, реализующие алгоритмы,
так или иначе связанные между собой.
142 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
В программе, исходный текст которой представлен в листинге 3.6, мы создали
класс MathOp. В этом классе есть только методы для выполнения некоторых арифме-
арифметических операций над целыми числами, но нет ни одного поля.
Листинг 3.6. Файл ch03\StaticMethod\StaticMethodApp.cs
using System;
namespace StaticMethod
{
class MathOp
{
public static int Increment(int x)
{
return ++x;
}
public static int Decrement(int x)
{
return --x;
}
public static int PowerOf2(int x)
{
return x * X;
}
}
class StaticMethod
{
static void Main(string[] args)
{
int op = 2;
Console.WriteLine(
"Инкремент: {О}, декремент: {1}, квадрат числа: {2}",
MathOp.Increment(op),
MathOp.Decrement(op),
MathOp.PowerOf2(op));
Console.ReadLine();
Все методы класса MathOp — статические. Метод Increment возвращает значение
переданного ему параметра, увеличенное на единицу, метод Decrement — уменьшенное
на единицу, а метод PowerOf 2 возвращает квадрат переданного ему значения.
Так как в классе MathOp нет полей, не имеет смысла создавать на его базе объект
и размещать данный объект в оперативной памяти компьютера. Вы можете использо-
использовать его методы и без создания объекта, ссылаясь на них при помощи имени класса.
Так мы и поступили в методе Main приведенной выше программы.
Если в вашей программе имеются методы, предназначенные для какой-либо алго-
алгоритмической обработки данных, логически связанные между собой и не имеющие от-
отношения к какому-либо объекту программы, то вы можете оформить их в виде класса
с набором статических методов. Если объявить эти методы с модификатором доступа
public, они будут доступны в любом месте программы. Для ссылки на них требуется
указать имя класса.
Глава 3. Объектно-ориентированное программирование 143
Перегрузка методов
Неотъемлемой возможностью, реализованной во всех современных объектно-
ориентированных языках программирования, является так называемая перегрузка
методов.
Чтобы понять, что это такое, рассмотрим следующую задачу.
Пусть нам нужно создать класс, содержащий статические методы для вывода зна-
значений типа int, bool и string на консоль. Вот один из возможных вариантов ре-
решения этой проблемы:
class ValuePrinter
{
static public void Printlnt(int x)
{
Console.WriteLine("Число int: {0}", x) ;
}
static public void PrintString(string s)
{
Console.WriteLine("Строка string: {0}", s);
}
static public void PrintBool(bool b)
{
Console.WriteLine("Логическое значение bool: {0}", b);
Здесь мы объявили 3 метода с различными именами, каждый из которых можно
использовать для вывода значения своего типа:
ValuePrinter.PrintlntA);
ValuePrinter.PrintString("Привет! " ) ;
ValuePrinter.PrintBool(true);
Как видите, для выполнения однотипных действий (вывода значения на консоль)
нам приходится вызывать 3 различных метода. Намного удобнее было бы придумать
такой способ вывода, когда метод с одним и тем же именем мог бы выводить
на консоль значения различных типов. При этом нам не пришлось бы запоминать име-
имена методов, предназначенных для работы с различными типами данных.
В языке С# существует возможность определения в одном классе нескольких мето-
методов с одинаковыми именами, но с различными списками параметров. Такие методы
называются перегруженными.
Взгляните на исходный текст программы, приведенный в листинге 3.7.
Листинг 3.7. Файл ch03\MethodOverload\MethodOverloadApp.cs
using System;
namespace MethodOverload
{
class ValuePrinter
1
public void Print (int x)
144 А В Фролов, Г. В Фролов. ЯэыкС# Самоучитель
Console.WriteLine("Число int: {0}", x);
}
static public void Print(string s)
{
Console.WriteLine ("Строка string: {0}", s ) ,-
}
static public void Print(bool b)
{
Console.WriteLine("Логическое значение bool: {0}", b)
}
)
class MethodOverloadApp
{
static void Main(string[] args)
{
ValuePrinter.Print A);
ValuePrinter.Print("Привет!");
ValuePrinter.Print(true);
Console.ReadLine();
Как видите, в классе ValuePrinter мы определили 3 статических метода
с именем Print. Эти методы отличаются друг от друга только типом значения пара-
параметра и решают одну и ту же задачу — вывода полученного значения на консоль.
Пользоваться нашими перегруженными методами очень просто — достаточно пе-
передать им значение, отображаемое на консоли:
ValuePrinter.PrintA) ;
ValuePrinter.Print("Привет!");
ValuePrinter.Print(true);
Теперь для вывода значений любого из перечисленных выше типов мы можем ис-
использовать метод с одним и тем же именем. При формировании кода компилятор под-
подставит вызов нужного варианта метода Print в зависимости от типа значения, пере-
передаваемого методу в качестве параметра.
Заметим, что перегруженные методы должны отличаться списком параметров,
при этом возвращаемое значение не играет никакой роли. Следующее определение
класса будет неверным:
class ValuePrinter
{
static public void Print(int x)
{
Console.WriteLine("Число int: {0}", x);
Глава З. Объектно-ориентированное программирование 145
static public int Print (int x) // Ошибка!
{
Console.WriteLine("Число int: {0}", x);
return x;
Здесь мы попытались создать перегруженный метод Print с тем же списком па-
параметров, но, в отличие от исходного метода, возвращающий значение. Такая попытка
будет рассматриваться компилятором как ошибка.
Вы можете перегружать не только обычные методы, но и конструкторы. Перегру-
Перегруженные конструкторы, как и перегруженные методы, должны отличаться друг от дру-
друга списком параметров.
Рассмотрим следующий пример:
class Point
{
int xPos;
int yPos;
public Point()
{
xPos = 0;
yPos = 0;
public Point(int x, int y)
{
XPOS = X;
yPos = y;
Здесь в классе Point мы определили два конструктора, один из которых имеет па-
параметры, а другой — нет. При помощи конструктора с параметрами мы можем при
создании объекта (точки) задать координаты этого объекта на плоскости. Конструктор
без параметров всегда располагает создаваемый объект в начале системы координат:
Point ptl, pt2;
ptl = new PointA0, 20);
pt2 = new Point();
Объект ptl будет размещен в точке с координатами A0, 20), а объект pt2 — в на-
начале системы координат, т. е. в точке @, 0).
Конструктор
Как мы уже говорили, класс представляет собой описание объекта, а не сам объект.
Чтобы создать объект на базе класса, необходим оператор new:
СоlorRectangle cr;
or - new Co]orRectangle();
146 А В. Фролов, Г В. Фролов Язык С# Самоучитель
В этом разделе мы рассмотрим процесс создания объекта класса подробнее. В ча-
частности, мы опишем работу конструктора класса.
Конструктор — это специальный метод, получающий управление при создании
экземпляра объекта. Обычно этот метод используется для инициализации полей класса
или выполнения других инициализирующих действий.
Имя метода, играющего роль конструктора класса, должно совпадать с названием
класса. Например, для класса Rectangle мы создали конструктор следующего вида:
class Rectangle
{
public int xPos;
public int yPos ;
protected int width;
protected int height;
public Rectangle()
{
xPos = yPos = width = height = 0;
Задачей этого конструктора является инициализация всех полей класса
Rectangle нулевыми значениями.
Обратите внимание на использование спецификатора доступа public при объяв-
объявлении конструктора. Если его не указать или применить вместо него спецификатор
доступа private, программа не сможет создать объект данного класса.
Зачем же нужны классы, которые нельзя использовать для создания объектов?
Конструкторы такого вида встречаются в классах, содержащих только статиче-
статические члены. Такие классы не используются для создания объектов. Разработчик
класса может запретить создание объектов класса, содержащего только статиче-
статические члены, так как, кроме непроизводительной потери памяти, это ни к чему
не приведет.
Конструктор по умолчанию
Если в классе нет конструктора, для него будет создан так называемый конструктор
по умолчанию (default constructor).
Задача конструктора по умолчанию — начальная инициализация полей класса.
При этом в числовые поля записываются нулевые значения.
Рассказывая о статических полях и методах, мы говорили, что если класс содержит
только статические члены, то программе не следует создавать объекты такого класса.
Как же запретить создание объектов класса?
Если не объявлять в классе конструктор, то для него будет использован конструк-
конструктор по умолчанию. Поэтому программа сможет создавать объекты на основе класса,
не имеющего собственного конструктора.
Глава 3. Объектно-ориентированное программирование 147
Но вы можете создать в классе конструктор с модификатором доступа private,
в результате чего создание объектов данного класса станет невозможным. Этот конст-
конструктор, называемый закрытым, может не содержать ни одного оператора, так как он
никогда не будет выполняться.
class ValuePrinter
{
private ValuePrinter () // закрытый конструктор
{
}
static public void Print(int x)
{
Console.WriteLine("Число int: {0}", x);
}
static public void Print(string s)
{
Console.WriteLine ("Строка string: {0}", s);
}
static public void Print(bool b)
{
Console.WriteLine("Логическое значение bool: {0}", b);
Компилятор не позволит создать объект класса, объявленного с закрытым конст-
конструктором.
Конструкторы и наследование
В языке С# конструкторы классов не наследуются. Это означает, что при создании
производного объекта сначала вызывается конструктор производного класса, а за-
затем — конструктор базового класса.
Рассмотрим следующий пример (листинг 3.8).
Листинг 3.8. Файл ch03\Constructors\ConstructorsApp.cs
using System;
namespace Constructors
{
class BaseClass
{
public BaseClass()
{
Console.WriteLine("Вызван конструктор базового класса");
class DerivedClass : BaseClass
f
public DerivedClass()
148 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Console.WriteLine("Вызван конструктор производного класса")
class ConstructorsApp
{
static void Main(string[] args)
{
DerivedClass derived;
derived = new DerivedClass();
Console.ReadLine();
Здесь мы объявили базовый класс BaseClass и производный от него класс
DerivedClass. В каждом из классов мы объявили собственный конструктор. Полу-
Получив управление, этот конструктор идентифицирует себя, отображая на консоли свое
сообщение. Вот что мы там увидим, запустив эту программу:
Вызван конструктор базового класса
Вызван конструктор производного класса
Таким образом, при создании производного класса вначале вызывается конструк-
конструктор базового класса.
А что будет, если в базовом классе определить несколько перегруженных конст-
конструкторов?
Теперь, когда конструкторов базового класса несколько, встает вопрос выбора кон-
конструктора при создании объекта производного класса.
Компилятор С# решает этот вопрос следующим образом:
• если в базовом классе объявлен конструктор без параметров, то при создании объ-
объекта производного класса вначале вызывается конструктор без параметров базово-
базового класса, а затем конструктор производного класса:
• в том случае, когда в базовом классе нет конструктора без параметров, компилятор
выдает сообщение об ошибке.
В некоторых случаях такое поведение может вызывать определенные неудобства.
Например, при создании объекта производного класса может потребоваться вызвать
один из перегруженных конструкторов базового класса с параметром.
Чтобы разрешить эту проблему, в языке С# предусмотрены так называемые ини-
инициализаторы конструкторов base и this.
Инициализатор конструктора base
Вначале мы рассмотрим инициализатор конструктора base. Этот инициализатор по-
позволяет вызвать конструктор базового класса перед выполнением конструктора произ-
производного класса. Рассмотрим использование этого инициализатора на примере про-
программы (листинг 3.9).
Глава 3. Объектно-ориентированное программирование 149
Листинг 3.9. Файл ch03\ConstructorsOverloaded\ConstructorsOverloadedApp.cs
using System;
namespace ConstructorsOverloaded
{
class BaseClass
{
public BaseClass()
{
Console.WriteLine(
"Конструктор базового класса без параметров");
)
public BaseClass(int i)
{
Console.WriteLine(
"Конструктор базового класса, параметр {0}", i) ;
class DerivedClass : BaseClass
{
public DerivedClass() : baseE)
{
Console.WriteLine("Вызван конструктор производного класса")
}
}
class ConstructorsOverloadedApp
{
static void Main(string(] args)
{
DerivedClass derived;
derived = new DerivedClass() ;
Console.ReadLine() ;
Здесь мы определили в базовом классе BaseClass два конструктора, один из ко-
которых не имеет параметров, а другой получает один параметр. Оба конструктора вы-
выводят сообщения на консоль, причем второй конструктор включает в текст этого со-
сообщения значение полученного параметра.
Конструктор производного класса DerivedClass тоже выводит сообщение
на консоль, но перед этим он вызывает конструктор базового класса, передавая ему
в качестве параметра число 5:
public DerivedClass() : baseE)
{
Console.WriteLine("Вызван конструктор производного класса");
150 А В Фролов, Г. В. Фролов. Язык С# Самоучитель
Если в базовом классе объявлено несколько конструкторов, то по количеству и ти-
типу параметров, указанных в скобках после ключевого слова base, компилятор может
выбрать нужный конструктор.
И действительно, после запуска приведенной выше программы на консоль выво-
выводится следующее:
Конструктор базового класса, параметр 5
Вызван конструктор производного класса
Это означает, что при создании объекта производного класса был вызван конструк-
конструктор базового класса с параметром.
Инициализатор конструктора this
С помощью инициализатора конструктора this, который вызывается аналогично
только что рассмотренному инициализатору base, существует возможность вызова
одного конструктора из другого в пределах класса. То есть если в классе имеется не-
несколько перегруженных конструкторов, то инициализатор this позволяет вызвать
один такой конструктор из другого.
Заметим, что возможность вызова одного конструктора класса из другого конст-
конструктора этого же класса отсутствует в языке программирования C++.
Статический конструктор
Если в классе определены статические поля, то для их инициализации используется
статический конструктор. При объявлении статического конструктора необходимо
указывать ключевое слово static.
Статические конструкторы аналогичны обычным конструкторам. В них, например,
вы можете вызвать инициализаторы конструкторов. Статические конструкторы базо-
базового класса вызываются перед статическими конструкторами производного класса.
Деструктор
Как мы говорили во Введении, для удаления объектов из оперативной памяти Micro-
Microsoft .NET Framework использует специальную систему сборки мусора. Поэтому объек-
объекты и локальные переменные уничтожаются автоматически, как только в них отпадает
необходимость.
Тем не менее встречаются такие ситуации, когда перед удалением объекта класса
из памяти необходимо выполнить какие-либо заключительные действия. Например,
если объект работает с файлами, перед удалением такого объекта все открытые файлы
следует закрыть.
Как контрпара конструктору, создающему объект, во многих объектно-
ориентированных языках программирования предусматривается специальный метод
с названием деструктор, отвечающий за выполнение заключительных действий при
ликвидации объекта.
Деструктор объявляется таким же образом, каким объявляется и конструктор, од-
однако перед его именем (совпадающим с именем класса) нужно поместить символ ~.
Глава 3. Объектно-ориентированное программирование 151
Следует заметить, что из-за асинхронного принципа работы системы сборки мусо-
мусора момент вызова деструктора трудно предугадать (если вообще возможно). Поэтому,
в частности, разработчикам программ С# деструкторы использовать не рекомендуется.
Еще о классах и полях
В этом разделе мы рассмотрим еще несколько особенностей языка программирования
С#, касающихся классов и полей. Это поля readonly, параметры ref, out и др.
Поля readonly
Выше, в разделе «Статические константы», мы рассказывали вам о том, что програм-
программам часто приходится иметь дело с константами — полями и переменными, содержи-
содержимое которых не изменяется на протяжении работы программы.
Значения констант кодируются непосредственно в исходном тексте программы,
поэтому они должны быть известны разработчику. Но так бывает далеко не всегда.
Некоторые параметры работы программа узнает только после запуска, причем в про-
процессе работы программы эти параметры остаются неизменными.
В качестве примера таких параметров можно привести, например, тип и версию
ОС, под управлением которой работает программа, экранное разрешение, максималь-
максимальное количество цветов, которое способен отображать видеоадаптер, локальный
адрес IP, полученный от администратора локальной сети или от специального сервера,
распределяющего такие адреса автоматически.
Для хранения всех этих параметров вам не удастся использовать поля, определенные
с ключевым словом static, так как их значения будут известны только после запуска
программы. Использование же для хранения параметров обычных полей хотя и возможно,
но небезопасно, так как программист по ошибке может изменить содержимое таких полей.
Чтобы выйти из этой затруднительной ситуации, в языке С# разрешается объявлять
поля с ключевым словом readonly. Содержимое таких полей можно изменять только
в конструкторе объекта, а после того, как объект создан, они превращаются в настоящие
константы. Попытка изменить содержимое поля readonly в каком-либо методе класса,
кроме конструктора, будет рассматриваться компилятором как ошибочная.
Таким образом, поля, определенные с ключевым словом readonly, доступны
только на чтение.
Пример использования ключевого слова readonly мы привели в программе, ис-
исходный текст которой вы найдете в листинге 3.10.
Листинг 3.10. Файл ch03\ReadOnlyFields\ReadOnlyFieldsApp.cs
using System;
namespace ReadOnlyFields
{
class NetworkParams
{
public static readonly string ip;
public static readonly uint port;
152 А В Фролов, Г. В. Фролов. Язык С# Самоучитель
static NetworkParams()
{
ip = 27.0.0.1";
port = 80;
class ReadOnlyFieldsApp
{
static void Main(string[] args)
{
Console.WriteLine("Адрес IP: {0}, порт: {1}
NetworkParams.ip, NetworkParams.port);
Console.ReadLine();
Здесь в классе NetworkParams мы определили два поля, ip и port, как доступ-
доступные только для чтения. Так как класс NetworkParams предназначен для хранения
констант, значение которых определяется в момент работы программы, нет никакой
необходимости создавать объекты этого класса. Поэтому наш класс содержит только
статические поля.
Но статические поля нельзя проинициализировать обычным конструктором, пред-
предполагающим создание объектов класса. Проблема решается при помощи статического
конструктора, на который и возлагается инициализация наших полей:
static NetworkParams()
{
ip = 27.0.0.1";
port = 80;
}
Только здесь, в статическом конструкторе, мы можем изменять значение констант
типа readonly. Статический конструктор работает только один раз при запуске про-
программы и до момента обращения программы к любому статическому члену класса.
Поэтому наша программа может обращаться с полями ip и port как с обычными ста-
статическими константами, получая их значение с помощью имени класса Network-
NetworkParams:
Console.WriteLine("Адрес IP: @), порт: {I}",
NetworkParams.ip, NetworkParams.port);
Разумеется, вы можете создавать поля типа readonly не только статическими,
но и обычными. В этом случае для работы с этими полями придется создавать объект
класса, а обращение к полям нужно будет выполнять с применением имени объекта,
а не имени класса.
Глава 3. Объектно-ориентированное программирование 153
Передача параметров по ссылке
До сих пор мы имели дело с методами, способными вернуть только одно значение.
Но иногда это неудобно. Было бы неплохо иметь возможность получить при вызове
метода сразу несколько значений.
Рассмотрим типичный пример. Пусть у нас имеется определение класса Rec-
Rectangle:
'class Rectangle
{
public int xPos;
public int yPos;
protected int width;
protected int height;
public Rectangle()
{
xPos = yPos = width = height = 0;
public void SetPosition(int x, int y)
{
xPos = x;
yPos = y;
public void SetSize(int w, int h)
{
width = w;
height = h;
public int GetXPos()
{
return xPos;
}
public int GetYPosO
{
return yPos;
}
public int GetWidthO
{
return width;
}
public int GetHeight(
{
return height;
154
А В. Фролов, Г. В. Фролов. ЯзыкС#. Самоучитель
Здесь методы SetPosition и SetSize устанавливают сразу два параметра.
Первый из них задает координаты левого нижнего угла прямоугольника, а второй —
ширину и высоту прямоугольника.
Однако для того чтобы получить координаты и размеры прямоугольника, нам нуж-
нужно не 2, а целых 4 метода, так как каждый метод принципиально может вернуть только
одно значение.
Чтобы объяснить, как можно найти выход из данной ситуации, нам придется разо-
разобраться в том, как методы получают параметры.
Во многих языках программирования существует два метода передачи парамет-
параметров — по значению и по ссылке.
Когда значение параметра передается методу по значению, то метод получает это
значение и сохраняет его в своей локальной памяти (в локальном стеке). Хотя метод
и может попытаться изменить значение переменной, передаваемой ему по значению,
это закончится безрезультатно. Если же параметр передается методу по ссылке, то ме-
метод может изменять значение соответствующей переменной.
Приведем конкретный пример.
Пусть наша программа создает объект класса Rectangle и вызывает методы для
установки его расположения и размера:
Rectangle rect;
rect = new Rectangle();
rect.SetPosition@, 10);
rect.SetSizeB0, 30);
Мы создадим метод GetSize, предназначенный для того, чтобы получить оба
размера прямоугольника (ширину и высоту) за один прием:
public void GetSize(int w, int h)
{
w = width;
h = height;
)
Этот метод мы попытаемся использовать следующим образом:
int wCurrent, hCurrent;
rect.GetSize(wCurrent, hCurrent);
Здесь мы вызываем метод GetSize в надежде на то, что он изменит содержимое
переменных, переданных ему в качестве параметров. Однако этого не произойдет,
так как метод GetSize копирует значения полученных параметров в свой стек и мо-
модифицирует в своей локальной памяти. При этом все изменения пропадут после того,
как управление возвратится вызывающему методу. В результате значения переменных
wCurrent и hCurrent останутся неизменными.
Чтобы заставить метод GetSize изменить содержимое переменных wCurrent
и hCurrent, нам нужно передать методу не значения переменных, а ссылки на них.
Упрощенно можно представить себе ссылку как адрес переменной в оперативной па-
памяти. Такой способ передачи параметров и называется передачей по ссылке.
Глава 3 Объектно-ориентированное программирование 155
Пользуясь полученной ссылкой, метод сможет изменить содержимое переменных.
В результате мы получим то, что хотели. Вызвав один метод, мы получим от него сра-
сразу два значения — ширину и высоту прямоугольника.
Для передачи параметров по ссылке в языке С# предусмотрено два ключевых сло-
слова— ref и out. Первое из них требует предварительной инициализации объекта,
на который передается ссылка, а второе — не требует.
Использование ключевого слова out демонстрируется в программе, исходный
текст которой приведен в листинге 3.11.
Листинг 3.11. Файл chO3\RefParam\RefParamApp.cs
using System;
namespace RefParam
{
class Rectangle
{
int xPos;
int yPos;
int width;
int height;
public Rectangle()
{
xPos = yPos = width = height = 0;
}
public void SetPosition(int x, int y)
{
xPos = x;
yPos = y;
}
public void SetSize(int w, int h)
{
width = w;
height = h;
public int GetXPos()
{
return xPos;
}
public int GetYPos()
{
return yPos;
}
public int GetWidthO
{
return width;
}
156 А В Фролов, Г. В. Фролов Язык С#. Самоучитель
public int GetHeight(
f
return height;
public void GetSize(out int w, out int h)
{
w = width;
h = height;
public void GetPosition(out int x, out int y)
{
x = xPos;
у = yPos;
class RefParamApp
{
static void Main(string[] args)
{
Rectangle rect;
rect = new RectangleO;
rect.SetPosition@, 10);
rect.SetSizeB0, 30);
Console.WriteLine("Расположение прямоугольника: ({0}, {1})
rect.GetXPos(), rect.GetYPos());
Console.WriteLine(
"Ширина прямоугольника: {0}, высота прямоугольника: {1}"
rect.GetWidthO, rect.GetHeight());
int xCurrent, yCurrent, wCurrent, hCurrent;
rect.GetPosition(out xCurrent, out yCurrent);
rect.GetSize(out wCurrent, out hCurrent);
Console.WriteLine("Расположение прямоугольника: ({0}, {1})
xCurrent, yCurrent);
Console.WriteLine(
"Ширина прямоугольника: @), высота прямоугольника: {1}"
wCurrent, hCurrent);
Console.ReadLine() ;
Здесь мы демонстрируем использование как уже знакомого нам метода получения
координат и размеров прямоугольника, предполагающего объявление четырех мето-
методов, так и нового, основанного на передаче параметров по ссылке.
Глава 3. Объектно-ориентированное программирование 157
Вот старый способ:
Console.WriteLine ("Расположение прямоугольника: ({0}, {1})",
rect.GetXPos(), rect.GetYPos() ) ;
Console.WriteLine(
"Ширина прямоугольника: @), высота прямоугольника: {1}",
rect.GetWidthO, rect.GetHeight());
Здесь все понятно: мы вызываем 4 метода и выводим на консоль возвращенные
ими значения.
Новый способ предполагает использование методов GetSize и GetPosition,
определенных следующим образом:
public void GetSize(out int w, out int h)
{
w = width;
h = height;
public void GetPosition(out int x, out int y)
{
x = xPos;
у = yPos;
}
Параметры этих методов мы отметили ключевым словом out, чтобы указать ком-
компилятору, что это выходные параметры функции, передаваемые по ссылке.
Как пользоваться этими методами?
Очень просто. Достаточно объявить 4 переменные, в которые наши методы запи-
запишут координаты и размеры прямоугольника, а затем передать их нашим методом,
снабдив ключевым словом out:
int xCurrent, yCurrent, wCurrent, hCurrent;
rect.GetPosition(out xCurrent, out yCurrent);
rect.GetSize(out wCurrent, out hCurrent);
Далее нам остается только вывести значения этих переменных на консоль:
Console.WriteLine("Расположение прямоугольника: ({0}, {1})",
xCurrent, yCurrent);
Console.WriteLine(
"Ширина прямоугольника: {0}, высота прямоугольника: {1}",
wCurrent, hCurrent);
Заметим, что если в приведенной выше программе заменить ключевое слово out
на ключевое слово ref, то на этапе трансляции исходного текста мы получим сооб-
сообщение об ошибке. Компилятору «не понравится», что переменные xCurrent,
yCurrent, wCurrent и hCurrent не проинициализированы перед использованием.
158 А В. Фролов, Г В. Фролов. Язык С#. Самоучитель
Ситуацию можно было бы исправить, выполнив инициализацию этих переменных
произвольным значением, например:
int xCurrent = 0, yCurrent = 0, wCurrent = 0, hCurrent = 0;
Но в нашем случае этот прием выглядит несколько искусственно, так как до вызова
методов GetSize и GetPosition в перечисленных выше переменных не может на-
находиться никакого осмысленного значения. Задача методов GetSize и GetPosi-
GetPosition как раз и заключается в инициализации этих четырех переменных!
Сказанное, однако, не означает, что ключевое слово ref бесполезно и его все-
всегда следует заменять ключевым словом out. Описание параметра с ключевым сло-
словом ref используется обычно тогда, когда метод должен с помощью этого пара-
параметра не только возвращать значение, но и получать его. Вот, например, метод, по-
позволяющий передвинуть точку одновременно по оси X и Y, вернув при этом новые
значения координат:
public void ChangePosition(ref int x, ref int y)
{
xPos += x;
yPos += y;
x = xPos;
у = yPos;
}
Здесь текущие координаты точки увеличиваются на значения, переданные методу
ChangePosition через параметры. Затем метод возвращает через те же самые пара-
параметры новые координаты точки.
Примененное здесь ключевое слово ref не только обеспечивает передачу пара-
параметров по ссылке, но и позволяет убедиться на этапе компиляции программы в том,
что методу ChangePosition передаются ссылки на проинициализированные пере-
переменные. Это облегчает отладку программы, потому что программисты часто забывают
инициализировать переменные, передаваемые подобным образом.
Запрет наследования классов
Если вы создали класс, который не должен использоваться для создания на его основе
производных классов, т. е. который не может выступать в роли базового для других
классов, его можно отметить ключевым словом sealed.
В следующем примере попытка создания производного класса ColorRectangle
на базе класса Rectangle будет пресечена компилятором:
sealed class Rectangle
{
int xPos;
int yPos;
int width;
int height;
Глава З. Объектно-ориентированное программирование 159
public Rectangle()
xPos = yPos = width = height = 0;
class ColorRectangle : Rectangle // Ошибка!
byte colorR;
byte colorG;
byte colorB;
Ключевое слово sealed позволяет застраховаться от ошибочного наследования
классов, не предназначенных для выступления в роли базовых классов.
160 А В Фролов, Г. В. Фролов Язык С# Самоучитель
Глава 4. Полиморфизм
Наряду с инкапсуляцией и наследованием полиморфизм представляет собой одну
из важнейших концепций ООП. Применение этой концепции позволяет значитель-
значительно облегчить разработку сложных программ.
Термин полиморфизм имеет греческое происхождение и означает «наличие многих
форм». Наряду с программированием этот термин имеет хождение в биологии
и химии. В одном из учебников по биологии мы нашли такое определение полимор-
полиморфизма: «Полиморфизм — наличие у одного вида нескольких форм тела или типов ок-
окраски» [5]. В химии этот термин применяется для обозначения соединений, которые
могут кристаллизоваться в различных формах. Явление изменения кристаллической
структуры одного и того же вещества при изменение внешних условий называется
в химии полиморфизмом [6]. Типичный пример — углерод, имеющий две кристалли-
кристаллические формы. И графит и алмаз являются углеродом, но какая между ними разница!
В программировании с полиморфизмом тесно связаны такие понятия, как абст-
абстрактные классы, виртуальные методы, перегрузка методов и операторов. Частично пе-
перегрузка методов уже была рассмотрена в предыдущей главе. Здесь же вы узнаете до-
дополнительные подробности.
Применение полиморфизма
Мы будем изучать применение полиморфизма в программах С# на простом примере,
имеющем некоторое отношение к геометрии. Пусть в нашем распоряжении имеются
объекты различных типов — точка, прямая, прямоугольник, круг и т. д. Нашей задачей
будет написать классы С#, с помощью которых программа может рисовать эти фигуры
на плоскости.
Применение классов
Вначале мы попытаемся решить эту задачу известным нам на данный момент спосо-
способом без использования полиморфизма (листинг 4.1).
Листинг 4.1. Файл chO4\Shape\ShapeApp.cs
using System;
namespace Shape
{
class Point
{
int x;
int y;
public Point(int x, int y)
{
this.x = x;
this.у = у;
161
6 Язык С# Самоучитель
public void Draw(int x, int y)
(
Console.WriteLine("Рисование точки в ({0}, {1})", x, у);
this.x = x;
this.у = у;
class Rectangle
{
int x;
int y;
int w;
int re-
republic Rectangle(int x, int y, int w, int h)
{
this.x = x;
this.у = у;
this.w = w;
this.h = h;
}
public void Draw(int x, int y)
{
Console.WriteLine("Рисование прямоугольника в ({0}, {1})
x, у) ;
this.x = х;
this.у = у;
class ShapeApp
{
static void Main(string[] args)
{
Point pt = new PointA0, 25);
pt.DrawD, 7);
Rectangle rect = new Rectangled, 4, 10, 20);
rect.DrawA0, 12);
Console.ReadLine();
Мы будем работать с точками и прямоугольниками. Точки инкапсулируются
в классе Point, а прямоугольники — в классе Rectangle.
Обратите внимание, как мы инициализируем поля класса Point в конструкторе:
162 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
public Point(int x, int y)
{
this.x = x;
this.у = у;
}
Здесь мы намеренно выбрали для параметров конструктора имена, совпадающие
с именами соответствующих полей класса. При выполнении присваивания нам нужно
помочь компилятору сделать различие между параметрами метода и полями класса,
называющимися одинаково.
Эта задача решается с применением ключевого слова this. В данном контексте
это ключевое слово означает текущий объект класса Point.
Если бы параметры конструктора и поля класса назывались по-разному, в исполь-
использовании ключевого слова this не было бы никакой необходимости:
public Point(int xNew, int yNew)
{
x = xNew;
у = yNew;
}
Для того чтобы программа могла нарисовать точку и прямоугольник, мы определи-
определили в классах Point и Rectangle методы с названием Draw. Эти методы имеют оди-
одинаковые параметры, однако несколько различаются по своему действию.
Исходный текст метода Draw, предназначенного для рисования точки, представлен
ниже:
public void Draw(int x, int y)
{
Console.WriteLine("Рисование точки в ({0}, {1})", x, у);
this.x = x;
this.у = у;
}
Собственно рисование мы заменяем выводом на консоль строки сообщения о том,
что точка нарисована в таком-то месте координатной плоскости. После этого метод
изменяет текущие координаты точки. Обратите внимание, что здесь мы использовали
ключевое слово this, чтобы компилятор смог сделать различие между названиями
параметров метода и названиями полей класса.
Метод Draw, с помощью которого можно нарисовать прямоугольник, выглядит
следующим образом:
public void Draw(int x, int у)
{
Console.WriteLine("Рисование прямоугольника в ({0}, {1})", х, у);
this.x = х;
this, у = у;
}
От предыдущего метода он отличается текстом сообщения, отображаемого на кон-
консоли.
Глава 4. Полиморфизм 163
Теперь, когда у нас есть классы с методами Draw, можно приступать к рисованию
фигур. Наша программа рисует фигуры, создавая по очереди объекты каждого класса
и вызывая для этих объектов метод Draw:
Point pt = new Point A0, 25);
pt.DrawD, 7);
Rectangle rect = new RectangleA, 4, 10, 20);
rect.DrawA0, 12);
Важно, что в первом случае вызывается метод Draw из класса Point, а во вто-
втором — метод Draw из класса Rectangle.
Попытка обобщения с помощью наследования
В предыдущем примере мы имели дело всего с двумя фигурами — точкой и прямо-
прямоугольником. Всего же, как известно из школьного курса геометрии, существует вели-
великое множество разных фигур.
Зададимся вопросом, можно ли обобщить поведение различных фигур в одном ба-
базовом классе, отразив особенности каждой фигуры в соответствующем производном
классе?
Создадим базовый класс Shape, призванный инкапсулировать в себе общие эле-
элементы поведения всех фигур. На базе класса Shape создадим классы Point и Rec-
Rectangle, инкапсулирующих в себе особенности поведения точек и прямоугольников.
Исходный текст программы, реализующей данную структуру классов, представлен
в листинге 4.2.
Листинг 4.2. Файл chO3\Shape1\Shape 1App.cs
using System;
namespace Shapel
{
class Shape
{
protected int xPos;
protected int yPos;
public Shape(int x, int y)
{
xPos = x;
xPos = y;
class Point : Shape
{
public Point(int x, int y) : base (x, y)
{
)
164 А В. Фролов, Г. В. Фролов. Язык С# Самоучитель
public void Drawfint x, int y)
{
Console.WriteLine("Рисование точки в ({0}, {1})", x, у);
xPos = x;
xPos = у;
class Rectangle : Shape
{
int width;
int height;
public Rectangle(int x, int y, int w, int h) : base(x, y)
{
width = w;
height = h;
}
public void Draw(int x, int y)
{
Console.WriteLine("Рисование прямоугольника в ({0}, {1})
x, у) ;
xPos = X;
yPos = у;
class ShapelApp
{
static void Main(string[] args)
{
Point pt = new PointA0, 2 5);
pt.DrawD, 7);
Rectangle rect = new RectangleA, 4, 10, 20);
rect.DrawA0, 12);
Console.ReadLine();
Что же нам удалось объединить в базовом классе Shape такого, что является ха-
характерным для фигур любого типа?
Каждая фигура имеет свои координаты на плоскости. Положение точки однозначно
определяется координатами по осям X и Y, а положение прямоугольника мы опреде-
определяем по координатам его левого нижнего угла.
Глава 4. Полиморфизм 165
Соответственно в базовом классе Shape мы определили поля xPos и yPos, пред-
предназначенные для хранения координат фигуры (не важно, какой именно) на плоскости.
В этом же классе имеется конструктор, инициализирующий данные поля:
class Shape
{
protected int xPos ;
protected int yPos;
public Shape(int x, int y)
{
xPos = x;
xPos = y;
Что же касается метода, выполняющего рисование фигур, то он для каждой фигуры
свой. Поэтому мы не сможем определить метод Draw в базовом классе, а вынуждены
создавать свой метод Draw в каждом производном классе.
Рассмотрим класс Point, созданный на базе класса Shape:
class Point : Shape
{
public Point(int x, int y) : base (x, y)
public void Draw(int x, int y)
{
Console.WriteLine("Рисование точки в ({0}, {1})", x, у);
xPos = x;
xPos = y;
Как видите, в нем определен конструктор, вызывающий конструктор базового ме-
метода с двумя параметрами. Наличие такого конструктора необходимо для того, чтобы
отменить вызов конструктора по умолчанию базового класса без параметров и вызвать
нужный конструктор базового класса с двумя параметрами.
Метод Draw класса Point имитирует рисование точки выводом на консоль соот-
соответствующего сообщения.
Аналогично устроен и класс Rectangle:
class Rectangle : Shape
{
int width;
int height;
public Rectangle(int x, int y, int w, int h) : base(x, y)
{
width = w;
height = h;
}
166 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
public void Draw(int x, int y)
{
Console.WriteLine("Рисование прямоугольника в ({0}, {1))", x, у);
XPOS = X;
yPos = у;
Этот класс также создан на базе класса Shape. По сравнению с базовым классом
мы добавили в него два новых поля, хранящих размеры прямоугольника, изменили
конструктор и метод Draw.
Конструктор производного класса Rectangle вначале вызывает конструктор ба-
базового класса, сохраняя в соответствующих полях (xPos и xPos) координаты распо-
расположения прямоугольника на плоскости. Далее этот конструктор инициализирует поля
width и height, хранящие размеры прямоугольника.
После применения наследования способ работы с классами, определенными в на-
нашей программе, никак не изменился по сравнению с предыдущей программой (см. ли-
листинг 4.1):
Point pt = new PointdO, 25);
pt.DrawD, 7);
Rectangle rect = new Rectangle(l, 4, 10, 20);
rect.Draw{10, 12);
По-прежнему для рисования фигур нам нужно создавать два объекта разных клас-
классов, вызывая для каждого из них свой метод Draw.
Применение наследования до некоторой степени улучшило структуру нашей про-
программы, так как нам удалось вынести в базовый класс все общее, что есть у наших фи-
фигур. Однако на этом совершенство еще не достигнуто — мы не можем использовать
базовый класс для рисования фигур.
Виртуальные методы
В предыдущей главе мы рассказывали вам о возможности переопределения методов
базового класса в производном классе. Тогда же мы упоминали ключевое слово new,
с помощью которого нужно отметить в производном классе переопределенный метод,
чтобы он использовался вместо метода с тем же именем, но объявленным в базовом
классе.
Можно ли использовать данную технику, чтобы рисовать фигуры с помощью ме-
метода Draw, объявленного в базовом классе?
Нет, так как метод дочернего класса, объявленный с ключевым словом new, полно-
полностью скрывает одноименный метод базового класса. Нам же требуется, чтобы про-
программа как-то использовала метод Draw базового класса для рисования фигуры до-
дочернего класса.
Глава 4. Полиморфизм 167
Рассмотрим следующий фрагмент кода, использующий нужную нам функциональ-
функциональность (здесь мы предполагаем, что производные классы Point и Rectangle образо-
образованы от одного базового класса Shape):
Shape pt = new Point A0, 25);
pt.DrawD, 7);
Shape rect = new RectangleA, 4, 10, 20);
rect.DrawA0, 12);
Обратите внимание, что мы создаем два объекта дочерних: классов, записывая
ссылки на эти объекты в переменную с типом базового класса. Такая операция вполне
допустима в языке С#.
Далее мы используем объекты базового класса для рисования разных фигур, вызы-
вызывая метод Draw, объявленный в базовом классе.
Чтобы такое стало возможным, необходимо в базовом и дочернем классах объ-
объявить метод Draw специальным образом (листинг 4.3).
Листинг 4.3. Файл chO4\ShapeVirtual\ShapeVirtualApp.cs
using System;
namespace ShapeVirtual
{
class Shape
{
protected int xPos;
protected int yPos;
public Shape(int x, int y)
{
xPos = x;
xPos = y;
public virtual void Draw(int x, int y)
{
}
}
class Point : Shape
{
public Point(int x, int y) : base (x, y)
{
}
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование точки в ({0}, {1})", x, у);
xPos = x,-
xPos = у;
}
}
168 А В Фролов. Г В. Фролов Язык С# Самоучитель
class Rectangle : Shape
{
int width;
int height;
public Rectangle(int x, int y, int w, int h) : base(x, y)
{
width = w;
height = h;
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование прямоугольника в ({0}, {1})
x, у) ;
xPos = х;
yPos = у;
class ShapeVirtualApp
{
static void Main(string[] args)
(
Shape pt = new PointA0, 25);
pt.DrawD, 7);
Shape rect = new Rectangled, 4, 10, 20);
rect.DrawA0, 12) ;
Shape[] allShapes = new Shape[4];
allShapes[0] = new PointA, 5) ;
allShapes[1] = new RectangleB, 3, 1, 5);
allShapes [2] = new Rectangled, 2, 3, 4);
allShapes[3] = new PointC1, 4);
foreach(Shape currentshape in allShapes)
currentShape.Draw@, 0);
Console.ReadLine();
Прежде всего нам нужно определить базовый класс Shape, представляющий собой
общие свойства фигур. От этого класса будут наследоваться классы Point
и Rectangle.
Глава 4. Полиморфизм 169
В базовом классе Shape, помимо полей xPos и yPos, нам нужно объявить конст-
конструктор и так называемый виртуальный метод Draw:
class Shape
{
protected int xPos;
protected int yPos;
public Shape(int x, int y)
{
xPos = x;
xPos = y;
}
public virtual void Draw(int x, int y)
Тело виртуального метода Draw, объявленного с помощью ключевого слова
virtual, не содержит ни одного оператора. Во время выполнения программы этот
виртуальный метод будет подменяться на одноименный метод из производных клас-
классов Point и Rectangle.
Ниже мы привели объявление производного класса Point:
class Point -. Shape
{
public Point(int x, int y) : base (x, y)
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование точки в ({0}, {1})", x, у);
xPos = x;
xPos = у;
Обратите внимание, что в этом классе, так же как и в базовом классе Shape, име-
имеется метод Draw, переопределяющий одноименный метод базового класса. Однако
этот метод объявлен в производном классе с использованием ключевого слова
override.
Аналогичный метод объявлен и в производном классе Rectangle:
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование прямоугольника в ({0}, 11})", x, у);
xPos = x;
yPos = y;
170 А В. Фролов, Г. В. Фролов Язык С#. Самоучитель
Напомним, что ранее в приведенных примерах программ мы уже переопределяли
методы базового класса, пользуясь для этого ключевым словом new. В результате
компилятор скрывал от программы переопределенный метод базового класса.
Применение ключевого слова override дает другой эффект. Виртуальный ме-
метод базового класса подменяется соответствующим методом производного класса
не во время компиляции, а во время работы программы.
Таким образом, в следующем фрагменте кода виртуальный метод Draw базового
класса Shape подменяется методом Draw производного класса Point:
Shape pt = new PointA0, 25);
pt.DrawD, 7);
Это происходит потому, что мы создали объект класса Point и записываем ссыл-
ссылку на него в переменную, имеющую тип базового класса Shape. Во время своего вы-
выполнения программа динамически определяет тип ссылки, хранящейся в переменной
pt, и вызывает для нее метод Draw из соответствующего производного класса (в дан-
данном случае из класса Point).
Аналогично в следующем фрагменте кода вместо виртуального метода Draw базо-
базового класса будет вызван метод Draw производного класса Rectangle:
Shape rect = new Rectanglefl, 4, 10, 20);
rect.DrawA0, 12);
Теперь, воспользовавшись полиморфизмом, мы сумели организовать обработку
объектов различных производных классов, вызывая для этого один виртуальный метод
базового класса. Такой прием позволяет упростить программу.
Однако наиболее очевидными эти упрощения становятся при необходимости обра-
обрабатывать объекты различных производных классов, размещенные в массивах или кон-
контейнерах различных типов.
В следующем фрагменте программы мы создали массив для хранения четырех
объектов базового класса Shape:
Shape[] allShapes = new Shape[4];
В созданный таким образом массив мы помещаем две точки и два прямоугольника:
allShapes[0] = new PointA, 5);
allShapes[l] = new RectangleB, 3, 1, 5) ;
allShapes[2] = new RectangleA, 2, 3, 4);
allShapes[3] = new PointC1, 4);
В результате наш массив будет содержать объекты двух разных производных клас-
классов, имеющих общий базовый класс Shape.
Допустим, нам нужно нарисовать все фигуры, хранящиеся в массиве. При исполь-
использовании полиморфизма это можно сделать простым вызовом виртуального метода
Draw базового класса Shape:
foreach(Shape currentshape in allShapes)
currentShape.Draw@, 0);
Глава 4. Полиморфизм 171
Здесь для простоты все фигуры рисуются в одной точке с координатами @, 0),
но вы можете легко изменить цикл таким образом, чтобы каждая фигура была
нарисована на своем месте.
Обращаем ваше внимание на следующий интересный факт.
Обрабатывая исходный текст программы, компилятор не может знать, объекты ка-
какого из производных классов и в какой последовательности записаны в массиве.
Эта информация становится доступной только во время выполнения программы.
у Механизм виртуальных функций как раз и позволяет программе определить тип
очередного объекта, извлекаемого в цикле из массива. В зависимости от того, какой
объект на очередной итерации цикла программа извлекла из массива (точку или пря-
прямоугольник), она вызывает метод Draw из соответствующего производного класса
(Point или Rectangle).
Раннее и позднее связывание
В программировании используются два термина, подходящие под описание случаев
перегрузки методов с использованием ключевых слов new и override.
При переопределении метода базового класса одноименным методом производного
класса, имеющим такой же список параметров и объявленным с помощью ключевого сло-
слова new, происходит так называемое раннее связывание (early binding). Оно называется
ранним, так как выполняется еще на стадии компиляции исходного текста программы.
Когда же задействован механизм виртуальных функций и в ходе работы програм-
программы виртуальная функция базового класса переопределяется функцией производного
класса, объявленной как override, выполняется позднее связывание (late binding).
Именно позднее связывание и позволяет нам вызывать виртуальные методы базо-
базового класса для обработки объектов различных производных классов. Кроме того,
применяя полиморфизм и виртуальные методы, вы можете применять программный
код, рассчитанный на использование базовых классов, для обработки объектов произ-
производных классов.
Абстрактные классы
Если все методы класса являются виртуальными, т. с. объявлены с ключевым словом
virtual, то такой класс называется абстрактным.
Каждый метод абстрактного класса должен быть переопределен соответствующим ме-
методом производного класса. Вы не можете создать объект абстрактного класса,
так как фактически такой класс содержит только шаблоны методов, но не сами методы.
В листинге 4.4 мы привели новый вариант программы, применяющей полимор-
полиморфизм для обработки объектов, хранящихся в массиве.
Листинг 4.4. Файл chO4\ShapeAbstract\ShapeAbstractApp.cs
using System;
namespace ShapeAbstract
abstract class Shape
172 А В Фролов, Г. В Фролов. Язык С#. Самоучитель
{
protected int xPos;
protected int yPos ;
public Shape(int x, int y)
xPos = x;
xPos = y;
abstract public void Draw(int x, int y);
class Point : Shape
{
public Point(int x, int y) : base (x, y)
{
}
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование точки в ({0}, {1})", x, у)
xPos = x;
xPos = y;
}
}
class Rectangle : Shape
{
int width;
int height;
public Rectangle(int x, int y, int w, int h) : base(x, y)
{
width = w;
height = h;
}
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование прямоугольника в ({0}, {1})",
x, у) ;
xPos = х;
yPos = у;
class ShapeAbstractApp
{
static void Main(string[] args)
{
ShapeU allShapes = new Shape[4];
allShapes[0] = new PointA, 5);
allShapes[l] = new RectangleB, 3, 1, 5);
allShapes[2] = new RectangleA, 2, 3, 4);
allShapes[3] = new PointC1, 4) ;
Глава 4. Полиморфизм 173
foreach(Shape currentshape in allShapes)
currentShape.Draw@, 0) ;
Console.ReadLine();
Теперь мы объявили класс Shape абстрактным, указав при его объявлении ключе-
ключевое слово abstract:
abstract class Shape
{
protected int xPos;
protected int yPos;
public Shape(int x, int y)
{
xPos = x;
xPos = y;
}
abstract public void Draw(int x, int y);
}
Кроме того, мы объявили абстрактным и метод Draw нашего абстрактного базово-
базового класса. Обратите внимание, что объявление абстрактного метода заканчивается
точкой с запятой и не имеет тела, ограниченного фигурными скобками.
Использование ключевого слова abstract при объявлении метода исключает не-
необходимость (а также возможность) указания ключевых слов override и virtual.
Кроме того, это ключевое слово несовместимо с ключевым словом static.
Перегрузка операторов
Рассмотренную в предыдущей главе перегрузку методов можно считать одним
из средств реализации полиморфизма, предусмотренных в языке программирования
С#. Другое такое средство — перегрузка операторов.
Мы уже говорили, что в числовых и строковых выражениях можно использовать
такие операторы, как оператор сложения, вычитания, умножения, равенства и т. п.
Язык С# позволяет вам переопределять действие многих операторов таким образом,
чтобы их можно было использовать при работе с любыми типами данных, в том числе
с типами данных, создаваемых на основе разработанных вами классов.
Ниже перечислены бинарные операторы, которые можно перегружать в языке С#:
А это список перегружаемых унарных операторов:
+ - ! - ++ -- true false
174 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Перегрузка операторов дает возможность создавать более прозрачные и понятные
исходные тексты программ, так как для выполнения многих действий с определенны-
определенными вами объектами можно использовать привычные операторы.
Обратите внимание, что в языке С# нет возможности перегрузить оператор при-
присваивания =. При использовании этого оператора с объектами любого типа происхо-
происходит простое копирование ссылки на объект.
Краткая теория комплексных чисел
Чтобы продемонстрировать использование перегруженных операторов, мы выбрали
способ, предлагаемый многими учебниками по программированию. Мы создадим соб-
собственный класс Complex, предназначенный для работы с комплексными числами.
Полное определение комплексного числа можно найти в любом справочнике
по математике, например в [7], поэтому мы ограничимся только самыми необходимы-
необходимыми сведениями.
Согласно [7], комплексным числом называется число вида a+bi, где а и b пред-
представляют собой действительные числа. Символ i является обозначением мнимой еди-
единицы, удовлетворяющей условию i2=-l. Числа а и b называются соответственно
действительной и мнимой частью комплексного числа.
Для комплексных чисел определены операции сравнения, сложения, вычитания,
умножения и деления. Именно эти операции нас будут интересовать в первую очередь.
Дадим несколько необходимых определений операций, выполняемых над ком-
комплексными числами.
Сравнение
Два комплексных числа равны между собой, когда равны их действительные и мни-
мнимые части. Например, если у нас есть два числа al+bli и a2+b2i, то они будут рав-
равны, если выполняется следующее условие:
{аг == Ь>!) && (а2 == Ь2)
Здесь для обозначения условия мы использовали операторы языка С#.
Сложение
Чтобы сложить два комплексных числа, надо сложить по отдельности их действитель-
действительные и мнимые части:
(ai+bii) + (a2+b2i) = (ai+a2) +
Вычитание
Вычитание комплексных чисел выполняется по следующему правилу:
(ai+bii) - (a2+b2i) = (а!-а2) + (b!-b2)i
Умножение
Для умножения комплексных чисел применяется следующая формула:
(aj+bii) * (a2+b2i) = (a!*a2 - bi*b2) +
Глава 4. Полиморфизм 175
Деление
Сложнее всего выглядит формула, по которой рассчитывается результат деления од-
одного комплексного числа на другое:
(ai+bii) /(a2+b2i) =
(а!*а2 ■+ b!*b2)/(a22 + b22) + ((b]*a2 - a!*b2)/(a22 + b22))i
Класс для представления комплексных чисел
Для представления комплексных чисел мы разработали класс с именем Complex.
В первом варианте этого класса мы предусмотрели поля re и im, предназначенные
соответственно для хранения действительной и мнимой части комплексного числа,
конструктор, а также два метода, Add и Sub, для сложения и вычитания комплексных
чисел:
class Complex
{
public double re;
public double im;
public Complex(double r, double i)
{
re = r;
im = i ;
public static Complex Add(Complex xl. Complex x2)
{
return new Complex(xl.re + x2.re, xl.im + x2.im)
}
public static Complex Sub(Complex xl, Complex x2)
{
return new Complex(xl.re - x2.re, xl.im - x2.im)
Конструктор копирует значения своих параметров в соответствующие поля класса.
Первый параметр задает действительную часть комплексного числа, а второй — мнимую.
Статические методы Add и Sub возвращают новый объект класса Complex, соз-
созданный при помощи оператора new. Конструкторам этих объектов передаются пара-
параметры, вычисленные в соответствии с правилами сложения и вычитания комплексных
чисел.
Приведем пример работы с этим классом:
Complex cl = new ComplexA, 2);
Complex c2 = new ComplexD, 5);
Console.WriteLine("cl = ({0}, (l)i)", cl.re, cl.im);
console.writeLine("c2 = (@}, (l)i)", c2.re, c2.im);
176 А. В. Фролов, Г В. Фролов. Язык С# Самоучитель
Complex sum = Complex.Add(cl, c2);
Console.WriteLine("cl + c2 = ({0}, (l)i)", sum.re, sum.im);
Complex sub = Complex.Sub(cl, c2);
Console.WriteLine("cl - c2 = ({0}, {l}i)M, sub.re, sub.im);
В этом фрагменте кода мы сначала создали два комплексных числа (l+2i)
и D + 5 i). Далее мы вывели эти числа на консоль в виде A, 2 i) и D, 5 i). И на-
наконец, мы по очереди вызвали методы Add и Sub, отобразив на консоли результат вы-
выполнения операций сложения и вычитания комплексных чисел.
Однако было бы намного удобнее использовать для сложения и вычитания ком-
комплексных чисел не методы Add и Sub, а привычные операторы + и -, например:
Complex sum = cl + с2;
Complex sub = cl - c2;
Чтобы использовать такие операции при работе не только с обычными, действи-
действительными, но и с комплексными числами, нам необходимо их перегрузить в классе
Complex.
Перегрузка бинарных операторов
Для перегрузки операторов нам нужно добавить в наш класс статические методы спе-
специального вида, причем этот вид зависит от того, будет перегружаться бинарный или
унарный оператор.
Сначала мы рассмотрим перегрузку бинарных операторов.
Ниже мы привели исходный текст метода класса Complex, перегружающего опе-
оператор сложения:
public static Complex operator +(Complex xl, Complex x2)
{
return new Complex(xl.re + x2.re, xl.im + x2.im);
}
Как видите, он очень похож на описанный выше метод Add, выполняющий ту же
самую операцию. Отличие заключается в том, что в качестве имени метода вместо
Add мы использовали конструкцию вида operator +.
Аналогичным образом перегружаются операторы вычитания, умножения и деления:
public static Complex operator -(Complex xl, Complex x2)
{
return new Complex(xl.re - x2.re, xl.im - x2.im);
public static Complex operator *(Complex xl, Complex x2)
{
return new Complex(
xl.re*x2.re - xl.im*x2.irn, xl.re*x2.im + x2.re*xl.im);
Глава 4. Полиморфизм 177
public static Complex operator /(Complex xl, Complex x2)
return new Complex(
(xl.re*x2.re + xl.im*x2.im)/(x2.re*x2.re + x2.im*x2.im),
(xl.im*x2.re - xl.re*x2.im)/(x2.re*x2.re + x2.im*x2.im));
Действия выполняются этими операторами в соответствии с правилами вычисле-
вычислений над комплексными числами, описанными ранее в разделе «Краткая теория ком-
комплексных чисел».
Добавив в класс Complex перечисленные выше методы, переопределяющие ос-
основные арифметические операции, мы можем записывать выражения с комплексными
числами более естественным образом:
Complex cl = new ComplexA, 2);
Complex c2 = new ComplexD, 5);
Complex suml = cl + c2;
Console.WriteLine("cl + c2 = ({0}, {l)i)", suml.re, suml.im);
Complex subl = cl - c2;
Console.WriteLine("cl - c2 = ({0}, {l}i)", subl.re, subl.im);
Complex mul = cl * c2;
Console.WriteLine("cl * c2 = ({0}, {l}i)", mul.re, mul.im);
Complex div = cl / c2;
Console.WriteLine("cl / c2 = ({0}, {l}i)", div.re, div.im);
Здесь мы создали два комплексных числа и вывели на консоль результаты выпол-
выполнения операций сложения, вычитания, умножения и деления.
Перегрузка унарных операторов
Унарные операторы, как известно, получают только один операнд. Хотя операции ин-
инкремента и декремента комплексных чисел обычно не используются, мы перегрузим
соответствующие операторы ++ и -- для демонстрации приемов перегрузки унарных
операторов.
При этом мы будем считать, что для инкремента комплексного числа нужно приба-
прибавить единицу к его действительной и мнимой части, а для декремента — вычесть еди-
единицу из действительной и мнимой части.
Методы для перегрузки унарных операций получает только один параметр:
public static Complex operator ++(Complex x)
return new Complex(x.re + 1, x.im + 1);
}
178 А. В Фролов, Г. В. Фролов. Язык С#. Самоучитель
public static Complex operator --(Complex x)
{
return new Complex(x.re - 1, x.im - 1);
}
Вот как мы можем использовать перефуженные таким способом операции инкре-
инкремента и декремента:
Complex cl = new ComplexA, 2);
Complex с2 = new ComplexD, 5);
Complex incr = ++cl;
Console.WriteLine("++cl = ({0}, {l}i)", incr.re, incr.im);
Complex deer = --cl;
Console.WriteLine C'--cl = ({0}, {l}i)"# deer.re, decr.im);
Как видите, теперь мы можем выполнять унарные операции с комплексными чис-
числами таким же способом, каким они выполняются и с обычными действительными
числами.
Перегрузка операторов сравнения
Для того чтобы сравнивать объекты, созданные на базе разработанных вами классов,
нужно перефузить все или некоторые операторы сравнения, такие, как ==, ! =, <, <=,
>, >=, а также методы базового класса ob j ect с именами Equals и GetHashCode.
Заметим, что если вы перефужаете оператор равенства (==), то вместе с ним нуж-
нужно обязательно перефужать и оператор неравенства (! =). При перефузке операторов
< и > нужно дополнительно перефужать операторы <= и >=.
Вот пример перефузки операторов равенства и неравенства для нашего класса
Complex:
public static bool operator ==(Complex xl. Complex x2)
{
if(xl.re == x2.re && xl.im == x2.im)
return true;
else
return false;
public static bool operator !=(Complex xl, Complex x2)
{
if(xl.re != x2.re || xl.im != x2.im)
return true;
else
return false;
public override bool Equals(object o)
{
return true;
}
Глава 4. Полиморфизм 179
public override int GetHashCode()
{
return 0;
)
Как видите, методы operator == и operator != получают два параметра
(ссылки на сравниваемые объекты) возвращают логическое значение true, если дей-
действительные и мнимые части сравниваемых комплексных чисел соответственно сов-
совпадают или не совпадают. Здесь все понятно, так как внешне эти методы очень похо-
похожи на только что рассмотренные методы перегруженных бинарных операторов.
Но необходимость перегрузки методов Equals и GetHashCode требует допол-
дополнительного пояснения.
Мы уже упоминали, что в языке программирования С# все классы наследуются
от одного базового класса Object. В этом классе объявлено несколько методов, кото-
которые иногда приходится переопределять в производных классах.
В частности, метод Equals позволяет сравнить два объекта, а метод GetHashCode
нужен для получения так называемого хеш-кода объекта. Хеш-код однозначно идентифи-
идентифицирует каждый объект класса и применяется для быстрого поиска объектов по ключу.
Методы Equals и GetHashCode взаимосвязаны в том смысле, что двум объек-
объектам, которые метод Equals считает одинаковыми, метод GetHashCode должен воз-
возвращать одинаковые значения хеш-кода.
Наша реализация метода Equals считает равными любые передаваемые ей объек-
объекты, возвращая всегда значение true. Что же касается метода GetHashCode, то он
всегда возвращает одинаковое значение 0. Для того чтобы сравнивать комплексные
числа класса Complex, такая реализация методов Equals и GetHashCode вполне
пригодна.
Пример программы
В листинге 4.5 мы привели исходный текст программы, в которой для работы с ком-
комплексными числами применяются как обычные методы, определенные в классе
Complex, так и перегруженные операторы.
Все приемы, примененные нами в этой программе, уже были описаны ранее, по-
поэтому мы оставляем эту программу вам для самостоятельного изучения.
Листинг 4.5. Файл ch04\ComplexNum\ComplexNumApp.cs
using System;
namespace ComplexNum
(
class Complex
{
public double re;
public double im;
public Complex(double r, double i)
{
re = r ;
im = i ,-
) ___^______
180 А В. Фролов, Г. В. Фролов. Язык С# Самоучитель
public static Complex Add(Complex xl, Complex x2)
return new Complex (xl. re •* x2.re, xl. im + x2.im);
public static Complex Sub(Complex xl, Complex x2)
return new Complex(xl.re - x2.re, xl.im - x2.im);
public static Complex operator +(Complex xl, Complex x2)
return new Complex(xl.re + x2.re, xl.im + x2.im);
public static Complex operator -(Complex xl, Complex x2)
return new Complex(xl.re - x2.re, xl.im - x2.im);
public static Complex operator ++(Complex x)
return new Complex(x.re + 1, x.im + 1);
public static Complex operator --(Complex x)
return new Complex(x.re - 1, x.im - 1) ;
public static Complex operator *(Complex xl, Complex x2)
return new Complex(
xl.re*x2.re - xl.im*x2.im,
xl.re*x2.im + x2.re*xl.im);
public static Complex operator /(Complex xl. Complex x2)
return new Complex(
(xl.re*x2.re + xl.im*x2.im)/(x2.re*x2.re + x2.im*x2.im),
(xl.im*x2.re - xl.re*x2.im)/(x2.re*x2.re + x2.im*x2.im));
public static bool operator ==(Complex xl. Complex x2)
if(xl.re == x2.re && xl.im == x2.im)
return true;
else
return false;
Глава 4. Полиморфизм 181
public static bool operator !=(Complex xl, Complex x2)
{
if(xl.re != x2.re || xl.im != x2.im)
return true;
else
return false;
public override bool Equals(object o)
{
return true;
}
public override int GetHashCode()
{
return 0;
}
}
class ComplexNumApp
{
static void Main(string[] args)
{
Complex cl = new Complex(l, 2);
Complex c2 ='new ComplexD, 5);
Console.WriteLineC'cl = ({0}, {l}i;
Console.WriteLine("c2 = ({0}, {l}i;
cl.re, cl.im);
c2.re, c2.im);
Complex sum = Complex.Add(cl, c2);
Console.WriteLineC'cl + c2 = ({0}, {l}i)
Complex sub = Complex.Sub(cl, c2);
Console.WriteLine("cl - c2 = ({0},
Complex suml = cl + c2;
Console.WriteLineC'cl + c2 = ({0},
Complex subl = cl - c2;
Console.WriteLineC'cl - c2 = ({0),
Complex mul = cl * c2;
Console.WriteLineC'cl * c2 = ({0},
Complex div = cl / c2;
Console.WriteLine("cl / c2 = ({0}, {l}i)", div.re, div.im);
}i)", incr.re, incr.im);
sum.re, sum.im);
sub.re, sub.im);
suml.re, suml.im),
subl.re, subl.im)
mul.re, mul.im);
Complex incr = ++cl;
Console.WriteLine("++cl = ({0),
Complex deer = --cl;
Console.WriteLine("--cl = ({0}, (l}i)", deer.re, deer.im);
182
А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
if(cl == c2)
Console.WriteLine("cl == c2");
else
Console.WriteLine("cl != c2");
Console.ReadLine();
Класс System.Object
Ранее мы уже неоднократно упоминали класс System.Object, от которого автома-
автоматически наследуются все объекты в программах С#. Как вы увидите в дальнейшем, на-
наличие единого корня в дереве наследования классов С# дает немалые преимущества.
Например, объекты любого класса можно хранить в готовых контейнерах, предостав-
предоставляемых библиотекой классов Microsoft .NET Framework.
В предыдущем разделе мы рассказывали вам о методах Equals и GetHashCode,
первый из которых имеет отношение к сравнению объектов, а второй предназначен
для получения называемого хеш-кода, однозначно идентифицирующего объект.
Помимо этих методов, для разработчиков программ С# могут представлять интерес
методы ToString, GetType и Finalize.
Метод ToString позволяет получить имя объекта. Переопределяя это имя в произ-
производных классах, можно использовать перегруженную версию этого метода для предостав-
предоставления расширенной информации об объекте, не ограничиваясь одним только именем.
Что касается метода GetType, то с его помощью можно получить дополнитель-
дополнительную информацию о типе объекта. Этот метод мы будем упоминать в разделе нашей
книги, посвященной атрибутам.
И наконец, переопределение метода Finalize нужно для освобождения каких-
либо ресурсов (файлов, соединений с базами данных и т. п.) перед тем, как объект бу-
будет уничтожен системой сборки мусора.
В листинге 4.6 мы привели пример программы, демонстрирующей использование
метода ToString.
Листинг 4.6. Файл ch04\SysObj\SysObjApp.cs
using System;
namespace SysObj
{
class Point
{
public double xPos;
public double yPos;
public Point(double x, double y)
{
xPos = x;
yPos = y;
)
Глава 4. Полиморфизм 183
public void Draw(double x, double y)
{
Console.WriteLine("Рисование точки в ({0}, {1})", x, у)
xPOS = X;
yPos = у;
class PointSmart : Point
public PointSmart(double x, double y) : base (x, y)
public override string ToString()
return String.Format{"Point at ({0}, {1})", xPos, yPos)
class SysObjApp
static void Main(string[] args)
int mylnt = 38;
Console.WriteLine("mylnt = {0}, toString: {1}",
mylnt, mylnt.ToString() ) ;
bool myBool = true;
Console.WriteLine("myBool = {0}, toString: {1}",
myBool, myBool.ToString());
Point pt = new PointA, 2);
Console.WriteLine("pt = ({0}, {1}), toString: {2}",
pt.xPos, pt.yPos, pt.ToString());
PointSmart pts = new PointSmartC, 4);
Console.WriteLine("pts = ({0}, {1}), toString: {2}",
pts.xPos, pts.yPos, pts.ToString());
Console.ReadLine();
Первое, что делает наша программа, — это создаст переменную mylnt типа int
и вызывает для нее метод ToString:
184 А В Фролов, Г. В. Фролов. Язык С# Самоучитель
int mylnt = 38;
Console.WriteLine("mylnt = {0}, toString: {1}",
mylnt, mylnt.ToString());
Мы уже говорили вам, что такие типы данных, как int, на самом деле являются
классами С#, поэтому в записи mylnt. ToString () нет ничего необычного.
Что же будет показано на консоли в результате работы этих строк программы?
Там появится текстовая строка 38, которая представляет объект mylnt:
mylnt = 38, toString: 38
Аналогично наша программа вызывает метод ToString для переменной myBool
ranabool:
bool myBool = true;
Console.WriteLine("myBool = {0}, toString: {1}",
myBool, myBool.ToString());
На консоли будет отображена следующая строка:
myBool = True, toString: True
Как видите, текстовое представление объектов простейших типов данных — это
просто значение, хранящееся в этих объектах.
А как выглядит текстовое представление класса Point?
Для того чтобы это узнать, в нашей программе предусмотрены следующие строки:
Point pt = new PointA, 2);
Console.WriteLine("pt = ({0}, {1}), toString: {2}",
pt.xPos, pt.yPos, pt.ToString());
Запустив программу на выполнение, можно убедиться, что для класса Point ме-
метод ToString выводит не координаты точки (как это можно было бы подумать), а на-
название класса и пространства имен, в котором этот класс определен:
pt = A, 2), toString: SysObj.Point
В классе PointSmart мы переопределили метод ToString таким образом, что-
чтобы он отображал на консоли именно координаты точки, а не название класса:
public override string ToString{)
{
return String.Format("Point at ({0}, {1})", xPos, yPos);
}
Наш вариант метода ToString возвращает строку с координатами точки, сфор-
сформированную при помощи класса string.Format, входящего в библиотеку классов
среды выполнения Microsoft .NET Framework.
Вот как мы вызываем переопределенный метод ToString в нашей программе:
PointSmart pts = new PointSmartC, 4);
Console.WriteLine("pts = ({0}, {1}), toString: {2}",
pts.xPos, pts.yPos, pts.ToString ());
Глава 4. Полиморфизм 185
Как и следовало ожидать, новый вариант метода ToString выводит на экран то,
что нам нужно, а именно координаты точки:
pts = C, 4), toString: Point at C, 4)
Переопределяя в создаваемых классах метод ToString, вы можете наделить его
функциями, полезными, например, для отладки или для отображения текущего со-
состояния объекта— словом, для получения любой информации об объекте, которую
можно представить в виде текста.
186 А. В. Фролов. Г. В. Фролов. Язык С#. Самоучитель
Глава 5. Преобразование типов
объектов
Как мы уже говорили, программы С# работают с локальными переменными и полями
классов различных типов. Это могут быть стандартные типы С# (числовые, строковые,
логические и т. д.), а также типы данных, определенных при помощи классов (напри-
(например, рассмотренные в предыдущей главе комплексные числа).
При составлении выражений, включающих в себя переменные и поля различных
типов, выполняется операция преобразования типов (приведения типов). Эта операция
может быть явной, когда программист сам указывает, к какому типу нужно привести
данный тип, а также неявной. В последнем случае компилятор выполняет преобразо-
преобразование типов автоматически.
Программист может также определить собственный способ преобразования типов.
Такая возможность нужна при использовании в выражениях типов данных, созданных
программистом с помощью классов.
Неявное преобразование числовых типов
Составляя выражения с числами, в предыдущих главах мы использовали такую воз-
возможность языка С#, как явное и неявное преобразование типов. В процессе такого
преобразования объекты одного числового типа автоматически приводились к объек-
объектам другого типа (также числового).
Например, рассмотрим следующую программу (листинг 5.1).
Листинг 5.1. Файл ch05\AutoCast\AutoCastApp.cs
using System;
namespace AutoCast
{
class AutoCastApp
{
static void Main(string[] args)
{
int intNuber = 5;
double doubleNumber = 2.5;
double result = doubleNumber + intNuber;
Console.WriteLineCРезультат сложения 5 +2.5 = {0}", result);
Console.ReadLine();
Ti?
Здесь мы объявили две локальные переменные intNuber и doubleNuraber, пер-
первая из которых предназначена для хранения целых чисел, а вторая — для хранения чи-
чисел с плавающей точкой.
Наша программа предпринимает попытку сложения чисел разного типа с записью
результата в переменную типа double:
double result = doubleNumber + intNuber;
В процессе компиляции программы выполняется автоматическое неявное преобра-
преобразование типа переменной intNuber в тип double, в результате чего сложение вы-
выполняется правильно. Вот что наша программа выводит на консоль:
Результат сложения 5 + 2.5 = 7,5
Заметим, что автоматическое преобразование числовых типов возможно не всегда.
Чтобы компилятор смог его выполнить, в результате преобразования не должны те-
теряться значимые цифры результата. Поэтому если исходные преобразуемые данные
занимают в памяти меньше места, чем данные результата преобразования, то автома-
автомагическое преобразование возможно, а если больше, то нет.
Числа без знака
Схемы возможных неявных преобразований чисел без знака и чисел со знаком отли-
отличаются друг от друга. Сначала мы рассмотрим преобразования чисел без знака.
На рис. 5.1 мы показали схему допустимых преобразований для таких чисел.
Рис. 5.1. Неявные преобразования чисел без знака
Как видите, значения типа byte могут быть приведены к типу short, ushort,
uint, ulong, а также к float и decimal. Что же касается автоматического приве-
приведения типа ui nt к типу byte или ushort, то оно невозможно.
188
А В Фролов, Г. В Фролов Язык С# Самоучи-ель
Числа со знаком
Допустимые неявные преобразования для чисел со знаком показаны на рис. 5.2.
Рис. 5.2. Неявные преобразования чисел со знаком
Как видите, транслятор не может автоматически преобразовать числа со знаком
в числа без знака. Таким образом, следующий фрагмент кода транслироваться
не будет:
int i = 5;
uint ui = 6;
uint re = ui + i; // Ошибка!
Текстовые символы char
Как мы уже говорили, текстовые символы char языка С# хранятся в кодировке
UNICODE, причем для каждого символа требуется 2 байта памяти. Поэтому тип char
может быть автоматически преобразован в типы ushort и int (рис. 5.3), а также
в другие типы в соответствии с рис. 5.1 и 5.2.
Дальнейшие Дальнейшие
преобразования преобразования
Рис. 5.3. Преобразования типа chare числа
Глава 5. Преобразование типов объектов
189
Числа с плавающей точкой
Проще всего схема преобразований выглядит для типа float, предназначенного для
представления чисел с плавающей точкой (рис. 5.4).
float
double
Рис. 5.4. Преобразования типа float
Тип данных float может быть автоматически приведен только к типу double.
Явное преобразование числовых типов
В некоторых случаях необходимо использовать явное преобразование типов. Если,
например, у вас есть исходная переменная типа ulong, но вы точно знаете, что в ней
хранятся значения, не превосходящие 255, то можно выполнить следующее преобра-
преобразование:
ulong ulongNuber = 11;
byte resultByte = (byte)ulongNuber;
Обратите внимание, что перед именем переменной ulongNuber мы указали
в круглых скобках тип, к которому нужно привести исходный тип данных. Это так на-
называемый оператор приведения типа.
Оператор приведения типа заставляет компилятор выполнить заданное преобразо-
преобразование типов, даже если при этом исходное значение числа окажется искаженным.
Рассмотрим пример программы (листинг 5.2), демонстрирующей такое искажение.
Листинг 5.2. Файл chO5\Cast\CastApp.cs
using System;
namespace Cast
{
class CastApp
{
static void Main(string[] args)
{
ulong ulongNuber = 0x1122334455;
byte resultByte = (byte)ulongNuber;
ushort resultUshort = (ushort)ulongNuber;
uint resultuint = (uint)ulongNuber;
Console.WriteLine("Исходное число типа ulong = {0:X}\n",
ulongNuber);
190 А. В Фролов, Г. В. Фролов Язык С# Самоучитель
Console.WriteLine("Результат типа byte\t= {0:X}",
resultByte);
Console.WriteLine("Результат типа ushort\t= {0:X}",
resultUshort);
Console.WriteLine("Результат типа uint\t= {0:X}",
resultUint);
Console.ReadLine();
}
Эта программа последовательно присваивает значение 0x1122334455, хранящее-
хранящееся в исходной переменной ulongNuber типа ulong, переменным типа byte,
ushort и uint. Далее программа отображает исходное значение и результат выпол-
выполнения операций присваивания в шестнадцатеричном виде:
Исходное число типа ulong = 1122334455
Результат типа byte = 55
Результат типа ushort = 4455
Результат типа uint = 22334455
Как видите, любое из примененных в программе преобразований типов вызывает
искажение исходного значения. Именно этого и следовало ожидать, так как для ре-
результата отводится меньше места, чем занимает исходное число. При этом старшие
разряды преобразуемого значения попросту отбрасываются.
Главный вывод отсюда заключается в том, что использовать явное преобразование
числовых типов нужно с осторожностью, так как оно может привести к искажению
исходного значения.
Проверка преобразования числовых типов
Для того чтобы застраховаться от ошибок, возникающих в процессе приведения типов,
в языке С# предусмотрена специальная конструкция checked.
Вот пример ее использования:
ulong ulongNuber = 0x1122334455;
ushort resultUshort;
checked
{
resultUshort = (ushort)ulongNuber;
}
В блок checked помещаются операторы с приведением типа, при выполнении ко-
которых может происходить потеря старших разрядов исходного значения. Проверка
выполняется во время работы программы, так как на этапе компиляции значение
исходной переменной может быть неизвестно.
Глава 5. Преобразование типов объектов 191
В приведенном выше фрагменте кода при выполнении присваивания произойдем
потеря старших разрядов исходного значения. Так как эта операция находится в блоке
checked, в результате возникнет исключение System. Overf lowException.
Об исключениях мы расскажем позже, в гл. 9. Пока вам достаточно знать, что про-
программа может «перехватывать» исключения и обрабатывать ошибочные ситуации Ес-
Если же обработка исключения не предусмотрена, то программа завершит свою работу
аварийно и выведет соответствующее сообщение. В нашем случае это будет сообще-
сообщение, приведенное ниже:
An unhandled exception of type 'System.OverflowException' occurred in
Cast.exe
Additional information: Arithmetic operation resulted in an overflow.
Преобразования типов и классы
Мы уже говорили, что в языке С# все данные (даже числа и символы) представляются
как объекты соответствующих классов, унаследованных от корневого класса
System.Object. Управление типами в Microsoft .NET Framework заведует универ-
универсальная система типов .NET Common Type System (CTS), определяющая типы и пра-
правила их приведения.
Наличие CTS обеспечивает использование единой системы типов в рамках любых
языков программирования, разработанных для платформы Microsoft .NET Framework.
Это, в частности, позволяет создавать отдельные компоненты и объекты создаваемой
программы на разных языках программирования.
Псевдонимы типов данных
В языке С# все числовые типы данных представляют собой классы, объявленные
в пространстве имен System. Для простоты компилятор С# допускает использование
вместо имен этих уже знакомых вам псевдонимов.
В табл. 5.1 мы привели псевдонимы числовых типов, упоминавшихся в гл. 1.
Таблица 5.1. Псевдонимы для имен классов числовых типов данных
Псевдоним
byte
ushort
uint
ulong
sbyte
short
int
long
float
double
decimal
Имя класса
System.Byte
System.Uintl6
System.Uint32
System.Uint64
System.Sbyte
System.Intl6
System.Int32
System.Int64
System.Single
System.Double
System.Decimal
192 А В Фролов, Г. В Фролов Язык С# Самоучитель
В языке С# предусмотрен также набор псевдонимов для классов для логических,
строковых и символьных данных, а также для базового класса System.Object
(табл. 5.2).
Таблица 5.2. Псевдонимы для имен классов прочих типов данных
Псевдоним
bool
char
string
object
Имя класса
System.Boolean
System.Char
System.String
System.Object
В своих программах вы можете использовать вместо псевдонимов имена соответ-
соответствующих классов, если считаете, что так программа будет понятнее. Однако тем, кто
уже создавал программы на других языках программирования, псевдонимы С# скорее
всего будут удобнее.
В листинге 5.3 мы показали немного измененный вариант предыдущей программы,
демонстрирующей явное преобразование числовых типов данных.
Листинг 5.3. Файл chO5\TypeAliases\TypeAliasesApp.cs
using System;
namespace TypeAliases
{
class TypeAliasesApp
{
static void Main(string[] args)
{
System.UInt64 ulongNuber = 0x1122334455;
System.Byte resultByte = (System.Byte)ulongNuber;
Ulntl6 resultUshort = (UIntl6)ulongNuber;
UInt32 resultUint = (UInt32)ulongNuber;
Console.WriteLine("Исходное число типа ulong = {0:X}\n",
ulongNuber);
Console.WriteLine("Результат типа byte\t= {0:X}",
resultByte);
Console.WriteLine("Результат типа ushort\t= {0:X}",
resultUshort);
Console.WriteLine("Результат типа uint\t= {0:X}",
resultUint);
Console.ReadLine();
Обратите внимание, что здесь мы заменили псевдонимы именами соответствую-
соответствующих классов. В следующих двух строках программы мы не указали пространство имен
System, так как его просмотр задан в начале программы оператором using:
Глава 5. Преобразование типов объектов 193
7 Язык С# Самоучитель
UIntl6 resultUshort = (Ulntl6)ulongNuber;
UInt32 resultuint = (UInt32)ulongNuber;
Когда названия классов использовать удобнее, чем псевдонимы?
Например, когда вам нужно следить за разрядностью переменных.
Например, сможете ли вы сразу вспомнить, какова разрядность переменной типа
short (т. е. сколько разрядов данных занимают значения этого типа)?
Название класса System. Intl6 сразу дает ответ на этот вопрос— данные зани-
занимают в памяти 16 разрядов. Таким образом, применение названий классов, перечис-
перечисленных в табл. 5.1 и 5.2, вместо соответствующих псевдонимов позволяет в некоторых
случаях упростить для программиста контроль использования типов данных.
Для повышения производительности при работе с объектами, представляющими
числа и символы, в языке С# применяется технология упаковки (boxing). Эта техноло-
технология предполагает автоматическое преобразование объектов классов в обычные пере-
переменные, размещаемые в оперативной памяти компьютера, когда это допустимо. Об-
Обратное преобразование также выполняется автоматически. Для программиста все
эти действия абсолютно прозрачны, поэтому упаковка не вызывает дополнительных
сложностей при создании программ.
Приведение производных и базовых классов
Рассказывая о полиморфизме в предыдущей главе, в одном из примеров программ
(см. листинг 4.4) мы создавали базовый класс Shape с абстрактным методом Draw,
а затем получали от него производные классы Point и Rectangle:
abstract class Shape
{
abstract public void Draw(int x, int y);
}
class Point : Shape
{
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование точки в ({0}, {1})", x, у);
xPos = x;
xPos = y;
class Rectangle : Shape
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование прямоугольника в ({0}, {1})", x, у)
xPos = x;
yPos = y;
194 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Вооружившись этими классами, в методе Main мы проделали следующие операции:
Shape[] allShapes = new Shape[4];
allShapesCO] = new Pointd, 5);
allShapes[1] = new RectangleB, 3, 1, 5);
allShapes[2] = new RectangleA, 2, 3, 4);
allShapes[3] = new PointC1, 4) ;
foreach(Shape currentShape in allShapes)
currentShape.Draw@, 0);
Обратите внимание, что здесь мы объявили массив, созданный из объектов
класса Shape, но записали в него ссылки на объекты производных классов Point
и Rectangle.
Аналогичные действия выполнялись и в программе, исходный текст которой был
приведен в листинге 4.3:
Shape pt = new PointA0, 25);
pt.DrawD, 7);
Shape rect = new Rectangle(l, 4, 10, 20);
rect.DrawA0, 12);
Здесь в локальные переменные pt и rect, созданные на базе класса Shape, тоже
записываются ссылки на объекты производных классов Point и Rectangle. Таким
образом, в процессе присваивания выполняется неявное преобразование типа ссыл-
ссылки — тип ссылок на производные классы приводится к типу ссылки на базовый класс.
Такое приведение типов называется восходящим (upcast).
Используя полиморфизм, мы можем вызвать методы производных классов при по-
помощи виртуальных или абстрактных методов базового класса.
Что же касается нисходящего (downcast) приведения ссылок на базовый класс к ти-
типу ссылки на производный класс, то оно недопустимо. Если мы в этом случае приме-
применим явное приведение типов, компилятор не выведет сообщение об ошибке, но в про-
процессе работы программы может возникнуть исключение.
Операторы is и as
Для того чтобы в процессе преобразования типов не возникло исключение, перед вы-
выполнением этой операции следует проверить ее допустимость. Для проверки возмож-
возможности выполнения преобразования типов во время работы программы в языке С# пре-
предусмотрены операторы is и as.
Оператор as выполняет явное преобразование типов. Если такое преобразование
закончилось неудачно, вместо ссылки на объект оператор as возвращает пустое зна-
значение null.
Что же касается оператора is, то он возвращает true или false в зависимости
от того, можно ли выполнить преобразование или нет.
Глава 5 Преобразование типов объектов 195
Применение обоих этих операторов демонстрируется в программе, исходный текст
которой приведен в листинге 5.4.
Листинг 5.4. Файл chO5\Asls\AslsApp.cs
using System;
namespace Asls
{
class Point
{
public int x;
public int y;
public Point(int xO, int yO)
{
x = xO;
У = УО;
class ColorPoint : Point
{
public int color;
public ColorPoint(int x, int y, int c) : base(x, y)
{
color = c;
}
}
class AsIsApp
{
static void Main(string[] args)
{
Point pt = new PointA, 2);
ColorPoint cpt = new ColorPointA, 2, 3);
Point xPoint = cpt;
ColorPoint xColorPoint = pt as ColorPoint;
Console.WriteLine("xPoint = ({0}, {1})", xPoint.x, xPoint.y);
if(xColorPoint != null)
{
Console.WriteLine("xColorPoint = ({0}, {1}, {2})",
xColorPoint.x,
xColorPoint.y,
xColorPoint.color);
}
else
Console.WriteL:ne("xColorPoint == null");
196 А В Фролов, Г В. Фролов. Язык С#. Самоучитель
Point[] pointArray = new Point 12];:
pointArray[01 = new PointC, 4);
pointArray [1] = new ColorPoint F , 4, 5 ) ,-
foreach (object obj in pointArray)
if(obj is ColorPoint)
Console.WriteLine("ColorPoint = ({0}, {1}, {2})",
((ColorPoint)obj).x,
((ColorPoint)obj).y,
((ColorPoint)obj).color);
else if (obj is Point)'
Console.WriteLine( "Point = ({0}, {I}I',
((Point)obj).x,
((Point)obj).y);
Console.ReadLine();
В этой программе мы определили базовый класс Point, представляющий точки
на плоскости, и производный от него класс цветных точек ColorPoint. По сравне-
сравнению с базовым классом производный класс ColorPoint дополнен полем color для
хранения значения цвета.
Все самое интересное в этой программе делает метод Main.
Прежде всего он создает два объекта pt и cpt классов Point и ColorPoint со-
соответственно:
Point pt = new PointA, 2) ;
ColorPoint cpt = new ColorPointA, 2, 3);
Далее мы записываем ссылку на объект cpt в переменную xPoint, имеющую тип
базового класса Point:
Point xPoint = cpt;
Так как операция приведения ссылки к типу базового класса выполняется автома-
автоматически, эта строка будет всегда выполнена без ошибок. Далее наша программа смо-
сможет отобразить координаты точки на консоли:
Console.WriteLine("xPoint = ({0}, {!})", xPoint.x, xPoint.у);
Глава 5. Преобразование типов объектов 197
Но далее мы пытаемся выполнить прямо противоположную операцию приведения
типов— записать в переменную xColorPoint с типом дочернего класса ссылку
на объект pt базового класса:
ColorPoint xColorPoint = pt as ColorPoint;
Обратите внимание, что здесь мы применили оператор as.
Если преобразование будет выполнено успешно, в переменной xColorPoint
появится нужная нам ссылка. Если же в процессе работы программы выяснится,
что такое преобразование выполнить невозможно, в переменную xColorPoint будет
записано значение null.
Перед тем как пытаться использовать содержимое переменной xColorPoint, его
необходимо проверить. Если в этой переменной находится пустое значение null, на-
наша программа не будет отображать координаты и цвет точки на консоли, а выведет
туда соответствующее сообщение:
if(xColorPoint != null)
{
Console.WriteLine("xColorPoint = ({0}, {1}, {2})",
xColorPoint.x,
xColorPoint.y,
xColorPoint.color) ;
}
else
Console.WriteLine("xColorPoint == null");
Запуская нашу программу для проверки, вы увидите, что описанное выше преобра-
преобразование выполнить так и не удалось.
В этом, однако, нет ничего удивительного. В самом деле, мы создали обычную, не-
нецветную точку pt и пытаемся записать ссылку на нее в переменную с типом
ColorPoint, предполагающим дополнительно хранение информации о цвете.
Если бы нам удалось создать «цветную» ссылку на «черно-белый» объект, то по-
попытка применения этой ссылки, например, для получения информации о цвете точки,
закончилась бы неудачей. Дело в том, что в исходном объекте класса Point нет ин-
информации о цвете!
Далее наша программа демонстрирует использование оператора is.
Мы создаем массив базового класса Point, состоящий из двух ячеек, а затем запи-
записываем в него ссылки на объекты базового и производного классов:
PointП pointArray = new Point[2];
pointArray[0] = new PointC, 4);
pointArray[1] = new ColorPointF, 4, 5);
Далее мы обрабатываем этот массив в цикле:
foreach (object obj in pointArray)
С
if(obj is ColorPoint)
{
Console.WriteLine("ColorPoint = ({0), {1}, {2})",
((ColorPoint)obj). x,
198 А. В. Фролов, Г. В. Фролов. ЯзыкС#. Самоучитель
((ColorPoint)obj).у,
((ColorPoint)obj).color);
}
else if(obj is Point)
{
Console.WriteLine("Point = ({0}, {1})
((Point)obj).x,
((Point)obj).y);
Здесь мы по очереди извлекаем все элементы массива и записываем ссылку на них
в переменную obj класса object. Класс object является базовым для всех классов
в программах С#, поэтому такая операция всегда закончится успехом.
Извлекая очередной элемент массива, программа не знает, какой тип имеет этот
элемент. У нас возможны два случая — ячейка массива содержит ссылку на базовый
класс Point или производный класс ColorPoint.
С помощью оператора is мы можем определить тип ссылки и выполнить соответ-
соответствующее действие. Если это ссылка на «цветную» точку, мы выводим на экран ее ко-
координаты и цвет, а если «черно-белая» — ограничиваемся только координатами.
Таким образом, оператор as позволяет выполнить безопасное преобразование типов,
а оператор is — определить тип данных динамически во время работы программы.
Нестандартное преобразование
Иногда при создании программ требуется выполнить приведение одного созданного
вами типа к другому. Для решения подобных задач в языке С# предусмотрены средст-
средства явного и неявного нестандартного преобразования типов.
Вернемся вновь к комплексным числам. Известно, что комплексное число мож-
можно представить в виде точки на координатной плоскости [7]. При этом действи-
действительная часть комплексного числа ставится в соответствие координатое по оси X,
а мнимая — по оси Y.
Ранее мы уже создавали для представления комплексных чисел класс Complex.
В этом классе мы определили все необходимые перегруженные операторы. Кроме то-
того, мы создавали и неоднократно использовали в примерах программ класс Point,
представляющий собой точки на плоскости.
Возникает вопрос, можно ли приводить друг к другу ссылки на объекты этих классов?
Если да, то мы сможем выполнять присваивание ссылок на объекты этих классов,
как это сделано ниже:
Point pt = new PointA, 2);
Complex с = new ComplexC, 4);
Complex resultl = pt;
Point result2 = с;
Глава 5. Преобразование типов объектов 199
Здесь мы определили два объекта классов Complex и Point, а также две локаль-
локальные переменные этих же классов. Затем программа записывает в переменную
resultl класса Complex ссылку на объект класса Point, а также в переменную
result2 класса Point ссылку на объект класса Compl ex.
На первый взгляд такое присваивание допустимо, так как существует однозначное
соответствие между действительной и мнимой частями комплексного числа и коор-
координатами (X, Y) точки.
Однако компилятор не знает о существовании подобного соответствия. Он не обладает
никакой информацией о том, как нужно преобразовывать объекты класса Complex в объ-
объекты класса Point, а также о том, как выполнить обратное преобразование.
Чтобы приведенный выше фрагмент кода транслировался без ошибок, мы должны
определить в классах Complex и Point соответствующие методы, определяющие не-
нестандартное преобразование между объектами этих классов.
Ниже мы привели исходный текст метода, выполняющего приведение типа ссылки
на объект класса Point к типу ссылки на объект класса Complex:
public static implicit operator Complex(Point pt)
{
return new Complex(pt.xPos, pt.yPos);
}
Объявление этого метода должно находиться в классе Complex.
Наш метод создает новый объект класса Complex, инициализируя его соответст-
соответствующим образом: координата по оси X записывается в реальную часть комплексного
числа, а координата по оси Y — в мнимую часть комплексного числа.
Обратите внимание, что здесь мы использовали ключевые слова implicit
и operator.
С ключевым словом operator вы уже знакомы — оно используется для пере-
перегрузки операторов (см. гл. 4). Что же касается implicit, то это ключевое слово ука-
указывает на допустимость неявного преобразования типов.
Вместо implicit при объявлении нестандартного преобразования можно указы-
указывать ключевое слово explicit. В этом случае придется задавать тип преобразования
явно, например:
Point pt = new Point(l, 2);
Complex с = new ComplexC, 4);
Complex resultl = (Complex)pt;
Point result2 = (Point)c;
Хотя операторы явного преобразования несколько загромождают исходный текст,
их применение позволит сократить количество ошибок при написании программ с не-
нестандартным преобразованием типов. Если нестандартное преобразование объявлено
как explicit, программист не сможет по ошибке выполнить неправильное приведе-
приведение типов — компилятор не даст ему этого сделать.
Чтобы можно было приводить ссылку на объект класса Complex к ссылке типа
Poin t, u классе Point необходимо объявить метод приведения типа следующего вида:
200 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
public static implicit operator Point(Complex c)
{
return new Point(c.re, c.im);
)
Этот метод создает новый объект класса Point, причем в качестве координаты
по оси X используется действительная часть комплексного числа, а в качестве коор-
координаты по оси Y — мнимая часть комплексного числа.
В листинге 5.5 мы привели исходный текст программы, демонстрирующей приме-
применение описанных выше нестандартных преобразований.
Листинг 5.5. Файл ch05\ClassCustomCast\ClassCustomCastApp.cs
using System;
namespace ClassCustomCast
{
class Complex
{
public double re;
public double im;
public Complex(double r, double i)
{
re = r ;
im = i ;
}
public static Complex Add(Complex xl, Complex x2)
{
return new Complex(xl.re + x2.re, xl.im + x2.im);
}
public static Complex Sub(Complex xl. Complex x2)
{
return new Complex(xl.re - x2.re, xl.im - x2.im);
}
public static Complex operator +(Complex xl. Complex x2)
{
return new Complex(xl.re + x2.re, xl.im + x2.im);
}
public static Complex operator -(Complex xl, Complex x2)
{
return new Complex(xl.re - x2.re, xl.im - x2.im);
}
public static Complex operator ++(Complex x)
{
return new Complex(x.re + 1, x.im + 1);
}
public static Complex operator --(Complex x)
{
return new Complex(x.re - 1, x.im - 1);
} ^____
Глава 5. Преобразование типов объектов 201
public static Complex operator *(Complex xl, Complex x2)
{
return new Complex)
xl.re*x2.re - xl.im*x2.im,
xl.re*x2.im + x2.re*xl.im) ;
}
public static Complex operator /(Complex xl, Complex x2)
(
return new Complex(
(xl.re*x2.re + xl.im*x2.im)/(x2.re*x2.re + x2.im*x2.im)
(xl.im*x2.re - xl.re*x2.im)/(x2.re*x2.re + x2.im*x2.im)
}
public static bool operator == (Complex xl, Complex x2)
{
if(xl.re == x2.re && xl.im == x2.im)
return true;
else
return false;
}
public static bool operator ! = (Complex xl, Complex x2)
С
if(xl.re != x2.re || xl.im != x2.im)
return true;
else
return false;
}
public override bool Equals(object o)
{
return true;
}
public override int GetHashCode()
{
return 0;
public static implicit operator Complex(Point pt)
return new Complex(pt.xPos, pt.yPos);
public static implicit operator Complex(double d)
return new Complex(d, 0);
public static implicit operator double(Complex c)
return с.re;
202 А В Фролов, Г. В Фролов. Язык С#. Самоучитель
class Point
public double xPos;
public double yPos;
public Point(double x, double y)
xPos = x;
yPos = y;
public void Draw(double x, double y)
Console. Writeliine( "Рисование точки в ({0}, {1})", x, у)
xPos = x;
yPos = y;
public static implicit operator Point(Complex c)
{
return new Point (ere, с . im) ;
}
public static implicit operator Point(double d)
{
return new Point(d, 0);
}
public static implicit operator double(Point pt)
{
return pt.xPos;
}
}
class ClassCustomCastApp
{
static void Main(stringП args)
{
Point pt = new PointA, 2);
Complex с = new ComplexC, 4) ,-
Complex resultl = pt;
Point result2 = c;
Console.WriteLine("resultl = ({0}, {1})",
resultl.re, resultl.im);
Console.WriteLine("result2 = ({0}, {1})",
result2.xPos, result2.yPos);
Point result3 = 5;
Console.WriteLine("result3 = ({0}, {1})",
result3.xPos, result3.yPos);
Глава 5. Преобразование типов объектов 203
double result4 = с;
Console.WriteLine("result4 = {0}", result4)
Console.ReadLine ();
Обратите внимание, что в классе Complex мы дополнительно определили преоб-
преобразование действительного числа типа double в комплексное число, а также преобра-
преобразование комплексного числа в действительное число:
public static implicit operator Complex(double d)
return new Complex(d, 0);
public static implicit operator double(Complex c)
return ere;
При выполнении преобразования комплексного числа в действительное наш метод
игнорирует существование мнимой части числа. При обратном преобразовании мни-
мнимая часть числа становится равной нулю.
Аналогичные методы определены и в классе Point:
public static implicit operator Point(double d)
return new Point(d, 0);
public static implicit operator double(Point pt)
return pt.xPos;
Заметим, что с помощью нестандартных преобразований вы можете выполнить
приведение любых типов объектов, если, конечно, это имеет смысл и вы знаете алго-
алгоритм такого приведения.
Хотя все классы в С# наследуются от общего класса System.Object, все они
имеют нечто общее. Однако это не означает, что вы должны предусматривать приве-
приведение типов для всех разработанных вами классов. Как и другие возможности языка
С#, эта возможность должна использоваться только при необходимости. Едва ли стоит
предусматривать приведение абсолютно не связанных между собой типов (вспомните
поговорку про бузину в огороде и дядьку в Киеве).
204 А В Фролов, Г. В. Фролов. Язык С# Самоучитель
Глава 6. Свойства объектов
До сих пор мы рассматривали объекты классов С# как объединение полей данных
и методов, предназначенных для работы с этими полями данных или для реализации
других свойств объектов. В этой главе мы расскажем вам о так называемых свойствах
(properties), представляющих собой атрибут, связанный с объектом или классом.
Для объяснения назначения свойств мы вернемся к описанию класса Tele-
visionSet, впервые упомянутого нами в разделе «Первые шаги к ООП» гл. 3. На-
Напомним, что этот класс представлял собой программную модель телевизора, который
можно было включать и выключать, переключать с канала на канал. Кроме того, мож-
можно было регулировать громкость звука.
Ниже для удобства мы воспроизвели в сокращенном виде исходный текст класса
TelevisionSet:
class TelevisionSet
{
bool isPowerOn;
byte maxChannel;
byte currentChannel ;
byte currentVolume;
// включен или выключен
// максимальный номер канала
// текущий номер канала
// текущая громкость звука
// Конструктор класса TelevisionSet
public TelevisionSet(byte numberOfChannels)
// Включить телевизор
public void SetPowerStateOnf)
// Выключить телевизор
public void SetPowerStateOff()
// Определить состояние телевизора - включен или выключен
public bool GetPowerState()
амюитоп
205
II Переключиться на прием заданного канала
public bool SetChannel(byte channel)
// Получить номер текущего канала
public byte GetChannel()
// Установить громкость
public void SetVolume(byte volume)
// Получить текущий уровень громкости
public byte GetVolumeO
Мы можем представить себе телевизор как объект, имеющий следующие атрибуты:
• состояние телевизора (включен или выключен),
• максимально допустимый номер канала,
• текущий номер канала,
• текущий уровень громкости звука.
Значение перечисленных выше атрибутов хранится в полях класса Tele-
visionSet, а для изменения этих значений и для получения текущих значений мы
предусмотрели соответствующие методы. Например, текущий уровень громкости
можно установить методом SetVolume, а определить— методом GetVolume. Что-
Чтобы переключить телевизор на нужный канал, следует вызвать метод SetChannel,
а для того, чтобы узнать номер канала, принимаемого телевизором в настоящий мо-
момент, — метод GetChannel.
Заметим, что выполнить все перечисленные выше действия можно было бы и по-
другому, например обратившись напрямую к полям класса, объявленным как public.
Однако, действуя подобным образом, легко допустить ошибку. В самом деле, ничто
не помешает программе записать в поле currentChannel, хранящее номер текуще-
текущего канала, слишком большое значение, превышающее максимально допустимое. В ре-
результате программа предпримет попытку переключить телевизор на несуществующий
канал.
206 А В Фролов, Г. В. Фролов Язык С#. Самоучитель
С другой стороны, прямое обращение к полям упрощает исходный текст програм-
программы и делает его нагляднее, так как для изменения атрибутов объекта вместо вызова
методов можно воспользоваться оператором присваивания.
Механизм свойств, реализованный в языке программирования С# и описанный
в этой главе нашей книги, позволяет скомбинировать достоинства использования ме-
методов для доступа к полям класса и прямого обращения к этим полям. Он позволяет
создать так называемые «умные» поля (smart fields), доступ к которым контролируется
с помощью процедур специального вида.
Объявление свойства
Каждый класс С# может содержать в себе объявления одного или нескольких свойств.
Свойства представляют собой методы специального вида, причем для каждого свойст-
свойства применяются два таких метода. Один метод используется для чтения значения
свойства, а другой — для записи.
Для того чтобы объявить свойство, нам потребуется конструкция следующего вида:
<Модификатор> <Тип> <Имя свойства>
{
get
{
return <Возвращаемое значение>;
}
set
{
<Поле класса> = value;
Здесь в качестве модификатора можно использовать уже знакомые вам ключевые
слова public, protected, private, internal, static и new.
Вот пример объявления свойства Channel в классе TelevisionSet:
class TelevisionSet
{
private byte currentChannel;
public byte Channel
{
set
{
if(value <= maxChannel && value > 0)
currentChannel = value;
}
get
{
return currentChannel;
Глава 6. Свойства объектов 207
Здесь мы указали модификатор доступа свойства public, разрешающий доступ
к свойству Channel любым методам программы. Тип свойства— byte, а имя —
Channel.
Блок свойства Channel содержит две процедуры доступа (accessor). Одна из этих
процедур, объявленная с ключевым словом get, предназначена для получения значе-
значения свойства, а другая, объявленная с ключевым словом set, — для установки значе-
значения свойства.
Процедура доступа set
С помощью процедуры доступа set программа может установить значение свойства,
используя обычный оператор присваивания.
Рассмотрим следующий фрагмент программы:
TelevisionSet tvSmall;
TelevisionSet tvLarge;
tvSmall = new TelevisionSetF);
tvLarge = new TelevisionSetD0) ;
tvSmall.Channel = 5;
tvLarge.Channel = 52;
Здесь мы создали два объекта класса TelevisionSet, содержащего объявление
свойства Channel (номер текущего канала). Далее наша программа переключила
один из этих телевизоров на 5-й канал, а другой — на 52-й канал, выполнив установку
свойства Channel в соответствующих объектах.
Как видите, ссылка на свойства объекта выполняется точно таким же образом, как
и на поле объекта. Однако действия, выполняемые в процессе присваивания значения
свойству, полностью отличаются от действий, выполняемых при изменении значения
поля. Когда программа изменяет свойство объекта, присваивая ему значение, выпол-
выполняется соответствующая процедура доступа. Она, например, может проверить при-
присваиваемое значение на допустимость, сохранить это значение не в оперативной памя-
памяти, а в базе данных или даже передать его через Интернет.
В нашем примере процедура доступа set свойства Channel объявлена в классе
TelevisionSet следующим образом:
set
if(value <= maxChannel && value > 0)
currentChannel = value;
Обратите внимание на использование ключевого слова value. Это слово обозна-
обозначает неявный параметр, передаваемый процедуре доступа set. Этот параметр содер-
содержит значение, которое программа пытается присвоить свойству.
208 А. В Фролов. Г В. Фролов. Язык С# Самоучитель
Наша реализация процедуры доступа set свойства Channel проверяет допусти-
допустимость выполнения операции присваивания, сравнивая значение параметра value
с максимально допустимым значением, а также с нулем. Изменение значения свойства
происходит только в том случае, если программа задает допустимый номер канала.
В противном случае текущий номер канала не изменяется. Другой способ обработки
ошибок, предполагающий применение исключений, будет рассмотрен позже в главе,
посвященной исключениям.
Процедура доступа get
Процедура доступа get предназначена для получения текущего значения свойства.
Она обязательно должна возвращать это значение с помощью оператора return.
Вот пример определения процедуры доступа get свойства Channel, объявленно-
объявленного в классе TelevisionSet:
get
{
return currentChannel;
}
Заметим, что значение свойства может храниться в поле (как в примере, приведен-
приведенном выше) или вычисляться динамически во время работы программы (например,
на основе содержимого базы данных или как-то еще).
Чтение свойства выполняется аналогично чтению обычного поля класса:
Console.WriteLine("@}, канал {1} из {2}, громкость {3}",
tvSmall.PowerOn ? "Включен" : "Выключен",
tvSmall.Channel, tvSmall.MaxChannel, tvSmall.Volume);
Так как чтение свойства не сводится к простому копированию содержимого поля
объекта, а приводит к вызову соответствующей процедуры доступа, это дает програм-
программисту возможность действовать более гибко.
Определяя свойства и процедуры доступа, программист может полностью скрыть
детали реализации класса, отвечающие за хранение данных и способ их предоставле-
предоставления внешнему миру. Более того, изменение этих деталей никак не скажется на работо-
работоспособности программ, использующих классы с интерфейсами.
Свойства только для чтения и только для записи
Если при объявлении свойства определить две процедуры доступа set и get,
то такое свойство будет доступно и для записи и для чтения. Но иногда бывает по-
полезно объявить такое свойство, которое можно только читать или в которое можно
только писать.
В языке С# очень легко объявить свойства, доступные только для чтения или толь-
только для записи. Если, например, вам нужно свойство, которое можно только читать,
следует определить метод доступа get и не определять метод доступа set. Объявле-
Объявление свойства, доступного только для записи, должно, наоборот, содержать определе-
определение одного только метода доступа set.
Глава 6. Свойства объектов 209
Вот пример объявления свойства MaxChannel, доступного только для чтения:
class TelevisionSet
I
private byte maxChannel ;
public byte MaxChannel
{
get
{
return maxChannel;
Это свойство хранит максимальный номер канала, который может принимать теле-
телевизор. Инициализация поля maxChannel, хранящего этот номер, выполняется один
раз конструктором при создании объекта.
После того как объект создан, программа не сможет изменить максимальный номер
принимаемого канала, так как поле maxChannel объявлено с модификатором досту-
доступа private.
Применение свойств, доступных только для чтения или только для записи, позво-
позволяет уменьшить количество ошибок, допускаемых программистом в результате ис-
использования операторов присваивания и других средств доступа к данным объекта.
Пример программы
В листинге 6.1 мы привели исходный текст программы, демонстрирующей использо-
использование свойств объектов, создаваемых на базе класса TelevisionSet. В новой реали-
реализации этого класса мы заменили все методы соответствующими свойствами, заметно
упростив работу программ с объектами данного класса.
Листинг 6.1. Файл ch06\TvProperties\TvPropertiesApp.cs
using System;
namespace TvProperties
{
class TelevisionSet
{
it . .
// Конструктор класса TelevisionSet
II .
public TelevisionSet(byte numberOfChannels)
{
// Устанавливаем исходное состояние телевизора
isPowerOn = false; // выключен
maxChannel = numberOfChannels; // макс, количество каналов
210 А В. Фролов, Г. В Фролов. Язык С#. Самоучитель
currentChannel = 1; // при включении показывать канал 1
currentVolume =10; // громкость при включении - 10%
// Свойство PowerOn -- включен или выключен
//
private bool isPowerOn;
public bool PowerOn
{
get
{
return isPowerOn;
}
set
{
isPowerOn = value;
}
}
//
// Свойство MaxChannel -- максимальный номер канала
//
private byte maxChannel;
public byte MaxChannel
{
get
{
return maxChannel;
}
}
//
// Свойство Channel -- текущий номер канала
//
private byte currentChannel;
public byte Channel
{
get
{
return currentChannel;
}
set
if(value <= maxChannel && value > 0)
currentChannel = value;
Глава 6. Свойства объектов 211
t I
// Свойство Volume -- текущий уровень громкости
/ / „
private byte currentVolume;
public byte Volume
{
get
{
return currentVolume;
}
set
{
if(value > 0 && value <= 100)
currentVolume = value;
else
currentVolume = 0;
class TvPropertiesApp
{
static void Main(string[] args)
{
TelevisionSet tvSmall;
TelevisionSet tvLarge;
tvSmall = new TelevisionSetF) ;
tvLarge = new TelevisionSetD0) ;
tvSmall.PowerOn = true;
tvSmall.Channel = 5;
tvSmall.Volume = 50;
tvLarge.PowerOn = true;
tvLarge.Channel = 27;
tvLarge.Volume = 3 0;
Console.Write("Телевизор tvSmall: ");
Console.WriteLine("{0}, канал {1} из {2}, громкость {3}",
tvSmall.PowerOn ? "Включен" : "Выключен",
tvSmall.Channel, tvSmall.MaxChannel, tvSmall.Volume);
Console.Write("Телевизор tvLarge: ");
Console.WriteLine("{0}, канал {1} из {2}, громкость {3}",
tvLarge.PowerOn ? "Включен" : "Выключен",
tvLarge. Channel, tvLarge. MaxChannel, tvLarge .Volume) ,•
212 А В. Фролов, Г. В. Фролов. Язык С# Самоучитель
tvSmall.Channel = 3 ;
tvSmall.Volume = 80;
tvLarge.Channel = 39;
tvLarge.Volume = 60;
tvSmall.PowerOn = false;
tvLarge.PowerOn = false;
Console.Write("Телевизор tvSmall: ");
Console.WriteLine("{0}, канал {1} из {2}, громкость {3}",
tvSmall.PowerOn ? "Включен" : "Выключен",
tvSmall.Channel, tvSmall.MaxChannel, tvSmall.Volume);
Console.Write("Телевизор tvLarge: ");
Console.WriteLine("{0}, канал {1} из {2}, громкость {3}",
tvLarge.PowerOn ? "Включен" : "Выключен",
tvLarge.Channel, tvLarge.MaxChannel, tvLarge.Volume);
Console.ReadLine();
В программе объявлен класс TelevisionSet, представляющий виртуальный те-
телевизор. Все управление этим виртуальным телевизором реализовано через свойства.
Конструктор класса TelevisionSet выполняет начальную инициализацию по-
полей класса, устанавливая тем самым исходное состояние телевизора:
public TelevisionSet(byte numberOfChannels)
{
isPowerOn = false; // выключен
maxChannel = numberOfChannels; // макс, количество каналов
currentchannel= 1; // при включении показывать канал 1
currentVolume = 10; // громкость при включении - 10%
}
Все поля, инициализируемые конструктором, объявлены в классе Televi-
TelevisionSet как private, поэтому объекты программы, внешние по отношению к дан-
данному классу, не имеют к этим полям никакого доступа.
Чтобы программа могла включать и выключать виртуальный телевизор, в классе
TelevisionSet определено свойство PowerOn:
private bool isPowerOn;
public bool PowerOn
{
get
Глава 6. Свойства объектов 213
return isPowerOn;
}
set
{
isPowerOn = value;
Чтобы включить телевизор, в это свойство нужно записать логическое значение
true, а чтобы выключить — логическое значение false.
Так как в методе PowerOn определены две процедуры доступа, set и get, про-
программа может не только включать или выключать телевизор, но и определять его те-
текущее состояние (включен или выключен).
Выше в этой главе мы уже рассказывали про свойство MaxChannel, хранящее
максимально допустимый номер канала:
private byte maxChannel;
public byte MaxChannel
{
get
{
return maxChannel;
Так как в этом свойстве объявлена только одна процедура доступа get, программа
сможет читать данное свойство, но не записывать в него новые значения.
Свойство Channel доступно как для чтения, так и для записи:
private byte currentchannel;
public byte Channel
{
get
{
return currentchannel;
}
set
{
if(value <= maxChannel && value > 0)
currentchannel = value;
При чтении данного свойства программа получит текущий номер канала, храня-
хранящийся в поле currentchannel.
Что же касается попытки установить новый номер канала, то она будет успешна
только в том случае, если программа записывает в свойство Channel допустимое
214 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
значение (большее нуля и меньшее максимально допустимого номера канала). Попыт-
Попытки переключить телевизор на недопустимый канал будут проигнорированы.
Аналогичным образом работает и свойство Volume, хранящее текущую громкость
звука:
private byte currentVolume;
public byte Volume
{
get
{
return currentVolume;
}
set
{
if(value > 0 && value <= 100)
currentVolume = value;
else
currentVolume = 0;
Попытка установить неправильное значение для громкости звука приведет к тому,
что звук будет просто отключен.
Теперь мы рассмотрим метод Main, получающий управление при запуске про-
программы.
Свою работу этот метод начинает с того, что создает два телевизора, один из кото-
которых способен принимать 6 каналов, а другой — 40 каналов:
TelevisionSet tvSmall;
TelevisionSet tvLarge;
tvSmall = new TelevisionSetF);
tvLarge = new TelevisionSetD0) ;
Далее программа включает первый телевизор, переключает его на 5-й канал и ус-
устанавливает уровень громкости, равный 50 % от максимального уровня:
tvSmall. PowerOn = true ;
tvSmall .Channel = 5;
tvSmall.Volume = 50;
Обратите внимание, что все эти действия выполняются при помощи простого опе-
оператора присваивания. Программа записывает нужные значения в свойства объекта,
а методы доступа set делают все необходимые проверки и другие действия.
Далее наша программа включает второй телевизор, переключает его на 27-й канал
и устанавливает уровень громкости 30 %:
Глава 6. Свойства объектов 215
tvLarge.PowerOn = true;
tvLarge.Channel = 27;
tvLarge.Volume = 30;
Текущее состояние первого телевизора отображается на консоли:
Console.Write("Телевизор tvSmall: ");
Console.WriteLine("{0}, канал {1} из {2), громкость {3}",
tvSmall.PowerOn ? "Включен" : "Выключен",
tvSmall.Channel, tvSmall.MaxChannel, tvSmall.Volume);
Обратите внимание, что для получения текущего состояния телевизора мы обраща-
обращаемся к свойствам соответствующего объекта. Аналогичная процедура выполняется
В для второго телевизора:
Console.Write("Телевизор tvLarge: ");
Console.WriteLine("@}, канал A} из {2}, громкость C}",
tvLarge.PowerOn ? "Включен" : "Выключен",
tvLarge.Channel, tvLarge.MaxChannel, tvLarge.Volume);
На втором этапе своей работы программа изменяет состояние телевизоров, пере-
переключая их на другие каналы:
tvSmall.Channel = 3,-
tvSmall.Volume = 80;
tvLarge. Channel = 39,-
tvLarge.Volume = 60;
Затем программа выключает оба телевизора:
tvSmall.PowerOn = false;
tvLarge.PowerOn = false;
И наконец, новое состояние телевизоров отображается на консоли:
Console.Write("Телевизор tvSmall: ");
Console.WriteLine("{0}, канал {1} из {2}, громкость {3}",
tvSmall.PowerOn ? "Включен" : "Выключен",
tvSmall.Channel, tvSmall.MaxChannel, tvSmall.Volume);
Console.Write("Телевизор tvLarge: ");
Console.WriteLine("@}, канал A) из B}, громкость C)",
tvLarge.PowerOn ? "Включен" : "Выключен",
tvLarge.Channel, tvLarge.MaxChannel, tvLarge.Volume);
Вот что вы увидите на экране компьютера, запустив нашу программу:
216 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Телевизор tvSmall: Включен, канал 5 из 6, громкость 50
Телевизор tvLarge: Включен, канал 27 из 40, громкость 30
Телевизор tvSmall: Выключен, канал 3 из б, громкость 80
Телевизор tvLarge: Выключен, канал 39 из 40, громкость 60
Как видите, механизм свойств позволил заменить все методы класса и свести выполне-
выполнение всех действий над виртуальными телевизорами к простым операциям присваивания.
Стоит ли вам в своих программах заменять вес методы свойствами?
Очевидно, что нет.
Применение свойств оправдано в тех случаях, когда необходимо контролировать
значение какого-либо атрибута объекта, такого, как номер канала телевизора или уро-
уровень громкости. Свойство связывается только с одним значением.
Методы же более универсальны. Вы можете, например, передавать методам не-
несколько параметров по ссылке или по значению, что невозможно при использовании
свойств. Как и любое средство языка программирования С#, свойства должны приме-
применяться там, где они действительно необходимы и помогают упростить программу,
а также улучшить ее структуру.
Наследование свойств
Если вы создаете производный класс, то можете унаследовать в дочернем классе свойства
базового класса. При этом допускается создание виртуальных и абстрактных свойств.
Для того чтобы объявить виртуальное свойство, используйте ключевое слово
virtual, как это показано ниже:
public virtual int X
{
set
{
xPos = value;
}
get
{
return xPos;
Объявление виртуального свойства может содержать в себе одну или две процеду-
процедуры доступа.
Объявление абстрактного метода доступа выполняется с применением ключевого
слова abstract:
public abstract int Y
{
set;
get;
Глава 6. Свойства объектов 217
Обращаем ваше внимание на то, что процедуры доступа виртуального свойства
должны быть определены в производном классе.
Рассмотрим исходный текст программы, демонстрирующей наследование свойств
(листинг 6.2).
Листинг 6.2. Файл ch06\Proplnheritance\ProplnheritanceApp.cs
using System;
namespace Proplnheritance
{
abstract class Shape
{
protected int xPos;
protected int yPos;
public Shape(int x, int y)
{
xPos = x;
xPos = y;
}
public abstract void Draw(int x, int y);
public virtual int X
{
set
{
xPos = value;
}
get
{
return xPos;
public abstract int Y
{
set;
get;
class Point : Shape
{
public Point(int x, int y) : base (x, y)
{
}
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование точки в ({0}, {1})",
this.X, this.Y);
this.X = x;
this.Y = y;
>
218 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
public override int X
{
set
{
xPos = value;
}
get
{
return xPos;
}
}
public override int Y
{
set
{
yPos = value;
}
get
{
return yPos;
}
}
}
class Rectangle : Shape
{
int width;
int height;
public Rectangle(int x, int y, int w, int h) : base(x, y)
{
width = w;
height = h;
}
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование прямоугольника в ({0}, {1})
this.X, this.Y);
this.X = x;
this.Y = y;
}
public override int Y
{
set
{
yPos = value;
}
get
{
return yPos;
Глава 6. Свойства объектов 219
class PropInheritanceApp
{
static void Main(string[] args)
{
Shape pt = new PointA0, 25);
pt.DrawD, 7);
Shape rect = new RectangleA, 4, 10, 20);
rect.DrawA0, 12);
Shape[] allShapes = new Shape[4];
allShapes[0] = new PointA, 5);
allShapes[ll = new RectangleB, 3, 1, 5);
allshapes[2] = new RectangleA, 2, 3, 4);
allShapes[3] = new PointC1, 4);
foreach(Shape currentShape in allShapes)
currentshape.Draw@, 0);
Console.ReadLine();
В этой программе объявлен абстрактный класс Shape и производные от него клас-
классы Point и Rectangle.
В абстрактном классе мы объявили два поля, xPos и yPos, хранящие координаты
фигуры — объекта класса, созданного на базе класса Shape, а также конструктор,
инициализирующий эти поля при создании объекта.
Кроме того, в этом классе объявлен абстрактный метод Draw, предназначенный
для рисования фигуры в заданной точке координатной плоскости, а также два свойст-
свойства, X и Y, хранящие текущие координаты фигуры.
Ниже мы привели исходный текст класса Point:
class Point : Shape
{
public Point(int x, int y) : base (x, y)
{
}
public override void Draw(int x, int y)
{
Console.WriteLine("Рисование точки в (@), A})",
this.X, this.Y);
this.X = x;
this.Y = y;
}
public override int X
{
set
{
xPos = value;
}
220 А В Фролов, Г В Фролов. Язык С# Самоучитель
get
{
return xPos;
}
}
public override int Y
{
set
{
yPos = value;
}
get
{
return yPos;
Конструктор класса Point вызывает конструктор базового класса.
Метод Draw, объявленный с ключевым словом override, выводит сообщение
о рисовании точки в заданных координатах, а затем сохраняет эти координаты
в свойствах объекта X и Y.
В классе Point мы определили свойства X и Y с ключевым словом override,
так как они переопределяют соответствующие свойства базового класса, первое
из которых является виртуальным, а второе — абстрактным.
В классе Rectangle мы переопределили только абстрактное свойство Y (абст-
(абстрактное свойство должно быть переопределено в производном классе). Что же касает-
касается свойства X, то здесь используется соответствующее свойство базового класса.
Метод Main нашей программы демонстрирует некоторые приемы работы с объек-
объектами базового класса и производных классов.
Прежде всего он создает новый объект класса Point, сохраняя ссылку на этот
объект в переменной базового класса Shape. Далее этот объект рисуется при помощи
метода Draw:
Shape pt = new PointA0, 25);
pt.DrawD, 7);
В базовом классе метод Draw объявлен как абстрактный. В данном случае исполь-
используется реализация этого метода из класса Point.
Аналогичные действия выполняются для объекта класса Rectangle:
Shape rect = new RectangleA, 4, 10, 20) ;
rect.DrawA0, 12);
тЗ завершение своей работы программа создает массив объектов классов Point
и Rectangle:
Shape[] allShapes = new Shape[4];
allShapes[0] = new PointA, 5);
allShapes[l] = new RectangleB, 3, 1, 5);
allShapes[2] = new Rectangled, 2, 3, 4);
allShapes[3] = new PointC1, 4);
Глава 6. Свойства объектов 221
foreach(Shape currentshape in allShapes)
currentshape.Draw@, 0);
Содержимое этого массива рисуется с помощью виртуального метода базового
класса Draw.
Статические свойства
С помощью ключевого слова static можно определить статические свойства. Пра-
Правила применения таких свойств аналогичны правилам прменения статических полей
и методов. Для ссылки на статические свойства необходимо использовать имя класса,
а не ссылку на объект класса. Кроме того, в объявлении статического свойства нельзя
употреблять ключевые слова virtual, abstract и override.
В листинге 6.3 мы привели исходный текст программы, в которой демонстрируется
использование статических свойств.
Листинг 6.3. Файл ch06\PropStatic\PropStaticApp.cs
using System;
namespace PropStatic
{
class Syslnfo
{
static Syslnfo()
{
ScrWidth = 640;
ScrHeight = 480;
}
public static string Version
{
get
{
return "Microsoft Windows";
private static uint ScrWidth;
public static uint ScreenWidth
{
get
{
return ScrWidth;
private static uint ScrHeight;
public static uint ScreenHeight
{
get
222 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
return ScrHeight;
}
}
}
class PropStaticApp
{
static void Main(string[] args)
{
Console.WriteLine("Операционная система: {0}",
Syslnfо.Version);
Console.WriteLine("Экранное разрешение: {0} x {1}
Syslnfо.ScreenWidth, Syslnfo.ScreenHeight);
Console.ReadLine();
Здесь мы объявили класс Syslnf о, в котором есть статический конструктор и три
статических свойства — Version, ScreenWidth и ScreenHeight.
В задачу статического конструктора входит инициализация двух статических по-
полей, хранящих текущее разрешение экрана монитора:
static SysInfoO
{
ScrWidth = 640;
ScrHeight = 480;
}
В нашем простейшем примере мы инициализируем эти поля числами, однако ре-
реальная программа может определить фактическое разрешение и сохранить его в полях
класса.
Свойства ScreenWidth и ScreenHeight позволяют получить текущее разре-
разрешение соответственно по горизонтали и вертикали:
private static uint ScrWidth;
public static uint ScreenWidth
{
get
{
return ScrWidth;
private static uint ScrHeight;
public static uint ScreenHeight
{
get
{
return ScrHeight;
Глава 6. Свойства объектов 223
Заметим, что программа не сможет изменить значение этих свойств, так как мы оп-
определили только процедуру доступа get.
Аналогичным образом объявлен статический метод Version:
public static string Version
I
get
{
return "Microsoft Windows";
Единственная процедура доступа позволяет извлечь название ОС.
Для того чтобы отобразить значение перечисленных выше свойств, мы использова-
использовали в методе Main нашей программы следующие строки:
Console.WriteLine("Операционная система: @}", Syslnfо.Version);
Console.WriteLine("Экранное разрешение: {0} х {1}",
Syslnfо.ScreenWidth, Syslnfо.ScreenHeight);
Вот что появится на экране в результате работы этих строк:
Операционная система: Microsoft Windows
Экранное разрешение: 640 х 480
Как видите, для ссылки на свойства мы указываем имя класса Syslnf о, не созда-
создавая при этом объектов данного класса. Внешне обращение к статическим свойствам
аналогично обращению к статическим полям класса. Однако, как вы уже знаете, свой-
свойства «умнее» обычных полей, так как они могут содержать определенную логику, реа-
реализованную в процедурах доступа.
224 А В Фролов, Г В Фролов. Язык С# Самоучитель
Глава 7. Массивы и индексаторы
Если программа должна работать с набором объектов одинакового типа, во многих случа-
случаях удобно образовать из этих объектов структуру данных, называемую массивом (array).
Каждый элемент массива имеет свой номер (индекс) и хранит один объект. Зная
индекс элемента массива, программа сможет извлечь или обновить нужный ей объект.
Заметим, что в одном массиве могут храниться объекты базового класса и произ-
производных классов. Ранее в нашей книге мы уже приводили подобные примеры, демонст-
демонстрирующие возможности полиморфизма в языке С#.
В языке программирования С# массивы обозначаются с помощью квадратных ско-
скобок, расположенных справа от обозначения типа объектов, составляющих массив. Ни-
Ниже мы привели пример определения массива из 10 целых чисел:
int[] bonus = new int[10];
Здесь объявлена ссылка bonus на одномерный массив, содержащий 10 ячеек
для хранения целых чисел со знаком типа int.
Обратите внимание, что мы не просто объявили ссылку на массив, а сразу же создали
массив оператором new, указав размер массива. Без этого программа не сможет использо-
использовать ссылку для работы с массивом, так как в ней будет храниться значение nul 1.
При определении массива не резервируется память, поэтому в объявлении ссылки
размеры массива не указываются. После выполнения резервирования памяти операто-
оператором new размер массива становится фиксированным.
В том случае, когда программист заранее не знает размеры массива, можно указать
эти размеры динамически во время работы программы, например:
int BonusArraySize = 5;
int[] bonus2 = new int[BonusArraySize];
Здесь создается массив, размеры которого предварительно записываются в пере-
переменную с именем BonusArraySize.
Типы массивов
Как и большинство языков программирования, язык С# позволяет создавать одномер-
одномерные и многомерные массивы. Кроме того, возможно создание массивов, содержащих
другие массивы. Рассмотрим способы объявления массивов перечисленных выше ти-
типов и приемы работы с ними.
Одномерные массивы
Одномерный массив можно представить себе в виде линейной последовательности
ячеек, каждая из которых имеет свой номер. Самая первая ячейка имеет номер 0, вто-
вторая — 1 и т. д.
ЛИМОШКВИ 225
8 Язык С# Самоучитель
На рис. 7.1 мы схематически показали одномерный массив с именем Array, со-
содержащий Size элементов. Эти элементы нумеруются от0до31ге-1.
Аггау(О]
Аггау[1]
Аггау[2]
Агтау[3]
Аггау[4]
Array[Size-1]
Рис. 7.1. Одномерный массив Array
Чтобы сослаться в программе на ячейку с заданным номером, необходимо указать
этот номер в квадратных скобках, расположенных справа от имени массива:
int[] bonus = new int[10];
bonus[0] = 5;
bonus[1] = 7;
bonus[2] = 10;
Console.WriteLine("Бонусы: bonus[0] = {0}, bonus[1] = {1}, bonus[2] ={2}",
bonus[0], bonus[1], bonus[ 2 ] ) ;
Здесь мы создали одномерный массив bonus, записали в 3 первые ячейки этого
массива числа 5, 7 и 10, а затем вывели содержимое проинициализированных таким
способом ячеек на консоль.
В данном случае была использована так называемая динамическая инициализация
массива. Программа инициализирует массив, записывая объекты в его ячейки.
Возможна также статическая инициализация массива, когда содержимое его ячеек
определяется в момент компиляции программы. Вот пример статической инициализа-
инициализации массива:
int[] bonusl = {3, 2, 11};
Здесь после оператора присваивания мы указали список инициализации массива,
заключенный в фигурные скобки.
Обратите внимание, что при статической инициализации нет никакой необходимо-
необходимости указывать размер массива, так как он определяется автоматически исходя из
количества элементов в списке инициализации.
Массивы С# удобны тем, что в них можно хранить объекты любого типа. Ниже мы
объявили два строковых массива:
stringU Words = new string[3];
string[] HelloWords = {"Hello", "C#", "world!11};
Console.WriteLine("HelloWords: {0}, {1} {2}",
HelloWords[0], HelloWords[1], HelloWords[2]);
Массив HelloWords проинициализирован статически, а его содержимое отобра-
отображается на консоли.
226
А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Многомерные массивы
Многомерные массивы можно представить себе в виде многомерной матрицы, в узлах
которой хранятся объекты.
На рис. 7.2 мы показали пример двумерного массива Array.
Столбцы
о
о.
О
Array[O,O]
Array[1,0]
Аггау[2,0]
Array[3,0]
Array[4,0]
Аггау[0,1]
Аггау[1,1]
Аггау[2,1]
Аггау[3,1]
Аггау[4,1]
Ячейка Аггау[3,2]
строка 3,
столбец 2
Аггау[0,2]
Аггау[1,2]
Аггау[2,2]
1 Аггау[3,2]
/ Аггау[4,2]
/
Рис. 7.2. Двумерный массив Array
Такой массив можно представить себе в виде строк и столбцов. Для адресации
ячейки двумерного массива нужно указывать два индекса — индекс строки и индекс
столбца.
Чтобы создать двумерный массив, например, целых чисел, используйте конструк-
конструкцию следующего вида:
intt.l TwoDimArray = new int[2,3];
Здесь при объявлении массива мы использовали запятую для того, чтобы указать
компилятору на необходимость создания ссылки TwoDimArray на двумерный мас-
массив. Кроме того, в операторе new мы указали количество строк и столбцов создавае-
создаваемого двумерного массива.
Если нужно объявить многомерный массив, используйте несколько запятых:
int[,,,] MultiDimArray = new int[2,3,7,2];
Количество запятых должно быть равно размерности массива, уменьшенной
на единицу. Таким образом, в предыдущем примере мы создали четырехмерный массив.
Многомерные массивы можно инициализировать динамически или статически.
Ниже приведен пример динамической инициализации двумерного массива:
int[,] TwoDimArray = new int[2,3];
TwoDimArray[0, 0] = 5;
TwoDimArray[1, 0] = 2 ;
TwoDimArray[0, 1] = 15;
TwoDimArray[1, 1] = 5;
TwoDimArray[0, 2] = 52;
TwoDimArray[1, 2] =32;
Глава 7. Массивы и индексаторы
227
Console.WriteLine("TwoDimArray:\n {0}, {1}",
TwoDimArray[0, 0], TwoDimArray[1, 0]);
Console.WriteLine("{0}, {1}", TwoDimArray[0,
Console.WriteLine("{0}, {1}", TwoDimArray[0,
1], TwoDimArray[1, 1]>
2], TwoDimArray[1, 2])
После инициализации содержимое массива выводится на консоль.
Вот пример статической инициализации двумерного массива CrossZero, кото-
который можно использовать, например, для создания программы известной игры в крес-
крестики-нолики:
char[,] CrossZero =
{
{'X', '0', 'X'},
{'0' , '0' , 'X'},
{'X', 'X1, -О1},
В этом массиве хранятся символы типа char.
Массивы массивов
В языке С# допускается создавать массивы массивов, называемые также несиммет-
несимметричными массивами. Другие названия для несимметричных массивов, встречающиеся
в литературе и в документации на язык С#, — ступенчатые или «рваные» (jagged)
массивы.
На рис. 7.3 мы показали массив, содержащий 5 одномерных массивов разного размера:
Аггау[О,О]
Аггау[1,0]
Аггау[2,0]
Аггау[0,1]
Аггау[1,1] Аггау[1,2] Аггау[1,3]
Агтау[3,0] |* Аггау[3,1]
Аггау[4,0]
Аггау[4,1]
Аггау[3,2]
Аггау[4,2]
Аггау[4,3]
Array [4,4]
Рис. 7.3. Массив массивов Array
Первый массив содержит 2 ячейки, второй — 4, третий — 1 и т. д. Изображение та-
такого массива похоже на лестницу, отсюда, видимо, и произошел перевод слов jagged
array как «ступенчатый массив».
При необходимости вы можете объединять в массивы не только одномерные,
но и многомерные массивы. Однако работа с такими объектами потребует от вас не-
недюжинного пространственного воображения.
Объявление массива массивов выполняется при помощи нескольких пар квадрат-
квадратных скобок. Ниже, например, мы объявили массив массивов, содержащих текстовые
строки:
SLri ng I
JaggedArray = new string[2]
228
А В Фролов, Г. В. Фролов. ЯзыкС# Самоучитель
Обратите внимание, что мы указали размерность нашего массива массивов, равную
двум. Соответственно нам необходимо инициализировать два массива строк:
JaggedArray[0] = new string[2];
JaggedArray[1] = new string[4];
После инициализации доступ к элементам массивов выполняется следующим образом:
JaggedArray[0][0] = "Hello, С#";
JaggedArray[0][1] = "World!";
JaggedArray[1][0] = "It";
JaggedArray[1][1] - "is";
JaggedArray[1][2] = "C#";
JaggedArray[1][3] = "JaggedArray";
При помощи первой пары квадратных скобок мы указываем индекс массива, а при
помощи второй — индекс элемента в массиве. Язык С# допускает создание довольно
сложных многомерных массивов, содержащих в себе другие многомерные массивы.
Например, ниже мы объявили ссылку на двумерный массив, содержащий четырехмер-
четырехмерный массив, который, в свою очередь, содержит трехмерный массив текстовых строк:
string[,] [,,,] [,,] VeryComplexArray;
Мы, однако, не рекомендуем увлекаться созданием подобных конструкций
без крайней на то необходимости. Они могут запутать программу и сделать ее исход-
исходный текст непонятным не только для посторонних людей, но и для самого разработчи-
разработчика программы.
Пример программы
В листинге 7.1 мы привели исходный текст программы, демонстрирующей выполне-
выполнение действий с одномерными, многомерными и несимметричными массивами, опи-
описанными ранее в этой главе.
Так как отдельные фрагменты этой программы уже были описаны, мы оставляем ее
вам для самостоятельного изучения.
Листинг 7.1. Файл chOASampleArray\SampleArrayApp.cs
using System;
namespace SampleArray
{
class SampleArrayApp
{
static void Main(string[] args)
{
int[] bonus = new int[10];
bonus@] = 5;
bonus[1] - 1;
bonus[2] = 10;
Глава 7. Массивы и индексаторы 229
Console.WriteLine("Бонусы: bonus[0]=CO}, bonus[1]={1},
bonus[2]={2}", bonus[0], bonus[1], bonus[2]);
int BonusArraySize = 5;
int[] bonus2 = new int[BonusArraySize];
int[] bonusl = {3, 2, 11};
Console.WriteLine("Бонусы: bonusl[0]={0}, bonusl[1]={1},
bonusl[2]={2}M, bonusl[0], bonusl[1], bonusl[2]);
string[] Words = new string[3],-
string [] HelloWords = {"Hello", "C#", "world!"};
Console.WriteLineC'HelloWords: {0}, {1} {2}",
HelloWords[0], HelloWords[1], HelloWords[2]);
int[,] TwoDimArray = new int[2,3];
TwoDimArray[0, 0] = 5 ;
TwoDimArray[1, 0] = 2;
TwoDimArray[0, 1] = 15;
TwoDimArray[1, 1] = 5;
TwoDimArray[0, 2] = 52;
TwoDimArray[1, 2] = 32;
Console.WriteLine("TwoDimArray:\n {0}, {1}",
TwoDimArray[0, 0], TwoDiraArray[1, 0 ] ) ;
Console.WriteLine("{0}, {I}",
TwoDimArray[0, 1], TwoDimArray[1, 1]);
Console.WriteLine("{0}, {1}",
TwoDimArray[0, 2], TwoDimArray[1, 2]);
chart,] CrossZero =
{
{
{
'X',
•о-,
■X1 ,
'0' ,
'0' ,
'X' ,
'X1
■х1
'0'
},
},
},
string[][] JaggedArray = new string[2][)
JaggedArray[0] = new string[2];
JaggedArray[1] = new string[4];
JaggedArray[0][0] = "Hello, C#";
JaggedArray[0][1] = "World!";
JaggedArray[1][0] = "It"
JaggedArray[1][1] = "is"
JaggedArray[1][2] = "C#'
JaggedArray[1][3] = "JaggedArray"
Console.ReadLine();
230 А. В. Фролов, Г. В. Фролов. Язык С# Самоучитель
Массивы и циклы
В предыдущих примерах программ мы обращались к отдельным ячейкам массивов,
указывая их номер (индекс). Однако чаще всего обработка массивов выполняется
в цикле с использованием таких операторов, как for, while, do и foreach.
При этом программа последовательно обращается к ячейкам массива, записывая или
извлекая данные.
Обработка одномерного массива чисел
Вначале мы рассмотрим самый простой прием циклической обработки одномерного
массива, основанный на прменении оператора for (листинг 7.2). Этот прием исполь-
используется во многих языках программирования, в частности в языках С и C++.
Листинг 7.2. Файл ch07\ArrayLoop\ArrayLoopApp.cs
using System;
namespace ArrayLoop
{
class ArrayLoopApp
{
static void Main(string[] args)
{
int[] Numbers;
Numbers = new int[10];
int i ;
for(i =0; i < 10; i++)
Numbers[i] = i;
Console.Write("Numbers: [" + Numbers[0]);
for(i = 1; i < Numbers.Length; i++)
Console.Write("," + Numbers[i]);
Console.WriteLine("]");
Console.ReadLine();
Внутри метода Main мы объявили массив Numbers, содержащий переменные ти-
типа int:
int[] Numbers;
Здесь мы не указываем размер массива, так как при объявлении массива память для
него не резервируется.
Глава 7. Массивы и индексаторы 231
В следующей строке мы резервируем память для хранения 10 переменных типа
int. Ссылку на эту память мы записываем в переменную Numbers, завершая созда-
создание массива:
Numbers = new int[10];
Дополнительно мы объявляем переменную i, которая будет применяться для ин-
индексации массива:
int i;
Запись значений в ячейки массива (т. е. инициализация массива) выполняется про-
простым присваиванием в цикле:
for(i = 0; i < 10; i++)
Numbers[i] = i;
После завершения этого процесса программа отображает на консоли значения,
хранящиеся в массиве:
Console.Write("Numbers: [" + Numbers[0]);
for(i =1; i < Numbers.Length; i++)
Console.Write("," + Numbers[i]);
Console.WriteLine("]");
Здесь мы вначале выводим на консоль содержимое первого элемента массива, а за-
затем и всех остальных:
Numbers: [0,1,2,3,4,5,6,7,8,9]
Обратите внимание на то, как мы проверяем условие выхода за пределы массива.
Параметр цикла i сравнивается со значением Numbers . Length.
Что это за значение?
Все массивы в С# являются объектами библиотечного класса System. Array. По-
Поле Length этого класса содержит размер массива.
Обработка одномерного массива строк
В листинге 7.3 мы привели пример программы, отображающей на консоли содержи-
содержимое одномерного массива строк, проинициализированного статически.
Листинг 7.3. Файл ch07\ArrayLoop1\ArrayLoop1 App.cs
using System;
namespace ArrayLoopl
{
class ArrayLooplApp
{
static void Madn(string[] args)
(
svringM Lines -
232 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
"This",
"is" ,
"C#",
"string",
"array"
int i ;
Console.Write(Lines[0]);
for(i=l; i < Lines.Length; i++)
Console.Write(" " + Lines [i])
Console.ReadLine() ;
Отображение элементов массива на консоли выполняется в цикле и не имеет ника-
никаких особенностей:
Console.Write(Lines[0]);
for(i=l; i < Lines.Length; i++)
Console.Write(" " + Lines[i]);
Вначале на консоль выводится самый первый элемент массива с нулевым индек-
индексом, а затем все остальные:
This is C# string array
Параметр цикла изменяется от единицы до размера массива, равного значению
Lines.Length.
Использование оператора foreach
Хотя для циклической обработки массивов можно использовать любые циклические
операторы (и даже цикл, организованный с помощью нерекомендуемого к использо-
использованию оператора goto), удобнее всего применять специализированный оператор
foreach.
В листинге 7.4 мы привели второй вариант программы, описанной в предыдущем
разделе и предназначенной для вывода на консоль содержимого массива, проинициа-
лизированного статически.
Листинг 7.4. Файл ch07\ArrayForeach\ArrayForeachApp.cs
using System;
namespace ArrayForeach
{
class ArrayForeachApp
{
static void Main(string[] args)
Глава 7. Массивы и индексаторы 233
string[] Lines =
{
"This",
"is",
"C#'\
"string",
"array"
foreach (string CurrentString in Lines)
{
Console.Write{CurrentString);
Console.Write(" ");
Console.ReadLine();
Как видите, цикл обработки массива выглядит очень просто:
foreach (string CurrentString in Lines)
{
Console.Write(CurrentString) ;
Console.Write(" ") ;
}
В круглых скобках после оператора foreach мы объявили переменную
CurrentString типа string, которой в процессе обработки массива Lines будет
последовательно присваиваться содержимое каждой его ячейки.
Далее вы сможете использовать переменную CurrentString в теле цикла для про-
просмотра элементов массива. К сожалению, оператор foreach не позволяет изменять со-
содержимое элементов массива, но для просмотра массива (и, как вы узнаете позже, для про-
просмотра содержимого контейнеров объектов других типов) он очень удобен.
Многомерный массив объектов класса String
Для работы с многомерными массивами удобно использовать вложенные операторы
цикла, такие, например, как for.
В листинге 7.5 мы привели исходный текст программы, которая создает двумерный
массив текстовых строк, заполняет его, а затем выводит содержимое массива
на консоль.
Листинг 7.5. Файл chO7\ArrayMultiDim\ArrayMultiDimApp.cs
using System;
namespace ArrayMultiDim
f
class ArrayMultiDimApp
{
static void Main(string[] args)
234 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
string[,] Colors = new string[2,4];
int i, j;
ford = 0; i < 2; i
{
for{j = 0; j < 3; j++)
{
Colors[i,j] = String.Format("Color ({0}, {1})", i,j)
for(i =0; i < 2; i
{
for(j = 0; j < 3; j
{
Console.WriteLine(Colors[i,j])
}
}
Console.ReadLine();
Вначале мы объявляем двумерный массив объектов класса string и сразу резер-
резервируем для него память:
string!,] Colors = new string[2,4];
Таким образом, здесь создается массив из двух строк и четырех столбцов.
Для работы со строками таблицы мы будем использовать переменные i (для пере-
перебора строк таблицы) и j (для перебора столбцов таблицы):
int i,j;
Массив Colors заполняется в двойном вложенном цикле:
for(i =0; i < 2; i
for(j = 0; j < 3; j-
Colors[i,j] = String.Format("Color ({0}, {1})", i,j);
В каждую ячейку массива записывается строка вида Color (х,у), где х и у —
номер строки и столбца таблицы соответственно.
Вывод содержимого таблицы на консоль выполняется также в двойном вложенном
цикле, как это показано ниже:
Глава 7. Массивы и индексаторы 235
for(i = 0; i < 2 ; i++)
{
for(j = 0; j < 3; j++)
{
Console.WriteLine(Colors[i,j]);
Несимметричный массив объектов класса String
В нашей следующей программе (листинг 7.6) мы демонстрируем циклическую обра-
обработку несимметричного массива текстовых строк.
Листинг 7.6. Файл ch07\AssymArrayLoop\AssymArrayLoopApp.cs
using System;
namespace AssymArrayLoop
{
class AssymArrayLoopApp
{
static void Main(string[] args)
{
string[][] Asymm;
int i, j;
Asymm = new string[2] [] ;
Asymm[0] = new string[3];
Asymmfl] = new string [4],-
for(i = 0; i < Asymm.Length; i++)
{
for(j = 0; j < Asymm[i].Length; j++)
{
Asymm[i][j] = String.Format("Asymm ({0}, {1})", i,j);
for(i = 0; i < Asymm.Length; i
{
for(j = 0; j < Asymm[i].Length; j
{
Console.WriteLine(Asymm[i][j])
}
}
Console.ReadLine();
236 А В Фролов, Г. В. Фролов Язык С# Самоучитель
Определяя несимметричный массив Asymm, мы сначала не указываем, сколько
в нем столбцов и строк:
String!][] Asymm;
Далее при резервировании памяти для массива мы задаем только количество строк,
равное двум:
Asymm = new string[2] [].;
Затем в каждой строке задается разное количество столбцов: в первой строке —
3 столбца, а во второй — 4:
Asymm[0] = new string[3];
Asymm[l] = new string[4];
Инициализируя несимметричный массив в двойном вложенном цикле, мы не мо-
можем полагаться на то, что каждая строка содержит одинаковое количество столбцов.
Поэтому предельное значение переменной внутреннего цикла определяется как
Asymm[i].Length:
for(i =0; i < Asymm.Length;
{
for(j = 0; j < Asyiran[i].Length;
{
Asymm[i][j] = String.Format("Asymm ({0}, {1})", i,j
Для первой строки оно равно трем, а для второй — четырем.
Содержимое несимметричного массива распечатывается на консоли следующим
образом:
for(i =0; i < Asymm.Length;
{
for(j = 0; j < Asymm[i].Length;
{
Console.WriteLine(Asymm[i][j]
Здесь границы переменных внешнего и внутреннего циклов указаны соответствен-
соответственно как Asymm. Length и Asymm [ i ] . Length.
Индексаторы
В предыдущей главе мы рассказывали о свойствах объектов (properties), с помощью
которых можно создавать «умные» поля. Доступ к таким полям осуществляется толь-
только с помощью специальных процедур get и set. Напомним, что вы можете получать
и изменять значение свойства с помощью обычного оператора присваивания, однако
вместо простого копирования значения выполняется соответствующая процедура дос-
доступа. Эта процедура может, например, сохранять значение свойства не в поле класса,
а в базе данных или где-то еще, выполняя дополнительную обработку данных.
Глава 7. Массивы и индексаторы 237
В языке С# можно наделить «интеллектом» не только поля, но и массивы. Это де-
делается с помощью конструкции, называемой индексатором (indexer).
Чтобы назначение индексатора было более понятным, рассмотрим практический
пример.
Пусть нам нужно где-то хранить названия телевизионных каналов. При этом каж-
каждому номеру телевизионного канала должно ставиться в соответствие то или иное на-
название. Доступ к названиям каналов должен осуществляться по номеру канала, причем
в том случае, если указан неправильный или несуществующий канал, вместо названия
программа должна получить строку «Канал недоступен».
Безусловно, эту задачу можно решить с помощью обычного одномерного массива
текстовых строк.
При инициализации массива в его ячейки следует записать названия каналов. По-
Получая название канала по его номеру, программа должна проверять этот номер на до-
допустимость. Если указан правильный номер, программа может извлечь название кана-
канала из соответствующей ячейки массива, а если неправильный — вернуть строку «Ка-
«Канал недоступен».
Для упрощения программы нам хотелось бы инкапсулировать алгоритм работы
с названиями каналов в каком-либо классе, например в классе с названием
ChannelNaraes. Снабдив этот класс индексатором, можно организовать доступ к на-
названиям каналов, как будто бы они хранятся в обычном массиве:
ChannelNames ch = new ChannelNamesE);
ch[0] = "Спорт";
ch[l] = "Мир кино";
ch[2] = "Боевик";
ch[3] = "Наше кино";
ch[4] = "MTV";
for(uint i = 0; i < ch.Size; i++)
{
string s = ch[i];
Console.WriteLine(s) ;
}
Как видите, здесь мы создали объект ch класса ChannelNames, передав конст-
конструктору количество каналов, равное пяти. Далее мы инициализируем список, последо-
последовательно записывая названия каналов в объект ch. При этом номер канала указывается
таким же способом, как и индекс элемента массива, — с помощью квадратных скобок.
Получение названия канала по его номеру выполняется тоже с применением квадрат-
квадратных скобок.
Созданный таким способом объект ch ведет себя подобно массиву, однако он
представляет собой нечто большее, чем обычный массив.
Рассмотрим исходный текст программы, в которой определен класс Channel-
Names (листинг 7.7).
238 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Листинг 7.7. Файл chO7\Arraylndexer\ArraylndexerApp.cs
using System;
namespace Arraylndexer
{
class ChannelNames
{
private string [] Channels;
private uint ArraySize;
public ChannelNames(uint Count)
{
Channels = new string[Count];
ArraySize = Count;
public uint Size
{
get
{
return ArraySize;
}
}
public string this[uint index]
{
get
{
if(index >= 0 && index < Channels.Length)
return Channels[index];
else
return "Канал недоступен";
set
if(index >= 0 && index < Channels.Length)
Channels[index] = value;
class ArraylndexerApp
{
static void Main(string[] args)
{
ChannelNames ch = new ChannelNamesE);
ch[0] = "Спорт";
ch[l] = "Мир кино";
ch[2] = "Боевик";
ch[3] = "Наше кино";
ch[4] = "MTV ;
Глава 7. Массивы и индексаторы 239
for(uint i = 0; i < ch.Size; i++)
С
string s = ch[i];
Console.WriteLine(s);
Console.ReadLine();
В классе ChannelNames мы объявили ссылку на массив текстовых строк
Channels. Соответствующее поле имеет модификатор доступа private, поэтому
доступ к нему возможен только для методов класса ChannelNames:
private string[] Channels;
Кроме того, в классе объявлено поле Size, хранящее размер массива названий ка-
каналов:
private uint ArraySize;
К этому полю тоже имеют доступ только члены класса ChannelNames.
Конструктор класса ChannelNames создает массив заданного размера и сохраня-
сохраняет этот размер в поле ArraySize:
public ChannelNames(uint Count)
{
Channels = new string[Count];
ArraySize = Count;
}
Чтобы программа, внешняя по отношению к классу ChannelNames, могла опреде-
определять количество элементов в массиве Channels, мы объявили в классе свойство Size:
public uint Size
{
get
{
return ArraySize;
В этом свойстве предусмотрен только один метод доступа get, поэтому программа
не сможет изменить содержимое поля ArraySize после создания объекта класса
ChannelNames.
Объявление индексатора
Теперь мы переходим к самому интересному — к объявлению индексатора:
public string this[uint index]
{
get
240 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
if(index >= 0 && index < Channels.Length)
return Channels[index];
else
return "Канал недоступен";
set
if(index >= 0 && index < Channels.Length)
Channels[index] = value;
Как видите, объявление индексатора очень похоже на объявление свойства. В нем
тоже могут быть процедуры доступа get и set, причем допускается объявлять либо
обе эти процедуры, либо только какую-то одну из них. Процедуры доступа полностью
управляют процессом записи данных в «умный» массив, а также извлечением данных
из этого массива.
При объявлении индексатора, как и при объявлении свойства, мы должны указать
модификатор доступа и тип. В нашем случае мы создали общедоступный индексатор,
укачав модификатор доступа public. Так как названия каналов представляют собой
текстовые строки, тип индексатора задан как string.
Класс, содержащий индексатор, выступает в роли массива. Ссылка на такой массив
выполняется либо с использованием имени объекта класса, либо через имя класса (ес-
(если индексатор объявлен как статический). Поэтому для индексатора не требуется ука-
указывать какое-то особенное имя. В качестве имени индексатора выступает ключевое
слово this, обозначающее ссылку на объект данного класса.
После ключевого слова this в объявлении индексатора следует параметр, заклю-
заключенный в квадратные скобки. Этот параметр играет роль индекса и в нашем случае
имеет целочисленный тип u int. При необходимости вы можете использовать для ин-
индексации параметры любого типа, например текстовые строки.
В процедурах доступа get и set параметр индексатора применяется для получе-
получения доступа к элементу массива. В нашем случае названия каналов хранятся в простом
одномерном массиве текстовых строк Channels. Однако при необходимости проце-
процедура доступа могла бы извлекать эти названия из базы данных или получать через Ин-
Интернет, пользуясь для идентификации канала значением параметра index.
Процедура доступа set индексатора, так же как и аналогичная процедура доступа
свойства, пользуется ключевым словом value для установки нового значения масси-
массива. При этом нужный элемент массива идентифицируется при помощи параметра ин-
индексатора.
Приведенная выше реализация процедуры доступа get выполняет необходимый
нам алгоритм определения названия канала по его номеру. Если указан неправильный
номер, она возвращает строку «Канал недоступен».
Что же касается процедуры доступа set, то она тоже выполняет некоторые про-
проверки. В частности, при указании недопустимого номера канала она игнорирует по-
попытку записи в массив нового значения.
Глава 7. Массивы и индексаторы 241
Индексаторы многомерных массивов
В предыдущем разделе мы показали, как пользоваться индексаторами для получения
«интеллектуального» доступа к одномерному массиву строк. При необходимости
вы также можете создавать индексаторы и для многомерных массивов.
Теперь немного усовершенствуем программу, исходный текст которой был приве-
приведен в листинге 7.7. Мы потребуем, чтобы программа хранила для каждого номер кана-
канала не одно, а два названия. Первое название пусть будет соответствовать, например,
отечественному каналу, а второе — зарубежному.
Для реализации этой логики нам потребуется двумерный массив текстовых строк.
В новом варианте программы (листинг 7.8) мы объявили такой массив, а также индек-
индексатор для организации к нему «интеллектуального» доступа.
Листинг 7.8. Файл chO7\ArraylndexerMulti\ArraylndexerMultiApp.cs
using System;
namespace ArraylndexerMulti
{
class ChannelNames
{
private string[,] Channels;
private uint ArraySize;
public ChannelNames(uint Count)
{
Channels = new string[Count, 2];
ArraySize = Count;
public uint Size
get
return ArraySize;
public string this[uint index, uint language]
get
if(index >= 0 && index < Channels.Length)
return Channels[index, language];
else
return "Канал недоступен";
242 А В. Фролов, Г. В Фролов. Язык С#. Самоучитель
set
if(index >= 0 && index < Channels.Length)
Channels[index, language] = value;
class ArraylndexerMultiApp
{
static void Main(string[] args)
{
ChannelNames ch = new ChannelNamesE);
ch[0, 0] = "Спорт";
ch[l, 0] = "Мир кино";
ch[2, 0] = "Боевик";
ch[3, 0] = "Наше кино";
ch[4, 0] = "MTV";
ch[0, 1] = "Eurosport";
ch[l, 1] = "Discovery";
ch[2, 1] = "TV 5";
ch[3, 1] = "Fashion";
ch[4, 1] = "Euronews";
for(uint i = 0; i < ch.Size; i++)
{
string s = String.Format("{0} ({1})", ch[i, 0], ch[i, 1])
Console.WriteLine(s);
}
Console.ReadLine();
Ссылка на двумерный массив объявлена следующим образом:
private string!,] Channels;
Она инициализируется в конструкторе, задающем количество строк и столбцов
двумерного массива:
public ChannelNames(uint Count)
Channels = new string[Count, 2];
ArraySize = Count;
Количество строк определяется параметром конструктора, а количество столбцов
фиксировано и равно двум.
Для индексатора нам теперь потребуется два параметра, первый из которых будет за-
задавать номер канала, а второй — номер языка @ для русского языка, и 1 для английского):
Глава 7. Массивы и индексаторы 243
public string this[uint index, uint language]
{
get
{
if(index >= 0 && index < Channels.Length)
return Channels[index, language];
else
return "Канал недоступен";
set
if(index >= 0 && index < Channels.Length)
Channels[index, language] = value;
Здесь процедура доступа get возвращает название канала с заданным номером
и на заданном языке или строку «Канал недоступен» в случае ошибки. Процедура дос-
доступа set изменяет название заданного канала с учетом номера языка.
В теле метода Main, получающего управление сразу после запуска программы, про-
программа создает объект ch класса ChannelNames, передавая конструктору значение 5:
ChannelNames ch = new ChannelNamesE);
В результате этот объект сможет хранить информацию о пяти каналах на одном
из двух языков.
Далее программа инициализирует список каналов, обращаясь неявным образом
к индексатору класса ChannelNames:
ch[0, 0] = "Спорт";
ch[l, 0] = "Мир кино";
ch[2, 0] = "Боевик";
ch[3, 0] = "Наше кино";
ch[4, 0] ='"MTV";
ch[0, 1] = "Eurosport";
ch[l, 1] = "Discovery";
ch[2, 1] = "TV 5";
ch[3, 1] = "Fashion";
ch[4, 1] = "Euronews";
Как видите, вначале инициализируются русские каналы, затем — зарубежные.
После инициализации программа отображает на экране полный список каналов,
также неявно обращаясь к индексатору:
for(uint i = 0; i < ch.Size; i++)
{
string s = String. Format (" {0} ({1})", ch[i, 0], ch[i, lib-
Console .WriteLine(s) ;
244 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Вот что появится на консоли после запуска нашей программы:
Спорт (Eurosport)
Мир кино (Discovery)
Боевик (TV 5)
Наше кино (Fashion)
MTV (Euronews)
Дополнительные операции
с массивами в С#
Рассказывая о таких типах данных, как int, char и т. п., мы обращали ваше внима-
внимание на то, что эти типы данных созданы на базе соответствующих классов. Что же ка-
касается массивов, то они тоже созданы на базе класса System.Array. Функциональ-
Функциональность, инкапсулированная в этом классе, наделяет массивы С# дополнительными воз-
возможностями, недоступными в массивах, реализованных средствами других языков
программирования. Массивы С# в отличие от массивов C++, можно, например, копи-
копировать и сортировать.
В этом разделе мы рассмотрим наиболее полезные методы и свойства класса
System. Array, которые вы можете применять при работе с массивами.
Определение размера массива
Ранее в наших программах мы уже определяли количество элементов, имеющихся
в массиве, обращаясь для этого к свойству Length.
Существуют и другие средства, позволяющие получить информацию о размере
массива.
Например, с помощью свойства Rank можно узнать размерность (ранг) масси-
массива. Для одномерных массивов значение ранга равно единице, для двумерных —
двум и т. д.
Методы GetLowerBound и GetUpperBound позволяют узнать соответственно
минимальное и максимальное значение индекса элементов, хранящихся в массиве.
В качестве параметров этим методам нужно передать значение ранга массива, умень-
уменьшенное на единицу.
Для изменения значения, хранящегося в ячейке массива, можно использовать не
только квадратные скобки, но и метод SetValue. При работе с одномерными масси-
массивами в качестве первого параметра этому методу нужно передать изменяемое значе-
значение, а в качестве второго — индекс соответствующей ячейки массива. На наш взгляд,
однако, использование скобок представляет собой более наглядный способ работы
с массивами.
В листинге 7.9 мы привели пример программы, демонстрирующей использование
перечисленных выше свойств и методов.
Глава 7. Массивы и индексаторы 245
Листинг 7.9. Файл ch07\ArrayMore\ArrayMoreApp.cs
using System;
namespace ArrayMore
(
class ArrayMoreApp
{
static void Main{string[] args)
{
int[] arrayOfNumbers = {1, 3, 5, 7, 9, 11};
Console.WriteLine("Размер массива {0}",
arrayOfNumbers.Length);
foreach(int i in arrayOfNumbers)
{
Console.Write("{0} ", i);
}
Console.WriteLine("ХпПосле изменения элемента массива:");
arrayOfNumbers.SetValueA4, 5);
foreach(int i in arrayOfNumbers)
{
Console.Write("{0} ", i);
Console.WriteLine("\пРанг массива: {0}",
arrayOfNumbers.Rank);
Console.WriteLine("Нижняя граница массива: {0}",
arrayOfNumbers.GetLowerBound(O));
Console.WriteLine("Верхняя граница массива: {0}",
arrayOfNumbers.GetUpperBound@) ) ;
Console.ReadLine();
Получив управление, программа создает массив целых чисел arrayOfNumbers,
инициализируя его статически:
int[] arrayOfNumbers = {1, 3, 5, 7, 9, 11};
Далее программа отображает на консоли количество элементов (чисел), хранящих-
хранящихся в массиве, а также выводит все эти элементы на консоль:
Console.WriteLine{"Размер массива {0}", arrayOfNumbers.Length);
foreachfint i in arrayOfNumbers)
{
Console.Write("{0} ", i);
_}
246 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Количество элементов, хранящихся в массиве, извлекается из свойства Length,
определенного в классе System .Array.
Далее мы записываем в ячейку массива с индексом 5 значение, равное 14, вызывая
для этого метод SetValue:
arrayOfNumbers.SetValue{14, 5);
Аналогичный результат можно получить и с помощью квадратных скобок:
arrayOfNumbers[5] = 14;
Изменив значение элемента массива, программа вновь выводит его содержимое
на консоль.
На следующем этапе своей работы программа выводит на консоль размерность
массива, обращаясь для ее определения к свойству Rank:
Console.WriteLine("\пРанг массива: {0}", arrayOfNumbers.Rank);
И наконец, перед тем как завершить свое выполнение, программа выводит на кон-
консоль минимальное и максимальное значение индекса для элементов, хранящихся
в массиве:
Console.WriteLine("Нижняя граница массива: {0}",
arrayOfNumbers.GetLowerBound@));
Console.WriteLine("Верхняя граница массива: {0}",
arrayOfNumbers.GetUpperBound(O));
Обратите внимание, что у нас одномерный размер, ранг которого равен единице.
Тем не менее мы передаем методам GetLowerBound и GetUpperBound нулевое
значение, т. е. значение ранга, уменьшенное на единицу.
Сортировка и реверсирование массивов
С помощью статических методов Array. Sort и Array. Reverse можно соответст-
соответственно отсортировать элементы массива и переставить их в обратном порядке. Исполь-
Использование этих методов демонстрируется в программе, исходный текст которой приве-
приведен в листинге 7.10.
Листинг 7.10, Файл ch07\SortReverse\SortReverseApp.cs
using System;
namespace SortReverse
{
class SortReverseApp
{
static void Main(string[] args)
{
int[] arrayOf Numbers = {21, 3, 51, 7, 29, lib-
Console.Write ("Исходный массив: ");
foreach(int i in arrayOfNumbers)
Console.Write("{0} ", i);
Глава 7. Массивы и индексаторы 247
Array.Sort(arrayOfNumbers);
Console.Write("\пСортированный массив: ");
foreach(int i in arrayOfNumbers)
Console.Write("{0} ", i);
Array.Reverse(arrayOfNumbers);
Console.Write("ХпРеверсированный массив: ");
foreach(int i in arrayOfNumbers)
Console.Write("{0} ", i);
Console.ReadLine() ;
В ней объявляется массив целых чисел с применением статической инициализации:
int[] arrayOfNumbers = {21, 3, 51, 7, 29, 11};
Далее наша программа сортирует массив, вызывая метод Array. Sort:
Array.Sort(arrayOfNumbers);
Содержимое исходного и отсортированного массива выводится на консоль при по-
помощи простейшего цикла:
foreach(int i in arrayOfNumbers)
Console.Write("{0} ", i) ;
Этот же цикл применяется и для вывода содержимого массива, элементы которого
были переставлены в обратном порядке при помощи метода Array. Reverse:
Array.Reverse(arrayOfNumbers);
Вот что наша программа выводит на консоль:
Исходный массив: 213517 29 11
Сортированный массив: 3 7 11 21 29 51
Реверсированный массив: 51 29 21 11 7 3
Заметим, что оба описанных выше метода позволяют работать с массивами любых
объектов, а не только чисел. При необходимости вы можете отсортировать методом
Array. Sort, например, массив текстовых строк.
Поиск в массиве
С помощью статического метода Array .BinarySearch можно организовать поиск
элементов в одномерном массиве. В качестве первого параметра этому методу нужно
передать ссылку на массив, а в качестве второго — искомый элемент. При успехе ме-
метод возвратит индекс найденного элемента, а в том случае, если элемент не найден - от-
отрицательное значение.
248 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Исходный текст программы, демонстрирующей применение метода Array.Bi
narySearch для поиска текстовой строки в массиве, приведен в листинге 7.11.
Листинг 7.11. Файл chO7\BinarySearch\BinarySearchApp.cs
using System;
namespace BinarySearch
{
class BinarySearchApp
{
static void Main{string[] args)
{
string[] arrayOfNumbers =
{
"Каждый",
"охотник",
"желает",
"знать",
11 где",
"сидит",
"фазан"
};
string searchString = "фазан";
int index=Array.BinarySearch(arrayOfNumbers, searchString);
if (index < 0 )
Console.WriteLine("Строка \"{0}\" не найдена.",
searchString);
else
Console.WriteLine("Индекс строки \"{0}\" равен {1}.",
searchString, index );
Console.ReadLine();
В программе объявлен и проинициализирован статически массив текстовых строк
arrayOfNumbers.
Метод Main ищет в массиве слово «фазан», вызывая для этого статический метод
Array. BinarySearch. Данный метод получает в качестве первого параметра ссыл-
ссылку на массив, а в качестве второго — ссылку на искомую строку.
Если искомая строка не найдена, метод Array .BinarySearch возвращает отри-
отрицательное значение. В случае успеха возвращается индекс найденной строки.
Наша программа отображает на консоли искомую строку и ее индекс.
Глава 7. Массивы и индексаторы 249
Глава 8. Интерфейсы
Как мы уже говорили в гл. 3, класс С# может наследовать свойства только одного ба-
базового класса. Таким образом, в отличие от других языков ООП (например, C++)
в языке С# каждый производный класс может иметь только один базовый класс.
На первый взгляд такое ограничение может показаться довольно существенным, одна-
однако во многих случаях оно успешно обходится при помощи мехатпмаинтерфейсов.
В то время как классы представляют собой механизм для представления неких
сущностей (таких, например, как геометрические фигуры, телевизоры и т. п.), интер-
интерфейсы применяются для описания неких действий над этими сущностями.
Понятие интерфейса очевидно на обычном житейском уровне. Представьте себе,
например, плеер для проигрывания компакт-дисков. Существует бесчисленное множе-
множество различных моделей таких плееров, отличающихся друг от друга формой корпуса,
цветом, размером и другими атрибутами. Тем не менее все они имеют практически
одинаковый набор кнопок, с помощью которых можно запускать или останавливать
проигрывание компакт-диска, переходить с одной дорожки на другую, а также извле-
извлекать компакт-диск из корпуса плеера.
Такая унификация «пользовательского интерфейса» плеера позволяет любому
из вас быстро освоить новый плеер, не разбираясь в деталях его внутреннего устрой-
устройства. Аналогично вы легко сумеете воспользоваться банкоматом любой модели для
получения денег с кредитной карточки, так как все банкоматы имеют один и тот же
«интерфейс».
Возвращаясь к языку С#, заметим, что использование рассмотренных ранее
свойств, индексаторов, событий (о которых мы расскажем позже), а также интерфей-
интерфейсов позволяет создавать объекты, которые можно использовать, не вникая в детали их
реализации. При этом объект (класс) может реализовать набор интерфейсов, каждый
из которых отвечает за выполнение над объектом каких-либо действий.
Интерфейсы более всего похожи на виртуальные методы абстрактного класса, ко-
которые должны быть определены в базовом классе. Хорошей новостью является то,
что каждый класс С# может реализовать произвольное количество интерфейсов.
Именно это обстоятельство делает несущественным ограничение С#, касающееся не-
невозможности множественного наследования классов.
Применение интерфейсов
Интерфейсы объявляются с помощью ключевого слова interface аналогично клас-
классам. Внутри объявления интерфейса необходимо перечислить методы, из которых об-
образуется интерфейс. Кроме методов, внутри интерфейсов можно также объявлять
свойства, индексаторы и события.
Класс, реализующий интерфейс, должен содержать в себе тело методов, объявлен-
объявленных в рамках всех реализуемых им интерфейсов.
Чтобы тго было понятнее, приведем конкретный пример.
250
Объявление интерфейса
Пусть нам нужно создать класс Point, отражающий поведение точки на плоскости.
Этот объект должен хранить текущие координаты точки, делая их доступными при
помощи свойств X и Y. Кроме того, класс Point должен реализовывать два интерфей-
интерфейса IPrint и IMail, первый из которых позволяет распечатывать точку на принтере
и выполнять предварительный просмотр результатов печати, а второй — отправлять
точку по электронной почте по заданному адресу.
Ниже мы привели объявление интерфейсов IPrint и IMail:
interface IPrint
{
void Print();
void PrintPreview();
}
interface IMail
{
void SendMail(string mailAddress);
}
Как видите, в объявлении интерфейсов мы указываем только прототипы методов,
не включая тело этих методов. За конкретную реализацию методов интерфейса отве-
отвечает класс, который реализует этот интерфейс.
Кроме того, в прототипах методов не требуется (и не допускается) указывать мо-
модификаторы доступа, такие, как public или private. Все методы интерфейса явля-
являются общедоступными по умолчанию.
Обратите также внимание на названия интерфейсов. В С# принято, что название ин-
интерфейса начинается с прописной буквы I. И хотя это не строгое правило, мы рекоменду-
рекомендуем его придерживаться, так как при этом исходный текст программы будет понятнее.
Реализация интерфейса
При объявлении интерфейсов мы устанавливаем соглашение (контракт), которому
должны удовлетворять методы, свойства, индексаторы и события, объявленные в рам-
рамках интерфейса. Что же касается конкретной реализации интерфейса, то она, как мы
уже говорили, возлагается на класс, реализующий интерфейс.
Ниже мы привели в сокращенном виде исходный текст класса Point, реализую-
реализующего интерфейсы IPrint и IMail:
class Point : IPrint, IMail
{
protected int xPos;
protected int yPos;
public Point(int x, int y)
{
xPos = x;
yPos = y;
Глава 8. Интерфейсы 251
void IPrint.Print()
Console.WriteLine("Печать точки ({0}, {1})", this.X, this.Y);
}
void IPrint.PrintPreview()
Console.WriteLine(
"Просмотр перед печатью точки ({0}, {1})", this.X, this.Y);
void IMail.SendMail(string mailAddress)
Console.WriteLine(
"Отправка точки ({0}, {1}) по адресу {2}",
this.X, this.Y, mailAddress);
Тот факт, что класс реализует те или иные интерфейсы, отражается в ключевом
слове class. Названия реализуемых интерфейсов перечисляются после этого слова
через запятую:
class Point : IPrint, IMail
Если бы наш класс Point был унаследован от базового класса с именем, напри-
например, Shape и дополнительно реализовывал интерфейсы IPrint и IMail, это можно
было бы записать следующим образом:
class Point : Shape, IPrint, IMail
Однако простого перечисления реализуемых интерфейсов недостаточно. В классе
необходимо расположить тело методов этих интерфейсов. В нашем случае класс
Point должен содержать тело всех методов интерфейсов IPrint и IMail.
Обратите внимание, как мы определили метод Print интерфейса IPrint:
void IPrint.Print ()
Console.WriteLine("Печать точки ({0}, {1})", this.X, this.Y);
Имя метода указано как IPrint. Print. Здесь мы снабдили имя метода префик-
префиксом в виде имени интерфейса. Хотя такой префикс необязателен, все же лучше его
указывать. Это позволит избежать неоднозначности, если класс реализует интерфейсы
с одинаковыми названиями методов. Например, если бы в интерфейсах IPrint
и IMail был определен метод, при реализации методов мы могли бы их различать
по полным именам IPrint. Check и IMai I . Check соответственно.
252
А В Фролов, Г. В Фролов Язык С#. Самоучитель
Вызов методов интерфейса
После того как мы объявили интерфейс и реализовали его в классе, интерфейс готов
к употреблению.
Как же им пользоваться?
Достаточно просто. Вначале нужно создать объект класса, реализующего наш ин-
интерфейс:
Point pt;
pt = new Point{10, 20);
Далее нам нужно создать ссылку на интерфейс. Это делается следующим образом:
IPrint ptPrinter = (IPrint)pt;
Здесь мы объявили переменную ptPrinter типа IPrint и записали в нее ссыл-
ссылку, приведя ее тип явным образом к типу IPrint. С помощью такой ссылки можно
адресоваться к методам интерфейса IPrint, но нельзя получить доступ к полям и ме-
методам класса Point:
ptPrinter.PrintPreview{);
ptPrinter.Print();
Таким образом, обращаясь к объекту через интерфейс, программа может не иметь
никакой информации относительно того, какие свойства, поля и методы определены
в этом классе. Этим достигается независимость программы, работающей с объектом,
от внутренних деталей реализации объекта.
Пример программы
В листинге 8.1 мы привели полный исходный текст программы, демонстрирующей
объявление, реализацию и использование описанных выше интерфейсов.
Листинг 8.1. Файл chO8\RectPrintable\RectPrintableApp.cs
using System;
namespace RectPrintable
{
interface IPrint
{
void Print();
void PrintPreview();
}
interface IMail
{
void SendMail(string mailAddress);
Глава 8. Интерфейсы 253
class Point : IPrint, IMail
{
protected int xPos;
protected int yPos;
public Point(int x, int y)
{
xPos = x;
yPos = y;
public int X
{
set
{
xPos = value;
}
get
{
return xPos;
}
}
public int Y
{
set
{
yPos = value;
}
get
{
return yPos;
}
}
void IPrint.Print()
{
Console.WriteLine("Печать точки ({0), {1})", this.X,
this.Y);
}
void IPrint.PrintPreview()
{
Console.WriteLine(
"Просмотр перед печатью точки ({0}, {1})",
this.X, this.Y);
}
void IMail.SendMail(string mailAddress)
{
254 А В Фролов, Г В Фролов. Язык С#. Самоучитель
Console.WriteLine(
"Отправка точки ({0), {1}) по адресу {2}
this.X, this.Y, mailAddress);
class RectPrintableApp
{
static void Main(string[] args)
{
Point pt;
pt = new PointA0, 20);
IPrint ptPrinter = (IPrint)pt;
ptPrinter.PrintPreview();
ptPrinter.Print{);
IMail ptMailer = (IMail)pt;
ptMailer.SendMail("alexandre@frolov.pp.ru")
Console.ReadLine();
В классе Point определен конструктор, а также свойства X и Y, предназначенные
для работы с координатами точки. Этот класс реализует интерфейсы IPrint
и IMail, для чего в нем расположено объявление методов IPrint .Print,
IPrint. PrintPreview и IMail. SendMail. Эти методы стимулируют выполне-
выполнение операций, отображая соответствующие текстовые сообщения на консоли.
Проверка реализации интерфейса
В предыдущем разделе мы описали один из возможных способов использования интер-
интерфейса, основанный на явном приведении типа ссылки на объект класса к типу интерфейса:
Point pt;
pt = new PointA0, 20);
IPrint ptPrinter = (IPrint)pt;
ptPrinter.PrintPreview();
ptPrinter.Print();
Здесь мы создали объект pt класса Point, а также переменную ptPrinter,
предназначенную для хранения ссылки на интерфейс IPrint. Затем мы привели тип
переменной pt к типу IPrint с помощью скобок.
На следующем этапе аналогичная операция была проведена и для интерфейса IMail:
IMail ptMailer = (IMail)pt;
ptMailer.SendMail("alexandre@frolov.pp.ru");
Глава 8 Интерфейсы 255
Так как класс Point реализует оба интерфейса IPrint и IMail, в процессе пре-
преобразования типов у нас не возникает никаких проблем. Однако если бы мы попыта-
попытались привести ссылку на объект класса к типу ссылки на интерфейс, реализация кото-
которого отсутствует в этом классе, такая операция вызвала бы исключение на этапе вы-
выполнения программы.
Изучим эту ситуацию на примере программы, исходный текст которой приведен
в листинге 8.2.
Листинг 8.2. Файл ch08\ITelevision\ITelevisionApp.cs
using System;
namespace ITelevision
{
interface IChannel
{
void switchTo(uint channelNumber);
uint current();
}
interface IVolume
{
void setLevel(uint volumeLevel);
uint current();
}
interface ITunning
{
void next();
void prev{);
uint current();
}
class TvSet : IChannel, IVolume
{
private uint channel;
private uint volume;
public TvSet()
{
channel = 1;
volume = 10;
}
void IChannel.switchTo(uint channelNumber)
{
channel = channelNumber;
256 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
uint IChannel.current()
return channel;
void IVolume.setLevel(uint volumeLevel)
volume = volumeLevel;
uint IVolume.current()
return volume;
class RadioSet : ITunning, IVolume
private uint channel;
private uint volume;
public RadioSet()
channel = 1;
volume = 10;
vo id ITunn ing.next{)
channel++;
void ITunning.prev()
if(channel > 0)
channel—;
uint ITunning.current()
return channel;
void IVolume.setLevel(uint volumeLevel)
volume = volumeLevel;
uint IVolume.current()
return volume;
)
Глава 8. Интерфейсы 257
Q Язык С.» Самоучитель
class ITelevisionApp
{
static void Main(string[] args)
{
TvSet tv;
tv = new TvSet( ) ;
IChannel channelTv = {IChannel)tv;
IVolume volTv = (IVolume)tv;
channelTv.switchToA0);
volTv.setLevelE0);
RadioSet radio;
radio = new RadioSet();
ITunning tuneRadio = (ITunning)radio;
IVolume volRadio = (IVolume)radio;
tuneRadio.next();
volRadio.setLevelC0);
Console.WriteLine("Телевизор: канал {0}, громкость {1}",
channelTv.current(), volTv.current() ) ;
Console.WriteLine("Радио: канал {0}, громкость {1}",
tuneRadio.current(), volRadio.current());
// ITunning tuneTv = (ITunning)tv;
if(tv is ITunning)
{
ITunning tuneTv = (ITunning)tv;
}
else
Console.WriteLine("Интерфейс ITunning не реализован");
ITunning tuneTvl = tv as ITunning;
if(tuneTvl == null)
Console.WriteLine("Интерфейс ITunning не реализован");
Console.ReadLine();
Здесь мы объявили интерфейсы IChannel, IVolume и ITunning:
interface IChannel
{
void switchTo(uint channelNumber);
uint current();
258 А. В. Фролов, Г. В Фролов. Язык С#. Самоучитель
interface IVolume
{
void setLevel(uint volumeLevel);
uint current();
interface ITunning
{
void next();
void prev();
uint current();
}
Интерфейс IChannel предназначен для использования в виртуальном телевизоре
или любом другом подобном устройстве. Он позволяет переключить устройство на за-
заданный номер канала, а также получить номер текущего установленного канала.
С помощью интерфейса IVolume программа может установить или определить
текущий уровень громкости устройства.
Интерфейс ITunning тоже позволяет переключать каналы и определять номер
текущего канала. В отличие от интерфейса IChannel, однако, с помощью данного
интерфейса невозможно переключиться на канал с заданным номером. Методы next
и prev интерфейса ITunning дают возможность последовательно переключаться
на следующий и предыдущий канал.
Как видите, данные интерфейсы можно с успехом использовать для управления
любыми устройствами, в которых есть возможность установки каналов и громкости.
В нашей программе мы создали классы для моделирования двух таких устройств —
телевизора и радиоприемника:
class TvSet : IChannel, IVolume
{
private uint channel;
private uint volume;
public TvSetO
{
channel = 1;
volume = 10;
class RadioSet : ITunning, IVolume
{
private uint channel;
private uint volume;
public RadioSet()
{
channel = 1;
volume = 10;
Глава 8. Интерфейсы 259
Класс TvSet моделирует телевизор (фактически только возможность переключе-
переключения каналов и регулировки громкости), а класс RadioSet — радиоприемник.
Оба эти класса реализуют интерфейс I Volume, предназначенный для регулировки
громкости. Что же касается переключения каналов, то телевизор можно переключать
на любой канал с заданным номером посредством интерфейса IChannel, а прием-
приемник — только перестраивать на соседние каналы с помощью интерфейса ITunning.
Ниже мы привели исходный текст реализации интерфейса IChannel, предусмот-
предусмотренной в классе TvSet:
void IChannel.switchTo(uint channelNumber)
{
channel = channelNumber;
}
uint IChannel.current()
С
return channel;
}
Метод switchTo записывает новый номер канала в поле класса channel, а метод
current возвращает значение, хранящееся в этом поле.
Реализация интерфейса ITunning, имеющаяся в классе RadioSet, не намного
сложнее:
void ITunning.next()
{
channel++;
}
void ITunning.prev()
{
if(channel > 0)
channel--;
}
uint ITunning.current()
{
return channel;
}
Метод next увеличивает номер канала на единицу при каждом обращении, а ме-
метод prev уменьшает этот номер, при условии, что он больше нуля. С помощью метода
current программа может определить текущий номер канала.
Что же касается интерфейса IVolume, то он реализован одинаково и в классе
TvSet и в классе RadioSet:
void IVolume.setLevel(uint volumeLevel)
{
volume = volumeLevel;
260 А В. Фролов. Г. В. Фролов. Язык С#. Самоучитель
uint IVolume.current()
{
return volume;
}
Метод setLevel позволяет установить нужный уровень громкости, а метод
current — определить текущий уровень громкости.
Итак, теперь, когда у нас есть 3 интерфейса и 2 класса, реализующих эти интер-
интерфейсы, мы можем испытать все это в работе.
Вначале метод Main нашей программы создает объект-телевизор tv класса
TvSet, а также два интерфейса channelTv и volTv, первый из которых позволяет
переключать каналы, а второй — регулировать громкость:
TvSet tv;
tv = new TvSet();
IChannel channelTv = (IChannel)tv;
IVolume volTv = (I/Volume) tv;
Для использования интерфейсов мы вызываем соответствующие методы:
channelTv.switchTo(lO);
volTv.setLevelE0);
Здесь мы переключаем телевизор на 10-й канал и устанавливаем уровень громко-
громкости, равный 50 %.
Аналогичным образом создается объект-радиоприемник и ссылки на интерфейсы
для управления радиоприемником:
RadioSet radio,-
radio = new RadioSet();
ITunning tuneRadio = (ITunning)radio;
IVolume volRadio = (IVolume)radio;
Сразу после создания радиоприемник будет принимать 1-й канал (это задается
соответствующим конструктором). Наша программа переключает его на следующий,
2-й канал, а затем устанавливает уровень громкости, равный 30 %:
tuneRadio.next();
volRadio.setLevelC0);
После выполнения всех этих действий наша программа выводит на консоль теку-
текущие номера каналов, а также уровни громкости телевизора и приемника:
Console.WriteLine("Телевизор: канал {0}, громкость {1}",
channelTv.current(}, volTv.current());
Console.WriteLine("Радио: канал {0}, громкость {1}",
tuneRadio.current(), volRadio.current());
Глава 8. Интерфейсы 261
Для получения этих значений вызываются методы current соответствующих ин-
интерфейсов.
А что будет, если мы попытаемся использовать интерфейс ITunning для управ-
управления телевизором?
Такая попытка может выглядеть следующим образом:
// Ошибка!
ITunning tuneTv = (ITunning)tv;
Здесь мы попытались преобразовать ссылку tv на объект класса TVSet в тип ин-
интерфейса ITunning, чтобы в дальнейшем использовать методы этого интерфейса
next и prev для последовательного переключения каналов.
Компилятор не найдет в этой строке никаких ошибок, так как интерфейс
ITunning был определен в нашей программе. Однако попытка исполнения приве-
приведенной выше строки вызовет исключение System. InvalidCastException. Это
исключение появится потому, что интерфейс ITunning не был реализован в классе
TvSet. Таким образом, наш телевизор «не умеет» переключать каналы с помощью
этого интерфейса.
Чтобы не допустить аварийного завершения программы, необходимо либо обрабо-
обработать исключение (о том, как это сделать, мы расскажем позже), либо использовать
в процессе преобразования типов операторы is или as.
Оператор is применяется в нашей программе вместе с условным оператором if
следующим образом:
if(tv is ITunning)
{
ITunning tuneTv = (ITunning)tv;
}
else
Console.WriteLine("Интерфейс ITunning не реализован");
Здесь мы вначале проверяем допустимость преобразования и только затем выпол-
выполняем это преобразование. Если же преобразование недопустимо, программа выводит
предупреждающее сообщение на консоль.
Другой способ основан на применении оператора as:
ITunning tuneTvl = tv as ITunning;
if(tuneTvl == null)
Console.WriteLine("Интерфейс ITunning не реализован");
В том случае, когда преобразование недопустимо, этот оператор вернет значение
null. Программе остается только проверить результат выполнения операции as,
предприняв соответствующие действия.
Заметим, что второй способ преобразования эффективнее, так как он исключает
необходимость двойной проверки допустимости преобразования, неизбежной в случае
использования оператора is.
262 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Комбинированные интерфейсы
Как мы уже говорили, невозможность множественного наследования классов в С#
с успехом компенсируется наличием мощного механизма интерфейсов. Производный
класс может быть унаследован только от одного базового класса, однако при этом он
может реализовать произвольное количество интерфейсов.
Интерфейсы группируют описания наборов методов, имеющих схожее назначение
и функциональность. При необходимости можно комбинировать интерфейсы, созда-
создавая новые интерфейсы на базе уже имеющихся.
В листинге 8.3 мы привели исходный текст программы, в которой применяются
комбинированные интерфейсы.
Листинг 8.3. Файл ch08\Combine\CombineApp.cs
using System;
namespace Combine
{
interface IChannel
{
void switchTo(uint channelNumber);
uint current();
}
interface IVolume
{
void setLevel{uint volumeLevel),-
uint current();
}
interface ITunning
{
void next();
void prev();
uint current{);
}
interface ITvControl : IChannel, IVolume
{
void on();
void off();
}
interface IRadioControl : ITunning, IVolume
class TvSet : ITvControl
{
private uint channel;
private uint volume;
private bool power;
Глава 8. Интерфейсы 263
public TvSet()
{
channel = 1;
volume = 10;
power = false;
}
void ITvControl.on()
{
power = true;
}
void ITvControl.off()
{
power = false;
}
void IChannel.switchTo(uint channelNumber)
{
channel = channelNumber;
}
uint IChannel.current()
{
return channel;
}
void IVolume.setLevel(uint volumeLevel)
{
volume = volumeLevel;
}
uint IVolume.current()
{
return volume;
}
}
class RadioSet : IRadioControl
{
private uint channel;
private uint volume;
public RadioSet()
{
channel = 1;
volume = 10;
}
void ITunning.next()
{
channel 4+■
264 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
void ITunning.prev()
{
if(channel > 0)
channel--;
uint ITunning.current()
{
return channel;
}
void IVolume.setLevel(uint volumeLevel)
{
volume = volumeLevel;
)
uint IVolume.current()
{
return volume;
}
}
class CombineApp
{
static void Main(string[] args)
{
TvSet tv;
tv = new TvSet();
ITvControl tvControl = tv as ITvControl;
if(tvControl != null)
{
tvControl.on() ;
tvControl.switchToA0);
tvControl.setLevelE0);
tvControl.off();
}
else
Console.WriteLine("He реализован интерфейс ITvControl");
RadioSet radio,-
radio = new RadioSet();
IRadioControl radioControl = radio as IRadioControl;
if(radioControl 1- null)
{
radioControl.next();
radioControl.setLevelC0);
Глава 8. Интерфейсы 265
else
Console.WriteLine("He реализован интерфейс IRadioControl");
Console.WriteLine("Телевизор: канал {0}, громкость {1}",
((IChannel)tvControl).current(),
((IVolume)tvControl).current() ) ;
Console.WriteLine("Радио: канал {0}, громкость {1}",
((ITunning)radioControl).current(),
((IVolume)radioControl).current());
Console.ReadLine{);
Здесь, так же как и в предыдущей программе, мы определили интерфейсы
IChannel, IVolume и ITunning, предназначенные для управления каналами
и громкостью звука:
interface IChannel
{
void switchTo(uint channelNumber);
uint current();
}
interface IVolume
{
void setLevel(uint volumeLevel);
uint current();
}
interface ITunning
{
void next();
void prev();
uint current();
}
Некоторые из этих интерфейсов (IChannel и IVolume) применяются для управ-
управления телевизором, а некоторые (ITunning и IVolume)— для управления радио-
радиоприемником.
С целью упрощения программы мы попарно скомбинировали интерфейсы, образо-
образовав новые интерфейсы ITvControl и IRadioControl:
interface ITvControl : IChannel, IVolume
{
void on();
void off();
266 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
interface IRadioControl : ITunning, IVolume
Первый из этих интерфейсов с именем ITVControl объединяет интерфейсы
IChannel и IVolume, добавляя к ним еще два метода (on и off), позволяющие
включать и выключать телевизор. Таким образом, этот комбинированный интерфейс
предназначен для управления телевизором.
Комбинированный интерфейс IRadioControl представляет собой чистую ком-
комбинацию интерфейсов ITunning и IVolume без каких-либо добавлений. Он создан
для управления радиоприемником.
Теперь при объявлении класса TvSet нам достаточно указать, что он реализует
комбинированный интерфейс ITvControl, не перечисляя составляющие его интер-
интерфейсы IChannelиIVolume:
class TvSet : ITvControl
{
private uint channel;
private uint volume;
private bool power;
public TvSet()
{
channel = 1;
volume =10;
power = false;
void ITvControl .on ()
{
power = true;
}
void ITvControl.off()
{
power = false;
Так как комбинированный интерфейс ITvControl включает в себя методы on
и of f, нам необходимо реализовать их в классе TvSet. Для хранения текущего со-
состояния телевизора (включен или выключен) мы предусмотрели в этом классе поле
power.
Аналогично при объявлении класса RadioSet мы указали, что он реализует ком-
комбинированный интерфейс IRadioControl:
class RadioSet : IRadioControl
{
private uint channel;
private uint volume;
Глава 8. Интерфейсы 267
public RadioSet(;
{
channel = 1;
volume = 10;
Так как в рамках этого интерфейса нет никаких дополнительных методов, отсутст-
отсутствующих в составляющих его интерфейсах, при объявлении класса RadioSet нам
не пришлось объявлять дополнительные методы.
Теперь мы расскажем о том, как пользоваться комбинированными интерфейсами.
Создав объект класса TvSet, мы объявляем переменную tvControl, предназна-
предназначенную для хранения ссылки на интерфейс ITvControl:
TvSet tv;
tv = new TvSet();
ITvControl tvControl = tv as ITvControl;
Эта переменная инициализируется безопасным способом с применением оператора as.
Если ссылка на интерфейс ITvControl получена успешно, мы используем се для
вызова методов комбинированного интерфейса:
if(tvControl != null)
{
tvControl.on();
tvControl.switchToA0);
tvControl.setLevelE0) ;
tvControl.off();
}
else
Console.WriteLine("He реализован интерфейс ITvControl");
Обратите внимание, что с помощью единственной ссылки на комбинированный
интерфейс ITvControl мы вызываем методы входящих в него интерфейсов
IChannel и IVolume. Так как все эти методы называются по-разному, в данном
случае не возникают конфликты между одинаковыми именами методов, принадлежа-
принадлежащих разным интерфейсам.
Аналогичным образом создается и используется интерфейс IRadioControl,
предназначенный для управления радиоприемником:
RadioSet radio;
radio = new RadioSet();
IRadioControl radioControl = radio as IRadioControl;
if (radioControl != null)
{
radioControl.next();
radioControl.setLevelC0) ;
}
else
Console.WriteLine("He реализован интерфейс IRadioControl");
268 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
А теперь мы рассмотрим более интересный случай, связанный с вызовом метода
current. Этот метод, возвращающий текущее значение номера канала или уровня гром-
громкости, определен во всех базовых интерфейсах, имеющихся в нашей программе.
Попытки обращения к этому методу через ссылки на интерфейсы вида tvCon-
trol. current () и radioControl. current () приведут к появлению на этапе
компиляции сообщения об ошибке. Дело в том, что в такой записи не ясно, о какой
именно реализации метода current идет речь.
В самом деле, комбинированный интерфейс ITvControl состоит из интерфейсов
IChannel и IVolurae. В каждом из них имеется свой метод current. Аналогично
комбинированный интерфейс IRadioControl, состоящий из интерфейсов I tun-
tunning и IVolume, тоже содержит в себе две различные реализации метода current.
Для того чтобы избежать неоднозначности, необходимо приводить ссылку на ком-
комбинированный интерфейс к типу того интерфейса, для которого нужно вызвать наш
метод:
Console.WriteLine("Телевизор: канал {0}, громкость {1}",
((IChannel)tvControl).current(),
((IVolume)tvControl).current());
Здесь мы вначале вызываем метод current интерфейса IChannel (получая при
этом текущий номер канала), а затем этот же метод, но для интерфейса IVolurae
(для определения текущего уровня громкости).
Аналогичным образом эта операция выполняется и для интерфейса Iradio-
Control, управляющего радиоприемником:
Console.WriteLine("Радио: канал {0}, громкость {1}",
((ITunning)radioControl).current!),
((IVolume)radioControl).current());
Для того чтобы избежать возникновения исключений, для преобразования типов
вы можете воспользоваться описанными выше операторами is и as.
Интерфейсы и наследование классов
Если базовый класс реализует какие-либо интерфейсы, то они наследуются производ-
производными классами. При необходимости производный класс может переопределить все
или некоторые методы интерфейсов базового класса.
Рассмотрим простой пример.
Пусть у нас есть базовый класс TvSet, описывающий телевизор. В этом классе
мы предусмотрели средства переключения каналов и регулировки громкости, реали-
реализованные с помощью интерфейсов.
Теперь на базе класса TvSet нам бы хотелось создать новый класс Inter-
nationalTvSet, в котором четные каналы были бы русскими, а нечетные— анг-
английскими.
Глава 8. Интерфейсы 269
Исходный текст программы, объявляющей и использующей эти классы, мы приве-
привели в листинге 8.4.
Листинг 8.4. Файл chO8\lnterfacelnheritance\lnterfacelnheritanceApp.cs
using System;
namespace Interfacelnheritance
{
interface IChannel
{
void switchTo(uint channelNumber);
uint current();
interface IVolume
{
void setLevel(uint volumeLevel)
uint current();
class TvSet : IChannel, IVolume
{
protected uint channel;
protected uint volume;
public TvSetO
{
channel = 1;
volume = 10;
void IChannel.switchTo(uint channelNumber)
{
channel = channelNumber;
}
uint IChannel.current()
{
return channel;
}
void IVolume.setLevel(uint volumeLevel)
{
volume = volumeLevel;
}
uint IVolume.current()
{
return volume;
}
}
270 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
interface ILanguage
string current();
class InternationalTvSet : TvSet, IChannel, IVolume, ILanguage
private string interfaceLanguage;
public InternationalTvSet()
interfaceLanguage = "русский";
void IChannel.switchTo(uint channelNumber)
channel = channelNumber;
if(channelNumber%2 == 0)
interfaceLanguage = "русский";
else
interfaceLanguage = "english";
string ILanguage.current()
return interfaceLanguage;
}
class interfacelnheritanceApp
static void Main(string[] args)
InternationalTvSet tv;
tv = new InternationalTvSet();
IChannel tvChannel = tv as IChannel;
IVolume tvVolume = tv as IVolume;
ILanguage tvLanguage = tv as ILanguage;
if(tvChannel != null && tvVolume != null
&& tvLanguage != null)
tvVolume.setLevelE0);
tvChannel.switchToA0);
Console.WriteLine(
"Телевизор: канал {0} ({1}), громкость {2}",
tvChannel.current(), tvLanguage.current(),
tvVolume.current());
Глава 8. Интерфейсы 271
tvchannel.swi tchToA1);
Console.WriteLine(
"Телевизор: канал {0} ({1}), громкость B)",
tvChannel.current(), tvLanguage.current(),
tvVolume.current());
}
else
Console.WriteLine("He реализован интерфейс IChannel,
IVolume или ILanguage");
Console.ReadLine();
Для переключения каналов и регулировки громкости мы определили уже знакомые
вам интерфейсы IChannel и IVolume:
interface IChannel
{
void switchTo(uint channelNumber);
uint current () ;
interface IVolume
void setLevel(uint volumeLevel);
uint current();
Эти интерфейсы реализованы в базовом классе TvSet аналогично тому, как это
было сделано в предыдущей программе (см. листинг 8.3).
Что же касается производного класса InternationalTvSet, то он реализует
еще один интерфейс ILanguage, позволяющий определить национальный язык те-
текущего канала:
Interface ILanguage
string current();
При объявлении производного класса InternationalTvSet мы указали базо-
базовый класс TvSet, а также перечислили все интерфейсы, реализуемые в рамках базо-
базового и производного класса:
class InternationalTvSet : TvSet, IChannel, IVolume, ILanguage
private string interfaceLanguage;
272 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
public InternationalTvSet()
{
interfaceLanguage = "русский";
}
void IChannel.switchTo(uint channelNumber)
{
channel = channelNumber•
if(channelNumber%2 == 0)
interfaceLanguage = "русский";
else
interfaceLanguage = "english";
>
string ILanguage.current ()
{
return interfaceLanguage;
Конструктор класса InternationalTvSet устанавливает по умолчанию рус-
русский язык, инициализируя соответствующим образом поле interfaceLanguage.
В дальнейшем содержимое этого поля будет зависеть от номера канала, принимаемого
телевизором.
В интерфейсе IChannel, реализованном базовым классом, нет средств изменения
содержимого поля interfaceLanguage, поэтому нам придется в производном
классе переопределить метод IChannel. switchTo.
Новая реализация этого метода, предусмотренная в производном классе
InternationalTvSet, скрывает реализацию этого же метода базовым классом.
Дополнительно класс InternationalTvSet реализует метод current интер-
интерфейса ILanguage, предназначенный для определения текущего языка.
Теперь мы расскажем о том, как метод Main нашей программы использует опи-
описанные выше интерфейсы.
Прежде всего этот метод создает объект производного класса Interna-
InternationalTvSet и получает ссылки на все необходимые интерфейсы:
InternationalTvSet tv;
tv = new InternationalTvSet();
IChannel tvChannel = tv as IChannel;
IVolume tvVolume = tv as IVolume;
ILanguage tvLanguage = tv as ILanguage;
Если все эти ссылки получены успешно, программа продолжает работу, а в про-
противном случае выводит на консоль сообщение об ошибке:
if(tvChannel != null && tvVolume != null && tvLanguage != null)
2
Глава 8. Интерфейсы 273
else
Console.WriteLine(
"He реализован интерфейс IChannel, IVolume или ILanguage");
В том случае, если ссылки на все интерфейсы получены успешно, программа уста-
устанавливает уровень громкости 50 %, переключает телевизор на 10-й канал, а затем ото-
отображает на консоли его состояние:
tvVolume.setLevelE0);
tvChannel.switchToA0);
Console.WriteLine("Телевизор: канал {0} ({1}), громкость {2}",
tvChannel.current(), tvLanguage.current(), tvVolume.current());
Так как номер 10-го канала четный, для него будет выбран русский интерфейс.
На следующем этапе программа переключает телевизор на 11-й (нечетный) канал,
повторяя вывод состояния на консоль:
tvChannel.switchToA1);
Console.WriteLine("Телевизор: канал {0} ({1}), громкость {2}",
tvChannel.current(), tvLanguage.current(), tvVolume.current());
Обратите внимание, что здесь мы используем без каких бы то ни было изменений
интерфейс IVolume, реализованный в базовом классе TvSet.
Обращаясь к интерфейсу IChannel, наша программа имеет дело с методами это-
этого интерфейса, реализованными как в базовом, так и в производном классе. Например,
метод current реализован в базовом классе и не изменяется в производном классе,
а метод switchTo, напротив, переопределен в дочернем классе.
И наконец, наша программа использует метод интерфейса ILanguage, реализо-
реализованный в производном классе InternationalTvSet и никак не упоминающийся
в базовом классе TvSet.
Таким образом, производные классы могут наследовать не только методы, но и ин-
интерфейсы базового класса, при необходимости переопределяя их.
Свойства в интерфейсах
В предыдущих примерах программ мы создавали интерфейсы, содержащие исключи-
исключительно методы. Между тем в рамках интерфейсов С# допускается объявлять свойства,
индексаторы и события.
В этом разделе мы рассмотрим пример программы управления телевизором, функ-
функционирование которой основано на использовании свойств, объявленных в рамках ин-
интерфейса ITvControl (листинг 8.5).
Листинг 8.5. Файл ch08\Properties\PropertiesApp.cs
using System;
namespace Properties
{
interface ITvControl
274 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
bool PowerOn
{
get;
set;
}
byte MaxChannel
{
get;
}
byte Channel
{
get;
set;
}
byte Volume
{
get;
set;
}
}
class TelevisionSet : ITvControl
{
private bool isPowerOn;
private byte maxChannel;
private byte currentChannel;
private byte currentVolume;
public TelevisionSet(byte numberOfChannels)
{
isPowerOn = false;
maxChannel = numberOfChannels;
currentChannel= 1;
currentVolume = 10;
}
bool ITvControl.PowerOn
{
get
{
return isPowerOn;
}
set
{
isPowerOn = value;
Глава 8. Интерфейсы 275
byte ITvControl.MaxChannel
{
get
{
return maxchannel;
}
}
byte ITvControl.Channel
{
get
{
return currentChannel;
}
set
<
if(value <= maxchannel && value > 0)
currentChannel = value;
byte ITvControl.Volume
С
get
{
return currentVolume;
}
set
{
if(value > 0 && value <= 100)
currentVolume = value;
else
currentVolume = 0;
class PropertiesApp
{
static void Main(string[] args)
{
TelevisionSet tvSmall;
TelevisionSet tvLarge;
tvSmall = new TelevisionSetF) ;
tvLarge = new TelevisionSetD0) ;
ITvControl tvsControl = tvSmall as ITvControl;
ITvControl tvlControl = tvLarge as ITvControl;
if(tvsControl == null || tvlControl == null)
{
Console.WriteLine("He реализован интерфейс ITvControl
return;
276 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
tvsControl.PowerOn = true;
tvsControl.Channel = 5 ;
tvsControl.Volume = 50;
tvlControl.PowerOn = true;
tvlControl.Channel = 27;
tvlControl.Volume = 30;
Console.Write("Телевизор tvSmall: ");
Console.WriteLine("{0}, канал {1} из {2}, громкость {3}
tvsControl.PowerOn ? "Включен" : "Выключен",
tvsControl.Channel, tvsControl.MaxChannel,
tvsControl.Volume);
Console.Write("Телевизор tvLarge: ");
Console.WriteLine("{0}, канал {1} из {2}, громкость {3}
tvlControl.PowerOn ? "Включен" : "Выключен",
tvlControl.Channel, tvlControl.MaxChannel,
tvlControl.Volume);
Console.ReadLine();
Ранее в этой главе мы уже проводили аналогию между интерфейсами С# и абст-
абстрактными классами. В гл. 6 мы рассказывали про абстрактные интерфейсы, объявле-
объявление которых не содержит тела функций доступа get и set.
Аналогичным образом объявляются свойства в рамках интерфейса. Ниже мы при-
привели исходный текст интерфейса ITvControl, содержащего объявления свойств,
с помощью которых можно управлять нашим виртуальным телевизором:
interface ITvControl
{
bool PowerOn
{
get;
set;
}
byte MaxChannel
{
get;
}
byte Channel
{
get;
set;
Глава 8. Интерфейсы 277
byte Volume
{
get;
set ;
Свойство PowerOn позволяет включать или выключать телевизор посредством за-
записи в него значений true или false соответственно. Это же свойство может быть
использовано для определения состояния, в котором находится телевизор, — вклю-
включенное или выключенное.
С помощью свойства MaxChannel можно определить максимальный номер кана-
канала, принимаемого телевизором. Это свойство доступно только для чтения, так как
в нем предусмотрена только одна функция доступа get.
Свойство Channel позволяет переключать телевизор на любой заданный канал,
а также определять номер текущего канала.
И наконец, посредством свойства Volume можно регулировать уровень громкости,
а также определять текущий уровень громкости.
При объявлении класса TelevisionSet мы указали, что он реализует только что
описанный интерфейс ITVControl:
class TelevisionSet : ITvControl
{
private bool isPowerOn;
private byte maxChannel;
private byte currentChannel;
private byte currentVolume;
public TelevisionSet(byte numberOfChannels)
{
isPowerOn = false;
maxChannel = numberOfChannels;
currentChannel = 1;
currentVolume = 10;
Обратите внимание, что при реализации интерфейса мы не указываем модифика-
модификатор доступа, а имя свойства задаем вместе с именем интерфейса:
byte ITvControl.Channel
{
get
{
return currentChannel;
}
set
278 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
if(value <= maxChannel && value > 0)
currentChannel = value;
Реализация процедур доступа особенностей не имеет. Она была описана в гл. 6, по-
поэтому вы сможете разобраться в этом самостоятельно.
Индексаторы в интерфейсах
Если класс создан для представления объекта с массивом, для доступа к нему можно
использовать индексаторы. Об этом мы рассказывали вам в предыдущей главе. Как мы
уже говорили, индексатор может быть включен в объявление интерфейса наряду
с методами и другими членами.
В листинге 8.6 мы привели модифицированный вариант программы, рассмотрен-
рассмотренной нами ранее в разделе «Индексаторы» предыдущей главы. Эта программа исполь-
использует класс, хранящий названия телевизионных каналов в массиве.
Листинг 8.6. Файл chO8\lndexertlndexerApp.cs
using System;
namespace Indexer
{
interface IChannelNames
{
uint Size
{
get;
}
string this[uint index]
{
get;
set;
class ChannelNames : IChannelNames
{
private stringП Channels;
private uint ArraySize;
public ChannelNames(uint Count)
{
Channels = new string[Countl
ArraySize = Count;
Глава 8. Интерфейсы 279
uint IChannelNames.Size
{
get
{
return ArraySize;
}
}
string IChannelNames.this[uint index]
{
get
{
if(index >= 0 && index < Channels.Length)
return Channels[index];
else
return "Канал недоступен";
set
if(index >= 0 && index < Channels.Length)
Channels[index] = value;
class IndexerApp
{
static void Main(string!] args)
{
ChannelNames ch = new ChannelNamesE);
IChannelNames chNames = ch as IChannelNames;
if(chNames == null)
Console.WriteLine{"He реализован интерфейс IChannelNames"
chNames[0] = "Спорт";
chNames[1] = "Мир кино";
chNames[2] = "Боевик";
chNames[3] = "Наше кино";
chNames[4] = "MTV";
for(uint i = 0; i < chNames.Size; i++)
{
string s = chNames[i];
Console.WriteLine(s) ;
}
Console.ReadLine() ;
280 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Для получения доступа к элементам массива названий каналов мы объявили ин-
интерфейс IChannelNames:
interface IChannelNames
{
uint Size
{
get;
}
string this[uint index]
{
get;
set;
Этот интерфейс включает в себя свойство Size, а также индексатор.
Свойство Size позволяет узнать текущий размер массива, созданного конструкто-
конструктором, поэтому оно имеет только одну процедуру доступа get. Что же касается индек-
индексатора, то мы объявили для него обе процедуры доступа — get и set.
Таким образом, через интерфейс IChannelNames программа сможет определить
размер массива, а также получить или изменить содержимое его отдельных ячеек.
При объявлении класса ChannelNames мы указали, что он реализует интерфейс
IChannelNames:
class ChannelNames : IChannelNames
{
private string[] Channels;
private uint ArraySize;
public ChannelNames(uint Count)
{
Channels = new string[Count];
ArraySize = Count;
Конструктор этого класса обеспечивает создание массива необходимых размеров,
а также инициализацию поля ArraySize, хранящего размер массива.
Реализация свойства Size не имеет никаких особенностей:
uint IChannelNames.Size
{
get
{
return ArraySize;
Здесь используется полное имя свойства, включающее имя интерфейса; модифика-
модификаторы доступа не указываются.
Глава 8. Интерфейсы 281
Что же касается индексатора, то он реализован следующим образом:
string IChannelNames.this[uint index]
get
if(index >= 0 && index < Channels.Length)
return Channels[index];
else
return "Канал недоступен";
set
if(index >= 0 && index < Channels.Length)
Channels[index] = value;
Мы добавили к имени индексатора название интерфейса, в результате чего получи-
получили полное имя IChannelNames . this. В остальном этот индексатор полностью ана-
аналогичен индексатору, описанному в предыдущей главе (см. листинг 7.7).
Теперь мы расскажем о том, как пользоваться созданным нами интерфейсом.
Вначале необходимо создать объект (массив названий каналов), а также ссылку
на интерфейс:
ChannelNames ch = new ChannelNamesE);
IChannelNames chNames = ch as IChannelNames;
Созданная ссылка затем проверяется на равенство значению null:
if(chNames == null)
Console.WriteLine("He реализован интерфейс IChannelNames");
Если ссылка на интерфейс была получена успешно, мы инициализируем 5 ячеек
массива, используя ссылку на интерфейс IChannelNames:
chNames[0] = "Спорт";
chNames[1] = "Мир кино";
chNames[2] = "Боевик";
chNames[3] = "Наше кино";
chNames[4] = "MTV";
Как видите, адресация ячеек массива через индексатор, объявленный в рамках ин-
интерфейса, выполняется очевидным и ожидаемым образом.
Чтение ячеек массива происходит аналогично:
for(uint i = 0; i < chNames.Size; i++)
{
string s = chNaraes[i];
Console.WriteLine (s) ,-
}
Перебирая ячейки, мы получаем очередную строку названия канала и выводим ее
на консоль.
282 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Глава 9. Обработка исключений
Общеизвестно мнение о том, что человек может ошибаться, а компьютеры — нет. На-
Наверное, только профессиональные программисты и системные администраторы знают,
насколько вторая часть этого утверждения не соответствует действительности.
Какова же причина появления компьютерных ошибок и можно ли предотвратить
их появление?
На наш взгляд, большинство таких ошибок связано с тем, что компьютеры и про-
программы для них создаются людьми. Современные компьютеры и программы чрезвы-
чрезвычайно сложны, и человек просто не в состоянии удержать в голове все детали и осо-
особенности их работы.
Проблемы начинаются уже в центральном процессоре, содержащем миллионы
компонентов. Когда появилась первая ошибка в процессоре Pentium компании Intel,
это вызвало шоковую реакцию у многих пользователей компьютеров, после чего на-
началась массовая замена дефектных процессоров. Мы тоже в свое время были счастли-
счастливыми обладателями дефектного процессора Pentium 90 и, пользуясь случаем, даже на-
написали пару статей на эту тему. Современные процессоры допускают исправление не-
некоторых ошибок программным способом, для чего в компании Intel была разработана
специальная технология.
Надо сказать, что ошибки в процессорах появляются вовсе не потому, что разра-
разработчик уделяет недостаточно внимания тестированию новых моделей, стремясь вы-
выпустить их на рынок как можно быстрее (хотя какая-то доля правды тут есть). На тес-
тестирование и поиск ошибок тратятся огромные людские и финансовые ресурсы, однако
из-за невообразимой сложности процессоров нереально провести полное тестирование
всех его компонентов за разумное время.
Современные ОС и сложные программы также изобилуют ошибками, некоторые
из которых мешают пользователям, а некоторые так и остаются незамеченными. Не
затихают споры о том, какая ОС работает надежнее — Microsoft Windows или Linux.
Мы полагаем, что все существующие версии этих и других ОС ненадежны по причине
присутствия в них ошибок. В зависимости от ситуации (конфигурации аппаратного
обеспечения, драйверов, набора установленных программ и т. п.) эта ненадежность
может сказываться в большей или меньшей степени.
Еще одна важная причина возникновения ошибок при работе программного обес-
обеспечения — отказы аппаратуры компьютера. Ничто не вечно под Луной, и компьютер
тоже не является исключением. Износу подвержены в первую очередь такие компо-
компоненты, как накопители данных на дисках, хотя отказывают и другие компоненты.
Последствия компьютерных ошибок могут быть очень тяжелыми. В результате
их появления могут пропасть очень важные данные, стоимость которых порой много-
многократно превышает стоимость всей компьютерной системы. Искажение информации
о пациентах в медицинской базе данных может даже привести к трагическому исходу.
Как же застраховать себя от появления ошибок или по крайней мерс сократить
ущерб от их возникновения?
~~ШБШШ 283
Пользователи и системные администраторы должны ответственно относиться
к выбору и настройке программного обеспечения компьютерной системы, к резервно-
резервному копированию данных и антивирусной защите. На сайте службы восстановления
данных DataRecovery.Ru (http://www.datarecovery.ru) вы найдете многочисленные ма-
материалы, которые помогут вам избежать потерь данных или снизить ущерб, если дан-
данные уже пропали.
Что же касается программистов, разрабатывающих новые приложения, то они
должны самым внимательным образом обрабатывать ошибочные ситуации, возни-
возникающие при работе этих приложений. И хотя едва ли возможно полностью избавиться
от компьютерных ошибок, постараться не увеличивать сильно их количество в новых
разработках, полагаем, все же стоит.
Классические способы обработки ошибок
Отвлекаясь от философских причин возникновения компьютерных ошибок, рассмот-
рассмотрим ситуации, в которых такие ошибки обычно возникают при создании программ.
Простейшие примеры — деление на нуль и выход за границы массива.
Рассмотрим программу, исходный текст которой приведен в листинге 9.1.
Листинг 9.1. Файл ch09\DivideByZero\DivideByZeroApp.cs
using System;
namespace DivideByZero
{
class DivideByZeroApp
{
static void Main(string[] args)
{
int i = 0;
int j = 5 / i; // Ошибка деления на нуль!
Console.ReadLine();
Мы объявили в ней две переменные типа int с именами i и j. Переменной i при-
присваивается нулевое значение, после чего предпринимается попытка разделить число 5
на содержимое i, записав результат в переменную j.
Компилятор не следит за содержимым переменных, которое они получат в процес-
процессе выполнения программы, поэтому он не сможет предугадать, что будет предпринята
попытка деления числа 5 на 0. В результате компиляция закончится успешно.
Однако при попытке запустить программу на экране появится следующее сообще-
сообщение об ошибке:
An unhandled exception of type 'System.DivideByZeroException'
occurred in DivideByZero.exe
Addicional information: Attempted to divide by zero.
284 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
В этом сообщении говорится, что при выполнении программы DivideByZero.exe
произошло исключение System.DivideByZeroException, для которого в нашей
программе не был предусмотрен обработчик. Сообщается также, что исключение воз-
возникло при попытке выполнить деление на нуль.
Напомним, что программы С# выполняются под управлением системы Microsoft
.NET Framework, контролирующей появление ошибок подобного рода. Если в прог-
программе не предусмотреть обработку ошибочной ситуации, то при появлении ошибки
система Microsoft .NET Framework просто завершит ее работу с выдачей на экран со-
соответствующего сообщения.
Аналогичным образом ведут себя программы, составленные на других языках про-
программирования. При этом ответственность за аварийное прекращение работы сбойной
программы лежит на ОС. Если она не справится с этой задачей должным образом, мо-
могут возникнуть серьезные проблемы, вплоть до «зависания» компьютера и потери
данных.
Предварительная проверка параметров
Ваша программа не должна выдавать пользователю системное сообщение об ошибке,
аналогичное приведенному выше. Обнаружив ошибку, программа должна сообщить
пользователю причины возникновения ошибки, а также рекомендовать какие-либо
действия. Все это невозможно без создания в программе собственной системы обра-
обработки ошибок.
Ниже мы представили простейший способ предотвращения ошибки деления
на нуль (заметьте: именно предотвращения, а не обработки):
if(i != 0)
{
int j = 5 / i;
}
else
Console.WriteLine("Ошибка деления на нуль");
Если делитель равен нулю, то операция деления не выполняется. Вместо этого
на консоль выводится сообщение об ошибке.
Много ошибок вызывается тем, что функциям или методам передаются неправиль-
неправильные значения параметров. Чтобы предотвратить возникновение таких ошибок, ваша
программа должна перед вызовом функций или методов проверять значения парамет-
параметров на допустимость. Однако в реальных программах такие проверки часто опускают-
опускаются, что и приводит к появлению неожиданных и трудно обнаруживаемых ошибок.
Проверка кодов возврата функций и методов
Рассмотрим теперь другой случай (листинг 9.2). Здесь мы вызываем в цикле метод
Divide, выполняющий деление первого своего параметра на второй. В качестве де-
делителя используется переменная цикла i, которая во время работы цикла принимает
нулевое значение. Поэтому при работе программы происходит ошибка деления
на нуль.
Глава 9. Обработка исключений 285
Листинг 9.2. Файл ch09\DivideByZero1\DivideByZero1App.cs
using System;
namespace DivideByZerol
{
class DivideByZerolApp
{
static int Divide(int x, int y)
{
int result;
result = x / y;
return result;
}
static void Main(string[] args)
{
int j;
for(int i = -3; i <= 3; i
j = Divide{10, i); // Ошибка деления на нуль!
}
Console.ReadLine();
Как исключить появление этой ошибки?
Мы могли бы немного изменить метод Divide, чтобы он проверял значение де-
делителя перед выполнением операции деления:
static int Divide(int x, int y)
{
int result;
if(y != 0)
{
result = x / y; '
return result;
}
else
return 0;
}
Теперь ошибка деления на нуль не возникнет, так как, если делитель равен нулю,
деление не выполняется. Однако метод Divide должен каким-то образом просигна-
просигнализировать вызывающему методу о возникновении ошибки.
Наша реализация этого метода при ошибке возвращает нулевое значение, однако
очевидно, это не лучший способ. В самом деле, нулевое значение возвращается
и в том случае, если делитель равен нулю, а эта ситуация не является ошибочной.
Заметим, что возвращение особого значения методом или функцией используется
очень часто в других языках программирования в качестве признака ошибки. Естест-
286 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
венный программный интерфейс (Application Program Interface, API) ОС построен
на базе набора функций, возвращающих в случае возникновения ошибки какое-либо
особое значение.
Предполагается, что после обращения к функции операционной системы (такой,
например, как открытие файла, чтение блока данных из этого файла и т. п.) вызываю-
вызывающая программа проверяет код возврата, предпринимая при возникновении ошибки ка-
какие-либо действия.
Вот как мы можем использовать модифицированный вариант метода Divide, воз-
возвращающий нулевое значение при возникновении ошибки деления на нуль:
j = Divide A0, i) ,-
if (j == 0)
Console.WriteLine("Ошибка деления на нуль");
В реальных программах встречается довольно большая глубина вложенности
функций и методов, когда один метод вызывает другой, тот, в свою очередь, обраща-
обращается к третьему и четвертому и т. д.
Строки листинга программы, предназначенные для обработки кодов возврата для
всех этих функций или методов, загромождают исходный текст программы, делая его
«нечитаемым». Кроме того, программист может по забывчивости опустить проверку
кода возврата какой-либо одной функции или метода, что приведет к аварийному за-
завершению приложения во время его работы.
Есть и еще одна проблема.
Обычно функция, обнаружившая ошибку, не знает, что с ней нужно делать. Дейст-
Действия по обработке ошибки должны выбираться исходя из контекста вызова этой функ-
функции, так как одно и то же событие в разных случаях может интерпретироваться по-
разному.
Рассмотрим следующую ситуацию.
Пусть мы сканируем диск, читая по очереди все его секторы до достижения конца
диска. Мы выполняем эту операцию с целью определения общего количества секто-
секторов, расположенных на диске. Далее, пусть у нас есть функция ReadSector, единст-
единственная задача которой — чтение сектора диска с заданным номером.
Когда в процессе последовательного чтения программа доберется до конца диска,
возникнет ошибка, так как функция ReadSector попытается прочитать сектор с не-
несуществующим номером. Ошибка может возникнуть и в другой ситуации — если су-
существующий сектор диска окажется испорченным.
Сама по себе наша функция ReadSector не знает, что делать с ошибкой, так как
она может вызываться из разных программ. При обнаружении ошибки вызывающая
программа может, например, повторить выполнение операции в надежде на то, что
сектор все же будет прочитан правильно, а может аварийно завершить работу про-
программы. В описанном выше случае, когда программа определяет объем диска, возник-
возникновение ошибки чтения сектора — нормальное ожидаемое событие, которое не долж-
должно рассматриваться как ошибочное.
Глава 9. Обработка исключений 287
Таким образом, обычно реакция на ошибку выбирается не той функцией, которая
обнаружила ошибку, а вызывающей программой. В случае многочисленных вложен-
вложенных вызовов функций ошибка может обрабатываться на том или ином уровне вложен-
вложенности, причем заранее обычно трудно определить, на каком уровне именно.
В результате анализ кодов возврата функций превращается в непростую задачу.
По невнимательности программист может пропустить проверку кода возврата какой-
либо функции, в результате чего программа станет работать с ошибками.
Применение механизма исключений
Как видите, классические схемы обработки ошибок, основанные на проверке допус-
допустимости параметров и анализе кодов возврата функций и методов, обладают сущест-
существенными недостатками.
Практически все современные языки программирования снабжены мощным сред-
средством обработки ошибок, основанным на использовании так называемых исключений
(exceptions). He является исключением (простите за тавтологию) и язык программиро-
программирования С#.
Средства обработки исключений в языке С# делают исходный текст программ по-
понятнее и проще. С их помощью программист может организовать структурную обра-
обработку ошибок, как прогнозируемых, так и возникающих неожиданно.
Система обработки ошибок, использованная в библиотеке классов Microsoft NET
Framework, работает исключительно с применением механизма исключений. Поэтому,
какую бы программу вы ни разрабатывали на языке С#, вы обязательно столкнетесь
с необходимостью обрабатывать или вызывать исключения (в других языках програм-
программирования это не так; программы C++, например, можно создавать и без обработки
исключений).
Блоки try-catch
Для того чтобы организовать в своей программе С# обработку ошибок с использова-
использованием исключений, нужно применить блоки, созданные при помощи ключевых слов
try, catch и finally. Создание исключения (или, как еще говорят, передачу или
возбуждение исключения) выполняют с помощью ключевого слова throw.
В блок try заключается код, который может вызвать возникновение ошибок,
т. с. «ненадежный» код. Следом за блоком try может располагаться один или не-
несколько блоков catch, в которых находится код для обработки ошибок.
Рассмотрим пример обработки ошибки деления на нуль с помощью исключений
(листинг 9.3).
Листинг 9.3. Файл ch09\D'mdeByZeroEx\DMdeByZeroExApp.cs
using System,-
namespace DivideByZeroEx
{
class DivideByZeroExApp
288 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
static void Main(string[] args)
{
int i = 0;
try
{
int j = 5 / i;
}
catch(System.Exception ex)
{
Console.WriteLine(
"»> Ошибка в программе {0} [ {1} ] \n\n{2}
ex.Source, ex.Message, ex.StackTrace);
}
Console.ReadLine();
Здесь мы заключили в блок try строку программы, при выполнении которой мо-
может произойти ошибка деления на нуль:
try
{
int j =5 / i;
}
В блоке try может располагаться произвольное количество программных строк,
содержащих вызовы методов, обращения к интерфейсам, свойствам и индексаторам,
операторы цикла и т. п. При отсутствии ошибок все эти строки исполняются обычным
образом.
Если же в процессе выполнения какой-либо программной строки возникает ошиб-
ошибка, нормальная последовательность работы программы прерывается и управление пе-
передается ближайшему блоку catch, расположенному вслед за блоком try. Вот как
в нашей программе выглядит блок catch, предназначенный для обработки ошибки
деления на нуль:
catch(System.Exception ex)
{
Console.WriteLine(">>> Ошибка в программе {0} [{1)]\n\n{2}",
ex.Source, ex.Message, ex.StackTrace);
}
В круглых скобках после ключевого слова catch указывается тип (класс) обраба-
обрабатываемого исключения. В данном случае мы указали базовый класс System.Ex-
System.Exception, от которого «происходят» все остальные классы обработки исключений.
Сконструированный таким способом обработчик исключений способен перехватывать
исключения всех типов, возникающих при выполнении кода, ограниченного предше-
предшествующим блоком try.
Глава 9. Обработка исключений 289
10 Язык С# Самоучитель
Вслед за обозначением класса обрабатываемого исключения мы указали имя пере-
переменной ex. Когда блок catch получает управление, эта переменная содержит ссылку
на объект класса System. Exception. Свойства этого класса содержат информацию,
которая может оказаться полезной при обработке исключения:
• свойство System. Exception. Source содержит название программы, в которой
произошло исключение;
• в свойстве System. Exception. Message хранится текст сообщения об ошибке;
• свойство System. Exception. StackTrace содержит точную информацию
о том, в каком месте программы произошло исключение.
Вот как выглядит отформатированное сообщение об ошибке, отображаемое нашей
программой на консоли после попытки деления на нуль:
>>> Ошибка в программе DivideByZeroEx [Attempted to divide by zero.]
at DivideByZeroEx.DivideByZeroExApp.Main(String[] args) in
h:\ [beginner c# bo
ok]\src\ch09\dividebyzeroex\dividebyzeroexapp.cs:line 11
Как видите, эта информация позволяет точно определить тип исключения, а также
место, в котором оно произошло.
Использование нескольких блоков catch
Теперь вы знаете, что, заключив «ненадежный» с точки зрения возникновения ошибок
код в блок try, вы можете перехватить и обработать исключения в блоке catch.
Заметим, что исключения возникают в таких распространенных ситуациях, как вы-
выход за пределы массива или строки в процессе индексации, деление на нуль, попытка
использования ссылки со значением null, недопустимое преобразование типа ссылки
и т. д. С помощью механизма исключений все подобные ошибки нетрудно обнаружить
и обработать во время выполнения приложения.
Программа может предусмотреть общий блок обработки исключений различных
типов, определив один блок catch, или создать несколько таких блоков — по одному
для исключений каждого типа.
Пример программы, в которой реализован такой способ обработки исключений не-
нескольких типов, приведен в листинге 9.3.
Листинг 9.3. Файл chO9\MultiCatch\MultiCatchApp.cs
using System;
namespace MultiCatch
{
class MultiCatchApp
{
static void Main(string[] args)
{
string errType;
290 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
do
{
Console.WriteLine("\пВыберите тип ошибки:")
Console
Console
Console
Console
Console
Console
.WriteLine(.
.WriteLine(.
.WriteLine(.
.WriteLine(.
.WriteLine(.
.WriteLine(.
Завершить работу программы");
Деление на нуль");
Выход за границы массива");
Неправильное приведение типов")
Отрицательный размер массива");
Использование ссыпки null");
еггТуре = Console.ReadLine
try
{
switch(еггТуре)
case " 1" -.
int i = 0;
int j =5 /
break;
i;
case "
int[] array = new int[5]
array[6] = 0;
break;
case ":
object ch = new Char();
ch = "*";
byte b = (byte)ch;
break;
case "
int i = -5;
int[] array = new int[i];
array[0] = 0;
break;
int[] array = null;
array[0] = 0;
break;
Глава 9. Обработка исключений
291
catch(System.DivideByZeroException ex)
{
Console.WriteLine{">>>>> Попытка деления на нуль");
}
catch(System.IndexOutOfRangeException ex)
{
Console.WriteLine(">>>>> Выход за границы массива");
}
catch(System.InvalidCastException ex)
{
Console.WriteLine ("»»> Неправильное приведение типов"
}
catch(System.Exception ex)
{
Console.WriteLine(">>>>> Исключение (О)",
ex.ToString ());
}
} while (errType != ");
Эта программа позволяет выбрать с консоли тип ошибки, а затем выполняет про-
программный код, содержащий данную ошибку.
При вводе с консоли числа 1 будет выполнен описанный ранее код, предприни-
предпринимающий попытку выполнить деление на нуль:
case ":
{
int i = 0 ;
int j = 5 / i;
break;
}
Попытка выполнения этого кода во время работы программы приведет к возникно-
возникновению исключения System.DivideByZeroException. Он обрабатывается при
помощи следующего блока catch:
catch(System.DivideByZeroException ex)
{
Console.WriteLine(">>>>> Попытка деления на нуль");
}
Как видите, в нашем случае вся обработка сводится к выводу на консоль соответ-
соответствующего сообщения об ошибке.
Если ввести на консоли число 2, а затем нажать клавишу Enter, будет выполнен
следующий код:
case ":
{
int [ ] array =• new int [5];
292 А. В Фролов, Г. В. Фролов. Язык С#. Самоучитель
arrayF] = 0;
break;
}
Здесь мы создаем массив целых чисел, содержащих 5 ячеек, а затем пытаемся запи-
записать значение в шестую ячейку. Компилятор такую ошибку обнаружить не в состоя-
состоянии, однако при выполнении этого кода возникает исключение System. IndexOut-
OfRangeException.
Для обработки этого исключения мы предусмотрели такой блок catch:
catch(System.IndexOutOfRangeException ex)
{
Console.WriteLine(">>>>> Выход за границы массива");
}
Исключение типа System. InvalidCastException (неправильное приведение
типов) создается следующим фрагментом программы:
case " :
{
object ch = new Char();
ch = " * " ;
byte b = (byte)ch;
break;
}
Здесь мы создаем переменную ch типа object (напомним, что все классы в С#
наследуются от класса object). Далее мы записываем в эту переменную ссылку
на объект класса Char (предназначенный для представления символов UNICODE),
после чего инициализируем переменную ch символом звездочки.
Финальное действие, вызывающее возникновение исключения, — это попытка
преобразовать ссылку, хранящуюся в переменной ch, в тип byte. Компилятор
не возражает против такого преобразования, так как переменная ch имеет тип
obj ect, а преобразования ссылки базового класса в ссылку на дочерний класс разре-
разрешены. Теперь в роли дочернего класса у нас выступает класс byte.
Однако во время работы программы выясняется, что выполнить необходимое пре-
преобразование типов невозможно, в результате чего возникает исключение Sys-
System. InvalidCastException. Вот его обработчик:
catch(System.InvalidCastException ex)
{
Console.WriteLine(">>>>> Неправильное приведение типов");
}
В нашей программе мы намеренно предусмотрели индивидуальную обработку
не для всех исключений, а только для трех (деление на нуль System. Divide-
ByZeroException, выход за границы массива System. IndexOutOfRange-
Exception и неправильное приведение типов System. InvalidCastException).
Все остальные исключения обрабатываются одним универсальным способом:
Глава 9. Обработка исключений 293
catch(System.Exception ex)
{
Console.WriteLine(">>>>> Исключение {0}", ex.ToString());
}
Вот что мы увидим на экране при попытке использования ссылки, содержащей
значение null:
Выберите тип ошибки:
0. Завершить работу программы
1. Деление на нуль
2. Выход за границы массива
3. Неправильное приведение типов
4. Отрицательный размер массива
5. Использование ссылки null
5
>>>>> Исключение System.NullReferenceException: Object reference not
set to an instance of an object.
at MultiCatch.MultiCatchApp.Main(String[] args) in h:\ [beginner c#
book]\srс\chO9\multicatch\multicatchapp.es:line 59
Для получения текста сообщения с информацией об исключении мы обратились
к методу ToString. Напомним, что этот метод определен в языке С# для всех объек-
объектов и позволяет получить текстовое представление объекта.
Если же вам нужна детализированная информация об исключении, необходимо об-
обращаться к свойствам класса System.Exception, таким, как System.Excep-
System.Exception. Source (название программы), System.Exception.Message (текст сооб-
сообщения об ошибке) и System.Exception.StackTrace (информация о месте воз-
возникновения ошибки).
Исключение при арифметическом переполнении
Так как для хранения целых чисел используется фиксированное количество двоичных
разрядов, зависящее от типа числа, то при выполнении над этими числами арифмети-
арифметических операций возможно переполнение.
Если не предусмотреть специальных мер, в результате переполнения результат вы-
вычислений будет искажен. Однако механизм исключений позволяет избавить програм-
программы от ошибок подобного рода.
Рассмотрим программу, исходный текст которой приведен в листинге 9.4.
Листинг 9.4. Файл ch09\Overflow\OverflowApp.cs
using System;
namespace Overflow
{
class OverflowApp
{
static void Main(string[] args)
{
int x = 200000;
int у = 100000;
294 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
int z = x * у;
Console.WriteLine{00000 * 100000 = {0}", z) ,-
try
{
int b = checked(x * y);
Console.WriteLineC200000 * 100000 = {0}", b);
}
catch(System.OverflowException ex)
{
Console.WriteLine("Исключение: {0}", ex.Message);
int zl;
int z2;
try
{
unchecked
{
zl = x * y;
Console.WriteLine("zl = {0}", zl) ;
}
checked
{
z2 = x * y;
Console.WriteLine("z2 = {0}", z2);
catch(System.OverflowException ex)
{
Console.WriteLine("Исключение: {0}", ex.Message);
}
Console.ReadLine();
В начальном фрагменте этой программы мы умножаем число 200 000 на 100 000,
в результате чего по идее должно получиться значение 20 000 000 000:
int х = 200000;
int у = 100000;
int z = х * у;
Console.WriteLine(00000 * 100000 = {0}", z) ;
Однако, запустив эту программу на выполнение, нетрудно убедиться, что результат
будет равен отрицательному числу -1 474 836 480. В этом нет ничего удивительного:
разрядной сетки данных типа int недостаточно для правильного представления числа
20 000 000 000.
Глава 9. Обработка исключений 295
Коварство этой ошибки в том, что она не обнаруживает себя ни на стадии компи-
компиляции программы, ни на стадии выполнения. Программа получает неправильный ре-
результат, а программист может об этом даже не догадываться.
Однако, используя оператор checked, мы можем обнаружить ошибку на стадии
выполнения программы:
try
{
int b = checked(x * у);
Console.WriteLine(00000 * 100000 = {0}", b);
}
catch(System.OverflowException ex)
{
Console.WriteLine("Исключение: {0}", ex.Message);
}
В данном случае оператор checked проверяет результат выполняемой операции
на переполнение. При возникновении переполнения оператор создает исключение
System.Overf lowException, которое может быть обработано программой.
Оператор unchecked, напротив, позволяет отключить проверку на переполнение.
Ключевые слова checked и unchecked можно использовать для включения
и выключения проверки переполнения и по-другому:
int zl;
int z2;
try
{
unchecked
{
zl = x * y;
Console.WriteLine("zl = {0}", zl);
}
checked
{
z2 = x * y;
Console.WriteLine("z2 = {0}", z2);
catch(System.OverflowException ex)
{
Console.WriteLine("Исключение: {0}", ex.Message);
}
Здесь мы показали, как программа может включить или отключить проверку пере-
переполнения для целых фрагментов кода. Если при выполнении фрагмента кода нужно
создавать исключение System. Overf lowException при обнаружении переполне-
переполнения, такой фрагмент кода следует заключить в блок checked.
Аналогично блок unchecked предназначен для отключения проверки переполнения.
296 А В Фролов, Г. В Фролов. Язык С#. Самоучитель
Стандартные классы исключений
Класс Exception обычно применяется в качестве универсального средства, позво-
позволяющего обрабатывать ошибки любого типа. Для более тонкой классификации оши-
ошибок лучше использовать стандартные классы, порожденные от класса Exception или
разработанные вами самостоятельно.
Стандартные классы обработки ошибок предусмотрены практически для каждой
библиотеки классов Microsoft .NET Framework. В описании методов указано, какие ис-
исключения могут создаваться при их вызове в случае возникновения тех или иных
ошибочных ситуаций.
Теперь вы знаете, что для одного блока try можно определить несколько блоков
catch, которые будут обрабатываться последовательно. Если возникнет исключение,
то будет выполнен тот блок catch, в параметре которого это исключение объявлено.
В том случае, когда ни один блок не подходит, выполняется блок с объявлением клас-
класса Exception. А если такой блок не предусмотрен, исключение будет обработано
на уровне системы исполнения .NET Framework.
В табл. 9.1 мы перечислили стандартные классы исключений С#.
Таблица 9.1. Стандартные классы исключений С#
Класс
System.Ari thmetic-
Exception
System.ArrayType-
MismatchException
System.DivideBy-
ZeroException
System.IndexOutOf-
RangeException
System.InvalidCast-
Exception
System.Multicast-
No tSupportedExcept ion
System.NullReference-
Exception
Описание
Базовый класс для обработки исключений, возни-
возникающих при выполнении арифметических опера-
операций. Это такие исключения, как
System.DivideByZeroException
и System.OverflowException
Происходит при попытке записать в элемент мас-
массива данные неправильного типа, отличного от ти-
типа массива
Попытка деления целочисленного значения
на нуль
Возникает при попытке использовать отрицатель-
отрицательный индекс в массиве или индекс, выходящий
за границы массива
Ошибка в процессе явного преобразования ссылки
на объект базового класса в ссылку на объект про-
производного класса
Ошибка при попытке комбинирования двух деле-
делегатов, не равных null. Возникает из-за того, что
тип возвращаемого значения делегатов отличен
от void
Попытка использования ссылки, содержимое кото-
которой равно null
Глава 9. Обработка исключений
297
Класс
System.OutOfMemory-
Exception
Sys tem.Overflow-
Exception
System.StackOverflow-
Exception
System.Typelnitia-
lizationException
Описание
Возникает, когда для выполнения операции соз-
создания объекта при помощи ключевого слова new
не хватает оперативной памяти
Переполнение при выполнении арифметической
операции
Переполнение стека. Может возникнуть при глу-
глубоких рекурсивных вызовах методов
Возникает в том случае, когда статический конст-
конструктор создает исключение, а в программе не пре-
предусмотрена его обработка при помощи блока
catch
Создание исключений
Механизм обработки ошибок С# был бы неполным, если бы программам предоставля-
предоставлялась возможностью обрабатывать исключения, но не создавать их. Как и следовало
ожидать, программы С# могут создавать (иными словами, возбуждать или передавать)
исключения при помощи ключевого слова throw.
Рассмотрим различные способы создания исключений.
Создание исключений класса Exception
Представим себе следующую ситуацию, в которой нам бы пригодилось создавать
и обрабатывать исключение.
Пусть мы определили в своей программе класс Point, предназначенный для хра-
хранения точек с положительными координатами:
class Point
{
public uint xPos;
public uint yPos;
public Point (int x, int y)
{
xPos = (uint)x;
yPos = (uint)y;
При создании нового объекта класса Point мы передаем координаты точки конст-
конструктору:
ptl = new Point@, 0);
pt2 = new Point (-1, 2) ;
298
А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Проблема заключается в том, что в этой реализации класса Point конструктор
не проверяет знак передаваемых ему координат, поэтому программа по ошибке может
создать точку с отрицательными координатами.
Конечно, конструктор может проверить знак своих параметров, но как сообщить вызы-
вызывающей программе об ошибке? Ведь конструктор не может возвращать никаких значений!
Выход — в создании исключения при обнаружении отрицательных координат
(листинг 9.5).
Листинг 9.5. Файл ch09\PositivePoint\PositivePointApp.cs
using System;
namespace PositivePoint
{
class Point
{
public uint xPos;
public uint yPos;
public Point (int x, int y)
{
if(x >= 0 && у >= 0)
{
xPos = (uint)x;
yPos = (uint)у;
}
else
throw new Exception(
"Обнаружена отрицательная координата точки");
class PositivePointApp
{
static void Main(string[] args)
{
Point ptl;
Point pt2;
try
{
ptl = new Point@, 0);
pt2 = new Point(-1, 2);
}
catch(System.Exception ex)
{
Console. WriteLine("»>» Исключение {0}", ex.ToString () ) ;
}
Console.ReadLine();
Глава 9. Обработка исключений 299
Вот новый вариант конструктора:
public Point (int x, int у)
{
if(x >= 0 && у >= 0)
{
xPos = (uint)x;
yPos = (uint)y;
}
else
throw new Exception(
"Обнаружена отрицательная координата точки");
}
Теперь при обнаружении ошибки конструктор объекта класса Point возбуждает
состояние исключения, прерывая нормальный ход выполнения программы. При этом
в качестве параметра ключевому слову throw передается ссылка на новый объект
класса Exception.
Обратите внимание, что при создании исключения мы передаем конструктору объекта
класса Exception параметр — текстовую строку описания ошибочной ситуации.
Вот как вызывающий метод обрабатывает наше исключение:
Point ptl;
Point pt2;
try
{
ptl = new Point{0, 0);
pt2 = new Point(-1, 2);
}
catch(System.Exception ex)
{
Console.WriteLine(">>>>> Исключение {0}", ex.ToString());
}
Как видите, техника обработки созданного нами исключения класса Exception
ничем не отличается от техники обработки описанных выше исключений, создавае-
создаваемых системой исполнения .Net Framework.
Новый класс на базе класса Exception
Описанная выше методика создания исключения класса Exception не позволяем пе-
передать в вызывающую программу никакой дополнительной информации, кроме тек-
текстового сообщения об ошибке.
Чтобы программа могла выполнить более полную диагностику причин возникно-
возникновения исключения, создайте на базе класса Exception собственный производный
класс. Поля такого класса могут использоваться для хранения расширенной информа-
информации об ошибке. При этом дополнительная информация может быть передана, напри-
например, через параметры конструктора класса, производного от класса Exception.
Создание собственного класса для обработки исключений позволяет выполнить
более тонкую обработку ошибок по сравнению той, что позволяет класс Except ion.
300 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Рассмотрим программу, исходный текст которой представлен в листинге 9.6.
Листинг 9.6. Файл ch09\PositivePointEx\PositivePointExApp.cs
using System;
namespace PositivePointEx
{
class PointException : Exception
{
public int xErr;
public int yErr;
public PointException(int x, int y, string errMessage)
: base(errMessage)
{
XErr = x;
yErr = y;
class Point
{
public uint xPos;
public uint yPos;
public Point (int x, int y)
{
if(x >= 0 && у >= 0)
{
xPos = (uint)x;
yPos = (uint)y;
}
else
throw new PointException(x, y,
"Обнаружена отрицательная координата точки");
class PositivePointExApp
{
static void Main(string[] args)
{
Point ptl;
Point pt2;
try
ptl = new Point@, 0);
pt2 = new Point(-1, 2);
Глава 9. Обработка исключений 301
catch(PointException ex)
{
Console.WriteLine(
">>>>> Исключение: {0}\пТочка: ({1},{2})
ex.Message, ex.xErr, ex.yErr);
Console.ReadLine();
Здесь на базе класса Exception мы создали производный класс PointExcep-
PointException:
class PointException : Exception
{
public int xErr;
public int yErr;
public PointException(int x, int y, string errMessage)
: base(errMessage}
{
xErr = X;
yErr = y;
Обратите внимание, что конструктор класса PointException вызывает конст-
конструктор базового класса Exception. Это необходимо для правильного функциониро-
функционирования системы обработки исключений.
Расширенная информация об ошибке, а именно ошибочные координаты точки,
хранятся в полях xErr и yErr класса PointException. Они инициализируются
конструктором.
При обнаружении ошибки в новом варианте конструктора класса Point мы соз-
создаем исключение класса PointException, используя для этого ключевое слово
throw:
public Point (int x, int у)
{
if(x >= 0 && у >= 0)
{
xPos = (uint)x;
yPos = (uint)y;
}
else
throw new PointException(x, y,
"Обнаружена отрицательная координата точки");
}
302 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Помимо текста сообщения об ошибке мы передаем конструктору класса
PointException дополнительную информацию — ошибочные координаты точки.
Обработчик нашего исключения извлекает из полей объекта ех класса Point-
Exception сообщение об ошибке и ошибочные координаты, отображая все это
на консоли:
try
{
ptl = new Point @, 0);
pt2 = new Point (-1, 2);
}
catch(PointException ex)
{
Console.WriteLine(">>>>> Исключение: {0}\пТочка: ({1},{2})",
ex.Message, ex.xErr, ex.yErr);
}
Таким образом, теперь наша программа в состоянии не только обнаружить ошибку,
но и определить точные причины ее возникновения, сообщив их пользователю.
Конструкторы класса Exception
Как мы уже говорили, класс Exception является базовым для всех классов
исключений С#. В классе Exception предусмотрено несколько перегруженных
конструкторов:
public Exception();
public Exception(string);
public Exception(string, Exception);
public Exception(SerializationInfo, StreamingContext);
Первый из этих конструкторов самый простой. Он предназначен для того, чтобы
возбуждать состояние исключения без текстового сообщения об ошибке.
Второй конструктор вам уже знаком — его единственный параметр предназначен
для передачи текстового сообщения в обработчик исключения.
С помощью третьего конструктора можно обрабатывать так называемые внутрен-
внутренние (inner) исключения. Этот конструктор позволяет изменить исключение, а затем пе-
передать его для дальнейшей обработки.
И наконец, четвертый конструктор позволяет инициализировать объект класса
Exception последовательными данными (такие данные будут рассмотрены позже).
В предыдущем примере программы мы, создавая производный класс Point-
Exception, переопределили в нем только второй вариант конструктора. Однако кор-
корректнее было бы предусмотреть перегрузку всех четырех вариантов, например так:
class PointException : Exception
{
public int xErr;
public int yErr;
Глава 9. Обработка исключений 303
public PointException(int x, int y)
: base (|
{
xErr = x;
yErr = y;
public PointException(int x, int y, string errMessage)
: base(errMessage)
{
XErr = X;
yErr = y;
public PointException(int x, int y, string errMessage,
Exception innerException)
: base(errMessage, Exception innerException)
{
xErr = x;
yErr = y;
public PointException{int x, int y,
Serializationlnfо si, StreamingContext sc)
: base(errMessage, si, sc)
{
xErr = x;
yErr = y;
При создании собственных классов исключений принято добавлять к имени класса
суффикс «Exception»: так будет сразу понятно назначение класса. Именно поэтому
мы и назвали свой класс PointException, а не как-нибудь еще. Хотя, разумеется,
это правило нестрогое — вы можете выбрать для названия такого класса любое имя.
Передача исключения для повторной
обработки
Как мы уже говорили, функция или метод не всегда может решить, что же делать
при возникновении ошибки. Некоторые виды ошибок могут быть обработаны в том же
самом методе, где они произошли, а некоторые — только в одном из вызывающих ме-
методов, расположенных вверх по иерархии вызовов.
На самом верхнем уровне находится обработчик исключений среды исполнения
.Net Framework, которому передаются все исключения, не обработанные в программе.
Как правило, этот обработчик просто завершает работу программы с выводом на экран
сообщения об ошибке.
304 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
В листинге 9.7 мы привели исходный текст программы, в которой обработчик ис-
исключения выполняет некие обрабатывающие действия, а затем передает исключение
вверх по иерархии.
Листинг 9.7. Файл ch09\PointExlnternal\PointExlnternalApp.cs
using System;
namespace PointExInternal
{
class Point
{
public uint xPos;
public uint yPos;
public int ratio;
public Point (int x, int y)
{
try
{
ratio - x / y;
}
catch
{
Console.WriteLine("Произошло исключение");
throw;
class PointExInternalApp
{
static void Main(string[] args)
{
Point ptl;
Point pt2;
Point pt3;
try
{
ptl = new Point@, 0);
pt2 = new Point(-1, 2);
pt3 = new PointB00, 300);
}
catch(Exception ex)
{
Console.WriteLine(">>>>> Исключение: {0}", ex.Message)
}
Console.ReadLine();
Глава 9. Обработка исключений 305
В центре нашей программы находится класс Point, представляющий точку
на плоскости координат. Дополнительно к полям xPos и yPos, хранящим координаты
точки, мы объявили в этом классе поле ratio, предназначенное для хранения отно-
отношений координат:
class Point
{
public uint xPos;
public uint yPos;
public int ratio;
Конструктор класса Point вычисляет отношение координат по осям X и Y
при помощи операции деления, выполнение которой может привести к появлению ис-
исключения:
public Point (int x, int у)
{
try
{
ratio = x / у,-
}
catch
{
Console.WriteLine("Произошло исключение");
throw ,-
Обратите внимание на обработчик исключений, предусмотренный в конструкторе
класса Point.
Этот обработчик вначале выводит на консоль сообщение об ошибке (это имитация об-
обработки ошибки внутри того метода, где возникло исключение), а затем передает исклю-
исключение в вызывающий метод при помощи ключевого слова throw без параметров.
Внутри вызывающего метода мы тоже предусмотрели обработчик исключений:
try
ptl = new Point@, 0);
pt2 = new Point(-1, 2);
pt3 = new PointB00, 300);
catch(Exception ex)
Console.WriteLine(">>>>> Исключение: {0}", ex.Message);
Если запустить нашу программу на выполнение, то на консоли появятся следую-
следующие сообщения:
306 А В. Фролов, Г. В. Фролов. Язык С# Самоучитель
Произошло исключение
>>>>> Исключение: Attempted to divide by zero.
Первое сообщение выведет обработчик исключений, предусмотренный в конструк-
конструкторе класса Point, а второе — метод Main, создающий объект этого класса (и неявно
вызывающий упомянутый конструктор).
Применение блока finally
В некоторых случаях при обработке исключений с применением блоков try и catch
имеет смысл дополнительно предусмотреть блок finally. Этот блок выполняется
всегда, вне зависимости от того, произошло ли исключение в процессе работы блока
try или нет.
Наилучшее применение для блока finally— освобождение ресурсов, заказан-
заказанных программой перед возникновением исключений. Хотя система сборки мусора ав-
автоматически освобождает ненужную более оперативную память, другие ресурсы, та-
такие, как, например, открытые потоки, связанные с файлами, следует закрывать явным
образом, вызывая соответствующие методы.
В листинге 9.8 мы привели пример программы, демонстрирующей использование
блока finally.
Листинг 9.8. Файл chO9\Finally\FinallyApp.cs
using System;
namespace Finally
{
class FinallyApp
{
static void openFile(string path)
С
Console.WriteLine("Открытке файла");
}
static void closeFileO
{
Console.WriteLine("Закрытие файла");
static void writeFile(string data)
{
Console.WriteLine("Запись в файл строки {0}", data)
int x = 0;
int у = 5 / x;
static void Main(string[] args)
FinallyApp.openFile("numbers.txt");
Глава 9. Обработка исключений 307
try
{
FinallyApp.writeFile("test");
)
catch(Exception ex)
{
Console.WriteLine{">>>>> Исключение: {О}", ex.Message)
}
finally
{
FinallyApp.closeFile();
}
Console.ReadLine() ;
Так как мы еще не рассказывали вам о работе с файлами, вместо вызова реальных
методов, предназначенных для записи в файл, мы будем пользоваться имитаторами.
Перед тем как приступить к работе с файлом, его необходимо открыть. Эта опера-
операция имитируется в нашей программе методом openFile:
static void openFile(string path)
Console.WriteLine("Открытие файла");
В качестве параметра методу передается путь к открываемому файлу, который, од-
однако, в нашем имитаторе никак не используется.
После того как файл открыт, в него можно записывать данные. Эта операция ими-
имитируется в нашей программе методом writeFi le:
static void writeFile(string data)
Console.WriteLine("Запись в файл строки {0}", data);
int x = 0;
int у = 5 / x;
Вместо записи строки, передаваемой методу в качестве единственного параметра,
этот метод отображает строку на консоли. А затем он... выполняет деление числа 5
на 0, чтобы вызвать исключение. Таким способом мы имитируем возникновение
ошибки в процессе записи данных в файл.
И наконец, после работы необходимо закрыть файл. Если этого не сделать, содер-
содержимое файла на диске будет искажено. Для имитации закрытия файла в нашей про-
программе применяется метод closeFile:
static void closeFile()
Console.WriteLine("Закрытие файла");
308 А В Фролов, Г. В. Фролов Язык С# Самоучитель
Теперь займемся самой программой.
Метод Main нашей программы открывает файл посредством метода openFile,
а затем пытается записать в него текстовую строку методом writeFile:
FinallyApp.openFile("numbers.txt");
try
{
FinallyApp.writeFile("test");
}
catch(Exception ex)
{
Console.WriteLine(">>>>> Исключение: {0}", ex.Message);
}
finally
{
FinallyApp.closeFile();
}
Однако вызов метода writeFile приведет к возникновению ошибки деления
на нуль, в результате чего управление будет передано блоку catch, обрабатывающе-
обрабатывающему ошибку.
После завершения обработки в дело включится блок finally. Он будет выполнен
в любом случае, даже при возникновении исключения в процессе записи данных
в файл. В этом нетрудно убедиться, запустив программу и посмотрев на сообщения,
которые она выведет на консоль:
Открытие файла
Запись в файл строки test
>>>>> Исключение: Attempted to divide by zero.
Закрытие файла
Как видите, вначале произошло открытие файла, затем — попытка записи данных
в файл. После этого произошло исключение, и наша программа его обработала. В кон-
конце концов файл был закрыт при помощи метода closeFile, вызванного в блоке
finally.
Глава 9. Обработка исключений 309
Глава 10. Многопоточность
Как правило, нормальный человек способен выполнять несколько задач параллельно.
Несмотря на то что врачи считают это вредным, многие любят читать во время еды
или смотреть телевизор, ухитряясь при этом реагировать на реплики окружающих.
Персональные компьютеры также могут делать одновременно несколько дел. На-
Например, пока печатается документ, можно принять почту или отредактировать другие
документы. Разумеется, чтобы такое стало возможным, ОС должна допускать парал-
параллельную работу нескольких программ.
Если ОС на это способна, она называется многопоточной (multi-threaded). При этом
работа каждой программы осуществляется в рамках одного потока исполнения,
а центральный процессор постоянно переключается с одного потока на другой. Те ОС,
которые могут в каждый данный момент времени исполнять только одну программу,
называются однопоточными (single-threaded).
В мир персональных компьютеров многопоточность вторглась относительно не-
недавно, и далеко не каждый программист умеет использовать все ее преимущества.
Во многих случаях многопоточность в целом благоприятно сказывается на производи-
производительности системы, так как во время ожидания одних задач свою работу могут выпол-
выполнять другие задачи, готовые для этого.
Например, если вы работаете в Интернете, то можете одновременно подключиться
к нескольким серверам FTP и Web, «перекачивая» сразу несколько файлов и загружая
несколько документов HTML. При этом еще можно отправлять или получать элек-
электронную почту. Общая скорость передачи данных в этом случае будет выше, чем при
поочередной работе с серверами, — пока один из них находится в состоянии ожида-
ожидания, вы будете получать данные от другого. Такое увеличение средней скорости пере-
передачи данных возможно из-за того, что через общий канал Интернета могут одновре-
одновременно проходить пакеты данных, предназначенные для различных адресатов.
Однако все это касается пользователей. А какие преимущества получат от много-
поточности программисты?
Программисты смогут создавать приложения, работающие в многопоточном ре-
режиме. При этом отдельные компоненты таких приложений смогут работать одновре-
одновременно, не мешая друг другу.
Очень часто многопоточность используется при выполнении каких-либо длитель-
длительных операций. При этом код, обеспечивающий функционирование пользовательского
интерфейса, работает в рамках одного потока, а код, выполняющий длительную опе-
операцию, — в рамках другого.
В качестве примера мы можем привести программный комплекс восстановления
данных CrashUndo Laboratory (рис. 10.1), разработанный одним из авторов этой книги
для службы восстановления данных DataRecovery.Ru (http://www.datarecovery.ru).
~Ш зю ~™~ '==
REMOTE SYSTEM COHFIGVRATTOH
OS Version: S.O B195) 3P 2.0 -- admin --
Physical drives:
N Total Mb
Sectors
0 10108
1 614 92
Logical drives:
N Drv Label
19743885
120101940
Fsys
:J7 Automatic update evay [To
Siatut Inptogren
0 A:
1 C: WINDOWS 98 FAT
2 D: W2K_PR.OF_3Y3TEM WTFS
3 E: WORK_FAT32 FAT32
4 V: Workplace NTF3
5 G: TESTVOLUME NTP8
6 H: WorlcFast NTFS
: 1560100
ТоЫгаОас 13743885
FAT partbom: 4
NTFSpacbdom: 1
Partition 1аЫе ertn»: 0
7 I: System Main W2K NTFS . j^JLj.nkiw«n.jjPitontectOrt: 1G
8 J: Archive NTFS
9 К: ТГ O-
10 1: 0 0
11 M: 0 0
[R]
^artttontearch staged •
llotful drive Drive G:\ opened:
Puc. 10.1. Пример многопоточного приложения
В окне Partition Scanner отображается ход процесса сканирования диска, выпол-
выполняемого с целью поиска уцелевших разделов. При этом для выполнения длительного
процесса сканирования запускается дополнительный поток, работающий в рамках того
же процесса, в рамках которого работает и главный поток исполнения программы.
Зачем сканировать диск в отдельном потоке?
Только так мы смогли добиться, чтобы во время длительного сканирования можно
было продолжать работу с программой CrashUndo Laboratory — следить за ходом ска-
сканирования, просматривать предварительные результаты сканирования, прерывать
процесс и т. п.
Если комплекс CrashUndo Laboratory используется для удаленного восстановления
данных, то он создает отдельные потоки для передачи управляющих команд и резуль-
результатов их исполнения через Интернет.
Работа в Интернете (особенно через модем) связана с задержками и обрывами свя-
связи, поэтому для ее выполнения создается отдельный поток. При этом нестабильность
процесса передачи данных через Интернет не будет сказываться на общей работоспо-
работоспособности комплекса.
Глава 10. Многопоточность
311
Мы рекомендуем применять многопоточность в следующих случаях:
• для выполнения длительных процедур, ходом которых нужно управлять;
• для отделения программного кода, ответственного за функционирование пользова-
пользовательского интерфейса, от кода, выполняющего какие-либо длительные операции;
• при обращении к серверам и службам Интернета, базам данных или при передаче
данных по сети;
• в случае, когда нужно одновременно выполнять несколько задач, имеющих разный
приоритет.
Многопоточность следует применять только в тех случаях, когда она действитель-
действительно нужна. Иначе ни к чему, кроме ухудшения производительности, это не приведет.
Кроме того, многопоточные приложения намного сложнее отлаживать, чем однопо-
точные. Причина этого заключается в том, что приходится следить за правильной син-
синхронизацией потоков, работающих одновременно и зависящих друг от друга.
Виды многопоточности
За всю историю существования ОС для персональных компьютеров было разработано
несколько моделей многопоточности. Это переключательная, совместная и вытес-
вытесняющая многопоточность.
Расскажем об этих моделях многопоточности в порядке их внедрения в ОС персо-
персональных компьютеров.
Переключательная многопоточность
Во времена первых персональных компьютеров, когда повсеместно наибольшей попу-
популярностью пользовалась однопоточная ОС MS-DOS, пользователю была доступна так
называемая переключательная многопоточность, основанная главным образом
на применении так называемых резидентных программ.
Резидентные программы загружались в оперативную память компьютера, остава-
оставаясь там до перезагрузки ОС. Довольно популярные в свое время резидентные кальку-
калькуляторы позволяли, например, не прерывая работы программы редактора текста или
другой программы, выполнять арифметические вычисления. Для переключения от ре-
редактирования текста к вычислениям на резидентном калькуляторе и обратно нужно
было нажать на клавиатуре ту или иную комбинацию клавиш.
К моменту появления настоящих многопоточных ОС IBM OS/2 и Microsoft Win-
Windows было создано великое множество самых разнообразных и часто несовместимых
между собой резидентных программ для MS-DOS. Среди них были как простые, так
и достаточно сложные программы, например программа Borland SideKick, выполняю-
выполняющая функции персонального органайзера.
Совместная многопоточность
Появление ОС Microsoft Windows версии 3.0, работавшей как оболочка для MS-DOS,
стимулировало появление приложений для Microsoft Windows, работавших в режиме
так называемой совместной многопоточности (cooperative multi-threading).
312 А В Фролов, Г. В Фролов. Язык С#. Самоучитель
Для реализации совместной многопоточности приложения Microsoft Windows соз-
создавались определенным образом и время от времени передавали друг другу управле-
управление. В результате создавалась иллюзия одновременной работы нескольких приложе-
приложений. Аналогичный принцип применялся в сетевой ОС Novell NetWare, а также в ОС
компьютеров Macintosh компании Apple.
Совместная многопоточность решила проблемы совместимости, которые были
слабым местом резидентных программ. Теперь пользователь мог запустить сразу не-
несколько приложений и переключаться между ними при необходимости. Многие поль-
пользователи так и делали, однако возможности многопоточности при этом фактически
не задействовались, так как приложения работали по очереди.
Несмотря на то что ОС Microsoft Windows версии 3.1 позволяет запустить, напри-
например, форматирование дискеты и на этом фоне работать с другими приложениями, едва
ли найдется много желающих поступать таким образом. Дело в том, что, пока дискета
не будет отформатирована, все остальные запущенные приложения будут работать
очень медленно.
Еще один существенный недостаток совместной многопоточности проявляется при
запуске недостаточно хорошо отлаженных приложений. Если по какой-либо причине
приложение не сможет периодически передавать управление другим запущенным
приложениям, работа всей системы будет заблокирована и пользователю останется
только нажать комбинацию из трех известных клавиш Control+Alt+Delete либо кнопку
аппаратного сброса, расположенную на корпусе компьютера.
Вытесняющая многопоточность
В отличие от совместной многопоточности вытесняющая многопоточность
(preemptive mult-threading) предполагает выделение всем запущенным приложениям
квантов времени с использованием системного таймера.
Не следует думать, что у специалистов компании Microsoft не хватило ума приме-
применить вытесняющую многопоточность в первых версиях оболочки Microsoft Windows.
Она была использована в ОС OS/2 версий 1.0 — 1.3, которая в те времена разрабаты-
разрабатывалась совместно компаниями Microsoft и IBM.
К сожалению, слабая архитектура процессора Intel 80286, недостаточная произво-
производительность выпускавшихся тогда компьютеров и малый объем оперативной памяти,
установленной в компьютерах подавляющего числа пользователей A — 2 Мбайт), по-
помешали широкому распространению OS/2. Эта ОС с истинной вытесняющей мульти-
задачностью работала очень медленно и проиграла сражение более легковесной обо-
оболочке Microsoft Windows версии 3.1. Ведь в конечном счете пользователям было все
равно, какой тип многопоточности применяется в ОС, их интересовала скорость
и удобство работы.
Сегодня ситуация изменилась. Современные ОС для персональных компьютеров,
такие, как Microsoft Windows 95/98/МЕ, Microsoft Windows NT/2000/XP, IBM OS/2
Warp (впрочем, эту ОС уже не назовешь современной...), а также набирающая попу-
популярность ОС Linux работают в режиме истинной вытесняющей многопоточности.
Глава 10 Многопоточность 313
Все приложения, запущенные в среде таких ОС, гарантированно получают для себя
кванты времени по прерыванию от таймера. При этом накладные расходы на многопо-
точность компенсируются более разумным использованием ресурсов и высокой про-
производительностью компьютеров, поэтому пользователь их не почувствует (конечно,
если в компьютере установлена оперативная память достаточного объема).
Пользователи многопоточных ОС получили возможность не просто переключаться
с одной задачи на другую, а реально работать одновременно с несколькими активными
приложениями. Программисты же получили в свои руки новый инструмент, с помо-
помощью которого они могут реализовать многопоточную обработку данных, не заостряя
на этом внимания пользователя. Например, в процессе редактирования документа тек-
текстовый процессор может заниматься нумерацией листов или подготовкой документа
для печати на принтере.
Создавая программы С# для ОС Microsoft Windows NT/2000/XP, вы можете реали-
реализовать в них все преимущества многопоточности, причем, как вы скоро увидите, это
можно сделать относительно легко.
Процессы, потоки и приоритеты
Прежде чем приступить к описанию практических приемов применения многопоточ-
многопоточности в программах С#, следует уточнить некоторые термины. Обычно в любой мно-
многопоточной ОС выделяют такие объекты, как процессы и потоки. Между ними суще-
существует большая разница, которую следует четко себе представлять. В системе испол-
исполнения .Net Framework добавилось еще одно новое понятие— домен приложения,
представленный классом System.AppDoraain.
Процесс
Процесс (process) ОС — это объект, который создается ОС, когда пользователь запус-
запускает приложение. Процессу выделяется отдельное адресное пространство, причем это
пространство физически недоступно для других процессов.
Процесс может работать с файлами или с другими процессами через каналы, соз-
созданные ОС в оперативной памяти, в локальной сети или через Интернет. Когда в среде
Microsoft Windows вы запускаете текстовый процессор Microsoft Word for Windows
или программу калькулятора, вы тем самым создаете новый процесс.
Поток
Для каждого процесса ОС создает один главный поток (thread), который является на-
набором выполняющихся по очереди команд центрального процессора. При необходи-
необходимости главный поток может создавать другие потоки, пользуясь для этого программ-
программным интерфейсом ОС (Application Program Interface, API).
Все потоки, созданные процессом, выполняются в адресном пространстве этого
процесса и имеют доступ к ресурсам процесса. Однако поток одного процесса не име-
имеет никакого доступа к ресурсам задачи другого процесса, так как они работают в раз-
разных адресных пространствах. При необходимости взаимодействия между процессами
или потоками, принадлежащими разным процессам, следует пользоваться специально
предназначенными для этого системными средствами.
314 А В Фролов, Г. В. Фролов. Язык С# Самоучитель
Домен приложения AppDomain
Выше мы перечислили понятия и термины, применяющиеся при создании многопо-
многопоточных приложений для ОС Microsoft Windows. Здесь имеются в виду обычные ис-
исполняемые приложения. Что же касается программ С#, то, как мы уже говорили в на-
начале этой книги, они выполняются не сами по себе, а под управлением системы .Net
Framework. Эта же система отвечает и за реализацию многопоточности в приложени-
приложениях, написанных с использованием языка С#.
При этом .Net Framework может создавать на базе одного системного процесса не-
несколько вложенных логических процессов, называемых доменами приложений (appli-
(application domains).
Управляемые потоки С# (т. е. находящиеся под управлением Microsoft .NET Frame-
Framework) могут работать в рамках одного или нескольких доменов приложений и принад-
принадлежать при этом одному системному процессу.
При запуске любого приложения С# вначале создается один поток исполнения, ра-
работающий в рамках домена приложения. При необходимости программный код, ис-
исполняемый в рамках одного домена приложения, может создавать дополнительные
потоки и дополнительные домены приложений. Поэтому управляемые потоки С# мо-
могут пересекать границы доменов приложения, если, конечно, они остаются в пределах
одного системного процесса. Это позволяет использовать один и тот же поток в рам-
рамках нескольких доменов приложений.
Примеры многопоточных программ
Чтобы использовать многопоточность, обычные программы, составленные на таких
языках программирования, как C++ или Pascal, обращаются к программному интер-
интерфейсу ОС. Что же касается программ С#, то в них многопоточность реализуется с по-
помощью набора классов, входящих в библиотеку классов Microsoft .NET Framework.
Создание и запуск потока класса Thread
В нашей первой многопоточной программе мы будем работать с классом Sys-
System. Thread, объявленным в пространстве имен System. Threading.
Исходный текст программы очень прост и представлен в листинге 10.1.
Листинг 10.1. Файл ch10\ThreadDemo\ThreadDemoApp.cs
using System;
using System.Threading;
namespace ThreadDemo
{
class ThreadDemoApp
{
public static void MyThread()
{
Console.WriteLine("MyThread: поток запущен");
Глава 10. Многопоточность 315
[STAThread]
static void Main(string[] args)
ThreadStart myThreadDelegate = new ThreadStart(MyThread);
Thread thr = new Thread(myThreadDelegate);
Console.WriteLine("Запуск потока MyThread");
thr.Start();
Console.ReadLine();
}
}
Прежде всего обратите внимание на использование пространства имен Sys-
System .Threading:
using System.Threading;
Мы подключили его с помощью ключевого слова using.
В главном классе приложения ThreadDemoApp, где находится метод Main,
мы объявили статический метод MyThread, который будет работать в рамках отдель-
отдельного потока:
public static void MyThread()
Console.WriteLine("MyThread: поток запущен");
Единственная задача метода MyThread — вывод на консоль текстовой строки.
Что же касается метода Main, то он запускает метод MyThread в отдельном пото-
потоке, отображая перед этим сообщение на консоли, дожидается ввода произвольной
строки с клавиатуры, после чего завершает свою работу:
ThreadStart myThreadDelegate = new ThreadStart(MyThread);
Thread thr = new Thread(myThreadDelegate);
Console.WriteLine("Запуск потока MyThread");
thr.Start();
Console.ReadLine() ;
После запуска этой программы на консоли отображаются следующие строки:
Запуск потока MyThread
MyThread: поток запущен
Как все это работает?
Посмотрев внимательно на исходный текст метода Main, можно заметить, что
в нем нет прямого вызова метода MyThread. Вместо этого для метода MyThread соз-
создается так называемый делегат (delegate) myThreadDelegate типа ThreadStart
(о том, что это такое, мы расскажем чуть позже):
ThreadStart myThreadDelegate = new ThreadStart(MyThread);
316 А В Фролов, Г. В. Фролов. Язык С# Самоучитель
Конструктору Threads tart передается имя метода, используемого для создания
делегата. Далее с помощью этого делегата создается поток класса Thread:
Thread thr = new Thread(myThreadDelegate);
И наконец, созданный поток запускается при помощи метода Start, определенно-
определенного в классе Thread:
thr.Start();
Сразу после вызова метода Start начнется работа метода MyThread, о чем мож-
можно судить по сообщениям, появляющимся на консоли. Заметим, что основная про-
программа и созданный ей дополнительный поток работают одновременно и совершенно
независимо друг от друга.
Прежде чем мы продолжим дальнейшее изучение многопоточности, необходимо
сделать некоторые разъяснения по поводу назначения делегатов, являющихся одной
из особенностей языка С#.
Использование делегатов
Когда мы рассказывали о размещении переменных в оперативной памяти компьютера,
то говорили, что каждая такая переменная имеет свой адрес и размер. Используя меха-
механизм указателей, программы C++ могут напрямую адресоваться к областям оператив-
оперативной памяти, выделенным для переменных.
Аналогичная возможность прямой адресации имеется в языке C++ для функций.
Функции и методы тоже размещаются в оперативной памяти по определенным адре-
адресам. Записав эти адреса в переменные, называемые указателями на функции или мето-
методы, программы C++ могут передавать им управление.
Далее, программы C++ могут передавать указатели на функции другим функциям
и методам. При этом функция, используя полученный указатель на другую функцию,
может ее вызвать. Функции, вызываемые подобным образом через указатели, называ-
называются функциями обратного вызова.
Вы также знаете, что неправильное применение указателей может привести к появ-
появлению трудно обнаруживаемых ошибок. В самом деле, если перед использованием со-
содержимое указателя на переменную или функцию будет задано неправильно, послед-
последствия окажутся непредсказуемыми. В немалой степени из-за этого обстоятельства раз-
разработчики С# отказались от использования указателей.
Тем не менее потребность в функциях обратного вызова осталась. Например,
при использовании многопоточности нужно каким-то образом указать системе .Net
Framework метод, который будет работать в рамках отдельного потока. Функции об-
обратного вызова необходимы и для обработки событий (events), о которых мы будем
рассказывать позже.
Для реализации безопасных указателей на методы в языке С# был разработан ме-
механизм делегатов. В роли делегата может выступать статический метод класса или
статическое свойство.
Обычно метод-делегат объявляется с помощью ключевого слова delegate, одна-
однако в нашем случае для реализации многопоточности оно не потребовалось:
Глава 10. Многопоточность 317
public static void MyThread()
{
Console.WriteLine("MyThread: поток запущен");
)
Мы создаем делегат из обычного статического метода, пользуясь для этого классом
ThreadStart:
ThreadStart myThreadDelegate = new ThreadStart(MyThread);
После выполнения этой строки в переменную myThreadDelegate будет записан
делегат — ссылка на метод MyThread. Эта ссылка реализована без применения ука-
указателей и потому безопасна в использовании. Она может ссылаться (или, если хотите,
«указывать») только на реально существующий объект класса ThreadStart.
По своему назначению метод MyThread является функцией обратного вызова.
Наша программа (т. е. метод Main) никогда не вызывает этот метод напрямую. Вместо
этого она создает для метода MyThread делегат myThreadDelegate, а затем пере-
передает этот делегат конструктору класса Thread:
Thread thr = new Thread(myThreadDelegate);
Конструктор класса Thread создает новый поток, а наша программа записывает
ссылку на этот поток в переменную thr. В дальнейшем с помощью этой ссылки про-
программа может управлять созданным потоком. Для запуска потока на выполнение, на-
например, используется метод Start, определенный в классе Thread:
thr.Start();
Модели многопоточности
Внимательный читатель заметит, что, помимо делегатов, мы использовали в преды-
предыдущей программе еще одну новую конструкцию С#, а именно атрибут (attribute)
STAThread, задающий так называемую модель многопоточности Single Thread Apart-
Apartments (STA):
[STAThread]
static void Main(string[] args)
Если говорить упрощенно, то атрибуты языка С# позволяют определить характери-
характеристики объектов, перед которыми они расположены. В данном случае атрибут
STAThread относится к методу Main, определяя модель многопоточности, в которой
он будет работать при запуске программы.
Программисты познакомились с моделями многопоточности, когда компания Mi-
Microsoft приступила к внедрению в практику своей модели компонентных объектов
(Component Object Model, COM), а также технологии ActiveX. Детальное описание
этих технологий выходит за рамки нашей книги. Читателей, интересующихся данным
вопросом, мы отсылаем к [8] и [9]. Примеры создания компонентов ActiveX для Web-
приложений мы описали в [2].
318 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Говоря кратко, реализация многопоточности в компонентах СОМ и ActiveX, реали-
реализующих визуальный пользовательский интерфейс, требует учета ряда особенностей.
Это, например, наличие механизма обмена сообщениями, с применением которого
реализован пользовательский интерфейс стандартных приложений Microsoft Windows.
С целью обеспечения передачи данных между потоками различных компонентов
и синхронизации их работы был создан механизм разделов, или апартаментов (apart-
(apartments). В рамках этого механизма были определены различные модели многопоточно-
многопоточности, отличающиеся друг от друга способом взаимодействия компонентов, а также спо-
способом синхронизации их работы:
• модель разделенных потоков (apartment threading);
• модель свободных потоков (free threading).
Если используется модель разделенных потоков, то заботу о синхронизации вызо-
вызовов компонентов берет на себя система СОМ, а если модель свободных потоков —
то проблемы синхронизации и обеспечения передачи данных между компонентами
ложатся на сами эти компоненты.
Модель разделенных потоков, в свою очередь, членится на две подмодели:
• однопоточную модель (single-threaded apartment, STA);
• многопоточную модель (multi-threaded apartments, MTA).
Возвращаясь к программам С#, заметим, что они делятся на консольные програм-
программы (к ним относятся все программы, приведенные в нашей книге), а также на про-
программы с графическим пользовательским интерфейсом. Последние из них создаются
с применением классов Windows Forms, входящих в состав Microsoft .NET Framework.
Так вот, программы С# с графическим пользовательским интерфейсом, созданные
на базе классов Windows Forms, должны работать в однопоточной модели STA.
Это происходит потому, что классы Windows Forms пользуются стандартным интер-
интерфейсом ОС Windows, предполагающим применение модели разделенных потоков.
Что же касается других программ С#, то они могут создаваться на базе модели сво-
свободных потоков.
Когда вы создаете новый проект С# при помощи визуального средства разработки
программ Microsoft Visual Studio .NET, то мастер проекта автоматически добавляет
определение атрибута STAThread перед объявлением метода Main. В предыдущих
примерах программ мы игнорировали этот атрибут, так как для однопоточных кон-
консольных программ его наличие несущественно.
Завершение работы созданного потока
В предыдущем примере программы мы создавали поток, который выводил сообщение
на консоль, после чего сразу же завершал свою работу. Реально, как мы уже говорили,
в отдельном потоке обычно выполняется какая-либо длительная процедура.
В следующей многопоточной программе (листинг 10.2) мы приведем подобный
пример.
Глава 10 Многопоточность 319
Листинг 10.2. Файл ch10\LoopThread\LoopThreadApp.cs
using System;
using System.Threading;
namespace LoopThread
{
class LoopThreadApp
{
static bool stopThread;
public static void MyThread()
{
for(int i = 0;; i++)
{
if(stopThread)
break;
Console.WriteLine("MyThread: {0}", i);
Thread.SleepB000);
}
Console.WriteLine("Поток MyThread остановлен");
[STAThread]
static void Main(string[] args)
{
ThreadStart myThreadDelegate = new ThreadStart(MyThread)
Thread thr = new Thread(myThreadDelegate);
Console.WriteLine("Запуск потока MyThread");
stopThread = false;
thr. Start () ,•
string str;
do
{
Console.WriteLine("Команда (х -- выход): ");
str = Console.ReadLine();
Console.WriteLine("основной поток: {0}", str);
} while (str != "x");
stopThread = true;
Console.ReadLine();
Метод, предназначенный для работы в отдельном потоке, содержит внутри себя
бесконечный цикл, отображающий на консоли постоянно возрастающее значение пе-
переменной i:
320 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
static bool stopThread;
public static void MyThread()
{
for(int i = 0;;
if (stopThread)
break;
Console.WriteLine("MyThread: {0}", i);
Thread.SleepB000)■
Console.WriteLine("Поток MyThread остановлен");
}
Условие выхода из цикла— равенство содержимого поля stopThread значению
true. Заметим, однако, что метод MyThread сам по себе не изменяет содержимое
этого поля, поэтому без посторонней «помощи» цикл будет работать бесконечно долго
(ну хорошо до перезагрузки компьютера).
Обратите также внимание на то, что внутри цикла мы вызываем статический метод
Thread. Sleep. Он приостанавливает работу потока, в рамках которого работает метод
MyThread, на 2000 мс. Поток приостанавливается таким образом, чтобы не уменьшать
ресурсы процессора
Рассмотрим теперь исходный текст метода Main, управляющего работой нашего
потока.
Прежде всего этот метод создает на базе метода MyThread делегат, передавая его
конструктору класса Thread:
ThreadStart myThreadDelegate = new ThreadStart(MyThread);
Thread thr = new Thread(myThreadDelegate);
Далее метод запускает поток на выполнение, записывая перед этим значение
false в поле stopThread:
Console.WriteLine("Запуск потока MyThread");
stopThread = false;
thr.Start();
В результате метод MyThread будет запущен как отдельный поток, работающий
параллельно с потоком, в рамках которого выполняется метод Main нашей програм-
программы. О работе этого потока можно судить по строкам вида MyThread:x, появляющим-
появляющимся на консоли, где х — постоянно возрастающее число:
Запуск потока MyThread
Команда (х -- выход):
MyThread: 0
MyThread: 1
Глава 10. Многопоточность 321
11 Язык С# Самоучитель
После запуска потока для метода MyThread метод Main не завершает свою рабо-
работу, а организует выполнение собственного цикла:
string str;
do
{
Console.WriteLine("Команда (x -- выход): ");
str = Console.ReadLine();
Console.WriteLine("основной поток: {0)", str);
} while (str != "x");
В этом цикле программа вводит текстовые команды с клавиатуры, отображая их
на консоли в следующем виде:
основной поток: <команда>
Условие завершения этого цикла — ввод строки q. В этом случае наша программа
записывает в поле stopThread значение true, что приводит к остановке потока
MyThread:
stopThread = true;
Console.ReadLine();
Таким образом, после запуска программы за право вывести сообщения на консоль
конкурируют два потока:
Запуск потока MyThread
Команда (х -- выход):
MyThread: 0
MyThread: 1
q
основной поток: q
Команда (х -- выход):
MyThread: 2
rMyThread: 3
основной поток: г
Команда (х -- выход):
MyThread: 4
ret
основной поток: ret
Команда (х -- выход) :
MyThread: 5
MyThread: 6
MyThread: 7
MyThread: 8
х
основной поток: х
Поток MyThread остановлен
В результате сообщения перемешиваются между собой. При вводе команды х оба
потока (главный, созданный для метода Main, а также поток, запущенный методом
Main) завершают свою работу.
322 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Потоки и классы
В предыдущих примерах программ мы запускали в отдельном потоке статический ме-
метод основного класса приложения, т. е. того класса, в котором объявлен метод Main.
Однако в ряде случаев было бы удобнее создавать объекты на базе классов С#, у кото-
которых один или несколько методов работали бы в отдельном потоке. Такие объекты
могли бы «жить» своей жизнью, работая одновременно друг с другом, а также с глав-
главным потоком программы.
Исходный текст подобной программы мы привели в листинге 10.3.
Листинг 10.3. Файл ch10\TwoTreads\TwoTreadsApp.cs
■using System;
using System.Threading;
namespace TwoTreads
{
class StringWriter
{
private string str;
private bool stopThread;
private ThreadStart myThreadDelegate;
private Thread thr;
public StringWriter(string s)
{
str = s;
stopThread = false;
public void WriteThread()
{
for (;;)
{
if(stopThread)
break;
Console.Write(str);
Thread.SleepE00);
}
Console.WriteLine("Поток WriteThread остановлен");
}
public void go()
{
myThreadDelegate = new ThreadStart(WriteThread);
thr = new Thread{myThreadDelegate);
thr.Start ();
}
Глава 10. Многопоточность 323
public void stop()
{
stopThread = true;
}
}
class TwoTreadsApp
{
[STAThread]
static void Main(string[] args)
{
StringWriter swl = new StringWriter("+");
StringWriter sw2 = new StringWriter("-");
Console.WriteLine("Потоки запущены. Нажмите Enter
для остановки потоков.");
swl.go();
Thread.SleepB50) ;
sw2.go ();
Console.ReadLine();
swl.stop();
sw2.stop();
Console.ReadLine();
В этой программе мы определили класс StringWriter, объекты которого цикли-
циклически выводят на консоль текстовую строку, переданную конструктору:
class StringWriter
{
private string str;
private bool stopThread;
private ThreadStart myThreadDelegate;
private Thread thr;
public StringWriter(string s)
{
str = s;
stopThread = false;
Конструктор сохраняет строку, предназначенную для циклического вывода на кон-
консоль, в поле str класса StringWriter.
324 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Процедура циклического вывода, предназначенная для работы в отдельном потоке,
не имеет никаких особенностей, за исключением того, что она теперь нестатическая:
public void WriteThread()
{
for(;;)
{
if(stopThread)
break;
Console.Write(str) ;
Thread.SleepE00);
}
Console.WriteLine("Поток WriteThread остановлен");
}
Так как в своей программе мы собираемся создавать несколько разных объектов
класса StringWriter, то нам пришлось избавиться от статических полей и методов.
Если бы, например, поле str было статическим, все объекты класса StringWriter
выводили бы на консоль одну и ту же строку. Причина этого в том, что для всех таких
объектов был бы создан единственный экземпляр статического поля str.
Чтобы запустить в отдельном потоке метод WriteThread, мы используем метод
до, объявленный в классе StringWriter следующим образом:
public void go()
{
myThreadDelegate = new ThreadStart(WriteThread);
thr = new Thread(myThreadDelegate);
thr.Start();
}
Этот метод создает делегат myThreadDelegate для метода WriteThread, а по-
потом на его базе создает поток класса Thread. Далее поток запускается при помощи
метода Start.
Метод stop класса StringWriter записывает значение true в поле stop-
Thread:
public void stop()
{
stopThread = true;
}
Это приводит к тому, что поток метода WriteThread прекращает свою работу,
так как прерывается бесконечный цикл, определенный в этом методе.
Рассмотрим теперь метод Main.
В этом методе мы создаем два объекта класса StringWriter, первый из которых
будет выводить на консоль в бесконечном цикле символ +, а второй — символ -:
StringWriter swl = new StringWriter("+");
StringWriter sw2 = new StringWriter("-");
Глава 10. Многопоточность 325
Однако для того, чтобы наши объекты заработали, нужно их не только создать,
но и запустить потоки на выполнение. Мы делаем это при помощи метода до, объяв-
объявленного в классе StringWriter:
swl.go();
Thread.SleepB50);
sw2.go();
Здесь мы сначала «запускаем» первый объект, а затем, с задержкой 250 мс, второй.
Далее главный поток программы (т. е. метод Main) переходит в состояние ожидания
ввода строки с клавиатуры:
Console.ReadLine();
Если в этот момент нажать клавишу Enter, метод Main остановит вывод символов
на консоль, вызвав метод stop для каждого из созданных объектов класса
StringWriter:
swl.stop();
sw2.stop();
Вот что вы увидите на консоли:
Потоки запущены. Нажмите Enter для остановки потоков.
^—1-_ + _н—1._ + _.|—h-н—|-_ц—i—i—1._.|—i—у
Поток WriteThread остановлен
Поток WriteThread остановлен
Обратите внимание, что в этом примере программы мы полностью скрыли от ме-
метода Main тот факт, что в классе StringWriter применяются средства многопоточ-
ности. Это упрощает работу с объектами класса StringWriter, делает код метода
Main проще и понятнее.
Управление потоками
Итак, теперь мы научились создавать потоки и запускать их на выполнение. Теперь мы
рассмотрим средства управления потоками более подробно.
Аварийное завершение потока
В предыдущих примерах программ создаваемые нами потоки завершались, «умирая
естественной смертью». Поток останавливался в момент выхода из метода, на базе ко-
которого этот поток был создан.
Помимо этого, существует и другая возможность. Запущенный поток можно за-
завершить аварийно, вызвав для этого метод Abort, определенный в классе Thread.
При этом возникнет исключение System.Threading.ThreadAbortException,
в результате чего нормальная работа потока будет прервана.
Аварийная остановка потоков демонстрируется в программе, исходный текст кото-
которой приведен в листинге 10.4.
326 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Листинг 10.4. Файл ch10\AbortThread\AbortThreadApp.cs
using System;
using System.Threading;
namespace AbortThread
{
class StringWriter
{
private string str;
private bool stopThread;
private ThreadStart myThreadDelegate;
private Thread thr;
public StringWriter(string s)
{
str = s;
stopThread = false;
public void WriteThread()
{
try
{
for(;;)
{
if(stopThread)
break;
Console.Write(str) ;
Thread.SleepE00);
}
Console.WriteLine("Поток WriteThread остановлен");
}
catch(System.Exception ex)
{
Console.WriteLine("Исключение: {0}", ex.ToString());
}
finally
{
Console.WriteLine(
"Аварийная остановка потока WriteThread");
}
}
public void go()
{
myThreadDelegate = new ThreadStart(WriteThread);
thr = new Thread(myThreadDelegate);
thr.Start();
}
Глава 10. Многопоточность 327
public void stop()
{
stopThread = true;
}
public void abort()
{
thr.Abort();
}
}
class AbortThreadApp
{
[STAThread]
static void Main(string[] args)
{
StringWriter swl = new StringWriter("+");
StringWriter sw2 = new StringWriter("-");
Console.WriteLine("Потоки запущены. Нажмите Enter
для аварийной остановки потоков.");
swl.go();
Thread.SleepB50);
sw2.go();
Console.ReadLine();
swl.abort();
sw2.abort();
Console.ReadLine();
Обратите внимания на изменения, которые мы внесли в исходный текст метода
WriteThread по сравнению с предыдущей программой:
public void WriteThread()
{
try
{
for(;;)
(
i f(s topThread)
break;
Console.Write(str);
Thread.SleepE00);
)
328 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Console.WriteLine("Поток WriteThread остановлен");
}
catch(System.Exception ex)
{
Console.WriteLine("Исключение: {0}", ex.ToString());
}
finally
{
Console.WriteLine("Аварийная остановка потока WriteThread");
Теперь все свои действия поток выполняет внутри блока try.
Когда родительский поток (т. е. поток, создавший дочерний поток на базе метода
WriteThread) завершает работу нашего потока аварийно, то, как мы только что го-
говорили, возникает исключение System. Threading. ThreadAbortException.
Наш метод перехватывает это исключение, выдавая на консоль соответствующие
сообщения:
Потоки запущены. Нажмите Enter для аварийной остановки потоков.
-Исключение: System.Threading.ThreadAbortException: Thread was being
aborted.
at System.Threading.Thread.Sleep(Int32 millisecondsTimeout)
at AbortThread.StringWriter.WriteThread() in h:\[beginner c#
book]\src\chlO\a
bortthread\abortthreadapp.cs:line 28
Аварийная остановка потока WriteThread
Исключение: System.Threading.ThreadAbortException: Thread was being
aborted.
at System.Threading.Thread.Sleep(Int32 millisecondsTimeout)
at AbortThread.StringWriter.WriteThread() in h:\[beginner c#
book]\src\chlO\a
bortthread\abortthreadapp.cs:line 28
Аварийная остановка потока WriteThread
Зачем нужно перехватывать исключение при аварийном завершении потока?
Дело в том, что внутри потока могут использоваться различные ресурсы, требую-
требующие явного освобождения. Это такие ресурсы, например, как открытые файлы, базы
данных, сетевые соединения и т. п. Хотя система сборки мусора, встроенная в среду
исполнения программ С#, автоматически освобождает ненужные более блоки опера-
оперативной памяти, она не в состоянии автоматически закрыть файл или базу данных.
В каком блоке лучше освобождать ресурсы: в блоке catch или в блоке finally?
Блок catch нужно использовать в том случае, когда поток по какой-то причине
не может немедленно завершить свою работу. В этом случае он должен отменить ава-
аварийное завершение, вызвав метод Thread. ResetAbort.
Если не вызывать метод Thread.ResetAbort в блоке catch при обработке
исключения ThreadAbortException, то это исключение возникнет повторно.
Глава 10. Многопоточность 329
Что же касается освобождения ресурсов при аварийном завершении потока, то для
этого лучше всего использовать блок finally.
Заметим, что аварийное завершение работы потока должно использоваться только
при необходимости. В нормальном режиме работы потоки должны завершаться сами,
например, как в программе, исходный текст которой был приведен в листинге 10.3.
Пауза в работе потока
В предыдущих примерах программ мы вызывали метод Sleep для приостановки ра-
работы потока на какое-то время.
В классе Thread имеется два перегруженных метода Sleep. Первый из этих ме-
методов принимает в качестве единственного аргумента значение интервала задержки
в миллисекундах, а второй — ссылку на объект класса TimeSpan.
Класс TimeSpan удобен для форматного интервала времени. В нем предусмотре-
предусмотрено несколько конструкторов:
public TimeSpan(long);
public TimeSpan(int, int, int);
public TimeSpan(int, int, int, int);
public TimeSpan(int, int, int, int, int);
Первый из этих конструкторов позволяет задавать период времени в интервалах
работы системного таймера A00 не). С помощью второго можно задавать интервал в
часах, минутах и секундах. Третий позволяет указывать количество дней, часов, минут
и секунд, а четвертый — количество дней, часов, минут, секунд и миллисекунд.
Ниже мы показали пример использования второго варианта перегруженного мето-
метода Sleep для задержки потока на 2 ч, 1 мин и 10 с:
int hour = 2;
int min = 1;
int sec = 10;
Thread.Sleep(new TimeSpan(hour, min, sec));
При необходимости вы можете остановить работу потока навсегда, указав в каче-
качестве интервала задержки методу Sleep константу System.Threading.Time-
System.Threading.Timeout . Infinite:
Thread.Sleep(System.Threading.Timeout.Infinite);
Если работа метода остановлена указанным выше способом, то единственное,
что можно сделать с потоком, — это завершить его работу аварийно методом
Thread.Abort.
Рассмотрим программу, исходный текст которой представлен в листинге 10.5.
Листинг 10.5. Файл ch10\Timeout\TimeoutApp.cs
using System;
using System.Threading;
class StringWriter
private string str;
private Thread thr;
330 А В. Фролов. Г. В. Фролов. Язык С#. Самоучитель
public StringWriter(string s)
{
str = s;
}
public void WriteThread()
{
Console.WriteLine("Поток запущен");
Console.Write(str) ;
try
{
// Thread.Sleep(new TimeSpan(O, 0, 10));
Thread.Sleep(System.Threading.Timeout.Infinite);
}
catch(System.Exception ex)
{
Console.WriteLine("Исключение: {0}", ex.ToString());
}
}
public void go()
{
thr = new Thread(new ThreadStart(WriteThread));
thr . Start () ;
}
public void abort()
{
thr.Abort();
}
}
namespace Timeout
{
class TimeoutApp
{
[STAThread]
static void Main(string[] args)
{
StringWriter sw = new StringWriter("+");
Console.WriteLine("Нажмите Enter для остановки потока.")
sw.go();
Console.ReadLine();
sw.abort();
Console.ReadLine();
Глава 10. Многопоточность 331
Здесь метод WriteThread работает в рамках отдельного потока:
public void WriteThread()
{
Console.WriteLine("Поток запущен");
Console.Write(str);
try
{
Thread.Sleep(System.Threading.Timeout.Infinite);
}
catch(System.Exception ex)
{
Console.WriteLine("Исключение: {0}", ex.ToString());
Получив управление, метод WriteThread выводит сообщение на консоль, а по-
потом останавливает свою работу на бесконечный период времени.
Что же касается метода Main, то он запускает поток на выполнение, а потом пре-
прерывает его работу вызовом метода abort:
StringWriter sw = new StringWriter("+");
Console.WriteLine("Нажмите Enter для остановки потока.");
sw.go();
Console.ReadLine();
sw.abort();
В результате на консоли появляется сообщение о возникновении уже известного
вам исключения System.Threading.ThreadAbortException:
Нажмите Enter для остановки потока.
Поток запущен
+
Исключение: System.Threading.ThreadAbortException: Thread was being
aborted.
at System.Threading.Thread.Sleep(Int32 millisecondsTimeout)
at StringWriter.WriteThread() in h:\[beginner c#
book]\src\chlO\timeout\timeo
utapp.cs:line 22
Таким образом, поток, приостановивший свою работу на некоторое время (или на-
навсегда), можно прервать. Если предусмотреть соответствующий обработчик исключе-
исключения, этот поток сможет выполнить перед завершением своей работы какие-либо фи-
финальные действия, например освободить ненужные более ресурсы, закрыть открытые
ранее файлы, базы данных и т. п.
Обратите внимание еще на одно новшество в нашей программе. Оно касается ме-
метода до класса StringWriter:
332 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
public void go()
{
thr = new Thread(new ThreadStart(WriteThread));
thr.Start ( ) ;
}
Так как делегат, созданный нами на базе метода WriteThread, нужен только
один раз для создания нового объекта класса Thread, мы немного упростили класс
StringWriter и метод WriteThread. Упрощение заключается в том, что мы те-
теперь не храним делегат в отдельном поле класса, а сразу после создания передаем его
конструктору класса Thread.
Приостановка и возобновление работы
Запустив поток на выполнение, программа может временно приостановить его работу,
а затем продолжить работу потока с прерванного места.
Для того чтобы приостановить поток, необходимо использовать метод Thread.
Suspend. Продолжение работы потока выполняется методом Thread. Resume.
В листинге 10.6 мы привели пример программы, демонстрирующей использование
упомянутых выше методов.
Листинг 10.6. Файл ch10\StartStop\StartStopApp.cs
using System;
using System.Threading;
class StringWriter
{
private string str;
private bool stopThread;
private Thread thr;
public StringWriter(string s)
{
str = s;
stopThread = false;
public void WriteThread()
{
for ( ; ; )
{
if(stopThread)
break;
Console.Write(str);
Thread.SleepE00);
}
Console.WriteLine("Поток WriteThread остановлен");
}
Глава 10. Многопоточность 333
public void go()
thr = new Thread(new ThreadStart(WriteThread));
thr.Start();
public void stopO
stopThread = true;
public void abort()
thr.Abort();
public void Suspend()
thr.Suspend();
public void Resume()
thr.Resume();
namespace StartStop
class StartStopApp
[STAThread]
static void Main(string[] args)
StringWriter swl = new StringWriter("+");
StringWriter sw2 = new StringWriter("-");
swl.go();
Thread.SleepB50);
sw2.go();
Console.WriteLine("Потоки запущены. Нажмите Enter
для приостановки потоков. ") ;
Console.ReadLine();
swl. Suspend () ,-
sw2.Suspend();
Console.WriteLine("Потоки приостановлены. Нажмите Enter
для возобновления работы потоков.");
Console.ReadLine();
334 А. В. Фролов, Г В Фролов. Язык С#. Самоучитель
swl.Resume() ;
sw2.Resume() ;
Console.WriteLine("Работа возобновлена. Нажмите Enter
для остановки потоков. ") ;
Console.ReadLine();
swl.stop();
sw2.stop();
Console.ReadLine();
Для приостановки и последующего возобновления работы потока мы предусмотре-
предусмотрели в классе StringWriter два метода:
public void Suspend()
{
thr.Suspend();
}
public void Resume()
{
thr.Resume();
}
Сразу после запуска главный поток нашей программы, работающий в рамках мето-
метода Main, создает два объекта класса StringWriter и запускает в каждом из них по-
потоки, отображающие символы на консоли:
StringWriter swl = new StringWriter("+");
StringWriter sw2 = new StringWriter("-");
swl.go();
Thread.SleepB50);
sw2.go ();
Далее наша программа ожидает, пока пользователь не нажмет клавишу Enter, после
чего работа потоков приостанавливается:
Console.WriteLine(
"Потоки запущены. Нажмите Enter для приостановки потоков.");
Console.ReadLine();
swl.Suspend();
sw2.Suspend();
Глава 10. Многопоточность 335
Чтобы вновь возобновить работу потоков, нужно снова нажать клавишу Enter:
Console.WriteLine ("Потоки приостановлены. Нажмите Enter для
возобновления работы потоков.");
Console.ReadLine();
swl.Resume();
sw2.Resume();
И наконец, нажав на клавишу Enter в третий раз, вы окончательно завершите рабо-
работу потоков методом stop:
Console.WriteLine(
"Работа возобновлена. Нажмите Enter для остановки потоков.");
Console.ReadLine();
swl.stop () ;
sw2.stop();
Вот что появится на консоли:
+Потоки запущены. Нажмите Enter для приостановки потоков.
- + -н—i—i—i—i—i—I-
-Потоки приостановлены. Нажмите Enter для возобновления работы
потоков.
Работа возобновлена. Нажмите Enter для остановки потоков.
+ - + - + -Н Н-+-+-+-+-+-
Поток WriteThread остановлен
Поток WriteThread остановлен
Таким образом, теперь вы знаете, что поток-родитель может прерывать, временно
приостанавливать и возобновлять работу созданных им дочерних потоков. Есть и еще
одна возможность управления потоками — изменение их приоритетов.
Управление приоритетами потоков
Если процесс создал несколько потоков, то все они выполняются параллельно, причем
время центрального процессора (или нескольких центральных процессоров в мульти-
мультипроцессорных системах) распределяется между этими задачами.
Распределением времени центрального процессора занимается специальный мо-
модуль ОС — планировщик. Планировщик по очереди передает управление отдельным
потокам, так что даже в однопроцессорной системе создается полная иллюзия парал-
параллельной работы запущенных потоков.
Как мы уже говорили, при использовании вытесняющей многопоточности распре-
распределение времени выполняется по прерываниям системного таймера. Поэтому каждому
потоку дается определенный интервал времени, в течение которого он находится в ак-
активном состоянии.
Заметим, что планировщик распределяет время для потоков, а не для процессов.
Потоки, созданные разными процессами, конкурируют между собой за получение
процессорного времени. В рамках каждого процесса может создаваться один или не-
несколько потоков.
336 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Каким именно образом происходит конкуренция между потоками за процессорное
время?
При помощи механизма приоритетов (priority). Приложения С# могут указывать
следующие значения для приоритетов отдельных потоков:
• Highest,
• AboveNormal,
• Normal,
• BelowNormal,
• Lowest.
По умолчанию вновь созданный поток имеет нормальный приоритет Normal. Если
остальные потоки в системе имеют тот же самый приоритет, то все потоки пользуются
процессорным временем на равных правах.
При необходимости вы можете повысить или понизить приоритет отдельных задач,
определив для них другие значения приоритета. Потоки с повышенным приоритетом
(AboveNormal и Highest) выполняются в первую очередь, а с пониженным (Below-
Normal и Lowest) — только при отсутствии готовых к выполнению потоков, имеющих
более высокий приоритет.
Зачем нужно изменять приоритеты потоков?
Если ваша программа выполняет какую-либо длительную работу и одновременно
ведет диалог с пользователем, имеет смысл повысить приоритет потока, отвечающего
за такой диалог. В противном случае пользователя будет раздражать замедленная ре-
реакция программы на операции, выполняемые при помощи мыши и клавиатуры.
Длительные процессы лучше выполнять с низким приоритетом, чтобы они не мешали
выполнению более важных и более срочных задач. В любом случае изменяйте приоритеты
потоков только в том случае, когда это действительно необходимо.
Теперь мы посмотрим на практике, как изменение приоритетов отдельных потоков
может влиять на работу многопоточной программы. В листинге 10.7 мы привели ис-
исходный текст такой программы, запускающей потоки с разными приоритетами.
Листинг 10.7. Файл chW\Priority\PriorityApp.cs
using System;
using System.Threading;
namespace Priority
{
class PriorityApp
{
public static void WriteThreadHi()
{
for (;; )
{
Console.Write("+");
for(long i=0; i<100000; i
Глава 10. Многопоточность 337
public static void WriteThreadLo()
{
for(;;)
{
Console.Write("-");
for(long i = 0; i<100000;
[STAThread]
static void Main(string[] args)
{
Thread hi = new Thread(new ThreadStart(WriteThreadHi));
Thread lo = new Thread(new ThreadStart(WriteThreadLo));
hi.Priority = System.Threading.ThreadPriority.AboveNormal;
lo.Priority = System.Threading.ThreadPriority.Normal;
hi.Start();
lo.Start();
Console.ReadLine();
В главном классе нашей программы мы объявили методы WriteThreadHi
и WriteThreadLo, первый из которых должен будет работать с высоким приорите-
приоритетом, а второй — с низким:
public static void WriteThreadHi()
{
for(;;)
{
Console.Write(" + ") ,-
for(long i=0; i<100000; i
public static void WriteThreadLo()
{
for(;;)
{
Console.Write("- " ) ;
for(long i=0; i<100000; i++)
Исходные тексты этих методов отличаются только символом, отображаемым
на консоли. Для того чтобы эффект изменения приоритетов стал заметнее, мы вклю-
338 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
чили в цикл отображения символов программную задержку, реализованную с помо-
помощью ничего не делающего цикла for:
fordong i = 0; i<100000; i + +);
Почему мы не стали использовать здесь метод Thread. Sleep?
Дело в том, что метод Thread.Sleep выполняет ожидание таким способом, ко-
который не нагружает центральный процессор компьютера. Иными словами, поток, вы-
выполнение которого задерживается методом Thread.Sleep, не уменьшает ресурсы
центрального процессора.
Но чтобы наша программа выводила символы на консоль не слишком быстро, нам
как раз нужно загрузить чем-нибудь центральный процессор. Именно эту задачу и ре-
решает бесполезный на вид цикл for. Заметим, что для достижения необходимого эф-
эффекта на вашем компьютере, возможно, придется изменить количество проходов этого
цикла.
Перед тем как запустить потоки, мы задаем их приоритеты, изменяя свойство
Priority:
Thread hi = new Thread(new ThreadStart(WriteThreadHi));
Thread lo = new Thread(new ThreadStart(WriteThreadLo));
hi.Priority = System.Threading.ThreadPriority.AboveNormal;
lo.Priority = System.Threading.ThreadPriority.Normal;
hi.Start();
lo.Start();
Первому потоку присваивается приоритет, немного превышающий нормальный
приоритет. Второй поток работает с нормальным приоритетом.
Проверяя работу этой программы, вы можете провести эксперимент, изменяя зна-
значения приоритетов. Если приоритеты одинаковы, на консоли будет нарисовано при-
примерно одинаковое количество плюсов и минусов:
Если же приоритет потока, выводящего на консоль плюсы, будет больше, чем при-
приоритет потока, отображающего минусы, картина пример следующий вид:
Как видите, плюсы теперь отображаются намного чаще минусов.
Глава 10. Многопоточность 339
Синхронизация потоков
Многопоточный режим работы открывает новые возможности для программистов, од-
однако за эти возможности приходится расплачиваться усложнением процесса проекти-
проектирования приложения и отладки. Основная трудность, с которой сталкиваются про-
программисты, ранее никогда не создававшие многопоточные программы, — это
синхронизация одновременно работающих потоков.
Для чего и когда она нужна?
Однопоточная программа, такая, например, как программа MS-DOS, при запуске
получает в монопольное распоряжение все ресурсы компьютера. Так как в однопоточ-
ной ОС существует только один процесс, он использует эти ресурсы в той последова-
последовательности, которая соответствует логике работы программы. Процессы и задачи, рабо-
работающие одновременно в многопоточной системе, обращаются одновременно к одним
и тем же ресурсам. Если при разработке программ не учитывать это обстоятельство,
программы будут работать с ошибками.
Поясним это на простом примере.
Пусть мы создаем программу, выполняющую операции с банковским счетом. Опе-
Операция снятия некоторой суммы денег со счета может происходить в такой последова-
последовательности:
• на первом шаге проверяется общая сумма денег, которая хранится на счету;
• если эта сумма равна или превышает размер снимаемой суммы денег, общая сумма
уменьшается на необходимую величину;
• значение остатка записывается на текущий счет.
Если операция уменьшения текущего счета выполняется в однопоточной системе,
то никаких проблем не возникнет. Однако представим себе, что два процесса (или по-
потока) пытаются одновременно выполнить только что описанную операцию с одним
и тем же счетом. Пусть при этом на счету находится 5 млн. долларов, а оба процесса
пытаются снять с него по 3 млн. долларов.
Допустим, события разворачиваются следующим образом:
• первый процесс проверяет состояние текущего счета и убеждается, что на нем хра-
хранится 5 млн.долларов;
• второй процесс проверяет состояние текущего счета и также убеждается,
что на нем хранится 5 млн. долларов;
• первый процесс уменьшает счет на 3 млн. долларов и записывает остаток B млн.
долларов) на текущий счет;
• второй процесс выполняет ту же самую операцию, так как после проверки считает,
что на счету по-прежнему хранится 5 млн. долларов.
В результате получилось, что со счета, на котором находилось 5 млн. долларов,
было снято 6 млн. долларов и при этом там осталось еще 2 млн. долларов! Итого —
банку нанесен ущерб в 3 млн. долларов.
Как же составить программу уменьшения счета, чтобы она не позволяла вытворять
подобное?
340 А. В. Фролов, Г В. Фролов. Язык С#. Самоучитель
Очень просто — на время выполнения операций над счетом одним процессом не-
необходимо запретить доступ к этому счету со стороны других процессов. В этом случае
сценарий работы программы должен быть следующим:
• процесс блокирует счет для выполнения операций другими процессами, получая
его в монопольное владение;
• процесс проводит процедуру уменьшения счета и записывает на текущий счет но-
новое значение остатка;
• процесс разблокирует счет, разрешая другим процессам выполнение операций.
Когда первый процесс блокирует счет, он становится недоступен другим процес-
процессам. Если второй процесс также попытается заблокировать этот же счет, он будет пе-
переведен в состояние ожидания. Когда первый процесс уменьшит счет и на нем оста-
останется 2 млн. долларов, второй процесс будет разблокирован. Он проверит остаток,
убедится, что сумма недостаточна и не будет проводить операцию.
Таким образом, в многопоточной среде необходима синхронизация процессов
и потоков при обращении к критическим ресурсам. Если над такими ресурсами будут
выполняться операции в неправильной последовательности, это приведет к возникно-
возникновению трудно обнаруживаемых ошибок.
В языке программирования С# предусмотрено несколько средств синхронизации
потоков, которые мы рассмотрим в этом разделе.
Ожидание завершения потока
С помощью метода Join, определенного в классе Thread, поток может ожидать за-
завершения работы другого потока, для которой этот метод вызван.
Предусмотрено 3 перегруженных определения метода Join:
public void Join();
public bool Join(int);
public bool Join(TimeSpan);
Первый из них выполняет ожидание без ограничения во времени. Для второго
ожидание будет прервано принудительно через интервал времени, заданный в милли-
миллисекундах. Третий метод позволяет указывать более длительные интервалы времени.
Мы рассказывали об этом ранее при описании метода Sleep.
Учтите, что реально вы не сможете указывать время с точностью до наносекунд,
так как дискретность системного таймера компьютера намного больше.
Метод Join может возвращать значение true или false. Первое из них возвращает-
возвращается в том случае, если поток завершил свою работу через заданный интервал времени,
а второе — если указанный период времени истек, но поток так и не завершился.
Анализируя значение, полученное от метода Join, ваша программа может опреде-
определить, сумел ли поток завершиться в отведенное для этого время или нет. В зависимо-
зависимости от логики работы программы в последнем случае можно прибегнуть, например,
к аварийному завершению работы «упрямого» потока.
В программе, исходный текст которой приведен в листинге 10.8, мы привели при-
пример использования метода Thread. Join.
Глава 10. Многопоточность 341
Листинг 10.8. Файл ch10\JoinDemo\JoinDemoApp.cs
using System;
using System.Threading;
namespace JoinDemo
{
class JoinDemoApp
{
public static void ReadThread()
{
string s;
for(;;)
{
Console.Write("Введите любую строку или \"exit\"
для завершения работы : ") ;
s = Console.ReadLine() ;
Console.WriteLineC'Bbi ввели строку \"{0}\"", s) ;
if(s == "exit")
break;
[STAThread]
static void Main(string[] args)
{
Thread thr = new Thread(new ThreadStart(ReadThread));
thr.Start();
thr.Join();
Console.WriteLine("Поток ReadThread завершил свою работу")
Console.ReadLine();
Метод Main этой программы создает новый поток ReadThread и запускает его
на выполнение методом Start. Затем главный поток программы (т. е. тот поток,
в рамках которого работает метод Main) переводится в состояние ожидания до тех
пор, пока не завершит свою работу поток ReadThread:
Thread thr = new Thread(new ThreadStart(ReadThread));
thr.Start();
thr.Join();
Conso]e.WriteLine("Поток ReadThread завершил свою работу");
342 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Когда же это, наконец, происходит, главный поток выводит на консоль сообщение
о завершении работы потока ReadThread.
Что же представляет собой поток ReadThread?
Он выполняет простую работу: в бесконечном цикле он вводит с клавиатуры тек-
текстовые строки и затем выводит их на консоль:
public static void ReadThread()
{
string s;
for(; ; )
{
Console.Write(
"Введите любую строку или \"exit\" для завершения работы :");
s = Console.ReadLine{);
Console.WriteLine{"Вы ввели строку \"{0}\"и, s);
if(s == "exit")
break;
Условием завершения цикла является ввод строки exit. При этом поток
ReadThread выходит из цикла и завершает свою работу.
Данное событие ожидается главным потоком приложения при помощи метода
Join. До тех пор, пока работает поток ReadThread, главный поток нашей програм-
программы находится в состоянии ожидания.
Критические секции
Критические секции (critical sections) являются наиболее простым средством синхро-
синхронизации потоков для обеспечения последовательного доступа к ресурсам. Они рабо-
работают быстро и не снижают производительности системы
Как работают критические секции?
Если один поток вошел в критическую секцию, но еще не вышел из нее, то при по-
попытке других потоков войти в ту же самую критическую секцию они будут переведе-
переведены в состояние ожидания. Потоки пробудут в этом состоянии до тех пор, пока поток,
который вошел в критическую секцию первым, не выйдет из нее.
Таким способом гарантируется, что фрагмент кода, заключенный внутри критиче-
критической секции, будет выполняться потоками последовательно, если все они работают
с одной и той же критической секцией.
Если поток работает с несколькими ресурсами, доступ к которым должен выпол-
выполняться последовательно, он может создать несколько критических секций. Заметим,
что, когда потоки работают с несколькими критическими секциями, все они должны
использовать одинаковую последовательность входа в эти критические секции и вы-
выхода из них, иначе возможны взаимные блокировки потоков.
Глава 10. Многопоточность 343
Применение ключевого слова lock
Простейший способ синхронизации потоков с помощью критических секций основан
на использовании ключевого слова lock, специально предусмотренного для решения
этой задачи в языке программирования С#.
Для того чтобы оформить фрагмент кода в виде критической секции, достаточно
заключить его в блок lock, как это показано ниже:
lock(this)
Ключевое слово this используется здесь для того, чтобы указать, что защищается
конкретный объект класса. В общем случае вы должны задавать в скобках после клю-
ключевого слова lock ссылку на объект, используемый для синхронизации (т. е. объект,
за доступ к которому будут конкурировать несколько потоков).
Рассмотрим следующий пример программы (листинг 10.9).
Листинг 10.9. Файл chW\LockDemo\LockDemoApp.cs
using System;
using System.Threading;
namespace LockDemo
{
class NumberWriter
{
public void WriteThread()
{
lock(this)
{
for(int i = 0; i < 10; i++)
{
Console.Write("{0}.", i);
Thread.SleepA00);
class LockDemoApp
[STAThread]
static void Main(string[] args)
{
NumberWriter nw = new NumberWriter();
Thread thrl = new Threadfnew ThreadStart(nw.WriteThread));
Thread thr2 = new Thread(new ThreadStart(nw.WriteThread));
344 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
thrl.Start ();
thr2.Start () ;
Console.ReadLine();
Эта программа создает два потока, каждый из которых выводит в цикле числа от О
до 9, причем после вывода создается небольшая задержка:
public void WriteThread()
{
lock(this)
{
forjint i = 0; i < 10; i++)
{
Console.Write("{0}." , i);
Thread.Sleep(lOO) ;
Если бы мы не заключили тело оператора цикла в блок lock, потоки конкурирова-
конкурировали бы между собой за консоль, отображая числа в хаотическом порядке:
0.1.2.0.3.1.4.2.5.3.6.4.7.5.8.6.9.7.8.9.
Наличие блока lock гарантирует, что, пока один поток не закончит вывод, другой
будет находиться в состоянии ожидания. Вот что мы увидим на консоли в результате
использования такой синхронизации:
0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.
Теперь числа упорядочены, так как вначале их выводил на консоль один поток,
а затем второй.
Использование класса Mutex
Если необходимо обеспечить последовательное использование ресурсов потоками,
созданными в рамках разных процессов, вместо критических секций необходимо ис-
использовать объекты синхронизации Mutex. Свое название они получили от выражения
mutually exclusive, что означает «взаимно исключающий».
Объект Mutex может находиться в отмеченном или неотмеченном состоянии. Ко-
Когда какой-либо поток, принадлежащий любому процессу, становится владельцем объ-
объекта Mutex, последний переключается в неотмеченное состояние. Если же поток «от-
«отказывается» от владения объектом Mutex, его состояние становится отмеченным.
Организация последовательного доступа к ресурсам с использованием объектов
Mutex возможна потому, что в каждый момент только один поток может владеть этим
объектом. Для того чтобы завладеть объектом, который уже захвачен, все остальные
потоки вынуждены ждать. Для того чтобы объект Mutex был доступен потокам, при-
принадлежащим различным процессам, при создании вы должны присвоить ему имя.
Глава 10. Многопоточность 345
Заметим, что объекты Mutex в языке С# реализуют механизм синхронизации пото-
потоков ОС Microsoft Windows, доступный на уровне программного интерфейса Win32
этой ОС. Средства многопоточности этой ОС мы подробно рассмотрели в [10].
Применение класса Mutex в программе С# демонстрируется в листинге 10.10.
Листинг 10.10. Файл ch10\MutexDemo\MutexDemoApp.cs
using System;
using System.Threading;
namespace MutexDemo
{
class NumberWriter
{
Mutex mutex;
public NumberWriter()
{
mutex = new Mutex(false, "MutexDemoAppMutex");
}
public void WriteThread()
{
mutex.WaitOne();
for(int i = 0; i < 10; i++)
{
Console.Write("{0}." , i);
Thread.SleepA00) ;
}
mutex.Close();
class MutexDemoApp
{
[STAThread]
static void Main(string! ] args)
{
NumberWriter nw = new NumberWriter();
Thread thrl = new Thread(new ThreadStart(nw.WriteThread));
Thread thr2 = new Thread(new ThreadStart(nw.WriteThread));
thrl.Start();
thr2.Start!);
Console.ReadLine();
346 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Эта программа очень напоминает предыдущую программу (см. листинг 9.10), од-
однако для реализации критической секции мы использовали в ней не ключевое слово
lock, а класс Mutex.
Изменения затрагивают только класс NumberWriter, отображающий на консоли
в отдельном потоке числа от 0 до 9:
class NumberWriter
{
Mutex mutex;
public NumberWriter()
{
mutex = new Mutex(false, "MutexDemoAppMutex");
}
public void WriteThreadO
{
mutex.Waitone();
forfint i = 0; i < 10; i++)
{
Console.Write("{0}.", i);
Thread.SleepA00) ;
}
mutex.Close();
Как видите, теперь в этом классе имеется конструктор, создающий объект класса
Mutex:
mutex = new Mutex(false, "MutexDemoAppMutex");
Существует несколько перегруженных конструкторов в классе Mutex. Их прото-
прототипы мы привели ниже:
public Mutex();
public Mutex(bool);
public Mutex(bool, string);
Первый из них создает объект класса Mutex, обладающий свойствами, принятыми
по умолчанию. Второй конструктор позволяет указать, должен ли объект класса
Mutex создаваться в неотмеченном состоянии, т. е. должен ли им владеть вызываю-
вызывающий поток. Третий конструктор аналогичен предыдущему, однако дополнительно по-
позволяет указать имя объекта Mutex, доступное за пределами текущего процесса.
В нашей программе мы создали объект класса Mutex в отмеченном состоянии. Та-
Таким образом, поток, создавший Mutex, не становится его владельцем.
Глава 10. Многопоточность 347
Вход в критическую секцию выполняется в нашей программе следующим образом:
mutex.Waitone();
Вызов метода WaitOne без параметров приведет к тому, что программа будет на-
находиться в критической секции неопределенно долго, пока она не покинет его, закрыв
объект класса Mutex явным образом.
В нашей программе выход из критической секции выполняется так:
mutex.Close() ;
Здесь мы просто вызываем метод Close, определенный в классе Mutex.
В классе Mutex имеется еще два перегруженных метода WaitOne, позволяющих
задать время ожидания, а также действия, предпринимаемые после его истечения:
bool WaitOne(int, bool);
bool WaitOne(TimeSpan, bool);
Они отличаются лишь способом определения временного интервала (в первом слу-
случае время указывается в миллисекундах, а во втором — задается при помощи класса
TimeSpan).
Использование класса Monitor
И наконец, еще одно средство синхронизации потоков, которое мы рассмотрим в на-
нашей книге, это класс Monitor. Он несколько проще в использовании по сравнению
столько что рассмотренным классом Mutex, однако он не позволяет синхронизиро-
синхронизировать потоки, выполняемые в рамках разных процессов.
В программе, исходный текст которой приведен в листинге 10.11, мы применили
класс Monitor для создания критической секции.
Листинг 10.11. Файл ch10\MonitorDemo\MonitorDemoApp.cs
using System;
using System.Threading;
namespace MonitorDemo
{
class NumberWriter
{
public void WriteThread()
{
Monitor.Enter(this) ;
for(int i = 0; i < 10; i++)
{
Console.Write("{0}.", i);
Thread.SleepA00);
}
Monitor.Exit(this);
348 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
class MonitorDemoApp
{
[STAThread]
static void Main(string[] args)
{
NumberWriter nw = new NumberWriter() ;
Thread thrl = new Thread(new ThreadStart(nw.WriteThread));
Thread thr2 = new Thread(new ThreadStart(nw.WriteThread));
thrl.Start();
thr2.Start() ;
Console.ReadLine();
Все самое интересное находится внутри объявления метода WriteThread, вы-
выполняющегося в рамках отдельного потока:
public void WriteThread()
{
Monitor.Enter(this) ;
for(int i = 0; i < 10; i
Console.Write("{0}.", i) ;
Thread.SleepA00);
Monitor.Exit(this) ;
}
Как видите, вход в критическую секцию выполняется при помощи метода Enter,
а выход из нее — при помощи метода Exit. Обоим этим методам в качестве парамет-
параметра необходимо передавать ссылку на объект синхронизации.
Помимо методов Enter и Exit, в классе Monitor есть еще ряд полезных методов.
Например, с помощью метода TryEnter программа может определить, заблокирова-
заблокирована ли критическая секция. Это позволит потоку, например, входить в критическую
секцию только в том случае, если она свободна, избегая блокировки.
Более подробную информацию о средствах синхронизации потоков, доступных
приложениям С#, вы найдете в справочной системе MSDN, входящей в состав систе-
системы разработки Microsoft Visual Studio .NET.
Глава 10. Много поточность 349
Глава 11. Делегаты и события
В предыдущей главе мы уже рассказывали вам о делегатах, служащих в качестве не-
неплохой замены указателям на функции. В этой главе мы рассмотрим делегаты более
детально.
Чтобы вам было понятнее назначение делегатов, расскажем подробнее о функциях
обратного вызова и их применении.
Вы знаете, что программы в качестве аргументов программы могут передавать ме-
методам ссылки на параметры — переменные, числа, массивы и т. п. Существует и еще
одна возможность: в качестве аргументов можно передавать методам ссылки на дру-
другие методы.
Представим себе, что нам нужно распечатывать массив строк на разных принтерах,
причем для каждого принтера предусмотрена собственная процедура печати. Эта про-
процедура может быть оформлена, например, в виде метода какого-либо класса.
В зависимости от тех или иных условий программа печати могла бы передавать
методу, ответственному за печать, ссылку на тот или иной метод, реализующий осо-
особенности конкретного принтера.
Другой пример, когда требуются функции обратного вызова, — сортировка произ-
произвольных объектов. Этот пример встречается практически во всех учебниках по про-
программированию. Создается общий код, пригодный для сортировки различных объек-
объектов, а процедуры сравнения объектов, которые зависят от типа сравниваемых объек-
объектов, оформляются в виде функций обратного вызова.
При создании многопоточных программ, рассмотренных в предыдущей главе, так-
также необходим механизм обратного вызова. Напомним, что для запуска метода в от-
отдельном потоке нам нужно создать для этого метода делегат, т. е. ссылку на метод.
При этом программа никогда не вызывает напрямую метод, предназначенный для ра-
работы в отдельном потоке. Вместо этого ссылка на данный метод передается конструк-
конструктору класса Thread, формирующему новый поток.
Функции обратного вызова часто используются для обработки редко возникающих
событий. Типичный пример — передача данных по сети. При этом программа ини-
инициирует процесс передачи или приема блока данных, но не дожидается его заверше-
завершения. Когда же данные будут получены, драйвер сетевого протокола, отвечающий
за передачу данных, вызовет функцию программы, предназначенную для обработки
данных. Ссылка на эту функцию передается драйверу при инициировании процесса
передачи данных.
Механизм функций обратного вызова реализован во многих языках программиро-
программирования. В языках С и C++ для этого применяются указатели на функции, т. е. перемен-
переменные, содержащие адрес функций. Однако, как мы неоднократно замечали, указатели С
и C++ обладают весьма существенным недостатком — они небезопасны в использова-
использовании: программа может по ошибке легко изменить содержимое указателя таким обра-
образом, что он будет указывать не туда, куда нужно. В результате поведение программы
станет непредсказуемым.
~ з£о
В этом смысле делегаты С# полностью безопасны. Делегат может содержать толь-
только ссылку на метод, объявленный в программе, и ничего больше. Вы не сможете изме-
изменить содержимое делегата никаким непредусмотренным способом, что существенно
уменьшает вероятность возникновения ошибки.
Использование делегатов
Рассмотрим простейший пример делегата для хранения ссылки на методы, реализую-
реализующий возможности разных принтеров.
Наша программа (листинг 11.1) дважды распечатает на виртуальном принтере (вы-
(выполняющем вместо настоящей печати вывод строк на консоль) содержимое массива
текстовых строк.
Листинг 11.1. Файл ch11\DelegateDemo\DelegateDemoApp.cs
using System;
namespace DelegateDemo
{
class MyPrinter
{
public delegate void StringPrinter(string str);
public void PrintString(string str, StringPrinter printer)
{
printer(str);
class DelegateDemoApp
{
public static void SimplePrinter(string str)
{
Console.WriteC {0} ", str);
}
public static void SmartPrinter(string str)
{
Console.WriteC1 {0} . ", str);
}
static void Main(string[] args)
{
string[] Lines =
{
"This",
"is",
11 C#",
"string",
"array"
);
Глава 11. Делегаты и события 351
MyPrinter mp = new MyPrinter();
MyPrinter.StringPrinter stringPrinter =
new MyPrinter.StringPrinter(DelegateDemoApp.SimplePrinter) ;
Console.Write("Печать на первом принтере: ") ;
foreach (string CurrentString in Lines)
rap.PrintString(CurrentString, stringPrinter);
stringPrinter =
new MyPrinter.StringPrinter(DelegateDemoApp.SmartPrinter);
Console.Write("\п\пПечать на втором принтере: ");
foreach (string CurrentString in Lines)
mp.PrintString(CurrentString, stringPrinter);
Console.ReadLine();
В первый раз программа распечатает массив строк на «простом» принтере, который
просто выведет все строки массива на консоль, разделив их пробелами. Второй, «умный»
принтер добавит после каждой отображаемой строки дополнительную точку:
Печать на первом принтере: This is C# string array
Печать на втором принтере: This. is. C#. string, array.
Для реализации функции печати в нашей программе предусмотрен класс
MyPrinter, объявленный следующим образом:
class MyPrinter
{
public delegate void StringPrinter(string str);
public void PrintString(string str, StringPrinter printer)
{
printer(str);
Обратите внимание, что в качестве члена этого класса мы объявили поле-делегат
StringPrinter:
public delegate void StringPrinter(string str);
Получив управление, метод Main создает объект класса MyPrinter, при помощи
которого наша программа будет распечатывать строки массива:
MyPrinter mp = new MyPrinter();
352 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Далее она инициализирует делегат stringPrinter, записывая в него ссылку
на «простой» принтер SimplePrinter:
MyPrinter.StringPrinter stringPrinter =
new MyPrinter.StringPrinter(DelegateDemoApp.SimplePrinter);
После такой инициализации можно приступать к печати:
Console.Write("Печать на первом принтере: ");
foreach (string CurrentString in Lines)
mp.PrintString(CurrentString, stringPrinter);
В процессе печати, выполняемом методом PrintString из класса MyPrinter,
мы передаем этому методу делегат stringPrinter. Таким способом мы сообщаем
методу PrintString, какой метод должен использоваться для печати.
Когда содержимое массива печатается в первый раз, в роли виртуального принтера
будет использован метод SimplePrinter:
public static void SimplePrinter(string str)
{
Console.Write{"{0} ", str);
}
Перед тем как печатать содержимое массива во второй раз, программа записывает
в делегат stringPrinter ссылку на другой виртуальный принтер, реализованный
методом SmartPrinter:
stringPrinter =
new MyPrinter.StringPrinter(DelegateDemoApp.SmartPrinter);
Далее мы повторяем цикл печати в неизменном виде:
Console.Write("\п\пПечать на втором принтере: ");
foreach (string CurrentString in Lines)
mp.PrintString(CurrentString, stringPrinter);
Теперь, однако, печать будет выполняться по-другому, так как за нее будет отве-
отвечать метод SmartPrinter:
public static void SmartPrinter(string str)
{
Console.Write("{0}. ", str);
}
От упомянутого выше метода SimplePrinter метод SmartPrinter отличается
тем, что он добавляет точку к каждой распечатываемой строке.
Статические делегаты
В предыдущем примере программы мы создали один делегат и затем, в процессе рабо-
работы программы, записывали в него ссылки на разные методы. Существует и другая
возможность, основанная на объявление делегатов как статических членов класса.
Глава 11. Делегаты и события 353
12 Язык С# Самоучитель
Такие делегаты инициализируются вместе с другими статическими членами перед ис-
использованием класса и проще в употреблении.
Рассмотрим пример программы, исходный текст которой приведен в листинге 11.2.
Листинг 11.2. Файл ch11\DelegateStatic\DelegateStaticApp.cs
using System;
namespace DelegateStatic
class MyPrinter
public delegate void StringPrinter(string str);
public void PrintString(string str, StringPrinter printer)
printer (str);
class DelegateStaticApp
public static void SimplePrinter(string str)
Console.Write("{0} ", str);
public static void SmartPrinter(string str)
Console.Write("{0}. ", str);
public static readonly MyPrinter.StringPrinter
stringSimplePrinter =
new MyPrinter.StringPrinter(SimplePrinter);
public static readonly MyPrinter.StringPrinter
stringSmartPrinter =
new MyPrinter.StringPrinter(SmartPrinter);
static void Main(string[] args)
string[] Lines =
"This",
"is",
"C#\
"string",
"array"
354 А В Фролов, Г. В Фролов. ЯзыкС*. Самоучитель
MyPrinter mp = new MyPrinter();
Console.Write("Печать на первом принтере: ");
foreach (string CurrentString in Lines)
mp.PrintString(CurrentString, stringSimplePrinter);
Console.Write("\п\пПечать на втором принтере: ");
foreach (string CurrentString in Lines)
mp.PrintString(CurrentString, stringSmartPrinter);
Console.ReadLine();
От предыдущей программы она отличается тем, что теперь мы объявили два стати-
статических делегата, первый из которых содержит ссылку на виртуальный принтер
SimplePrinter, а второй — на виртуальный принтер SmartPrinter:
public static readonly MyPrinter.StringPrinter stringSimplePrinter =
new MyPrinter.StringPrinter(SimplePrinter);
public static readonly MyPrinter.StringPrinter
stringSmartPrinter =
new MyPrinter.StringPrinter(SmartPrinter);
Эти делегаты объявлены только читаемыми с помощью ключевого слова
readonly, поэтому наша программа по ошибке не сможет изменить хранящиеся
в них ссылки.
Как метод Main пользуется статическими делегатами?
При печати на «простом» принтере делегат stringSimplePrinter передается
методу Printstring (выполняющему печать) в качестве параметра:
Console.Write("Печать на первом принтере: ");
foreach (string CurrentString in Lines)
mp.PrintString(CurrentString, stringSimplePrinter);
Этот делегат был проинициализирован на этапе инициализации программы — он
содержит ссылку на метод SimplePrinter.
Печать на втором принтере выполняется аналогично, но с использованием другого
делегата, а именно делегата stringSmartPrinter:
Console.Write("\п\пПечать на втором принтере: ");
foreach (string CurrentString in Lines)
mp.PrintString(CurrentString, stringSmartPrinter);
Так как этот делегат был проинициализирован ссылкой на метод SmartPrinter,
во второй раз печать будет выполняться «по-умному», т. е. с добавлением точки.
Глава 11. Делегаты и события 355
Массивы делегатов
При необходимости приложение может создавать массивы делегатов, инициализируя
их ссылками на методы. Это позволяет выбирать нужную для обработки функцию об-
обратного вызова динамически, во время работы программы.
Этот прием мы демонстрируем на примере программы, исходный текст которой
приведен в листинге 11.3.
Листинг 11.3. Файл ch11\DelegateArray\DelegateArrayApp.cs
using System;
namespace DelegateArray
{
class MyPrinter
{
public delegate void StringPrinter(string str);
public void PrintString(string str, StringPrinter printer)
{
printer(str);
class DelegateArrayApp
{
public static void Printerl(string str)
{
Console.Write("{0} ", str);
}
public static void Printer2(string str)
{
Console. Write (" {0} . '■, str);
}
public static void Printer3(string str)
{
Console.Write("{0}_ ", str) ;
)
static void Main(string[] args)
{
string [] Lines =
{
"This",
"is" ,
"C#" ,
"string",
"array"
U
356 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
MyPrinter mp = new MyPrinter();
MyPrinter.StringPrinter[] stringPrinter =
new MyPrinter.StringPrinter[3];
stringPrinter[0] = new MyPrinter.StringPrinter(Printerl);
stringPrinter[1] = new MyPrinter.StringPrinter(Printer2);
stringPrinter[2] = new MyPrinter.StringPrinter(Printer3);
Console.Write("Печать на первом принтере: ");
foreach (string CurrentString in Lines)
mp.PrintString(CurrentString, stringPrinter[0]);
Console.Write("\п\пПечать на втором принтере: ");
foreach (string CurrentString in Lines)
mp.PrintString(CurrentString, stringPrinter[1]);
Console.Write("\п\пПечать на третьем принтере: ");
foreach (string CurrentString in Lines)
mp.PrintString(CurrentString, stringPrinter[2]);
Console.ReadLine();
В основном классе программы DelegateArrayApp мы определили 3 метода, ка-
каждый из которых играет роль виртуального принтера:
public static void Printerl(string str)
{
Console.Write("{0} ", str);
}
public static void Printer2(string str)
{
Console.Write("{0}. ", str);
}
public static void Printer3(string str)
{
Console.Writef"{0}_ ", str);
}
Эти методы аналогичны тем, что мы использовали в нашей предыдущей програм-
программе. Первый из них при выводе строки добавляет к ее концу символ пробела, второй —
точку, а третий -— символ подчеркивания.
Глава 11. Делегаты и события 357
В теле метода Main мы объявили массив делегатов MyPrinter. StringPr inter:
MyPrinter.StringPrinter[] stringPrinter =
new MyPrinter.StringPrinter[3];
Далее наша программа инициализирует ячейки массива, записывая в них делегаты,
созданные на базе соответствующих методов:
stringPrinter[0] = new MyPrinter.StringPrinter(Printerl);
stringPrinter[1] = new MyPrinter.StringPrinter(Printer2);
stringPrinter[2] = new MyPrinter.StringPrinter(Printer3);
Теперь, чтобы выбрать нужный нам принтер, мы можем указать индекс соответст-
соответствующего делегата в массиве:
Console.Write("Печать на первом принтере: ");
foreach (string CurrentString in Lines)
mp.PrintString(CurrentString, stringPrinter[0]);
Console.Write("\п\пПечать на втором принтере: ") ;
foreach (string CurrentString in Lines)
mp.PrintString(CurrentString, stringPrinter[1]);
Console.Write("\п\пПечать на третьем принтере: ");
foreach (string CurrentString in Lines)
mp.PrintString(CurrentString, stringPrinter[2]);
Так как в процессе своей работы программа может динамически обращаться к лю-
любым нужным ей ячейкам массива, данный способ обеспечивает динамический выбор
необходимой функции обратного вызова (точнее говоря, метода обратного вызова, хо-
хотя этот термин и не является общепринятым).
Обработка событий
Самые первые программы, создаваемые для компьютеров, представляли собой потоки
машинных команд, исполняемых линейно. Линейный ход программы нарушали лишь ус-
условные или циклические операторы. Обычно такие программы получали в начале своей
работы все необходимые данные (например, через перфокарты или перфоленты), а после
завершения выдавали результат на устройство печати, перфокарты и перфоленты.
Описанный режим обработки данных называется пакетным, так как компьютер загру-
загружает по очереди пакеты программ с данными и обрабатывает их, выдавая результат.
Современные диалоговые программы, написанные для персональных компьюте-
компьютеров, работают совершенно по-другому. После запуска они воспринимают действия
пользователей над мышью и клавиатурой как события (events), выполняя при возник-
возникновении различных событий тс или иные действия. Говорят, что работа современных
диалоговых программ с графическим интерфейсом управляется событиями (events
driven program).
358 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Щелкая мышью, например, кнопки инструментальной линейки, пользователь мо-
может вызывать на экран те или иные диалоговые окна. При каждом таком щелчке ОС
обрабатывает события (т. с. щелчки мышью), передавая управление различным фраг-
фрагментам программы. Эти фрагменты программы выполняют асинхронную обработку
событий, так как действия пользователей над программой асинхронны по отношению
к работе самой программы и совершенно непредсказуемы.
Если программа работает в многопоточном режиме, то в ней могут возникать со-
события, связанные не только с действиями пользователей, но и с работой дополнитель-
дополнительных потоков. Если, например, программа запускает поток, выполняющий какую-либо
длительную работу (например, поиск информации в базе данных), то окончание этой
работы может рассматриваться в качестве примера такого события.
Публикация событий
В отличие от многих других языков программирования, в которых обработка событий
выполняется при помощи функций обратного вызова, в языке С# применяется более
развитая технология публикации событий (events publishing) и подписки на события
(events subscribing).
Идея публикации событий и подписки на них достаточно понятна. Проведем ана-
аналогию с интернетовскими сайтами новостей.
В данном случае публикация новости на таком сайте будет играть роль события.
Оно похоже на событие, возникающее в программе, в том смысле, что появляется не-
непредсказуемым образом и содержимое его также непредсказуемо.
Многие сайты новостей содержат раздел подписки, где каждый желающий может
оставить свой адрес электронной почты для отправки текстов новостей. Пользователи
Интернета могут сами подписываться на интересующие их новости на тех или иных
сайтах.
Когда на сайте появляется новость, она рассылается всем подписчикам по элек-
электронной почте. Получение и чтение новости в данном контексте можно рассматривать
как обработку события.
Создавая программу на языке С#, вы можете разрабатывать объекты классов, ини-
инициирующие появление событий, а также объекты классов, обрабатывающих эти собы-
события. Первые из них должны опубликовать события, а вторые — подписаться на них.
Для реализации обработчиков событий в языке С# используются уже знакомые вам
делегаты. Класс, публикующий события (т. е. класс, в котором эти события возника-
возникают), должен определить делегат. Этот делегат реализуется классом, подписывающим-
подписывающимся на событие, т. е. ответственным за обработку события. Как только событие возника-
возникает, происходит вызов метода обрабатывающего класса, заданного делегатом. Этот ме-
метод обычно называется обработчиком события (event handler).
Приведем пример программы.
Пусть мы создаем многопоточную программу, отображающую на консоли симво-
символы +, причем это отображение выполняется в рамках отдельного потока. Исходный
текст соответствующего класса представлен ниже:
Глава 11. Делегаты и события 359
public class StringWriter
{
private Thread thr;
private int count;
public StringWriter()
{
count = 0;
}
public void WriteThreadf)
{
for(;;)
{
Console.WriteLine(" + ") ;
count++ ,-
Thread.Sleep(lOOO);
public void go()
{
thr = new Thread(new ThreadStart(WriteThread));
thr .Start () ,-
Как видите, пока здесь для нас нет ничего нового. Метод WriteThread содержит
цикл отображения символа +, а метод до запускает процесс отображения в отдельном по-
потоке. В переменной count ведется подсчет общего количества выведенных символов.
Давайте дополним этот класс кодом, создающим и публикующим событие, связан-
связанное с отображением очередного символа на консоли. С этой целью мы добавим в класс
StringWriter два поля, первое из которых, с именем EventHandler, является де-
делегатом, а второе, с именем OnLoopDone, играет роль так называемой переменной
события:
public delegate void EventHandler(object writer,
InfoEventArgs countlnformation);
public event EventHandler OnLoopDone;
Делегат, предназначенный для обработки событий, должен всегда объявляться
с двумя параметрами. Первый из них задает объект, создающий событие, а второй —
передает информацию о событии (например, время возникновения события).
Переменная события OnLoopDone будет хранить ссылку на метод обработчика
события.
Остановимся подробнее на параметрах делегата.
Для того чтобы программа могла передавать информацию о событии обработчику
этого события, необходимо объявить производный класс, создав его на базе класса
EventArgs:
360 А. В. Фролов, Г. В. Фролов Язык С#. Самоучитель
public class InfoEventArgs : EventArgs
{
public InfoEventArgs(int count)
{
this.count = count;
dt = System.DateTime.Now;
public readonly int count;
public readonly System.DateTime dt;
}
В этом классе следует определить поля, хранящие всю необходимую информацию
о событии, а также конструктор, инициализирующий эти поля. В данном случае мы
определили поле count для хранения порядкового номера очередного отображаемого
на консоли символа, а также поле dt класса System.DateTime, в котором будет
храниться дата и время возникновения события (свойство System.DateTime.Now
хранит информацию о текущей дате и времени).
В своей программе вы будете определять другие поля, в соответствии с логикой
работы программы.
Заметим, что оба поля доступны только для чтения, так как объявлены при помощи
ключевого слова readonly. Их содержимое изменяется только конструктором в мо-
момент создания объекта.
Создание события
Рассмотрим теперь процедуру создания события. Мы немного модифицируем исход-
исходный код метода WriteThread, работающего в рамках отдельного потока:
public void WriteThread{)
{
for(;;)
{
Console.WriteLine("+");
count++;
Thread.Sleep(lOOO) ;
InfoEventArgs countlnformation = new InfoEventArgs(count);
if(OnLoopDone != null)
OnLoopDone(this, countlnformation);
Обратите внимание, что на каждой итерации цикла мы создаем новый объект клас-
класса InfoEventArgs, хранящий информацию о событии:
InfoEventArgs countlnformation = new InfoEventArgs(count);
При этом текущий номер отображаемого символа передается конструктору класса
InfoEventArgs, а время возникновения события конструктор сохраняет самостоя-
самостоятельно.
Глава 11. Делегаты и события 361
После того как объект, содержащий информацию о событии, сформирован, наша
программа вызывает обработчик события, обращаясь для этого к переменной события
OnLoopDone:
if(OnLoopDone != null)
OnLoopDone{this, countlnformation);
В качестве первого параметра мы передаем ссылку на объект, создающий событие
(указав ключевое слово this, означающее в этом контексте ссылку на данный объ-
объект), а в качестве второго— ссылку на объект countlnf ormation, содержащий
информацию о событии.
Обратите внимание, что перед вызовом обработчика события мы проверяем пере-
переменную события на равенство значению null. Это делается для того, чтобы прове-
проверить, имеются ли подписчики на данное событие. Если подписчиков нет, инициирова-
инициирование события приведет к возникновению исключения во время работы программы.
Ниже мы привели новый вариант класса StringWriter, публикующий событие
и вызывающий обработчик этого события:
public class StringWriter
{
public delegate void EventHandler(object writer,
InfoEventArgs countlnformation);
public event EventHandler OnLoopDone;
private Thread thr;
private int count;
public StringWriter()
{
count = 0;
public void WriteThread()
{
for(;;)
{
Console.WriteLine("+");
count++;
Thread.SleepA000);
InfoEventArgs countlnformation = new InfoEventArgs(count);
if (OnLoopDone != null)
OnLoopDone(this, countlnformation);
362 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
public void go( )
{
thr = new Thread(new ThreadStart(WriteThread));
thr.Start();
Подписка на события
Итак, теперь у нас есть объект, публикующий и создающий события. Теперь нам нуж-
нужны объекты, подписывающиеся на события и выполняющие их обработку. Заметим,
что каждое событие могут обрабатывать произвольное количество подписчиков. Здесь
язык С# не накладывает никаких ограничений.
В нашем примере мы создадим два подписчика— классы DisplayCount
и DisplayCount2:
public class DisplayCount
{
public void Subscribe(StringWriter writer)
{
writer.OnLoopDone +=
new StringWriter.EventHandler(CountHasChanged);
}
public void CountHasChanged(object writer, InfoEventArgs info)
{
System.DateTime dt = info.dt;
Console.WriteLine("Event at: {0 } : {1} : {2}",
dt.Hour, dt.Minute, dt.Second);
public class DisplayCount2
{
public void Subscribe(StringWriter writer)
{
wri t er.OnLoopDone +=
new StringWriter.EventHandler(CountHasChanged);
)
public void CountHasChanged{object writer, InfoEventArgs info)
{
Console.WriteLine("Current count: {0}", info.count);
В каждом из этих классов мы объявили два метода с именами Subscribe
и CountHasChanged. Первый из них играет роль подписчика, а второй — обра-
обрабатывает событие.
Глава 11. Делегаты и события 363
Методы-подписчики в обоих классах выглядят одинаково:
public void Subscribe(StringWriter writer)
{
writer.OnLoopDone +=
new StringWriter.EventHandler(CountHasChanged);
}
С помощью операции += они добавляют обработчик события в цепочку обработ-
обработчиков. В качестве параметра переменной события передается ссылка на метод
CountHasChanged, непосредственно обрабатывающий события.
После выполнения этой операции класс будет подписан на событие и сможет его
обрабатывать.
Что же касается методов CountHasChanged, выполняющих обработку событий,
то первый из них выводит на консоль время возникновения события, а второй — те-
текущий номер отображаемого символа. Как мы уже говорили, эта информация переда-
передается обработчику при возникновении события.
Программа с обработкой событий
Теперь мы рассмотрим полный исходный текст программы, обрабатывающей события
(листинг 11.4). В ней определены классы и другие объекты, рассмотренные нами ранее
в этом разделе.
Листинг 11.4. Файл ch11\EventsDemo\EventsDemoApp.cs
using System;
using System.Threading;
namespace EventsDemo
{
public class StringWriter
{
public delegate void EventHandler(object writer,
InfoEventArgs countlnformation);
public event EventHandler OnLoopDone;
private Thread thr;
private int count;
public StringWriter()
{
count = 0;
}
public void WriteThread()
364 А. В Фролов, Г. В. Фролов Язык С#. Самоучитель
Console.WriteLine("+");
count++;
Thread.SleepA000);
InfoEventArgs countlnformation = new InfoEventArgs(count)
if (OnLoopDone != null)
OnLoopDone(this, countlnformation);
public void go()
{
thr = new Thread(new ThreadStart(WriteThread));
thr.Start();
public class InfoEventArgs : EventArgs
{
public InfoEventArgs(int count)
{
this.count = count;
dt = System.DateTime.Now;
}
public readonly int count;
public readonly System.DateTime dt;
}
public class DisplayCount
{
public void Subscribe(StringWriter writer)
{
writer.OnLoopDone +=
new StringWriter.EventHandler(CountHasChanged);
}
public void CountHasChanged(object writer, InfoEventArgs info)
{
System.DateTime dt = info.dt;
Console.WriteLine("Event at: {0}:{1}:{2}",
dt.Hour, dt.Minute, dt.Second);
)
}
public class DisplayCount2
{
public void Subscribe(StringWriter writer)
{
writer.OnLoopDone +=
new StringWriter.EventHandler(CountHasChanged);
)
Глава 11. Делегаты и события 365
public void CountHasChanged(object writer, InfoEventArgs infc)
{
Console.WriteLine("Current count: {0}", infо.count);
}
}
class EventsDemoApp
{
[STAThread]
static void Main(string[] args)
{
StringWriter sw = new StringWriter();
DisplayCount dc = new DisplayCount();
dc.Subscribe(sw);
DisplayCount2 dc2 = new DisplayCount2();
dc2.Subscribe(sw);
sw.go();
Console.ReadLine();
Получив управление, метод Main нашей программы создает объект класса
StringWriter:
StringWriter sw = new StringWriter();
Далее он создает два объекта, предназначенные для обработки событий:
DisplayCount dc = new DisplayCount();
dc.Subscribe(sw);
DisplayCount2 dc2 = new DisplayCount2();
dc2.Subscribe(sw);
Первый из них отображает на консоли время возникновения события, а второй —
текущий номер отображаемого символа.
После создания обработчиков мы вызываем для каждого из них метод Sub-
Subscribe, осуществляющий подписку на события.
Далее нам остается только запустить вывод события в отдельном потоке, вызвав
для этого метод до:
sw.go () ;
Console.ReadLine();
Вот что вы увидите на экране, запустив нашу программу на выполнение:
366 А В Фролов, Г. В Фролов. Язык С# Самоучитель
Event at: 12:42:3
Current count: 1
+
Event at: 12:42:4
Current count: 2
+
Event at: 12:42:5
Current count: 3
-t-
Event at: 12:42:6
Current count: 4
+
Event at: 12:42:7
Current count: 5
+
Event at: 12:42:8
Current count: 6
После отображения символа + в дело включаются наши обработчики событий.
Первый из них показывает время отображения, а второй — текущий номер символа.
Заметим, что делегаты и обработчики событий нужны далеко не в каждой про-
программе. Используйте их (как и другие средства языка С#) только при необходимости.
Вместе с тем мощный, а главное, удобный в использовании механизм обработки собы-
событий обязательно вам пригодится в тех случаях, когда нужно сочетать длительную об-
обработку данных или ожидание чего-либо с функционированием пользовательского ин-
интерфейса программы. Эти средства будут вам нужны при работе в базами данных
и передаче данных по локальным сетям и через Интернет.
Глава 11. Делегаты и события 367
Глава 12. Работа с текстовыми
строками
В то время как первые компьютеры создавались главным образом для выполнения ма-
математических вычислений, сейчас они активно применяются для работы с текстами,
графикой, звуком и видео. При этом задача обработки текстовых символов и строк яв-
является одной из важнейших. Сегодня вы не найдете ни одной программы, в которой
так или иначе не использовались бы текстовые строки.
Практически в любом языке программирования предусмотрен богатый набор
функций, предназначенных для работы с символами и текстовыми строками. Эти
функции позволяют объединять строки, вырезать из строк фрагменты, менять строч-
строчные буквы на прописные и обратно, искать в строках другие строки и произвольные
последовательности символов и т. д.
В языках С и C++ стандартные функции, обрабатывающие символы и строки, яв-
являются частью библиотеки транслятора. Фактически они уже стали частью этих язы-
языков и потому описываются во всех учебниках, посвященных С и C++.
Что же касается языка программирования С#, то, как мы уже говорили, для пред-
представления текстовых строк в библиотеке классов Microsoft .NET Framework имеется
класс System.String. Вместе с набором операций, методов и индексаторов этот
класс представляет собой мощное средство обработки строк.
Помимо этого класса, существуют и другие, предназначенные для построения строк.
Это, например, класс StringBuilder, а также класс Regex. Последний из них предна-
предназначен для использования так называемых регулярных выражений (regular expressions),
представляющих собой мощнейшее средство обработки текста. Рассказ об этих возможно-
возможностях выходит за рамки нашей книги. При необходимости вы можете найти описание регу-
регулярных выражений в [11], атакже в учебниках по языку Perl.
Перед тем как мы приступим к изучению способов построения текстовых строк
в С#, напомним, чем строки С# отличаются от строк, с которыми имеют дело про-
программисты С, C++ и Pascal.
Прежде всего, программы, составленные на языках С, C++ и Pascal, имеют непо-
непосредственный доступ к представлению текстовых строк, хранящихся в оперативной
памяти компьютера. Такие программы могут, например, перезаписывать или изменять
содержимое строк по месту их расположения.
Что же касается программ С#, то все операции с текстовыми строками выполняют-
выполняются только при помощи методов, свойств, интерфейсов и индексаторов, предусмотрен-
предусмотренных в соответствующих классах библиотеки Microsoft .NET Framework.
Далее, программы С, C++ и Pascal могут работать с различными представлениями
текстовых строк, такими, как, например ASCII и UNICODE. В зависимости от пред-
представления (или, как еще говорят, кодировки) для хранения каждого символа строки
может использоваться 1, 2 или несколько байт памяти.
В языке С# текстовые строки представлены только в кодировке UNICODE, пред-
предполагающей представление одного текстового символа 2 байтами памяти.
~~Шютш зёв
Применение класса System.String
Методы и свойства класса System.String позволяют выполнять над строками вес
самые необходимые операции, такие, например, как сравнение, поиск подстроки, объ-
объединение строк, копирование символов из одной строки в другую, извлечение под-
подстроки и т. д.
Ниже мы рассмотрим приемы выполнения этих операций и приведем примеры
программ.
Создание строк
В гл. 1 мы рассказывали вам о текстовых строках и литералах, а также приводили не-
некоторые примеры их использования в программах С#.
До сих пор в наших программах встречались главным образом строки, созданные
при помощи литералов или полученные при работе тех или иных методов (например,
текстовые строки сообщений об ошибках). В классе System.String имеется набор
перегруженных конструкторов, позволяющих создавать текстовые строки и другими
способами.
Преобразование массива символов в строку
Если у вас есть массив символов UNICODE типа char, то его можно преобразовать
в строку string, воспользовавшись для этого конструктором, предусмотренным спе-
специально для этой цели в классе System. String.
Рассмотрим пример программы, исходный текст которой приведен в листинге 12.1.
Листинг 12.1. Файл ch12\CreateString\CreateStringApp.cs
using System;
namespace CreateString
{
class CreateStringApp
{
static void Main(string[] args)
{
char[] ch = { ■ H ' , ' e ' , ' 1 ■ , ■ 1' , ' о■, •! ■ };
string s = new String(ch);
Console.WriteLine(s) ;
Console.ReadLine();
Здесь мы объявили массив символов ch и проинициализировали его при помощи
символьных литералов:
char[] ch = { 'Н■, 'е', '1', '1', 'о', '!' };
Глава 12. Работа с текстовыми строками 369
Чтобы преобразовать этот массив символов в текстовую строку, мы воспользова-
воспользовались конструктором класса System. String:
string s = new String(ch);
Отобразив полученную строку методом WriteLine, мы получим ожидаемый ре-
результат, а именно увидим на консоли строку «Hello !».
Создание строки на базе фрагмента массива
В предыдущей программе мы преобразовали в строку целиком весь массив символов.
Существует способ создания текстовой строки на базе любого фрагмента такого мас-
массива.
Рассмотрим следующую программу (листинг 12.2).
Листинг 12.2. Файл ch12\SubArray\SubArrayApp.cs
using System;
namespace SubArray
{
class SubArrayApp
{
static void Main(string[] args)
{
chart] ch={'H\ 'e', '1', 'I1, 'o', ■,', ' \ 'W. 'o1,
'r1, '1', 'd1 , ■ ! ■ , };
string s = new String(ch, 7, 6);
Console.WriteLine(s) ;
Console.ReadLine()■
Здесь мы воспользовались конструктором класса System.String, имеющим три
параметра:
string s = new String(ch, 7, 6);
Первый параметр задает ссылку на массив символов UNICODE, из которых нужно
сделать строку. Второй и третий параметры определяют соответственно индекс перво-
первого элемента массива и количество символов, из которых будет создана строка.
В результате работы приведенной выше программы на консоль выводится только
последние 6 символов массива, образующие строку «World!».
Заполнение строки символом
Иногда программисту может потребоваться создать строку, содержащую большое ко-
количество одинаковых символов. Типичный пример — создание разделителей для заго-
заголовков в листингах.
В профамме, исходный текст которой представлен в листинге 12.3, демонстриру-
демонстрируются два способа решения этой проблемы.
370 А. В. Фролов, Г. В. Фролов. Язык С# Самоучитель
Листинг 12.3. Файл ch12\FHI\FHIApp.cs
using System;
namespace Fill
{
class FillApp
{
static void Main(string[] args)
string header = "=====================
string title= "Оформление заголовков";
Console.WriteLine(header);
Console.WriteLine(title);
Console.WriteLine(header);
string headerl = new String('=', 40);
Console.WriteLine(header1);
Console.WriteLine(title);
Console.WriteLine(headerl);
Console.ReadLine();
Первый способ предполагает «лобовое» решение. Мы используем для инициализа-
инициализации строки длинный текстовый литерал:
string header = ■■========================================";
Этот способ имеет, по крайней мере, два недостатка.
Во-первых, такие литералы могут загромождать листинг программы. Во-вторых,
создавая литерал из повторяющихся символов, приходится подсчитывать количество
этих символов вручную, что очень неудобно.
Другой способ основан на использовании одного из конструкторов, предусмотрен-
предусмотренных в классе System. String специально для инициализации подобных строк:
string headerl = new String('=', 40);
В качестве первого параметра этому конструктору нужно передать повторяющийся
символ, а в качестве второго — количество повторов символа.
Небезопасные конструкторы в классе System.String
Как мы уже говорили, программы С# работают под управлением системы исполнения
.NET Framework, и даже в случае наличия в них ошибок не представляют опасности
для других работающих программ или операционной системы. Такая безопасная рабо-
работа накладывает некоторые ограничения на программы, что не всегда приемлемо.
При обращении к программному интерфейсу API операционной системы часто
приходится иметь дело с указателями, запрещенными к применению в обычных про-
программах С#. Тем не менее, вы можете создать так называемую небезопасную про-
программу (или неконтролируемую программу), в которой использование указателей до-
допускается.
Глава 12. Работа с текстовыми строками 371
Пример такой программы приведен в листинге 12.4.
Листинг 12.4. Файл ch12\UnsafeString\UnsafeStringApp.cs
using System;
namespace UnsafeString
{
class UnsafeStringApp
{
unsafe static void Main(string[] args)
{
char[] ch = { 'H', 'e' , '1 ' , ' 1' , ' о ' , ' ! ' } ;
string s;
fixed(char* ptr = &ch[0])
{
s = new String(ptr);
}
Console.WriteLine(s) ;
Console.ReadLine();
Обратите внимание на определение метода Main:
unsafe static void Main(string!] args)
Как видите, мы добавили ключевое слово unsafe. Это ключевое слово сообщает
компилятору С# о том, что внутри данного метода будут использованы небезопасные
конструкции С# (такие, например, как указатели).
Внутри метода Main имеется массив символов UNICODE с именем ch, проини-
циализированный статически. Нашей задачей будет преобразование этого массива
в текстовую строку с использованием указателей.
Все самое интересное находится в следующем фрагменте кода:
fixed(char* ptr = &ch[0])
s = new String(ptr);
Этот фрагмент представляет собой блок fixed. В круглых скобках после ключе-
ключевого слова fixed расположен оператор, выполняющий инициализацию указателя
ptr. Инициализация выполняется посредством записи в переменную ptr адреса са-
самого первого элемента массива ch с индексом 0.
372 А. В. Фролов, Г. В. Фролов. Язык С# Самоучитель
Программистам, составлявшим ранее программы на языках С и C++, известно, что
для получения адреса расположения переменной в оперативной памяти компьютера
используется символ &. Этот же символ применяется и в небезопасном коде С#.
Таким образом, перед выполнением тела блока fixed программа записывает
в указатель ptr адрес начала массива ch. Далее этот указатель передается соответст-
соответствующему конструктору класса System. String, который и формирует из него тек-
текстовую строку.
В документации на С# вы найдете еще несколько небезопасных конструкторов, по-
позволяющих создавать строки из массивов 8-разрядных целых чисел, а также символов
UNICODE.
Заметим также, что при трансляции небезопасной программы, исходный текст ко-
которой приведен в листинге 12.4, необходимо использовать ключ /unsafe (для пакетной
трансляции в командной строке) или приравнять свойства проекта Allow unsafe code
blocks значению True в диалоговом окне свойств проекта (рис. 12.1).
Configuration: J*rtlve(Debug)
Configuration Manager... |
£j Common Properties
'Zi Configuration Properties :
» BuM
Debugging :;
Advanced
:
В Code Generation '
CondWooal Completion Constant DEBUG; TRACE
Optnize code False
V Check for Arithmetic Overflow/Ut Fake
Warr*igLevd Warning level 4
Treat Warnings As Errors False
Bitiutpgb;-.:1-,'.- ■ ■ ■ ;;" ;.'■■-■■ -,.':: ■ .
Output Path bin\Debug\
XML Documentation File
Generate Debugging Information True
■ R,.gy,., [~. ГГ.У .-,...,,.; р:;кд
Alow игааГе code blocks ,-,-•..
Enable use of the unsafe keyword (Ansafo).
.2]
J.
Рис. 12.1. Включение режима использования небезопасного кода
Чтобы отобразить это окно на экране, щелкните название проекта в окне Solution
Explorer, расположенном по умолчанию в правом верхнем углу главного окна систе-
системы разработки программ Microsoft Visual Studio .NET.
Копирование и клонирование строк
Если вам нужно создать копию текстовой строки, то это можно сделать при помощи
обыкновенной операции присваивания. В этом случае в памяти будет находиться два
одинаковых объекта.
Глава 12. Работа с текстовыми строками
373
Например, ниже мы объявили текстовые строки src и si, после чего скопировали
первую строку во вторую:
string src= "Hello, World!";
string si = src;
Теперь у нас есть два различных объекта, содержащих одинаковые текстовые
строки.
Для копирования строк можно также воспользоваться статическим методом Сору,
определенным в классе String:
string s2 = String.Copy(src);
Результат будет такой же, как и при использовании оператора присваивания, — бу-
будет создан новый объект, содержащий исходную строку. В переменную s2 будет за-
записана ссылка на этот объект.
Операция клонирования строк очень похожа на операцию копирования, однако она
не приводит к созданию объекта-близнеца. Вместо этого создается еще одна ссылка на
уже существующий объект:
string s3 = (string)src.Clone();
После выполнения этого кода в переменную s3 будет записана ссылка на строку
src. Так как метод clone возвращает данные базового типа object, нам здесь при-
пришлось выполнить явное приведение типов.
Описанные выше операции копирования и клонирования демонстрируются в про-
программе, исходный текст которой приведен в листинге 12.5.
Листинг 12.5. Файл ch12\StringCopy\StringCopyApp.cs
using System;
namespace StringCopy
{
class StringCopyApp
{
static void Main(string[] args)
{
string src= "Hello, World!";
string si = src;
string s2 = String.Copy(src) ;
string s3 = (string)src.Clone();
Console.WriteLine("Исходная строка: {0}", src);
Console.WriteLine("Копия 1: {0}", si);
Console.WriteLine("Копия 2: {0}", s2);
Console.WriteLine("Клон: {0}", s2);
Console.ReadLine() ;
374 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Конкатенация строк
Иногда перед программистом встает задача конкатенации (объединения) текстовых
строк. Эта операция выполняется следующим образом: вначале нужно объявить новую
строку, а затем записать в нее объединяемые строки при помощи оператора + или ме-
метода String.Concat.
Вот как оператор + объединяет строки si и s2:
string si = "Hello,
string s2 = "World!";
string resl = si + s2;
В результате res 1 будет содержать строку «Hello, World!».
Заметим, что таким образом можно объединять произвольное количество тексто-
текстовых строк.
Другой способ объединения текстовых строк — применение метода String. Concat:
string res2 = String.Concat{si, s2);
Объединяемые строки передаются этому методу в качестве параметров. Метод
String.Concat возвращает ссылку на новую строку, полученную в результате объ-
объединения исходных строк.
Существует несколько перегруженных методов String.Concat, позволяющих
объединять до четырех текстовых строк:
string res3 = String.Concat(si, s2, resl, res2);
Кроме того, метод String.Concat в состоянии объединить все строки, храня-
хранящиеся в массиве:
string[] array =
{
"Это ", "массив ", "строк"
};
string res4 = String.Concat(array);
После выполнения этого фрагмента кода в переменной res4 будет находиться
строка «Это массив строк».
Все описанные выше способы объединения текстовых строк демонстрируются
в программе StringConcatApp.cs (листинг 12.6).
Листинг 12.6. Файл ch12\StringConcat\StringConcatApp.cs
using System;
namespace StringConcat
{
class StringConcatApp
{
static void Main(string[] args)
{
string si = "Hello, ";
string s2 = "World!";
Глава 12. Работа с текстовыми строками 375
}
string resl = si + s2;
string res2 = String.Concat(si, s2);
string res3 = String.Concat(si, s2, resl, res2);
string!] array =
"Это ", "массив ", "строк"
string res4 = String.Concat(array);
Console.WriteLine("Результат 1: {0}", resl);
Console.WriteLine("Результат 2: {0}", res2);
Console.WriteLine("Результат З: {О}", res3);
Console.WriteLine("Результат 4: {0}", res4);
Console.ReadLine();
}
Извлечение подстроки
Чтобы создать новую строку на базе фрагмента существующей строки, можно вос-
воспользоваться методом Substring (листинг 12.7).
Листинг 12.7. Файл ch12\StringSubstring\StringSubstringApp.cs
using System;
namespace StringSubstring
class StringSubstringApp
static void Main(string[] args)
string src = "Hello, World!";
string res = src.SubstringG, 6);
Console.WriteLine(res) ;
Console.ReadLine() ;
Первый параметр метода Substring задает начальный индекс извлекаемой под-
подстроки, а второй, необязательный, размер подстроки. Ниже в строку res будет запи-
записан фрагмент строки src длиной 6 символов и начинающийся с 7-й позиции:
string res = src.SubstringG, б);
Если второй параметр метода Substring (определяющий размер подстроки)
не задан, копируются все символы, начиная с исходной позиции и до конца строки.
376 А. В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Вставка подстроки
Одна из особенностей строк С# заключается в том, что прямая модификация сущест-
существующей строки невозможна. Таким образом, если программа создала текстовую стро-
строку, то, чтобы изменить ее, придется создавать копию и уже потом переписывать туда
символы исходной строки (в неизменном или модифицированном виде).
Специально для выполнения операции вставки одной строки в заданное место дру-
другой строки был предусмотрен метод Insert. Его использование демонстрируется
в программе, исходный текст которой приведен в листинге 12.8.
Листинг 12.8. Файл ch12\ln$ertString\lnsertStringApp.cs
using System;
namespace InsertString
{
class InsertStringApp
{
static void Main(string[] args)
{
string si = "Hello!";
string res= si.InsertE, ", World");
Console.WriteLine(res);
Console.ReadLine();
В качестве первого параметра методу Insert нужно передать индекс позиции ис-
исходной строки, в которую нужно вставить другую строку. Через второй параметр пе-
передается ссылка на вставляемую строку.
Здесь мы вставили запятую и слово «World» между словом «Hello» и восклица-
восклицательным знаком:
string si = "Hello!";
string res = si.InsertE, ", World");
В результате, как нетрудно догадаться, мы получим бессмертную фразу «Hello,
World!».
Замена символов и строк
Если вам нужно заменить в строке какой-либо символ другим или выполнить такую
замену для последовательности символов, используйте метод Replace.
В качестве первого параметра этому методу нужно передать заменяемый символ
или заменяемую последовательность символов. Второй параметр задает новые симво-
символы, на которые необходимо заменить исходный символ или последовательность сим-
символов.
Глава 12. Работа с текстовыми строками 377
Пример использования этого метода приведен в листинге 12.9.
Листинг 12.9. Файл ch12\Repiace\ReplaceApp.cs
using System;
namespace Replace
class ReplaceApp
static void Main(string[] args)
string str = "Hello, World!";
string res = str.Replace("World", "C# World");
string resl = str.Replace("o", ");
Console.WriteLine(res);
Console.WriteLine(resl);
Console.ReadLine();
}
}
Прежде всего мы меняем в исходной строке «Hello, World!» слово «World»
на слова «С# World»:
string str = "Hello, World!";
string res = str.Replace("World", "C# World");
В результате получится фраза «Hello, С# World!».
Далее мы меняем в полученной строке все буквы о на цифру 0:
string resl = str.Replace("о", "О");
В итоге получим фразу в «хакерском» стиле: «HellO, WOrId!».
Удаление символов из строки
Теперь решим другую задачу — удалим из текстовой строки фрагмент с заданным на-
начальным индексом и длиной.
Для решения этой задачи нам потребуется метод Remove (листинг 12.10).
Листинг 12.10. Файл ch12\Remove\RemoveApp.cs
using System;
namespace Remove
class RemoveApp
static void Main(string[] args)
string str = "Hello, World!";
string res = str.RemoveE, 7);
378 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Console.WriteLine(res);
Console.ReadLine();
■ }
В качестве первого параметра мы передаем методу Remove индекс запятой, а в ка-
качестве второго — число 7:
string str = "Hello, World!";
string res = str.RemoveE, 7);
В результате мы получим из строки «Hello, World!» строку «Hello !».
Удаление незначащих пробелов
При обработке строк, введенных пользователем, часто возникает задача удаления
из полученной в результате ввода строки различных незначащих символов, или, как их
еще называют, «белых пробелов» (white spaces). Это обычные пробелы, символы табу-
табуляции, символы возврата каретки, перевода строки и т. п.
Метод Trim позволяет вам удалить незначащие пробелы, расположенные как
в начале, так и в конце текстовой строки. Ее применение демонстрируется в програм-
программе, исходный текст которой приведен в листинге 12.11.
Листинг 12.11. Файл ch12\Trim\TrimApp.cs
using System;
namespace Trim
{
class TrimApp
{
static void Main(string[] args)
{
string str = "\t Hello,\tWorld! ";
string res = str.TrimO;
Console.WriteLine(res);
Console.ReadLine();
Здесь метод Trim удалит символ табуляции и пробел, расположенный в начале
строки str, а также пробел, находящийся в конце этой строки:
string str = "\t Hello,\tWorld! ";
string res = str.TrimO;
При этом символ табуляции, разделяющий слова фразы и расположенный в сере-
середине строки, останется нетронутым.
Помимо только что описанного метода Tr im удаление незначащих пробелов мож-
можно выполнять методами TrimEnd и TrimStart. Они используются аналогично ме-
методу Trim. Первый из этих методов удаляет незначащие пробелы, расположенные
в конце строки, а второй — в ее начале.
Глава 12. Работа с текстовыми строками 379
Преобразование к верхнему и нижнему регистру
Для преобразования всех символов строки к верхнему или нижнему регистру вы мо-
можете использовать методы ToUpper и ToLower соответственно.
Работа с этими методами демонстрируется в программе, представленной в листин-
листинге 12.12.
Листинг 12.12. Файл ch12\ToLowerToUpper\ToLowerToUpperApp.cs
using System;
namespace ToLowerToUpper
{
class ToLowerToUpperApp
{
static void Main(string!] args)
{
string str = "Однажды, в студеную зимнюю пору...";
string resl = str .ToLower О;
string res2 = str.ToUpper();
Console.WriteLine(resl);
Console.WriteLine(res2);
Console.ReadLine();
Если запустить эту программу на вьшолнение, то на экране консоли появятся две стро-
строки, первая из которых состоит из букв нижнего регистра, а вторая — из букв верхнего ре-
регистра:
однажды, в студеную зимнюю пору...
ОДНАЖДЫ, В СТУДЕНУЮ ЗИМНЮЮ ПОРУ...
Так как в языке С# текстовые символы хранятся в универсальной кодировке
UNICODE, преобразование регистра будет выполняться правильно не только для ла-
латинских символов, но и для символов других алфавитов.
Выравнивание по левому и правому краю поля
При форматном выводе текста иногда требуется выровнять текстовую строку по лево-
левому или правому краю поля заданной ширины.
Это можно сделать при помощи методов PadLef t и PadRight соответственно.
В качестве первого параметра этим методам передается ширина поля (в символах),
внутри которого необходимо выполнить выравнивание, а в качестве второго — сим-
символ-заполнитель (например, пробел).
Применение этих методов демонстрируется в программе, исходный текст которой
представлен в листинге 12.13.
380 А В. Фролов, Г. В. Фролов Язык С#. Самоучитель
Листинг 12.13. Файл ch12\Padding\PaddingApp.cs
using System;
namespace Padding
{
class PaddingApp
{
static void Main(string[] args)
{
string str - "Однажды, в студеную зимнюю пору...";
string resl = str.PadLeft(80, '_');
string res2 = str.PadRight(80, '_■);
Console.WriteLine(resl);
Console.WriteLine(res2);
Console.ReadLine();
В качестве символа заполнителя мы применили символ подчеркивания. Вот что
наша программа выведет на консоль после запуска:
Однажды, в студеную зимнюю пору...
Однажды, в студеную зимнюю пору...
Объединение массива строк
Для объединения строк, хранящихся в массиве, удобно использовать метод
String.Join. В качестве первого параметра этому методу передается символ-
разделитель, который будет добавлен после вставки каждой строки массива. Через
второй параметр передается ссылка на объединяемый массив строк.
Третий и четвертый параметры необязательные. Они задают соответственно индекс
первого элемента массива, с которого должно начинаться объединение, и количество объ-
объединяемых элементов. Если эти параметры не заданы, объединяется весь массив.
Пример использования этого метода вы найдете в программе, исходный текст ко-
которой представлен в листинге 12.14.
Листинг 12.14. Файл ch12Uoin\JoinApp.cs
using System;
namespace Join
(
class JoinApp
{
static void Main(string[] args)
{
string[] array =
{
"Это", "массив", "строк"
Глава 12. Работа с текстовыми строками 381
string res = String.Join{" ", array, 0, 3) ;
Console.WriteLine(res);
Console.ReadLine();
Массив строк определен в этой программе статически:
string!] array =
"Это", "массив", "строк"
В качестве символа-заполнителя мы используем пробел:
string res = String.Join(" ", array, 0, 3);
Хотя мы объединяем все строки массива array, для примера мы указали необяза-
необязательные параметры метода String.Join, определяющие первую ячейку массива
и количество объединяемых ячеек.
Разбор строки
Метод Split, определенный в классе String, позволяет выполнять разбор строк,
содержащих ключевые слова, отделенные друг от друга символами-разделителями.
Рассмотрим программу, предназначенную для разбора строки запуска утилит с па-
параметрами вида «имя_параметра=значение» (листинг 12.15).
Листинг 12.15. Файл ch12\Split\SplitApp.cs
using System;
namespace Split
class SplitApp
static void Main(string[] args)
string str = "rayprg x=l,y=4,z=5";
string[] resArray = str.Split(new Char[] {','/ ' '});
foreach(string res in resArray)
Console.WriteLine(res);
Console.ReadLine() ;
}
382 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Исходная строка, подлежащая разбору, представлена ниже:
string str = "myprg х=1,у=4,z=5";
Здесь мы объявили строку, предназначенную для запуска программы с именем
myprg, в которой этой программе передаются 3 параметра с именами х, у, иг.
Разбор выполняется при помощи метода Split:
string[] resArray = str.Split(new Char[] {',', ' '});
В качестве первого параметра этому методу нужно передать ссылку на массив сим-
символов-разделителей. В качестве таких символов мы указали здесь запятую и пробел.
Метод Split разбирает строку, создавая массив из отдельных ключевых слов, обна-
обнаруженных в строке. При необходимости вы можете ограничить размер создаваемого
массива, указав максимально допустимое значение верхней границы при помощи вто-
второго параметра метода Split.
После запуска описанной выше программы на консоли появится результат разбора
строки:
myprg
х=1
у=4
Сравнение строк
Вы можете сравнивать строки таким же образом, как и числа. Об этом мы подробно
рассказывали в главе, посвященной условным операторам.
Однако есть и еще одна возможность — использование статического метода
Compare. Сравниваемые строки передаются этому методу в качестве параметров. До-
Дополнительно методу Compare можно указывать, каким именно образом выполнять
сравнение.
Если строки равны, метод Compare возвращает нулевое значение. Если первая
строка меньше второй (используется лексикографическое сравнение), возвращается
отрицательное значение, а если больше — положительное.
В программе, исходный текст которой приведен в листинге 12.16, мы передаем ме-
методу Compare 3 параметра. Первые два из них представляют собой ссылки на сравни-
сравниваемые строки, а третий, имеющий значение true, указывает, что при сравнении
не следует учитывать регистр букв.
Листинг 12.16. Файл ch12\Compare\CompareApp.cs
using System;
namespace Compare
{
class CompareApp
{
static void Main(string[] args)
Глава 12. Работа с текстовыми строками 383
for ( ;;)
{
Console.Write("введите строку: ");
string s = Console.ReadLine();
if(String.Compare(s, "exit", true) == 0)
break;
В нашей программе имеется бесконечный цикл, который прерывает свою работу,
если ввести с консоли слово exit. Заметим, что это слово может быть набрано на лю-
любом регистре (так как при сравнении регистр не учитывается).
Класс String содержит несколько перефуженных методов Compare. Ниже мы
привели прототип такого метода, предназначенного для лексикографического сравне-
сравнения текстовых строк с учетом особенностей национального алфавита:
public static int Compare(
String stringl,
Int32 indexl.
String string2,
Int32 index2,
Int32 length,
Boolean ignoreCase,
Culturelnfo culture);
Параметры stringl и indexl задают соответственно первую сравниваемую
строку и позицию внутри ее. Параметры string2 и index2 имеют то же назначение,
относятся ко второй сравниваемой строке. Заметим, что параметры indexl
и index2 можно не указывать. В этом случае происходит сравнение полных строк.
С помощью необязательного параметра length можно задать максимальную длину
сравниваемых строк.
Если значение параметра ignoreCase равно true, то метод Compare не будет
учитывать регистр сравниваемых строк.
Однако самый интересный параметр— culture. С его помощью можно указать
функции национальный алфавит, который должен применяться для лексикографиче-
лексикографического сравнения строк.
В листинге 12.17 мы привели пример использования метода Compare для сравне-
сравнения строк с учетом национального алфавита.
Листинг 12.17. Файл ch12\Culture\CultureApp.cs
using System;
namespace Culture
{
class Cu]tureApp
(
static void Main(string[] args)
384 А В Фролов, Г. В Фролов. Язык С#. Самоучитель
string strl =
string str2 =
string str3 =
string str4 =
"Я из лесу вышел";
"Был сильный мороз"
"Hello, World!11;
"Learning С# Now";
Console.WriteLine(String.Compare(strl, str2, true,
new System.Globalization.Culturelnfo("ru")));
Console.WriteLine(String.Compare(str3, str4, true,
new System.Globalization.Culturelnfo("en")));
Console.ReadLine();
Чтобы лексикографическое сравнение строк выполнялось правильно, необходимо
учитывать особенности национальных алфавитов, использованных для представления
строк. Библиотека классов .Net Framework содержит специальный класс Sys-
System.Globalization.Culturelnfo, позволяющий задать национальный язык,
или, в терминологии Microsoft.NET Framework, культуру (culture).
Для обозначения культуры программист может использовать символическое имя
или код. В табл. 12.1 мы привели имена и коды некоторых культур. (Более полный
список имен и кодов культур вы найдете в приложении 2 к этой книге.) Обратите вни-
внимание, что при сравнении строк strl и str2, содержащих символы кириллицы, мы
указали русский алфавит. Для этого мы передали обозначение русского алфавита ru
конструктору класса System.Globalization.Culturelnfo. Латинский алфавит,
использованный для сравнения строк str3 и str4, обозначается как en.
Таблица 12.1. Имена и коды некоторых культур
Национальный язык, страна или район
не учитывается
Английский
Английский (Канада)
Английский (Объединенное Королевство)
Английский (Соединенные Штаты Америки)
Арабский
Белорусский
Белорусский (Беларусь)
Испанский
Испанский (Испания)
Итальянский
Итальянский (Италия)
Китайский (Китай)
Имя культуры
"" (пустая строка)
en
en-CA
en-GB
en-US
ar
be
be-BY
es
es-ES
it
it-IT
zh-CN
Код культуры
0x007F
0x0009
0x1009
0x0809
0x0409
0x0001
0x0023
0x0423
OxOOOA
OxOCOA
0x0010
0x0410
0x0804
Глава 12. Работа с текстовыми строками
13 Язык С# Самоучитель
385
Национальный язык, страна или район
Корейский
Корейский (Корея)
Немецкий
Польский
Польский (Польша)
Русский
Русский (Россия)
Татарский
Татарский (Россия)
Узбекский
Узбекский (Узбекистан, кириллица)
Узбекский (Узбекистан, латиница)
Украинский
Украинский (Украина)
Французский
Французский (Франция)
Французский (Швейцария)
Хинди
Хинди (Индия)
Японский
Японский (Япония)
Имя культуры
ко
ko-KR
de
Pi
pl-PL
ru
ru-RU
tt
tt-RU
uz
Cy-uz-UZ
Lt-uz-UZ
uk
uk-UA
fr
fr-FR
fr-CH
hi
hi-IN
ja
ja-JP
Код культуры
0x0012
0x0412
0x0007
0x0015
0x0415
0x0019
0x0419
0x0044
0x0444
0x0043
0x0843
0x0443
0x0022
0x0422
ОхОООС
0x040C
OxlOOC
0x0039
0x0439
0x0011
0x0411
Форматирование текстовых строк
Одна из важнейших задач, которую приходится решать программисту при разработке
приложений любого типа, — форматирование текстовых строк. С помощью текстовых
строк обычно представляется числовая информация, такая, как номера заказов, коли-
количество каких-либо предметов, цены, дата, время и т. д.
Если вы программировали раньше на языке С или C++, то вам знакомы такие сред-
средства форматирования, как функции printf и sprintf, а также управляющие симво-
символы в потоках вывода. Эти средства, ставшие стандартными, позволяют представить
числа различных типов практически в любом формате.
Что же касается С#, то сам по себе этот язык не содержит средств форматирования
строк. Однако богатейшие возможности такого форматирования предоставляются
программисту в рамках библиотеки классов Microsoft .NET Framework. Мы уже ис-
использовали некоторые средства форматирования, предоставляемые методом Con-
Console .WriteLine, когда выводили в консольное окно шестнадцатеричные числа.
Аналогичное форматирование доступно при формировании текстовых строк методом
String. Format, не имеющим никакого отношения к консольному выводу.
386 А В Фролов. Г В Фролов. Язык С# Самоучитель
Так как данные любого типа в С# являются объектами некоторых классов, это от-
открывает новые возможности для форматного представления этих объектов в виде тек-
текстовых строк. В этом разделе мы рассмотрим этот вопрос применительно к форматно-
форматному представлению чисел.
Заметим, что библиотека классов .Net Framework позволяет легко решить и обрат-
обратную задачу — преобразование текстовых строк с числами в числовые значения. Это
необходимо, например, для обработки чисел, введенных пользователями при помощи
клавиатуры.
Представление целых чисел
Чтобы преобразовать целочисленное значение в текстовую строку с форматированием
при помощи метода string.Format, необходимо задать этому методу так называе-
называемую строку формата (format string), а также передать в качестве параметров одно или
несколько преобразуемых значений. В ответ данный метод возвратит отформатиро-
отформатированную строку.
По принципу использования метод String. Format больше всего похож на функ-
функцию sprintf, знакомую всем программистам С.
Строка формата задается методу String. Format в следующем виде:
{ N [, М ][: formatString ]}
Здесь число N задает номер преобразуемого аргумента, передаваемого методу
String. Format в качестве параметра.
Необязательное число М задает ширину области текстовой строки (в символах),
внутри которой необходимо поместить цифры преобразуемого значения. Если это
число отрицательное, цифры числа выравниваются по левой границе данной области,
а если положительное — по правой границе области.
И наконец, строка formatString задает коды форматирования, которые мы ско-
скоро рассмотрим. Для форматирования целых чисел используются эквивалентные коды
форматирования d и D.
Рассмотрим программу, исходный текст которой представлен в листинге 12.18.
Эта программа демонстрирует применение различных способов форматирования для
представления целых чисел в виде текстовых строк.
Листинг 12.18. Файл ch12\StringFormat\StringFormatApp.cs
using System;
namespace StringFormat
{
class StringFormatApp
{
static void Main(stringt] args)
{
int iSignedNumber = 777;
string result;
result = String.Format("{0}", iSignedNumber);
Console.WriteLine(result);
Глава 12. Работа с текстовыми строками 387
result = String.Format("{0:x}", 0x23fabc);
Console.WriteLine(result) ;
result = String.Format("{0:X}", 0x23fabc);
Console.WriteLine(result) ;
result = String.Format("{0:d2)", iSignedNumber);
Console.WriteLine(result) ;
result = String.Format("{0:d8}", iSignedNumber);
Console.WriteLine(result);
result = String.Format("{0,5:d}", iSignedNumber);
Console.WriteLine(result);
result = String.Format("{0,-5:d}3TO счастливое число",
iSignedNumber);
Console.WriteLine(result) ;
Console.ReadLine();
Автоматическое форматирование
В самом простейшем случае можно вообще не указывать код форматирования,
при этом будет использован формат по умолчанию:
int iSignedNumber = 777;
string result;
result = String.Format("{0}", iSignedNumber);
Console.WriteLine(result);
В данном случае на консоль будет выведена строка 777. Аналогичного результата
можно было бы Добиться и следующим более простым образом:
Console.WriteLine("{0}", iSignedNumber);
Именно так мы выводили на консоль данные в примерах программ, приведенных
в нашей книге. Заметим, однако, что этот способ подходит только для консольных
программ. Что же касается программ с оконным пользовательским интерфейсом,
то там, как правило, необходимо перед выводом преобразовывать числовые значения
в текстовые строки. Эта задача и решается при помощи метода String. Format.
Представление чисел в шестнадцатеричном формате
Если вам нужно представить целочисленное значение в шестнадцатеричном формате,
необходимо указать код форматирования х или X:
result = String.Format("{0:х)", 0x23fabc);
Console.WriteLine(result);
388 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
result = String.Format("{0:X)", 0x23fabc);
Console.WriteLine(result) ;
В первом случае шестнадцатеричное число 0x23fabc будет отображаться с исполь-
использованием нижнего регистра, а во втором — верхнего:
23fabc
2 3 FABC
При отображении шестнадцатеричных чисел префикс Ох не добавляется, так что
вы можете выделять шестнадцатеричные числа любым способом по вашему усмотре-
усмотрению или не выделять их вовсе.
Определение ширины поля вывода
Непосредственно после кода форматирования вы можете указать ширину поля вывода
в символах. При этом если в числе меньше цифр, чем ширина поля вывода, то при
формировании строки это число будет дополнено слева нулями. Если же цифр больше,
чем значение ширины вывода, то будут выведены все цифры числа.
Рассмотрим следующий пример, где мы форматируем трехзначное число:
int iSignedNumber = 777;
string result;
result = String.Format("{0:d2}", iSignedNumber);
Console.WriteLine(result);
result = String.Format("{0:d8}", iSignedNumber);
Console.WriteLine(result);
В результате работы этого фрагмента программы на консоли появятся следующие
две строки:
777
00000777
Несмотря на то что в первом случае мы указали в строке формата ширину поля,
равную двум, в полученной текстовой строке присутствуют все цифры исходного чис-
числа. Таким образом, форматирование не может исказить значение числа.
Во втором случае ширина поля вывода составляет 8 символов, но в числе только
3 значащих цифры. Поэтому при форматировании это число было дополнено слева
пятью нулями.
Ширина поля вывода может задаваться и при отображении числе в шестнадцате-
ричной системе счисления.
Выравнивание числа внутри поля вывода
Указав ширину поля вывода после номера аргумента (через запятую), можно выпол-
выполнить выравнивание числа внутри этого поля.
Глава 12. Работа с текстовыми строками 389
Ширина поля вывода может задаваться как положительными числами, так и отри-
отрицательными:
result = String.Format("{0,5:d}", iSignedNumber);
Console.WriteLine(result) ;
result = String.Format("{0/-5:d}3To счастливое число",
iSignedNumber);
В первом и втором случаях мы указали ширину поля вывода, равную пяти симво-
символам. Когда это число положительное, символы пробела добавляются слева, а когда от-
отрицательное — справа:
777
777 Это счастливое число
Таким образом, указывая ширину поля вывода, вы можете выравнивать отобра-
отображаемые числа по правой или левой границе поля.
Представление чисел с фиксированной
десятичной точкой
Для представления чисел с фиксированной десятичной точкой используются равно-
равнозначные коды формата d и D. Вслед за кодом формата обычно указывают необходимое
количество знаков после десятичной точки.
Пример программы форматного вывода чисел с фиксированной десятичной точкой
приведен в листинге 12.19.
Листинг 12.19. Файл ch12\Decimal\DecimalApp.cs
using System;
namespace Decimal
{
class DecimalApp
{
static void Main(string[] args)
{
double pi = 3.1415926;
string result;
result = String.Format("{0}", pi);
Console.WriteLine(result);
result = String.Format("{0:f}", pi);
Console.WriteLine(result);
result = String.Format("{0:f3}", pi);
Console.WriteLine(result);
result = String.Format("{0,5:f3)", pi);
Console.WriteLine(result);
390 А В Фролов, Г В. Фролов Язык С# Самоучитель
result = String.Format("{0:fO}", pi);
Console.WriteLine(result);
result = String.Format("{0:f10}", pi)
Console.WriteLine(result);
Console.ReadLine();
Формат по умолчанию
Прежде всего наша программа выводит значение числа тс, используя для этого форма-
форматирование по умолчанию:
double pi = 3.1415926;
string result;
result = String.Format("{0}", pi);
Console.WriteLine(result);
Данное значение будет выведено на консоль в следующем виде:
3,1415926
Можно использовать только код формата, не указывая количество знаков после де-
десятичной точки:
result = String.Format("{0:f}", pi);
Console.WriteLine(result);
В этом случае, однако, значение может быть округлено:
3,14
Как результат, точность отображаемого значения может оказаться недоста-
недостаточной.
Указание количества знаков после десятичной точки
Для получения предсказуемого результата мы рекомендуем всегда задавать необходи-
необходимое количество знаков после десятичной точки:
result = String.Format("{0:f3}", pi);
Console.WriteLine(result);
В этом примере после запятой будет отображено 3 цифры:
3,142
Глава 12. Работа с текстовыми строками 391
Ограничение ширины поля вывода
Для ограничения ширины поля вывода можно задать необходимое значение через за-
запятую после номера аргумента метода String. Format, как мы это делали для целых
чисел:
result = String.Format("{0,5:f3}", pi);
Console.WriteLine (result) ,-
В результате на консоль будет выведено только 5 символов нашего числа (включая
десятичную запятую):
3,142
Извлечение целой части числа
Если вам нужно отформатировать при выводе число с плавающей десятичной точкой
таким образом, чтобы отобразить только целую часть числа, укажите количество цифр
после десятичной точки, равное нулю:
result = String.Format("{0:f0}", pi) ;
Console.WriteLine(result);
Этот фрагмент кода выведет на консоль число 3.
Избыточная точность при выводе
Если ширина поля вывода превышает количество значащих цифр в числе с фиксиро-
фиксированной десятичной точкой, то такое число будет дополнено справа необходимым ко-
количеством нулей.
Например, здесь мы форматируем наше число для вывода в поле шириной 10 сим-
символов:
result = String.Format("{0:f10}", pi);
Console.WriteLine(result);
При этом к числу будет дописано справа 3 нуля:
3,1415926000
Обратите внимание, что таким способом мы не увеличили точность представления
числа к, а только добавили к числу дополнительные нули.
Представление чисел в научном формате
Научный, или экспоненциальный, формат обычно используется в научных расчетах
для представления чисел, лежащих в большом диапазоне значений. Для форматирова-
форматирования таких чисел применяются коды форматов е и Е.
Пример программы, отображающей на консоли отформатированные числа в науч-
научном формате, представлен в листинге 12.20.
392 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Листинг 12.20. Файл ch12\Science\ScienceApp.cs
using System;
namespace Science
{
class ScienceApp
{
static void Main(string[] args)
{
double pi = 0.31415926E1;
string result;
result = String.Format("{0}", pi);
Console.WriteLine(result) ;
result = String.Format("{0:e}", pi);
Console.WriteLine(result);
result = String.Format("{0:e3}", pi);
Console.WriteLine(result);
Console.ReadLine();
Здесь мы будем выводить значение числа п, представленного в научном формате:
double pi = 0.31415926Е1;
Такие числа можно выводить с использованием формата по умолчанию:
result = String.Format("{0}", pi);
Console.WriteLine(result);
Если при этом результат можно будет представить в формате с фиксированной де-
десятичной точкой, то он и будет использован. Вот что вы увидите на консоли, запустив
нашу программу:
3,1415926
3,141593е+000
3,142е+000
Первое число выводится с использованием формата по умолчанию.
Во втором случае мы указали формат вывода явным образом:
result = String.Format("{0:е)", pi);
Console.WriteLine(result);
И наконец, в третьем случае мы ограничили тремя количество цифр, отображаемых
после запятой:
result = String.Format("{0:еЗ}", pi);
Console.WriteLine(result);
Глава 12. Работа с текстовыми строками 393
Выделение тысяч при отображении больших чисел
При отображении больших многозначных чисел для большей наглядности часто отде-
отделяют друг от друга разряды тысяч при помощи пробела (по 3 разряда). Для подобного
форматирования применяется код формата п или N.
Рассмотрим пример программы (листинг 12.21).
Листинг 12.21. Файл ch12\Delemiter\DelemiterApp.cs
using System;
namespace Delemiter
{
class DelemiterApp
{
static void Main(string[] args)
{
double oneMByte = 104857 6;
string result;
result = String.Format("{0:n}", oneMByte);
Console.WriteLine(result);
result = String.Format("{0:n4}", oneMByte);
Console.WriteLine(result);
result = String.Format("{0:n3}", oneMByte);
Console.WriteLine(result);
Console.ReadLine();
Здесь мы сначала указываем код формата п, не дополняя его количеством цифр
дробной части:
double oneMByte = 1048576;
string result,-
result = String.Format("{0:n}", oneMByte);
Console.WriteLine(result);
В результате наше число (количество байтов в одном мегабайте) будет отображено
следующим образом:
1 048 576,00
При необходимости мы можем указать нужное количество цифр после запятой:
result = String.Format("{0:n4)", oneMByte);
Console.WriteLine(result);
result = String.Format("@:n3}", oneMByte);
Console.WriteLine(result);
394 А В. Фролов, Г. В Фролов. Язык С#. Самоучитель
В первом случае мы указали 4 цифры после запятой, а во втором — 3. Вот что по-
получилось после запуска программы:
1 048 576,0000
1 048 576,000
Универсальный формат для представления чисел
В тех случаях, когда результат вычислений должен быть представлен в наиболее на-
наглядном виде, удобнее использовать универсальный формат. Код этого формата — g
или G.
В зависимости от того, какое значение форматируется, система автоматически вы-
выбирает формат с плавающей десятичной точкой или научный формат.
Рассмотрим программу, исходный текст которой приведен в листинге 12.22.
Листинг 12.22. Файл ch12\General\GeneralApp.cs
using System;
namespace General
{
class GeneralApp
{
static void Main(string[] args)
{
double oneMByte = 1048576;
double small = 0.0000012345;
string result;
result = String.Format("{0:g}", oneMByte);
Console.WriteLine(result);
result = String.Format("{0:g}", small);
Console.WriteLine(result);
result = String.Format("@:g3}", oneMByte);
Console.WriteLine(result);
result = String.Format("{0:g3}", small);
Console.WriteLine(result);
Console.ReadLine();
Здесь мы выводим по очереди два числа, одно из которых по своей величине дос-
достаточно большое, а другое — маленькое:
double oneMByte = 104857 6;
double small = 0.0000012345;
string result;
Глава 12. Работа с текстовыми строками
395
result = String.Format("{0:g}", oneMByte);
Console.WriteLine(result);
result = String.Format("{0:g}", small);
Console.WriteLine(result);
При этом для первого числа будет автоматически выбран формат с плавающей де-
десятичной точкой, а для второго — научный формат:
1048576
1,2345е-06
При необходимости можно ограничить точность отображаемого значения, указав
количество значащих дробных разрядов:
result = String.Format("{0:g3}", oneMByte);
Console.WriteLine(result);
result = String.Format("{0:g3}", small);
Console.WriteLine(result);
Вот результат работы этого фрагмента кода:
1,05е+0б
1,23е-06
Формат для представления денежных сумм
Специально для вывода значений денежных сумм предусмотрены форматы с и С.
При использовании этих форматов числа выводятся с разделением разрядов на тысячи,
а также с добавлением обозначения денежной единицы. Символ-разделитель, а также
обозначение денежной единицы зависят от настройки локализации в управляющей па-
панели ОС Microsoft Windows.
Пример программы, отображающей на. консоли денежную сумму в различных
форматах, приведен в листинге 12.23.
Листинг 12.23. Файл ch12\Currency\CurrencyApp.cs
using System;
namespace Currency
{
class CurrencyApp
{
static void Main(string[] args)
{
double someMoney = 1048576.25;
string result;
result = String.Format("{0:c}", someMoney);
Console.WriteLine(result);
396 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
result = String.Format("{0:c2}", someMoney);
Console.WriteLine(result);
result = String.Format("{0:C3}", someMoney);
Console.WriteLine(result);
Console.ReadLine();
Вот что эта программа выводит на консоль:
1 048 576,25р.
1 048 576,25р.
1 048 576,250р.
При настройке локализации для России в качестве обозначения денежной единицы
используется рубль. Указав количество разрядов после запятой, равное двум, можно
отображать не только рубли, но и копейки.
Использование шаблонов
при форматировании
Шаблоны открывают перед программистами дополнительные возможности формати-
форматирования чисел в процессе их преобразования в текстовые строки.
Шаблоны создаются с помощью символов 0, #, % и точки с запятой. Использование
этих символов зависит от способа форматирования.
Форматирование целых чисел
При форматировании целых чисел символ 0 соответствует символу преобразуемой
строки или нулевому значению (если данная позиция не используется).
Позиция, в которой стоит символ #, будет игнорироваться, если в шаблоне перед
ней не стоит 0 или другой символ. В первом случае будет выведено нулевое значение
или символ преобразуемой строки, а во втором — данная позиция будет пропущена
или в ней будет символ преобразованной строки.
Рассмотрим программу, отображающую на консоли 7-значный телефонный номер
(листинг 12.24). (Телефонный номер приведен в ней только для примера, так что не
пытайтесь по нему звонить!)
Листинг 12.24. Файл ch12\Template1\Template1App.cs
using System;
namespace Templatel
{
class TemplatelApp
{
static void Main(string[] args)
Глава 12. Работа с текстовыми строками 397
uint number = 9367733; // только для примера!
string result;
result = String.Format("{0:#000-00-00}", number);
Console.WriteLine(result);
result = String.Format("{0:#0000-00-00}", number);
Console.WriteLine(result);
result = String.Format("{0:##00-00-00}", number);
Console.WriteLine(result);
Console.ReadLine();
В первом случае мы выделяем в телефонном номере группы цифр, разделяя их де-
дефисом:
result = String.Format("{0:#000-00-00}", number);
Console.WriteLine(result);
Вот что появится на консоли в результате выполнения этих строк:
936-77-33
Если нужно дополнить отображаемое целое число нулями с левой стороны, укажи-
укажите необходимое количество нулей в левой позиции:
result = String.Format("{0:#0000-00-00}", number);
Console. WriteLine (result) ,-
Теперь наш номер будет дополнен слева одним нулем:
0936-77-33
Если указать меньше нулей, чем надо, число все равно будет выведено полностью:
result = String.Format("{0:##00-00-00}", number);
Console.WriteLine(result);
Вот результат работы этого фрагмента программы:
936-77-33
Форматирование чисел с плавающей десятичной точкой
При форматировании чисел с плавающей десятичной точкой символы шаблона 0 и #
применяются аналогично тому, как они применяются при форматировании целых чи-
чисел. Дополнительно для выделения тысяч в шаблоне применяется запятая.
Мы приведем пример использования нестандартного форматирования для решения та-
такой распространенной задачи, как выделение тысяч при выводе чисел (листинг 12.25).
398 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Листинг 12.25. Файл ch12\Template2\Template2App.cs
using System;
namespace Template2
{
class Template2App
{
static void Main(string[] args)
{
double number = 478903208236.567956;
string result;
Console.WriteLine(number);
result = String.Format("{0:#,#.000000}", number);
Console.WriteLine(result);
Console.ReadLine(); }
Здесь мы указали в шаблоне между символами # символ запятой, в результате чего
при выводе числа разряды тысяч будут выделены. Наша программа выводит число
в исходном и отформатированном виде:
478903208236,568
478 903 208 236,568000
Обратите внимание, что мы задали отображение шести цифр после запятой.
В результате наше число после округления было дополнено тремя незначащими
нулями.
Форматирование чисел с процентами
Если к строке шаблона добавить символ процента (%), то перед выводом значение
числа будет увеличено в 100 раз. Кроме того, будет выведен и сам символ %.
Этот прием форматирования демонстрируется в программе, исходный текст кото-
которой приведен в листинге 12.26.
Листинг 12.26. Файл ch12\TemplatePercent\TemplatePercentApp.cs
using System,-
namespace TemplatePercent
{
class TemplatePercentApp
{
static void Main(string[] args)
{
double number = 0.4756;
string result;
Глава 12. Работа с текстовыми строками 399
result = String.Format("{0:##.#%}", number);
Console.WriteLine(result);
Console.ReadLine(); }
Вот что эта программа выведет на консоль:
47,6%
Заметим, что в одном шаблоне может встречаться несколько символов %. Каждый
из них будет действовать описанным выше образом.
Форматирование с учетом знака чисел
Если для положительных, отрицательных и нулевых чисел нужно использовать разное
форматирование, вы можете указать 3 шаблона, разделив их точкой с запятой. Первый
из этих шаблонов будет использоваться для вывода положительных чисел, второй —
отрицательных, а третий — равных нулю.
В листинге 12.27 мы привели исходный текст программы, демонстрирующей ис-
использование описанного выше форматирования.
Листинг 12.27. Файл ch12\TemplateSign\TemplateSignApp.cs
using System;
namespace TemplateSign
{
class TemplateSignApp
{
static void Main(string[] args)
{
string result;
result = String.Format("{0:плюс 0;минус 0;0}", 10);
Console.WriteLine(result);
result = String.Format("{0:плюс 0;минус 0;0}", 0);
Console.WriteLine(result);
result = String.Format("{0:плюс 0;минус 0;0}", -10);
Console.WriteLine(result);
Console.ReadLine(); }
Вот что наша программа вывела на консоль после запуска:
плюс 10
0
минус 1 О
Как видите, мы смогли заменить символы + и - на слова плюс и минус соответст-
соответственно.
400 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Создание новых форматов
Как вы знаете, объект любого класса может быть представлен в виде текстовой строки.
Для этого в классе необходимо определить метод ToString. Однако таким образом
можно получить только одно-единственное текстовое представление объекта, что не
всегда удобно.
Реализуя метод ToString интерфейса IForraattable, ваша программа сможет
выбирать различные форматы для преобразования объектов в текстовые строки.
Рассмотрим пример программы, исходный текст которой представлен в листин-
листинге 12.28.
Листинг 12.28. Файл ch 12\CustomFormat\CustomFormatApp.cs
using System;
namespace CustomFormat
{
class MyNumber: IFormattable
{
public uint number;
public MyNumber(uint n)
{
number = n;
public string ToString(string format, IFormatProvider fp)
{
i f(format.Equals("bin"))
{
return Convert.ToString(number, 2);
}
else if(format.Equals("dec"))
{
return Convert.ToString(number, 10);
}
else if(format.Equals("hex"))
{
return Convert.ToString(number, 16);
}
else
{
return number.ToString(format, fp);
class CustomFormatApp
С
static void Main(string[] args)
{
MyNumber n = new MyNumberA0241024);
Глава 12. Работа с текстовыми строками 401
String result = n.ToString("bin", null)
Console.WriteLine(result);
result = n.ToString{"dec", null);
Console.WriteLine(result);
result = n.ToString("hex", null);
Console.WriteLine(result);
Console.ReadLine();
В этой программе мы объявили класс MyNumber, реализующий интерфейс
IFormattable:
class MyNumber: IFormattable
{
public uint number;
public MyNumber(uint n)
{
number = n;
}
public string ToString(string format, IFormatProvider fp)
В рамках этого интерфейса мы определили метод ToString, которому передают-
передаются два параметра — текстовая строка с обозначением формата и ссылка на интерфейс
провайдера формата IFormatProvider.
Класс MyNumber содержит также конструктор и поле number для хранения целых
чисел без знака.
Вот исходный текст метода ToString, выполняющего то или иное форматирова-
форматирование в зависимости от строки формата, передаваемого в качестве первого параметра:
public string ToString(string format, IFormatProvider fp)
{
if(format.Equals ("bin"))
{
return Convert.ToString(number, 2);
}
else if(format.Equals("dec"))
{
return Convert.ToString(number, 10);
}
else if(format.Equals("hex"))
{
return Convert.ToString(number, 16);
__}
402 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
else
{
return number.ToString(format, fp);
}
}
Преобразование чисел в текстовые строки выполняется здесь методом Con-
Convert .ToString. В качестве первого параметра этому методу передается преобра-
преобразуемое число, а в качестве второго — основание (двоичное, десятичное или шестна-
дцатеричное).
Получив управление, метод Main нашей программы создает объект класса
MyNumber при помощи конструктора, а затем преобразует его в текстовую строку
тремя различными способами:
MyNumber n = new MyNumberA0241024);
String result = n.ToString("bin", null);
Console.WriteLine(result);
result = n.ToString("dec", null);
Console.WriteLine(result);
result = n.ToString("hex", null);
Console.WriteLine(result);
В первом случае значение числа выводится на консоль в двоичном формате,
во втором — в десятичном формате, а в третьем — в шестнадцатеричном формате:
100111000100010000000000
10241024
9с4400
В том случае, если ни один указанный формат не подойдет, наш метод ToString
использует форматирование по умолчанию.
Преобразование текстовых строк в числа
Очень часто перед программистом встает задача преобразования текстовых строк в числа.
Для решения этой задачи проще всего воспользоваться методом Parse, предусмотрен-
предусмотренным с этой целью в классах числовых типов (таких, как Intl 6, Int3 2 и т. д.).
Разумеется, не всякую текстовую строку можно преобразовать в число. Обычно
программистам приходится проверять, содержит ли такая строка только цифры
или другие допустимые символы. Метод Parse избавит вас от этой необходимости,
так как при невозможности выполнить преобразование образуется исключение Sys-
System .FormatException.
Использование метода Parse демонстрируется в программе, исходный текст кото-
которой приведен в листинге 12.29.
Глава 12. Работа с текстовыми строками 403
Листинг 12.29. Файл ch 12\Text2Number\Text2NumberApp.cs
using System;
namespace Text2Number
{
class Text2NumberApp
{
static void Main(string[] args)
{
string input;
int val = 0;
while(true)
{
Console.Write("\п\пВведите число: ");
input = Console.ReadLine();
if(input == "exit")
break;
try
{
val = Int32.Parse(input);
}
catch(System.FormatException ex)
{
Console.WriteLine(">>> Ошибка: {0}", ex.Message)
continue;
}
Console.WriteLine("Ввели число {0}", val);
Здесь мы в цикле вводим текстовые строки и выполняем попытку их преобразова-
преобразования в числа.
Вот как выглядит код, выполняющий преобразование:
try
{
val = Int32.Parse(input);
)
catch(System.FormatException ex)
{
Console.WriteLine(">>> Ошибка: {0}", ex.Messagel;
continue;
404 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Здесь мы вызываем статический метод Int32 .Parse, передавая ему преобразуе-
преобразуемую текстовую строку. Если текстовую строку удается преобразовать в числовое зна-
значение, метод возвращает результат преобразования. В противном случае возникает ис-
исключение System. FormatException. Если возникло исключение, наша программа
возобновляет работу цикла с самого начала.
Вот пример сценария работы с программой:
Введите число: 123
Ввели число 123
Введите число: qwerty
>>> Ошибка: Input string was not in a correct format.
Строка 123 была успешно преобразована в число 123, а попытка преобразования
в число строки qwerty завершилась аварийно с возникновением исключения Sys-
System .FormatException.
Глава 12. Работа с текстовыми строками 405
Глава 13. Контейнеры для хранения
объектов
При создании программ самого разного назначения часто приходится решать задачу
создания наборов данных, позволяющих хранить данные в структурированном виде.
Простейшим примером такого набора данных может послужить обыкновенный мас-
массив. Массив позволяет хранить объекты одного и того же типа, адресуясь к ним по но-
номерам, т. е. с использованием индекса элемента. Зная индекс, вы можете изменить или
извлечь содержимое любой ячейки массива.
Заметим, что, несмотря на удобство использования, обычным массивам присущи
довольно существенные недостатки:
• размер массива нельзя изменить после того, как массив был создан;
• массив навязывает упорядоченное расположение данных, даже если по своей внут-
внутренней природе данные неупорядоченны;
• для поиска данных в массиве необходимо последовательно перебрать все его ячейки.
Разумеется, эти недостатки будут сказываться далеко не всегда. Однако в ряде слу-
случаев данные намного удобнее хранить с помощью других средств, в частности с по-
помощью так называемых контейнеров. Контейнеры представляют собой классы, специ-
специально предназначенные для упорядоченного хранения данных различных типов.
В терминах языка С# такие контейнеры называются наборами данных (collections), од-
однако в нашей книге мы будем использовать более привычный термин — контейнер.
Контейнеры в библиотеке классов
.NET Framework
Библиотека классов Microsoft .NET Framework содержит несколько классов, позво-
позволяющих создавать контейнеры следующих типов:
• массив ArrayList,
• словарь Hashtable,
• сортированный список SortedList,
• стек Stack,
• очередь Queue,
• массив Bit Array.
Далее в этой главе мы подробно расскажем вам об использовании этих контейне-
контейнеров и приведем примеры программ.
/ШОГгИИСЗИ 406 ~~
Массив ArrayList
Как мы уже говорили, обычные массивы обладают недостатками, несколько
ограничивающими их применение. Ниже мы рассмотрим преимущества использо-
использования массива ArrayList, а также приведем примеры программ, работающих с та-
такими массивами.
Создание массива
Массив ArrayList создается обычным образом, как и объект любого другого клас-
класса, — с помощью конструктора. Например, здесь мы создали массив myWords:
ArrayList myWords = new ArrayList();
Это один из трех возможных конструкторов класса ArrayList.
Второй вариант конструктора позволяет при создании массива задать его началь-
начальный размер:
ArrayList myWords = new ArrayListA0);
Здесь сразу после создания массив myWords будет содержать 10 ячеек (по умолча-
умолчанию создается массив размером 16 ячеек). Заметим, что при необходимости по мере
добавления элементов размер такого массива будет автоматически увеличиваться.
При создании массива с помощью этого конструктора возможно появление исклю-
исключения ArgumentOutOfRangeException. Оно будет создано в том случае, если
попытаться задать отрицательный размер создаваемого массива.
И наконец, третий конструктор предназначен для создания массивов ArrayList
на базе уже существующих:
ArrayList myWords = new ArrayList();
ArrayList myWordsl= new ArrayList(myWords) ;
Если передать этому конструктору ссылку, содержащую пустое значение null,
возникнет исключение ArgumentNullException.
Программа, исходный текст которой представлен в листинге 13.1, демонстрирует
простейший способ создания массива класса ArrayList, добавления в него новых
элементов, а также способ извлечения элементов массива.
Листинг 13.1. Файл ch13\ArrayListDemo\ArrayListDemoApp.cs
using System;
using System.Collections;
namespace ArrayListDemo
{
class ArrayListDemoApp
{
static void Main(string[] args)
{
ArrayList myWords = new ArrayList();
myWords.Add("Hello");
myWords.Add(", C# ") ;
Глава 13. Контейнеры для хранения объектов 407
myWords.Add("World");
myWords.Add("!");
System.Collections.lEnumerator myEnumerator =
myWords.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.Write("{0}", myEnumerator.Current);
}
Console.ReadLine();
Обратите внимание, что для работы с контейнерами мы подключили к нашей про-
программе пространство имен System.Collections:
using System.Collections;
В дальнейшем мы еще вернемся к исходному тексту этой программы.
Добавление элементов в массив
Для добавления элементов в массив ArrayList предназначен метод Add. В качестве
единственного параметра этот метод получает ссылку на добавляемый элемент. После
добавления метод Add возвращает индекс нового элемента в массиве.
В программе, исходный текст которой был приведен в листинге 13.1, мы восполь-
воспользовались этим методом для начальной инициализации массива, добавив в него 4 эле-
элемента:
myWords.Add("Hello");
myWords.Add(", C# ");
myWords.Add("World");
myWords.Add("!");
Так как индексы элементов, возвращаемые методом Add, нам были не нужны,
мы проигнорировали возвращаемое значение.
Обратите внимание, что при создании массива ArrayList мы не указываем тип
объектов, которые он будет содержать. Это очень важный момент — подобно другим
контейнерам, рассмотренным в этой главе, массив ArrayList может содержать лю-
любые объекты, производные от класса object. А так как от этого класса образуются
все объекты, то это означает, что массив ArrayList может хранить элементы любого
типа.
Заметим, что массив ArrayList может быть создан в таком режиме, когда для не-
него допускается только операция чтения, но не записи или изменения количества эле-
элементов. При попытке добавить методом Add новый элемент в массив, для которого
допустима только операция чтения, возникает исключение NotSupportedEx-
ception.
408 А. В. Фролов, Г. В. Фролов. Язык С# Самоучитель
Чтение элементов массива
Элементы массива ArrayList можно читать с помощью специальных курсоров (ука-
(указателей или итераторов), а также более привычным способом, адресуясь к ним по ин-
индексу с помощью квадратных скобок.
Использование итераторов
Для последовательного извлечения всех элементов массива можно создать в програм-
программе специальный итератор в виде объекта класса System.Collections . Ienume-
rator. Этот итератор необходимо проинициализировать при помощи метода Get-
Enumerator, вызванного для обрабатываемого контейнера:
System.Collections.IEnumerator myEnumerator =
myWords.GetEnumerator();
Здесь в переменной myEnumerator будет храниться итератор для массива
myWords.
Для перемещения итератора в направлении от начала массива к его концу необхо-
необходимо использовать метод MoveNext. При этом текущий элемент массива (т. е. тот,
на который указывает итератор) можно будет извлечь с помощью свойства Current:
while(myEnumerator.MoveNext())
{
Console.Write("{0}", myEnumerator.Current);
}
В результате работы этого цикла на консоль будет выведено полное содержимое
массива.
Для установки итератора System.Collections . IEnumerator в исходное сос-
состояние нужно использовать метод Reset.
Использование индексов
Более привычный способ извлечения элементов массива ArrayList, не предпола-
предполагающий применения курсоров, демонстрируется в программе, исходный текст которой
представлен в листинге 13.2.
Листинг 13.2. Файл ch13\ltemDemo\ltemDemoApp.cs
using System;
using System.Collections;
namespace ItemDemo
{
class ItemDemoApp
{
static void Main(string[] args)
{
ArrayList myWords = new ArrayList();
myWords.Add("Hello");
Глава 13. Контейнеры для хранения объектов 409
myWords.AddC , C# ") ;
myWords.Add("World");
myWords.AddC ! " ) ;
for(int i = 0; i < myWords.Count; i
{
Console.Write("{0}", myWords[i]);
}
Console.ReadLine();
Здесь применяется классическое индексирование массива при помощи квадратных
скобок, причем размер массива определяется с помощью свойства Count:
for(int i = 0; i < myWords.Count; i++)
{
Console.Write("{0}", myWords[i]);
}
Изменение элементов массива
Для изменения элементов массива можно использовать обычные квадратные скобки.
Заметим, однако, что следующий фрагмент кода работать не будет:
ArrayList myWords = new ArrayListD);
myWords[0] = "Hello";
myWords[1] = ", C#
myWords[2] = "World";
myWords[3] = " ! " ;
В результате его работы возникнет исключение ArgumentOutOfRangeExcep-
tion. Прежде чем изменять содержимое элементов массива, их необходимо добавить
в массив методом Add.
Техника изменения добавленных ранее элементов массива с использованием квад-
квадратных скобок демонстрируется в программе, исходный текст которой вы найдете
в листинге 13.3.
Листинг 13.3. Файл ch13\ltemDemo 1 MtemDemo 1App. cs
using System;
using System.Collections;
namespace ItemDemol
{
class ItemDemolApp
{
static void Main(string[] args)
{
ArrayList myWords = new ArrayList();
myWords.Add("Hello");
410 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
myWords.Add(", C# ");
myWords.Add("World");
myWords.Add("!");
for(int i = 0; i < myWords.Count; i+
{
Console.Write("{0}", myWords[i]);
}
Console.WriteLine();
myWords[0] = "Привет";
myWords[1] = ", мир ";
myWords[2] = "языка ";
myWords[3] = "C#!";
for(int i = 0; i < myWords.Count; i
{
Console.Writep {0}", myWords [i] ) ;
}
Console.ReadLine();
Здесь мы вначале проинициализировали массив и вывели его содержимое на кон-
консоль. Затем содержимое каждой ячейки массива было изменено, после чего программа
вновь вывела массив на консоль.
Так как свойство итератора Current доступно только для чтения, с его помощью
вы не сможете изменить содержимое ячеек массива. Следующая попытка добавления
к каждой текстовой строке, хранящейся в массиве, символа подчеркивания будет пре-
пресечена еще на этапе компиляции исходного текста программы:
System.Collections.lEnumerator myEnumerator =
myWords.GetEnumerator();
while(myEnumerator.MoveNext())
{
myEnumerator.Current += "__";
Емкость и текущий размер массива
Наиболее серьезный недостаток обычного массива Array — невозможность измене-
изменения размера массива после его создания. Что же касается массивов, созданных на базе
класса ArrayList, то их размер увеличивается автоматически по мере добавления в
него новых объектов.
Для того чтобы установить начальный размер массива ArrayList, вы можете ис-
использовать свойство ArrayList .Capacity. Текущий размер массива доступен че-
через свойство ArrayList. Count.
Глава 13. Контейнеры для хранения объектов 411
Когда в процессе увеличения размеров массива значение ArrayList .Count дос-
достигает значения ArrayList .Capacity, размер массива автоматически увеличива-
увеличивается. При этом автоматически увеличивается объем памяти, полученной у ОС для хра-
хранения массива. Таким образом, значение ArrayList .Capacity всегда больше зна-
значения ArrayList.Count.
Сказанное демонстрируется в программе, исходный текст которой показан в лис-
листинге 13.4.
Листинг 13.4. Файл ch13\Capacity\CapacityApp.cs
using System;
using System.Collections;
namespace Capacity
{
class CapacityApp
{
static void Main(string[] args)
{
ArrayList myWords = new ArrayList();
myWords.Add("Hello")
myWords.Add(", C# ")
myWords.Add("World")
myWords.Add("!");
Console.WriteLine("Всего элементов: {0}, емкость массива {1}",
myWords.Count, myWords.Capacity);
Console.ReadLine();
После запуска этой программы на консоль будет выведено следующее:
Всего элементов: 4, емкость массива 16
Таким образом, емкость массива превышает текущее количество хранящихся в нем
элементов.
Объединение массивов
Помимо автоматического увеличения размера массив ArrayList предоставляет про-
программистам и другие возможности. В частности, программа может добавлять, встав-
вставлять или удалять как отдельные элементы массива, так и наборы таких элементов. На-
Напомним, что обычный массив допускает работу только с одним элементом массива,
причем программа может либо получить, либо изменить хранящееся там значение.
В листинге 13.5 мы показали способ слияния двух массивов при помощи метода
AddRange.
412 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Листинг 13.5. Файл ch13\AddRange\AddRangeApp.cs
using System;
using System.Collections;
namespace AddRange
{
class AddRangeApp
{
static void Main(string!] args)
{
ArrayList myWords = new ArrayListO;
myWords.Add("Hello");
myWords.Add(", C# ");
myWords.Add("World");
myWords.Add("!");
ArrayList myWordsl = new ArrayListO;
myWordsl.Add("Привет, ");
myWordsl.Add("мир ");
myWords1.Add("языка ");
myWordsl.Add("C#!");
myWords.AddRange(myWordsl);
Console.WriteLine("Всего элементов: {0}, емкость массива {1}
myWords.Count, myWords.Capacity);
System.Collections.IEnumerator myEnumerator =
myWords.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.Write("{0}", myEnumerator.Current);
}
Console.ReadLine();
Массивы создаются обычным образом — с помощью конструкторов и метода Add:
ArrayList myWords = new ArrayListO;
myWords.Add("Hello");
myWords.Add(", C# ");
myWords.Add("World");
myWords.Add("!");
ArrayList myWordsl = new ArrayListO;
myWordsl.Add("Привет, ");
myWordsl.Add("мир ");
myWordsl.Add("языка ");
myWordsl.Add("C#!");
Глава 13. Контейнеры для хранения объектов 413
Далее мы добавляем в первый массив все элементы из второго массива, как это по-
показано ниже:
myWords.AddRange(myWordsl);
Подобным образом можно объединять не только массивы, но и другие контейнеры.
Удаление элементов из массива
После создания обычного массива Array вы не можете удалить из него существую-
существующие элементы. Единственный способ выполнения такой операции заключается в соз-
создании нового массива и последующем переписывании в него всех элементов, кроме
удаляемых. Стоит ли говорить, что этот способ далеко не совершенен.
Что же касается массивов ArrayList, то для них вы можете использовать не-
несколько методов удаления элементов:
• метод Clear удаляет все элементы из массива, выполняя его полную очистку;
• с помощью метода Remove программа может удалить из массива любой заданный
элемент;
• метод RemoveAt предназначен для удаления элемента, заданного своим индексом;
• метод RemoveRange пригодится в том случае, когда нужно удалить сразу не-
несколько элементов массива, расположенных рядом.
Использование всех перечисленных выше методов демонстрируется в программе,
исходный текст которой приведен в листинге 13.6.
Листинг 13.6. Файл ch13\Remove\RemoveApp.cs
using System;
using System.Collections;
namespace Remove
{
class RemoveApp
{
static void Main(string[] args)
{
ArrayList myWords = new ArrayList();
myWords.Add("Hello, ");
myWords.Add("C# ") ;
myWords.Add("World") ;
myWords.Add("!") ;
Console.WriteLinef"Всего элементов: {0}, емкость массива {1}",
myWords . Count, myWords . Capacity) ,-
System.Collections.IEnumerator myEnumerator =
myWords.GetEnumerator();
while(myEnumerator.MoveNext())
414 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Console.Write("{0}", myEnumerator.Current);
myWords.Remove("C# ");
Console.WriteLine();
Console.WriteLine("Всего элементов: {0}, емкость массива {1}",
myWords.Count, myWords.Capacity);
System.Collections.IEnumerator myEnumeratorl =
myWords.GetEnumerator();
while(myEnumeratorl.MoveNext())
{
Console.Write("{0}", myEnumeratorl.Current);
myWords.Clear();
myWords.Add("Hello, ") ;
myWords.Add("C# ") ;
myWords.Add("World");
myWords.Add("!") ;
myWords.RemoveAtA);
Console.WriteLine();
Console.WriteLine{"Всего элементов: {0}, емкость массива {1}",
myWords.Count, myWords.Capacity);
System.Collections.IEnumerator myEnumerator2 =
myWords.GetEnumerator();
while(myEnumerator2.MoveNext())
{
Console.Write("{0}", myEnumerator2.Current);
myWords.RemoveRangeA, 2);
Console.WriteLine();
Console.WriteLine("Всего элементов: {0}, емкость массива {1}",
myWords.Count, myWords.Capacity);
System.Collections.IEnumerator myEnumerator3 =
myWords.GetEnumerator();
while(myEnumerator3.MoveNext())
{
Console.Write("{0}", myEnumerator3.Current);
}
Console.ReadLine();
Глава 13. Контейнеры для хранения объектов 415
Вначале наша программа создает исходный массив, добавляет в него новые эле-
элементы, а затем отображает на консоли размер массива и его содержимое:
ArrayList myWords = new ArrayList();
myWords.Add("Hello, »);
myWords.Add("C# ");
myWords.Add("World");
myWords.Add("!");
Console.WriteLine("Всего элементов: @}, емкость массива {1}",
myWords.Count, myWords.Capacity);
System.Collections.IEnumerator myEnumerator =
myWords.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.Write("{0}", myEnumerator.Current);
}
Далее наша программа ищет в массиве первый элемент, содержащий строку
«С# », и удаляет его при помощи метода Remove:
myWords.Remove("С# ");
Далее содержимое измененного массива отображается на консоли при помощи
итератора myEnumeratorl:
Console.WriteLine();
Console.WriteLine("Всего элементов: @), емкость массива {1}",
myWords.Count, myWords.Capacity);
System.Collections.IEnumerator myEnumeratorl =
myWords.GetEnumerator();
while(myEnumeratorl.MoveNext())
{
Console.Write("{0}", myEnumeratorl.Current);
}
Обратите внимание, что здесь нам пришлось создать новый итератор
myEnumeratorl, так как размеры массива были изменены. В результате итератор
myEnumerator, созданный ранее для этого массива, стал бесполезным. В дальней-
дальнейшем при каждом изменении массива наша программа создает для его отображения но-
новый итератор.
На следующем шаге программа удаляет все элементы из массива методом Clear,
а потом заполняет его заново при помощи метода Add:
myWords.Clear();
myWords.Add("Hello, ");
myWords.Add("C# ");
myWords.Add("World");
myWords.Add("!"); ,
416 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Далее из восстановленного таким способом массива удаляется элемент с индек-
индексом 1 (т. е. строка «С# »):
myWords.RemoveAtA);
В качестве единственного параметра этому методу нужно передать индекс удаляе-
удаляемого элемента.
Содержимое массива вновь отображается на консоли при помощи итератора
myEnumerator2.
И наконец, программа удаляет из массива два элемента, начиная с элемента,
имеющего индекс 1, для чего она вызывает метод RemoveRange:
myWords.RemoveRangeA, 2);
Через первый параметр методу RemoveRange нужно передать индекс первого
удаляемого элемента, а в качестве второго — количество удаляемых элементов.
Запрет записи данных в массив
При необходимости с помощью программа может создать массив ArrayList дос-
доступным только для чтения. Эта возможность отсутствует в обычных массивах, реали-
реализованных как с применением класса Array и языка программирования С#, так
и с применением других языков программирования.
Создание массива, доступного только для чтения, демонстрируется в программе,
исходный текст которой представлен в листинге 13.7.
Листинг 13.7. Файл ch13\ReadOnly\ReadOnlyApp.cs
using System,-
using System.Collections;
namespace Readonly
{
class ReadOnlyApp
{
static void Main(string[] args)
{
ArrayList myWords = new ArrayList();
myWords.Add("Hello");
myWords.Add(", C# ");
myWords.Add("World");
myWords.Add("!");
ArrayList myReadOnlyWords = ArrayList.Readonly(myWords);
if(myWords.IsReadOnly)
Console.WriteLine(
"Исходный массив myWords доступен только для чтения");
else
Console.WriteLine(
"Исходный массив myWords доступен для чтения и записи");
Глава 13. Контейнеры для хранения объектов 417
14 Язык С# Самоучитель
if(myReadOnlyWords.IsReadOnly)
Console.WriteLine(
"Массив myReadOnlyWords доступен только для чтения");
else
Сonsole.WriteLine(
"Массив myReadOnlyWords доступен для чтения и записи"
try
{
myReadOnlyWords.Add("Новый элемент");
}
catch (Exception ex)
{
Console.WriteLine("Возникло исключение: " + ex.ToString())
}
Console.ReadLine() ;
Вначале мы создаем обычный массив, доступный для чтения и записи:
ArrayList myWords = new ArrayList();
Далее этот массив инициализируется обычным образом с помощью метода Add:
myWords.Add("Hello");
myWords.Add(", C# ");
myWords.Add("World");
myWords.Add("!");
К сожалению, мы не можем сделать созданный таким способом массив доступным
только для чтения, однако есть другая возможность. Мы можем создать копию нашего
массива, доступную только для чтения. Эта операция выполняется при помощи метода
Readonly:
ArrayList myReadOnlyWords = ArrayList.Readonly(myWords);
В результате будет создан массив myReadOnlyWords, доступный только для чте-
чтения. Программа не сможет добавлять в него новые элементы, а также изменять содер-
содержимое существующих элементов.
В классе ArrayList определено свойство IsReadOnly, позволяющее опреде-
определить, возможно ли изменение содержимого массива, или этот массив доступен только
для записи. Вот как мы проверяем это свойство для исходного массива:
if(myWords.IsReadOnly)
Console.WriteLine("
Исходный массив myWords доступен только для чтения");
else
Console.WriteLine(
"Исходный массив myWords доступен для чтения и записи");
418 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Аналогичная операция выполняется и для нового массива, доступного только для
чтения:
if(myReadOnlyWords.IsReadOnly)
Console.WriteLine(
"Массив myReadOnlyWords доступен только для чтения");
else
Console.WriteLine(
"Массив myReadOnlyWords доступен для чтения и записи");
Далее наша программа выполняет попытку добавления нового элемента в массив,
доступный только для чтения. Так как эта операция обязательно вызовет исключение,
мы предусмотрели соответствующий обработчик:
try
{
myReadOnlyWords.Add("Новый элемент");
}
catch (Exception ex)
{
Console.WriteLine("Возникло исключение: " + ex.ToString());
}
Вот что появится на консоли после запуска программы:
Исходный массив myWords доступен для чтения и записи
Массив myReadOnlyWords доступен только для чтения
Возникло исключение: System.NotSupportedException: Collection is
read-only.
at System.Collections.ReadOnlyArrayList.Add(Object obj)
at Readonly.ReadOnlyApp.Main(String[] args) in h:\[beginner c#
book]\src\chl3\readonly\readonlyapp.es:line 3 0
Как видите, попытка изменения массива, доступного только для чтения, привела
к возникновению исключения System. NotSupportedException.
Ограничение размера массива
Если вам нужны все свойства массива ArrayList, кроме автоматического увеличе-
увеличения его размера, то такая возможность также предусмотрена. При необходимости
программа может зафиксировать размер массива ArrayList, предотвратив его авто-
автоматический рост (листинг 13.8).
Листинг 13.8. Файл ch13\FixedSize\FixedSizeApp.cs
using System;
using System.Collections;
namespace FixedSize
{
class FixedSizeApp
Глава 13. Контейнеры для хранения объектов 419
static void Main(string[] args)
{
ArrayList myWords = new ArrayList();
myWords.Add("Hello");
myWords.Add(", C# ") ;
myWords.Add("World");
myWords.Add("!");
ArrayList myFixedSizeWords = ArrayList.FixedSize(myWords);
if(myWords.IsFixedSize)
Console.WriteLine(
"Исходный массив myWords имеет фиксированный размер");
else
Console.WriteLine(
"Допускается изменение размеров массива myWords");
if(myFixedSizeWords.IsFixedSize)
Console.WriteLine(
"Массив myReadOnlyWords имеет фиксированный размер");
else
Console.WriteLine(
"Допускается изменение размеров массива myReadOnlyWords")
try
{
myFixedSizeWords.Add("Новый элемент");
}
catch (Exception ex)
{
Console.WriteLine("Возникло исключение: " + ex.ToString());
}
Console.ReadLine();
Здесь мы вначале создаем и инициализируем массив myWords, а затем создаем
на его базе массив myFixedSizeWords, имеющий фиксированный размер:
ArrayList myWords = new ArrayList();
myWords.Add("Hello");
myWords.Add(", C# ");
myWords.Add("World");
myWords.Add("!");
ArrayList myFixedSizeWords = ArrayList.FixedSize(myWords);
Такой массив создастся при помощи метода FixedSize.
420 А. В. Фролов, Г В Фролов. Язык С# Самоучитель
Программа может определить, фиксирован ли размер массива, проверив свойство
IsFixedSize:
if(myWords.IsFixedSize)
Console.WriteLine(
"Исходный массив myWords имеет фиксированный размер");
else
Console.WriteLine(
"Допускается изменение размеров массива myWords");
После создания массива myFixedSizeWords с фиксированным размером наша
программа пытается добавить в него новый элемент:
try
myFixedSizeWords.Add("Новый элемент");
catch (Exception ex)
Console.WriteLine("Возникло исключение: " + ex.ToString{));
Эта попытка неизбежно приведет к возникновению исключения System.Not-
SupportedException, о чем можно судить по информации, отображаемой нашей
программой на консоли:
Допускается изменение размеров массива myWords
Массив myReadOnlyWords имеет фиксированный размер
Возникло исключение: System.NotSupportedException: Collection was of
a fixed size.
at System.Collections.FixedSizeArrayList.Add(Object obj)
at FixedSize.FixedSizeApp.Main(String[] args) in h:\[beginner c#
book] \src\chl3\fixedsize\f ixedsizeapp.es -.line 30
Сортировка
Массивы класса ArrayList, так же как и массивы класса Array, удобно сортиро-
сортировать. Специально для этого предусмотрено несколько перегруженных вариантов мето-
метода Sort.
В простейшем случае сортировка массива выполняется с помощью метода Sort,
не имеющего параметров. Пример программы, сортирующей массив подобным обра-
образом, приведен в листинге 13.9.
Листинг 13.9. Файл ch13\Sort\SortApp.cs
using System;
using System.Collections;
namespace Sort
class SortApp
static void Main(string[] args)
Глава 13. Контейнеры для хранения объектов 421
ArrayList myWords = new ArrayList();
myWords.Add("каждый");
myWords.Add("охотник");
rayWords.Add{"желает");
myWords.Add("знать");
myWords.Add("где");
myWords.Add("сидит");
myWords.Add("фазан");
System.Collections.IEnumerator myEnumerator =
myWords.GetEnumerator{);
while(myEnumerator.MoveNext())
{
Console.Write("{0} ", myEnumerator.Current);
myWords.Sort();
Console.WriteLine();
System.Collections.IEnumerator myEnumeratorl =
myWords.GetEnumerator();
while(myEnumeratorl.MoveNext())
{
Console.Write("{0} ", myEnumerator1.Current)
}
Console.ReadLine();
Получив управление, наша программа создает массив, записывая в него слова из-
известной фразы, помогающей запомнить 7 цветов радуги:
ArrayList myWords = new ArrayList();
myWords.Add("каждый");
myWords.Add("охотник");
myWords.Add("желает");
myWords.Add("знать");
myWords.Add("где");
myWords.Add("сидит");
myWords.Add("фазан");
Далее мы отображаем содержимое массива с помощью итератора и сортируем мас-
массив, вызывая метод Sort без параметров:
myWords.Sort();
После сортировки содержимое массива отображается вновь:
каждый охотник желает знать где сидит фазан
где желает знать каждый охотник сидит фазан
422 А. В. Фролов, Г. В. Фролов. Язык С# Самоучитель
Как видите, сортировка текстовых строк с символами кириллицы была выполнена
правильно.
Каким образом осуществляется сортировка массива?
Для этого применяется алгоритм быстрой сортировки Quicksort. Сравнение эле-
элементов выполняется с использованием интерфейса IComparable, в рамках которого
определен метод CompareTo.
Методу CompareTo передается один параметр — ссылка на объект, сравниваемый
с текущим объектом. В зависимости от результата сравнения метод CompareTo воз-
возвращает отрицательное, нулевое или положительное значение. Отрицательное значе-
значение возвращается, если текущий объект меньше, чем объект, ссылка на который пере-
передается методу CompareTo через параметр, а положительное — если больше. В том
случае, когда объекты равны, метод CompareTo возвращает нулевое значение.
Когда вы сортируете массив, содержащий только встроенные типы данных (такие,
как числа и текстовые строки), можно использовать встроенную в эти типы данных
реализацию интерфейса IComparable.
Если же сортируется массив, содержащий созданные вами объекты, необходимо
реализовать интерфейс IComparable. Ссылка на реализованный интерфейс ICom-
IComparable должна быть передана методу Sort в качестве параметра.
Можно отсортировать не весь массив, а только его часть. В этом случае необходи-
необходимо использовать третий вариант метода Sort, принимающий 3 параметра. В качестве
первого параметра этому методу необходимо передать индекс первого элемента мас-
массива, с которого начинается сортируемый блок, а в качестве второго — количество
сортируемых элементов. Третий параметр должен содержать ссылку на интерфейс
IComparable либо на значение null, если будет использована реализация интер-
интерфейса IComparable объектов, содержащихся в массиве.
Обратное расположение элементов массива
С помощью метода Reverse программа может изменить расположение элементов
массива на обратное. Это демонстрируется в программе, исходный текст которой при-
приведен в листинге 13.10.
Листинг 13.10. Файл ch13\Reverse\ReverseApp.cs
using System,-
using System.Collections;
namespace Reverse
{
class ReverseApp
{
static void Main(string[] args)
{
ArrayList myWords = new ArrayList();
myWords.Add <"каждый") ;
myWords.Add("охотник") ;
Глава 13. Контейнеры для хранения объектов 423
myWords.Add("желает");
myWords.Add("знать");
myWords.Add("где");
myWords.Add("сидит");
myWords.Add("фазан");
System.Collections.IEnumerator myEnumerator =
myWords.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.Write("{0) ", myEnumerator.Current)
myWords.Reverse();
Console.WriteLine();
System.Collections.IEnumerator myEnumeratorl =
myWords.GetEnumerator();
while(myEnumeratorl.MoveNext())
{
Console.Write("{0} ", myEnumeratorl.Current)
}
Console.ReadLine();
От предыдущей программы, выполняющей сортировку массива, она отличается
только одной строкой:
myWords.Reverse();
Вначале программа выводит на консоль содержимое исходного массива, а затем
массива, элементы в котором были переставлены методом Reverse. Вот результат
работы нашей программы:
каждый охотник желает знать где сидит фазан
фазан сидит где знать желает охотник каждый
При необходимости можно обработать методом Reverse только часть массива.
Для этого нужно использовать перегруженную версию этого метода, принимающую
два параметра. Первый параметр задает индекс первого элемента обрабатываемого
блока, а второй — количество элементов в блоке.
Поиск в массиве
Подобно массивам Array, рассмотренным нами в гл. 7, массивы ArrayList допус-
допускают выполнение бинарного поиска элементов. Для этого в классе ArrayList опре-
определен метод BinarySearch (нестатический). В качестве параметра этому методу
424 А В. Фролов, Г. В Фролов. Язык С#. Самоучитель
нужно передать искомый элемент. При успехе метод возвратит индекс найденного
элемента, а в том случае, если элемент не найден, — отрицательное значение.
Исходный текст программы, демонстрирующий применение метода Binary-
Search для поиска текстовой строки в массиве класса ArrayList, приведен в лис-
листинге 13.11.
Листинг 13.11. Файл ch13\Search\SearchApp.cs
using System;
using System.Collections;
namespace Search
{
class SearchApp
{
static void Main(string[] args)
{
ArrayList myWords = new ArrayList();
myWords.Add("каждый") ;
myWords.Add("охотник") ;
myWords.Add("желает") ;
myWords.Add("знать");
myWords.Add("где");
myWords.Add("сидит");
myWords.Add("фазан");
string searchString = "фазан";
int index = myWords.BinarySearch(searchString);
if (index < 0 )
Console.WriteLine("Строка \"{0}\" не найдена.",
searchString);
else
Console.WriteLine("Индекс строки \"{0}\" равен {1}.",
searchString, index );
Console.ReadLineO ;
В программе объявлен и проинициализирован статически массив текстовых строк
myWords.
Метод Main ищет в массиве слово «фазан», вызывая для этого метод BinarySearch:
string searchString = "фазан";
int index = myWords.BinarySearch(searchString);
Метод BinarySearch получает в качестве параметра ссылку на искомую строку.
Если искомая строка не найдена, метод BinarySearch возвращает отрицательное
значение. В случае успеха возвращается индекс найденной строки.
Наша программа отображает на консоли искомую строку и ее индекс.
Глава 13. Контейнеры для хранения объектов 425
Работа в многопоточном режиме
Для того чтобы использовать массив класса ArrayList в многопоточных програм-
программах, необходимо создать не его базе так называемый синхронизированный массив.
Это можно сделать статическим методом Synchronized, специально предусмотрен-
предусмотренным для этой цели в классе ArrayList. Существует также возможность синхрониза-
синхронизации многопоточных приложений при помощи свойства SyncRoot.
Применение метода Synchronized
Один из самых простых способов работы с массивами ArrayList в многопоточных
приложениях предполагает создание синхронизированных массивов методом Synch-
Synchronized.
В листинге 13.12 мы привели исходный текст многопоточной программы, исполь-
использующей этот метод на практике.
Листинг 13.12. Файл ch13\Synchronized\SynchronizedApp.cs
using System;
using System.Collections;
using System.Threading;
namespace Synchronized
{
class SynchronizedApp
{
static ArrayList myWordsSynchro;
public static void MyThreadO
{
Console.WriteLine("MyThread: поток запущен");
System.Collections.IEnumerator myEnumerator =
SynchronizedApp.myWordsSynchro.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.Write("{0} ", myEnumerator.Current);
[STAThread]
static void Main(string(] args)
{
ArrayList myWords = new ArrayList();
myWords.Add("каждый");
myWords.Add("охотник");
myWords.Add("желает");
myWords.Add("знать");
myWords.Add("где");
myWords.Add("сидит");
myWords.Add("фазан");
426 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
myWordsSynchro = ArrayList.Synchronized(myWords);
ThreadStart myThreadDelegate = new ThreadStart(MyThread);
Thread thr = new Thread(myThreadDelegate);
Console.WriteLine("Запуск потока MyThread");
thr.Start();
Console.ReadLine() ;
Метод Main, работающий в рамках основного потока нашей программы, создает
массив myWords класса ArrayList и затем инициализирует его:
ArrayList myWords = new ArrayList();
myWords.Add("каждый");
myWords.Add("охотник");
myWords.Add("желает");
myWords.Add("знать");
myWords.Add("где");
myWords.Add("сидит");
myWords.Add("фазан");
Далее наша программа создает синхронизированный массив, вызывая для этого
статический метод Synchronized:
myWordsSynchro = ArrayList.Synchronized(myWords);
В качестве единственного параметра методу передается ссылка на исходный массив.
После этого программа создает новый поток для метода MyThread и запускает его:
ThreadStart myThreadDelegate = new ThreadStart(MyThread);
Thread thr = new Thread(myThreadDelegate);
Console.WriteLine("Запуск потока MyThread");
thr.StartO ;
Эта процедура была нами подробно описана в гл. 10 этой книги.
Метод MyThread, работающий в рамках отдельного потока, отображает содержи-
содержимое массива на консоли при помощи итератора:
public static void MyThread()
{
Console.WriteLine("MyThread: поток запущен");
System.Collections.IEnumerator myEnumerator =
SynchronizedApp.myWordsSynchro.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.Write("{0} ", myEnumerator.Current);
}
2
Глава 13. Контейнеры для хранения объектов 427
Свойство lsSynchronized
Для того чтобы определить, является ли массив синхронизированным или нет, можно
воспользоваться свойством lsSynchronized, доступным только для чтения.
Свойство SyncRoot
Вы можете обеспечить синхронизацию, необходимую для использования массивов
ArrayList в многопоточных приложениях, и без создания синхронизированного
массива. Для этого можно воспользоваться свойством SyncRoot исходного массива,
возвращающего объект синхронизации.
Поясним использование свойства SyncRoot на примере программы, исходный
текст которой представлен в листинге 13.13.
Листинг 13.13. Файл ch13\SyncRoot\SyncRootApp.cs
using System;
using System.Collections;
using System.Threading;
namespace SyncRoot
{
class SyncRootApp
{
static ArrayList myWords;
public static void MyThread()
{
Console.WriteLine("MyThread: поток запущен");
lock (myWords.SyncRoot)
{
System.Collections.IEnumerator myEnumerator =
SyncRootApp.myWords.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.Write("{0} ", myEnumerator.Current);
[STAThread]
static void Main(string[1 args)
{
myWords = new ArrayList();
myWords.Add("каждый");
myWords.Add("охотник");
myWords.Add("желает");
428 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
myWords.Add("знать");
myWords.Add("где");
myWords.Add("сидит");
myWords.Add("фазан") ;
ThreadStart myThreadDelegate = new ThreadStart(MyThread);
Thread thr = new Thread(myThreadDelegate);
Console.WriteLine("Запуск потока MyThread"),-
thr.Start();
Console.ReadLine() ;
Метод Main этой программы создает массив myWords, инициализирует его, а затем
запускает метод MyThread в отдельном потоке. Вот исходный текст этого метода:
public static void MyThread()
{
Console.WriteLine("MyThread: поток запущен");
lock (myWords.SyncRoot)
{
System.Collections.IEnumerator myEnumerator =
SyncRootApp.myWords.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.Write("{0} ", myEnumerator.Current);
Как видите, перед началом цикла итерации по массиву myWords метод блокирует
объект myWords .SyncRoot с помощью ключевого слова lock. Это необходимо,
так как процедура итерации по массиву не может выполняться в многопоточном ре-
режиме без синхронизации. В остальном наша программа похожа на предыдущую, ис-
исходный текст которой был представлен в листинге 13.12.
Недостатки массивов ArrayList
Разумеется, массивам ArrayList присущи и некоторые недостатки по сравнению
с массивами Array. Поэтому вы должны использовать массивы такого типа, который
в наилучшей степени соответствует логике работы вашей программы.
Вот основные недостатки массивов ArrayList по сравнению с массивами Array:
• массивы ArrayList в отличие от массивов Array не могут быть многомерными;
• массивы ArrayList «работают» медленнее массивов Array как при записи, так
и при чтении данных.
Глава 13. Контейнеры для хранения объектов 429
Словарь Hashtable
Помимо массивов с динамически изменяемым размером при создании программ часто
бывают нужны такие структуры данных, как словари. В отличие от массивов, где каж-
каждый хранящийся элемент идентифицируется своим индексом, в словарях для иденти-
идентификации элементов используются специальные объекты — ключи.
В качестве ключей могут выступать, например, текстовые строки. Тогда каждой
такой строке можно будет поставить в соответствие какой-либо объект. Простейший
пример — телефонный справочник, где каждой фамилии ставится в соответствие тот
или иной номер телефона.
С использованием библиотеки классов Microsoft .NET Framework такой словарь
можно создать на основе хеш-таблиц класса Hashtable. Рассмотрим основные
приемы использования этого класса.
Создание словаря
Для создания словарей (хеш-таблиц) в классе Hashtable предусмотрено несколько
перегруженных конструкторов.
Конструктор без параметров
Конструктор без параметров позволяет создать пустой словарь с параметрами, приня-
принятыми по умолчанию. Пользоваться им очень просто:
Hashtable myPhones = new Hashtable();
Исходный текст программы, в которой словарь Hashtable создается с помощью
такого конструктора, представлен в листинге 13.14.
Листинг 13.14. Файл ch13\HashTable\HashTableApp.cs
using System;
using System.Collections;
namespace HashTable
{
class HashTableApp
{
static void Main(string[] args)
{
Hashtable myPhones = new Hashtable();
myPhones.Add("Иванов", 22-44-22");
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", " 8-999-323-33-44");
System.Collections.IDictionaryEnumerator myEnumerator =
myPhones.GetEnumerator();
while(myEnumerator.MoveNext())
430 А В Фролов, Г В. Фролов. Язык С# Самоучитель
Console.WriteLineC1 @) : {1} ",
rayEnumerator.Key, myEnumerator.Value)
Console.ReadLine();
После запуска программа создает словарь и добавляет в него 3 записи. Далее эти
записи отображаются на консоли:
Сидоров: 8-999-323-33-44
Иванов: 322-44-22
Петров: 8-999-323-33-33
Операции добавления элементов и просмотра содержимого, выполняемые в этой
программе, будут рассмотрены позже в этом разделе.
Копирование словаря
Если передать конструктору класса Hashtable ссылку на уже имеющийся словарь,
то все элементы этого словаря будут скопированы в новый словарь. Это демонстриру-
демонстрируется в программе, исходный текст которой вы найдете в листинге 13.15.
Листинг 13.15. Файл ch13\HashTable1\HashTable1App.cs
using System;
using System.Collections;
namespace HashTablel
{
class HashTablelApp
{
static void Main(string[] args)
{
Hashtable myPhones = new Hashtable();
myPhones.Add("Иванов", 22-44-22");
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44");
System.Collections.IDictionaryEnumerator myEnumerator =
myPhones.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLineC {0} : {1} ",
myEnumerator.Key, myEnumerator.Value);
Hashtable myPhonesCopy = new Hashtable(myPhones);
Глава 13. Контейнеры для хранения объектов 431
System.Collections.IDictionaryEnumerator myEnumeratorl =
myPhones.GetEnumerator();
while(myEnumeratorl.MoveNext())
{
Console.WriteLinet"{0}: {1} ",
myEnumeratorl.Key, myEnumeratorl.Value);
Console.ReadLine();
Здесь мы вначале создаем словарь myPhones и добавляем в него 3 записи:
Hashtable myPhones = new Hashtable();
myPhones.Add("Иванов", 22-44-22");
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44");
Далее содержимое словаря myPhones копируется во вновь создаваемый словарь
myPhonesCopy:
Hashtable myPhonesCopy = new Hashtable(myPhones) ;
Программа отображает на консоли содержимое исходного словаря и его копии.
Указание начальной емкости словаря
Для увеличения эффективности работы со словарем вы можете указать, сколько при-
примерно элементов в нем будет храниться (если, конечно, это количество вам известно).
Для указания начальной емкости словаря используйте конструктор класса Hash-
Hashtable с одним параметром, как это показано ниже:
Hashtable myPhones = new HashtableA0) ;
Начальная емкость должна задаваться положительным числом, в противном случае
возникнет исключение ArgumentOutOf RangeException.
Другие конструкторы
В классе Hashtable имеются и другие конструкторы, которые подробно описаны
в документации на библиотеку классов Microsoft .NET Framework.
Среди них есть конструктор, позволяющий при создании нового словаря скопировать в
него содержимое существующего словаря и при этом дополнительно задать так называе-
называемый фактор загрузки (load factor). Фактор загрузки задается как отношение количества
имеющихся элементов к общей емкости словаря. Чем меньше фактор загрузки, тем быст-
быстрее выполняется поиск элементов в словаре и тем больше требуется памяти для хранения
словаря. По умолчанию фактор загрузки равен 1.0, что обеспечивает наилучший компро-
компромисс между скоростью работы словаря и занимаемой этим словарем памятью.
432 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Другие конструкторы обеспечивают создание словарей, имеющих нестандартный
алгоритм вычисления хеш-кодов элементов, словарей с заданной емкостью и факто-
фактором загрузки, словарей, допускающих запись содержимого в поток вывода (об этом
мы поговорим позже), а также нестандартные алгоритмы сравнения элементов, хра-
хранящихся в словаре.
Добавление новых элементов
Для добавления в словарь новых элементов вы можете воспользоваться методом Add:
Hashtable myPhones = new HashtableO;
myPhones.Add("Иванов", 22-44-22");
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44");
В качестве первого параметра этому методу нужно передать ключ, а в качестве
второго — значение, связанное с этим ключом. В нашем примере роль ключа играет
фамилия, а роль значения — номер телефона.
Чтение содержимого словаря
Программа может получить список всего содержимого словаря при помощи итератора
класса System.Collections.IDictionaryEnumerator.
Примеры использования этого итератора есть в листингах 13.14 и 13.15, приведен-
приведенных нами ранее. Вот соответствующий фрагмент кода:
System.Collections.IDictionaryEnumerator myEnumerator =
myPhones.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLine("{0}: {1} ",
myEnumerator.Key, myEnumerator.Value);
}
Здесь мы вначале создаем итератор, вызывая для этого метод GetEnumerator.
Эта операция аналогична операции получения итератора массива ArrayList, рас-
рассмотренного в начале этой главы.
Для получения пар ключ-значение мы создали цикл, в котором выполняется после-
последовательное перемещение итератора с помощью метода MoveNext. При необходимо-
необходимости вы можете снова установить итератор на начало словаря, воспользовавшись мето-
методом Reset.
Ключ извлекается с помощью свойства итератора с именем Key, а значение —
с помощью свойства итератора с именем Value.
Поиск по ключу
После добавления всех элементов в словарь ваша программа может извлекать нужные
элементы при помощи свойства Item. Это свойство доступно через квадратные скоб-
скобки (листинг 13.16).
Глава 13. Контейнеры для хранения объектов 433
Листинг 13.16. Файл ch13\HashTable2\HashTable2App.cs
using System;
using System.Collections;
namespace HashTable2
{
class HashTable2App
{
static void Main(string[] args)
{
Hashtable myPhones = new Hashtable(lO);
Console.WriteLine("Начальная емкость: {0}", myPhones.Count);
myPhones.Add("Иванов", 22-44-22");
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44");
Console.WriteLine("Телефон Иванова: {0}", myPhones["Иванов"]);
Console.ReadLine();
В этой программе мы создали словарь с фамилиями и телефонами, а потом извлек-
извлекли телефон Иванова, указав эту фамилию в качестве ключа:
Console.WriteLine("Телефон Иванова: {0}", myPhones["Иванов"]);
Заметим, что скобки можно использовать не только для извлечения элементов
по ключу, но и для изменения значения элемента с данным ключом.
Предварительная проверка содержимого словаря
При неудачной попытке извлечения из словаря элемента с несуществующим ключом
будет получена ссылка null. Чтобы избежать возникновения исключения в этой си-
ситуации, программа может предварительно проверить наличие элемента в словаре.
Этот прием демонстрируется в программе, исходный текст которой приведен
в листинге 13.17. (Здесь и далее телефоны приведены только для примера, не пытай-
пытайтесь по ним звонить.)
Листинг 13.17. Файл ch13\HashTable3\HashTable3App.cs
using System;
using System.Collections;
namespace HashTable3
{
class HashTable3App
{
static void Main(string[] args)
434 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Hashtable myPhones = new HashtableA0);
Console.WriteLine("Начальная емкость: {0}", myPhones.Count);
myPhones.Add("Иванов", 22-44-22");
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44") ;
if(myPhones.Contains("Иванов"))
Console.WriteLine("Телефон Иванова: {0}",
myPhones["Иванов"]);
else
Console.WriteLine("Телефон Иванова не найден");
Console.ReadLine();
Если словарь myPhones содержит элемент с ключом «Иванов», то программа из-
извлекает соответствующее значение (номер телефона) и отображает его на консоли:
if(myPhones.Contains("Иванов"))
Console.WriteLine("Телефон Иванова: {0}", myPhones["Иванов"]);
else
Console.WriteLine("Телефон Иванова не найден");
В противном случае на консоль выводится сообщение о том, что телефон Иванова
не найден.
Для проверки содержимого словаря вы также можете использовать свойства
ContainsKey и ContainsValue. Первое из них позволяет проверить, имеется ли
в словаре элемент с заданным ключом, а второй — имеется ли в словаре элемент с за-
заданным значением.
Метод ContainsKey аналогичен только что рассмотренному методу Contains.
Использование этого метода, а также метода ContainsValue демонстрируется
в программе, исходный текст которой приведен в листинге 13.18.
Листинг 13.18. Файл ch13\HashTable4\HashTable4App.cs
using System;
using System.Collections;
namespace HashTable4
{
class HashTable4App
{
static void Main(string[] args)
{
Hashtable myPhones = new HashtableA0);
Console.WriteLine("Начальная емкость: {0}", myPhones.Count);
Глава 13. Контейнеры для хранения объектов 435
myPhones.Add("Иванов", 22-44-22");
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", " 8-999-32 3-3 3-44");
i f(myPhones.ContainsKey("Иванов"))
Console.WriteLine("Телефон Иванова: {0}",
myPhones["Иванов"]);
else
Console.WriteLine("Телефон Иванова не найден");
if(myPhones.ContainsValue(22-44-22") )
Console.WriteLine("Телефон 322-44-22 есть в словаре");
else
Console.WriteLine("Телефон 322-44-22 в словаре не найден");
Console.ReadLine() ;
Здесь после создания словаря и добавления в него всех элементов мы вначале про-
проверяем, если ли там телефон Иванова:
if(myPhones.ContainsKey("Иванов"))
Console.WriteLine("Телефон Иванова: @)", myPhones["Иванов"]);
else
Console.WriteLine("Телефон Иванова не найден");
Если этот телефон имеется в словаре, то мы отображаем его на консоли. В против-
противном случае выводится сообщение о том, что телефон не найден.
Далее с помощью метода ContainsValue мы проверяем, есть ли в словаре за-
запись, содержащая телефон 322-44-22:
if(myPhones.ContainsValue(22-44-22"))
Console.WriteLine("Телефон 322-44-22 есть в словаре");
else
Console.WriteLine("Телефон 322-44-22 в словаре не найден");
Удаление элементов из словаря
Чтобы полностью очистить словарь Hashtable, вы можете использовать метод
Clear. Для удаления элемента с заданным ключом воспользуйтесь методом Remove.
Ключ передаваемого элемента передается этому методу в качестве единственного па-
параметра.
Если в словаре имеется элемент с заданным ключом, то он будет удален методом
Remove. Если же такого элемента в словаре нет, то содержимое словаря останется не-
неизменным.
Использование метода Remove демонстрируется в программе, исходный текст ко-
которой представлен в листинге 13.19.
436 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Листинг 13.19. Файл ch13\HashtableRemove\HashtableRemoveApp.cs
using System;
using System.Collections;
namespace HashtableRemove
{
class HashtableRemoveApp
{
static void Main(string[] args)
{
Hashtable myPhones = new Hashtable();
myPhones.Add("Иванов", 22-44-22");
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44");
System.Collections.IDictionaryEnumerator myEnumerator =
myPhones.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLine("{0}: {1} ",
myEnumerator.Key, myEnumerator.Value);
}
Console.WriteLine();
myPhones.Remove("Иванов");
System.Collections.IDictionaryEnumerator myEnumeratorl =
myPhones.GetEnumerator{);
while(myEnumeratorl.MoveNext())
{
Console.WriteLinel"{0}: {1} ",
myEnumeratorl.Key, myEnumeratorl.Value);
}
Console.ReadLineO ;
Получив управление, наша программа заполняет словарь фамилиями и телефона-
телефонами, после чего содержимое словаря отображается на консоли:
Hashtable myPhones = new Hashtable();
myPhones.Add("Иванов", 22-44-22");
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44");
Далее из словаря удаляется запись с телефоном Иванова:
myPhones.Remove)"Иванов") ;
Глава 13 Контейнеры для хранения объектов 437
После этого содержимое словаря снова отображается. Теперь в нем стало на одну
запись меньше:
Сидоров: 8-999-323-33-44
Иванов: 322-44-22
Петров: 8-999-323-33-33
Сидоров: 8-999-323-33-44
Петров: 8-999-323-33-33
Емкость и текущий размер словаря
При добавлении в словарь новых элементов происходит автоматическое увеличение
его размера.
Текущий размер словаря вы можете узнать при помощи свойства Count. Пример
использования этого свойства приведен в листинге 13.20.
Листинг 13.20. Файл ch13\HashTable5\Ha$hTable5App.cs
using System;
using System.Collections;
namespace HashTable5
{
class HashTableSApp
{
static void Main(string[] args)
{
Hashtable myPhones = new HashtableA0);
myPhones.Add{"Иванов", 22-44-22") ;
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44");
Console.WriteLine("Всего элементов в словаре: {0}",
myPhones.Count);
Console.ReadLine();
Словари и многопоточность
Если словари Hashtable используются в многопоточных программах, необходимо
позаботиться о синхронизации. Синхронизация словарей Hashtable выполняется
аналогично синхронизации массивов ArrayList — с применением статического ме-
метода Synchronized или свойства SyncRoot.
Применение метода Synchronized демонстрируется в программе, исходный
текст которой приведен в листинге 13.21.
438 А. В. Фролов, Г. В. Фролов. Язык С# Самоучитель
Листинг 13.21. Файл ch13\HashTable6\HashTable6App.cs
using System;
using System.Collections;
using System.Threading;
namespace HashTable6
{
class HashTable6App
{
static Hashtable myPhones;
static Hashtable myPhonesSynchro;
public static void MyThreadO
{
Console.WriteLine("MyThread: поток запущен");
System.Collections.IDictionaryEnumerator myEnumerator =
HashTable6App.myPhones.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLine("{0}: {1} ",
myEnumerator.Key, myEnumerator.Value);
[STAThread]
static void Main(string[] args)
{
myPhones = new Hashtable();
myPhones.Add("Иванов", 22-44-22");
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44");
myPhonesSynchro = Hashtable.Synchronized(myPhones);
ThreadStart myThreadDelegate = new ThreadStart(MyThread)
Thread thr = new Thread(myThreadDelegate);
Console.WriteLine("Запуск потока MyThread");
thr.Start();
Console.ReadLine() ;
Глава 13. Контейнеры для хранения объектов 439
Получив управление, метод Main, работающий в рамках основного потока, создает
словарь myPhones и заполняет его:
myPhones = new Hashtable();
myPhones.Add("Иванов", 22-44-22 ") ;
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44");
Далее на базе этого словаря создается новый, синхронизированный словарь
myPhonesSynchro:
myPhonesSynchro = Hashtable.Synchronized(myPhones);
Затем метод Main запускает поток MyThread, осуществляющий вывод содержи-
содержимого словаря на консоль.
В рамках этого метода создается итератор, который используется для циклического
просмотра содержимого массива:
System.Collections.IDictionaryEnumerator myEnumerator =
HashTable6App.myPhones.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLine("{0}: {1} ",
myEnumerator.Key, myEnumerator.Value);
}
Использование итераторов в многопоточных программах всегда требует синхрони-
синхронизации, но для этого вам не обязательно создавать новый, синхронизированный сло-
словарь. В программе, исходный текст которой представлен в листинге 13.22, использует-
используется объект синхронизации, полученный с помощью свойства SyncRoot.
Листинг 13.22. Файл ch13\HashTable7\HashTable7App.cs
using System;
using System.Collections;
using System.Threading;
namespace HashTable7
{
class HashTable7App
{
static Hashtable myPhones;
public static void MyThread()
{
Console.WriteLine("MyThread: поток запущен");
lock(myPhones.SyncRoot)
{
System.Collections.IDictionaryEnumerator myEnumerator =
HashTable7App.myPhones.GetEnumerator();
440 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
while(myEnumerator.MoveNext())
{
Console.WriteLine{"{0}: {1} ",
myEnumerator.Key, myEnumerator.Value);
[STAThread]
static void Main(string[] args)
{
myPhones = new Hashtable();
myPhones.Add("Иванов", 22-44-22") ;
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44");
ThreadStart myThreadDelegate = new ThreadStart(MyThread);
Thread thr = new Thread(myThreadDelegate);
Console.WriteLine("Запуск потока MyThread");
thr.Start();
Console.ReadLine();
Свойство SyncRoot мы используем в блоке lock, как это показано ниже:
lock(myPhones.SyncRoot)
{
System.Collections.IDictionaryEnumerator myEnumerator =
HashTable7App.myPhones.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLine("{0}: {1} ",
myEnumerator.Key, myEnumerator.Value);
Блок ограничивает фрагмент кода, внутри которого должна выполняться синхро-
синхронизация.
Сортированный список SortedList
Сортированный список класса SortedList представляет собой гибрид массива
и словаря, описанных ранее в этой главе. Вы можете записывать в этот список элемен-
элементы с ключами, как в словарь, а затем извлекать эти элементы по ключу. Но есть и дру-
другая возможность — программа может извлекать элементы из сортированного списка
по индексу, как это делается при работе с массивами.
Глава 13. Контейнеры для хранения объектов 441
Создание и наполнение списка
Создание и наполнение сортированного списка SortedList можно выполнять та-
таким же образом, как и в случае словаря Hashtable.
Чтобы создать сортированный список, воспользуйтесь одним из конструкторов.
Простейший конструктор не имеет параметров и создает пустой список:
SortedList myPhones = new SortedList();
Здесь мы создали пустой список телефонных номеров.
Для заполнения списка вы можете воспользоваться методом Add, как это показано
ниже:
myPhones.Add("Иванов", 22-44-22");
туPhones.Add("Петров", "8-999-323-33-33") ;
myPhones.Add("Сидоров", "8-999-323-33-44") ;
В качестве первого параметра методу Add передается ключ добавляемого элемен-
элемента, а в качестве второго — значение элемента. Здесь мы используем те же самые прие-
приемы, что и при работе со словарем Hashtable.
Заметим, что в классе SortedList предусмотрено несколько перегруженных конст-
конструкторов. С их помощью вы можете создавать сортированные списки на базе других кон-
контейнеров, задавать начальный размер словаря и определять свои собственные методы сор-
сортировки элементов. Эти конструкторы аналогичны соответствующим конструкторам клас-
класса Hashtable. При необходимости вы найдете более подробную информацию об этих
конструкторах в справочной системе Microsoft Visual Studio.NET.
Извлечение данных из списка
Как мы уже говорили, программа может извлекать данные из сортированного списка
двумя способами — по ключу (как из словаря) и по индексу (как из массива).
Оба эти способа демонстрируются в программе, исходный текст которой вы найде-
найдете в листинге 13.23.
Листинг 13.23. Файл ch13\SortedList\SortedListApp.cs
using System;
using System.Collections;
namespace SortedListNameSpace
{
class Classl
{
static void Main(string[] args)
{
SortedList myPhones = new SortedList();
myPhones.Add("Иванов", 22-44-22");
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44");
System.Collections.iDictionaryEnumerator myEnumerator =
myPhones.GetEnumerator();
442 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
while(myEnumerator.MoveNext())
{
Console.WriteLine{"{0}: {1} ",
myEnumerator.Key, myEnumerator.Value);
Console.WriteLine();
Console.WriteLine("Телефон Иванова: {0}\пп,
myPhones["Иванов"]);
for(int i = 0; i < myPhones.Count; i++)
{
Console.WriteLine("{0}. {1}: {2}
i, myPhones.GetKey(i), myPhones.GetBylndex(i));
Console.WriteLine();
Console.WriteLine(•{0}: {l}\n",
myPhones.GetKey@), myPhones.GetBylndex@));
Console.ReadLine();
Вначале программа создает и наполняет сортированный список, используя для это-
этого описанные выше приемы. Далее она отображает содержимое списка с помощью
итератора, отображая на консоли полное содержимое списка:
System.Collections.IDictionaryEnumerator myEnumerator =
myPhones.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLine("{0}: {1} ",
myEnumerator.Key, myEnumerator.Value);
}
Аналогичную операцию мы выполняли ранее для словарей класса Hashtable.
Сортированный список допускает использование квадратных скобок для извлече-
извлечения и изменения элементов по ключу. Мы применили этот способ для получения те-
телефона Иванова:
Console.WriteLine("Телефон Иванова: {0}\п" , myPhones["Иванов"]);
Теперь о том, как извлечь элементы сортированного списка Sor tedLis t по индексу.
Этот прием демонстрируется ниже:
for(int i = 0; i < myPhones.Count; i++)
{
Console.WriteLineC1 {0}. {1}: {2} ",
i, myPhones.GetKey(i), myPhones.GetBylndex(i));
}
Глава 13. Контейнеры для хранения объектов 443
Здесь мы в цикле перебираем все элементы сортированного списка, причем пере-
переменная цикла i играет роль индекса.
Для получения ключа элемента с заданным индексом мы используем здесь метод
GetKey, передавая ему этот индекс в качестве единственного параметра.
Что же касается значения элемента с заданным индексом, то его можно извлечь
с помощью метода Get By Index. Вы должны передать этому методу индекс нужного
элемента в качестве параметра.
Далее наша программа демонстрирует работу со списком как с массивом:
for(int i = 0; i < myPhones.Count;
Console.WriteLine("{0}. {1}: {2} ",
i, myPhones.GetKey(i), myPhones.GetBylndex(i));
}
Здесь переменная цикла i используется для прямой индексации списка. Ключ из-
извлекается методом GetKey, а значение — методом GetBylndex.
Перед тем как завершить свою работу, наша программа выводит на консоль ключ
и значение самого первого элемента сортированного списка (т. е. элемента с нулевым
индексом):
Console.WriteLine("{0}: {1}\п",
myPhones.GetKey@), myPhones.GetBylndex@));
Вот что программа выведет на консоль после своего запуска:
Иванов: 322-44-22
Петров: 8-999-323-33-33
Сидоров: 8-999-323-33-44
Телефон Иванова: 322-44-22
0. Иванов: 322-44-22
1. Петров: 8-999-323-33-33
2. Сидоров: 8-999-323-33-44
Иванов: 322-44-22
Определение индекса по ключу и значению
В классе SortedList, предназначенном для создания сортированных списков, опре-
определены все методы, рассмотренные нами ранее при описании словаря Hashtable.
Это означает, что при работе с таким списком вам доступны те же самые возможно-
возможности, что и при работе со словарем Hashtable.
Так как сортированный список SortedList ведет себя и как массив, в классе
SortedList предусмотрены и другие методы, позволяющие обращаться к элементам
списка по индексу. Два таких метода (GetKey и GetBylndex) были рассмотрены
нами в предыдущем разделе. Первый из них позволяет извлечь ключ для элемента
с заданным индексом, а второй — значение элемента с заданным индексом.
444 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Помимо этого в классе SortedList есть методы, позволяющие определить ин-
индекс элемента по ключу и значение элемента по ключу. Первая из этих операций вы-
выполняется методом IndexOf Key, а вторая — методом IndexOf Value.
Использование методов IndexOf Key и IndexOfValue демонстрируется в про-
программе, исходный текст которой приведен в листинге 13.24.
Листинг 13.24. Файл ch13\SortedList1\SortedList1App.cs
using System;
using System.Collections;
namespace SortedListl
{
class SortedListlApp
{
static void Main(string[] args)
{
SortedList myPhones = new SortedList();
myPhones.Add("Иванов", 22-44-22");
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44");
System.Collections.IDictionaryEnumerator myEnumerator =
myPhones.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLine("{0}. {1}: {2} ",
myPhones.IndexOfKey(myEnumerator.Key),
myEnumerator.Key, myEnumerator.Value);
}
Console.WriteLine();
for(int i = 0; i < myPhones.Count; i++)
{
Console.WriteLine("{0}. {I}: {2} ",
myPhones.IndexOfValue(myPhones.GetBylndex(i)),
myPhones.GetKey(i), myPhones.GetBylndex(i));
Console.ReadLine();
Получив управление, метод Main нашей программы создает сортированный спи-
список и заполняет его парами ключ-значение:
SortedList myPhones = new SortedList();
myPhones.Add("Иванов", 22-44-22 " ) ;
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44");
Глава 13. Контейнеры для хранения объектов 445
Далее в программе создается итератор для последовательного просмотра списка.
Просмотр осуществляется в цикле:
System.Collections.IDictionaryEnumerator myEnumerator =
myPhones.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLine("{0}. {1}: {2} ",
myPhones.IndexOfKey(myEnumerator.Key),
myEnumerator.Key, myEnumerator.Value);
}
Значение индекса элемента, отображаемого на консоли, по ключу мы получаем
при помощи выражения myPhones . IndexOfKey (myEnumerator .Key).
Использование метода IndexOfValue, возвращающего индекс элемента по зна-
значению, демонстрируется в представленном ниже цикле:
for(int i = 0; i < myPhones.Count; i++)
{
Console.WriteLine("{0}. {1}: {2} ",
myPhones.IndexOfValue(myPhones.GetBylndex(i)),
myPhones.GetKey(i), myPhones.GetBylndex(i));
}
Здесь мы передаем методу IndexOfValue значение элемента, полученного
по индексу с помощью метода GetBylndex. Очевидно, что индекс, полученный
от метода IndexOfValue, должен быть равен переменной цикла. В этом можно убе-
убедиться, запустив программу на выполнение:
0. Иванов: 322-44-22
1. Петров: 8-999-323-33-33
2. Сидоров: 8-999-323-33-44
0. Иванов: 322-44-22
1. Петров: 8-999-323-33-33
2. Сидоров: 8-999-323-33-44
Изменение значения по индексу
Выше мы рассказали Вам, как можно извлечь из сортированного списка ключ и значе-
значение элемента по его индексу. Предусмотрена и другая возможность — изменение зна-
значения элемента, заданного своим индексом.
Это можно сделать при помощи метода SetBylndex. Данному методу нужно пе-
передать два параметра — индекс изменяемого элемента и новое значение элемента. Ис-
Использование метода SetBylndex демонстрируется в программе, исходный текст ко-
которой приведен в листинге 13.25.
446 А. В. Фролов, Г. В. Фролов. Язык С# Самоучитель
Листинг 13.25. Файл ch13\SortedList2\SortedList2App.cs
using System;
using System.Collections;
namespace SortedList2
{
class SortedList2App
{
static void Main(string!] args)
{
SortedList myPhones = new SortedList();
myPhones.Add("Иванов", 22-44-22");
myPhones.Add("Петров", "8-999-323-33-33") ;
myPhones.Add("Сидоров", "8-999-323-33-44");
System.Collections.IDictionaryEnumerator myEnumerator
myPhones.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLine("{0}. {1}: {2} ",
myPhones.IndexOfKey(myEnumerator.Key),
myEnumerator.Key, myEnumerator.Value);
myPhones. SetBylndexd, "+7- @95)-888-88-88") ;
myPhones.SetBylndexB, "+7- @95)-777-77-77");
Console.WriteLine();
System.Collections.IDictionaryEnumerator myEnumeratorl
myPhones.GetEnumerator();
while(myEnumeratorl.MoveNext())
{
Console.WriteLine("{0}. {1}: {2} ",
myPhones.IndexOfKey(myEnumeratorl.Key),
myEnumeratorl.Key, myEnumeratorl.Value);
Console.ReadLine();
Здесь мы создаем сортированный список, используя для этого тот же прием, что
и в предыдущей программе (см. листинг 13.24). Затем мы изменяем значение элемен-
элементов с индексами 1 и 2, вызывая для этого метод SetBylndex:
myPhones. SetBylndexd, "+7-@95 )-888-88-88 ");
myPhones.SetBylndexB, "+7-@95)-777-77-77");
Глава 13. Контейнеры для хранения объектов 447
Программа отображает на консоли исходное содержимое списка, а также содержи-
содержимое списка после изменений:
0. Иванов: 322-44-22
1. Петров: 8-999-323-33-33
2. Сидоров: 8-999-323-33-44
0. Иванов: 322-44-22
1. Петров: +7-@95)-888-88-88
2. Сидоров: +7-@95)-777-77-77
Удаление элементов списка
В классе SortedList определены 3 метода, предназначенные для удаления элемен-
элементов из сортированного списка.
С двумя из них вы уже знакомы по словарям Hashtable— это метод Clear,
удаляющий из списка все имеющиеся там элементы, и метод Remove, с помощью ко-
которого можно удалить элемент с заданным ключом. Ключ удаляемого элемента пере-
передается методу Remove в качестве единственного параметра.
Третий метод с названием RemoveAt позволяет удалить элемент по его индексу.
Индекс удаляемого элемента передается методу RemoveAt через единственный пара-
параметр. Использование метода демонстрируется в программе, исходный текст которой
вы найдете в листинге 13.26.
Листинг 13.26. Файл ch13\SortedList3\SortedList3App.cs
using System,-
using System.Collections;
namespace SortedList3
{
class SortedList3App
{
static void Main(string[] args)
{
SortedList myPhones = new SortedList();
myPhones.Add("Иванов", 22-44-22") ;
myPhones.Add("Петров", "8-999-323-33-33");
myPhones.Add("Сидоров", "8-999-323-33-44") ;
System.Collections.IDictionaryEnumerator myEnumerator =
myPhones.GetEnumerator() ;
while(myEnumerator.MoveNext())
{
Console.WriteLineC {0} . {1}: {2}
myPhones.IndexOfKey(myEnumerator.Key),
myEnumerator.Key, myEnumerator.Value);
}
myPhones.RemoveAtA);
448 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Console.WriteLine();
System.Collections.IDictionaryEnumerator myEnumeratorl
myPhones.GetEnumerator();
while(myEnumeratorl.MoveNext())
{
Console.WriteLine("{0}. {1}: {2} ",
myPhones.IndexOfKey(rayEnumeratorl.Key),
myEnumeratorl.Key, myEnumeratorl.Value);
Console.ReadLine();
Вот как мы удаляем элемент с индексом 1:
myPhones.RemoveAtA);
Программа выводит на консоль содержимое исходного списка, а также содержимое
списка после удаления из него элемента:
0. Иванов: 322-44-22
1. Петров: 8-999-323-33-33
2. Сидоров: 8-999-323-33-44
0. Иванов: 322-44-22
1. Сидоров: 8-999-323-33-44
Обратите внимание, что индекс элемента с ключом «Сидоров» изменился и стал
равен единице. Добавление элементов в список или удаление из него приводит
к изменению индексов. Учтите это, если собираетесь обращаться к элементам сорти-
сортированного списка по индексам.
Работа в многопоточном режиме
Статические списки SortedList, объявленные как public, можно использовать
в многопоточных приложениях без принятия дополнительных мер по синхронизации.
Если же вы создаете нестатические объекты этого класса, то необходимо обеспечить
синхронизацию.
Это можно сделать либо создав синхронизированный список методом Synch-
Synchronized, либо получив объект синхронизации с помощью свойства SyncRoot. Оба
способа аналогичны способам, описанным в этой главе ранее для массива ArrayList
и словаря Hashtable, поэтому мы не будем останавливаться на этом подробно.
Стек Stack
Такой контейнер, как стек (stack), позволяет организовать хранение данных по прин-
принципу «первый вошел — последний вышел». Так работает магазин автомата Калашни-
Калашникова. Вы заполняете его патронами с одной стороны, причем те патроны, которые бы-
ли вставлены в магазин последними, будут использованы в первую очередь.
Глава 13. Контейнеры для хранения объектов 449
15 Язык С# Самоучитель
Для классического стека определены как минимум две операции:
• запись в стек (push),
• извлечение из стека (pop).
При выполнении записи добавляемый элемент оказывается в верхушке стека, а все
хранившиеся там ранее элементы сдвигаются вниз. При извлечении элемента из стека
все его элементы сдвигаются на одну позицию вверх.
Дополнительно может быть определена операция проверки содержимого верхушки
стека (peck), которая не приводит к извлечению элементов из стека.
Среди контейнеров библиотеки классов Microsoft .NET Framework имеется класс
Stack, специально предназначенный для организации стеков. Он очень удобен и не-
несложен в использовании.
Создание стека
Для создания стека класса Stack вы можете использовать один из трех конструкто-
конструкторов. Простейший конструктор не имеет параметров и предназначен для создания пус-
пустого стека с размером, заданным по умолчанию:
Stack wordsStack = new Stack();
Имеются также конструкторы с одним параметром. Первый из них позволяет соз-
создать стек на базе другого контейнера, а второй — пустой стек заданного размера.
Добавление элементов в стек
Чтобы добавить элемент в стек, необходимо вызвать метод Push и передать этому ме-
методу через единственный параметр ссылку на добавляемый элемент.
Ниже мы создали пустой стек и добавили в него несколько текстовых строк:
Stack wordsStack = new Stack();
wordsStack.Push("каждый") ;
wordsStack.Push("охотник") ;
wordsStack.Push("желает");
wordsStack.Push("знать") ;
wordsStack.Push("где") ;
wordsStack.Push("сидит") ;
wordsStack.Push("фазан") ;
Заметим, что при необходимости можно добавить в стек и пустой элемент, передав
методу Push значение null.
Извлечение элементов из стека
Для того чтобы извлечь элемент из стека, воспользуйтесь методом Pop. Этот метод извле-
извлекает из стека один элемент и возвращает вызывающей программе ссылку на этот элемент:
Console.WriteLine("Извлекаем один элемент: {0}", wordsStack.Pop());
При попытке извлечения элемента из пустого стека возникает исключение
InvalidOperationException. Если при работе вашей программы возможно воз-
возникновение данной ситуации, не забудьте предусмотреть соответствующий обработ-
обработчик исключений.
450 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Чтобы избежать выполнения операции извлечения данных из пустого стека, вы
также можете контролировать текущий размер стека при помощи свойства Count:
Console.WriteLine("Всего в стеке @} элементов:\п",
wordsStack.Count);
Проверка содержимого верхушки стека
При помощи метода Peek программа может проверить содержимое верхушки стека,
не извлекая из стека хранящийся там элемент:
Console.WriteLine("\пПервый элемент: @)", wordsStack.Peek());
Метод Peek возвращает элемент, записанный в верхушке стека. Если стек пуст,
при вызове этого метода возникает исключение InvalidOperationException.
Просмотр стека с помощью итератора
Содержимое стека можно просмотреть при помощи итератора класса Sys-
System.Collections . IEnuraerator, как это показано ниже:
System.Collections.IEnumerator myEnumerator =
wordsStack.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLine("{0}", myEnumerator.Current);
}
Особенность этого метода заключается в том, что элементы будут просматри-
просматриваться не в порядка добавления, а в обратном порядке, как будто они извлекаются
из стека. Процедура просмотра с помощью итератора, однако, не изменяет содер-
содержимого самого стека.
Удаление элементов стека
Для удаления всех элементов из стека предназначен метод Clear. Что же касается
удаления отдельных элементов, то эта операция выполняется при извлечении элемен-
элементов из стека методом Pop.
Другие методы и свойства класса Stack
Если стек создается программой, работающей в многопоточном режиме, необходимо
обеспечить синхронизацию.
Это можно сделать либо создав методом Synchonized синхронизированный
стек, либо получив объект синхронизации при помощи свойства SyncRoot. Свойство
IsSynchronized позволяет определить, является ли стек синхронизированным или
нет. Соответствующие приемы были описаны ранее, когда мы рассказывали о других
контейнерах.
Глава 13. Контейнеры для хранения объектов 451
С помощью метода Contains, имеющего один параметр, вы можете определить,
хранится ли заданный элемент в стеке. При вызове нужно передать этому методу
ссылку на элемент. Если такой элемент находится в стеке, метод Contains вернет
значение true, в противном случае — значение false.
При необходимости вы можете скопировать содержимое стека в другой стек (мето-
(методом Clone) или в одномерный массив класса Array (методом СоруТо).
Пример программы
В листинге 13.27 мы привели исходный текст программы, выполняющей основные
операции со стеком. Так как все эти операции были описаны ранее, мы оставляем вам
эту программу для самостоятельного изучения.
Листинг 13.27. Файл ch13\StackDemo\StackDemoApp.cs
using System,-
using System.Collections;
namespace StackDemo
{
class StackDemoApp
{
static void Main(string[] args)
{
Stack wordsStack = new Stack();
wordsStack.Push("каждый");
wordsStack.Push("охотник");
wordsStack.Push("желает");
wordsStack.Push("знать");
wordsStack.Push("где");
wordsStack.Push("сидит");
wordsStack.Push("фазан");
Console.WriteLine("Всего в стеке {0} элементов:\n",
wordsStack.Count);
System.Collections . "[Enumerator myEnumerator =
wordsStack.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLine("{0}", myEnumerator.Current);
Console.WriteLine("\пПервый элемент: @}", wordsStack.Peek());
Console.WriteLine("Извлекаем один элемент: @}",
wordsStack.Pop());
Console.WriteLine("Извлекаем один элемент: {0}",
wordsStack.Pop());
452 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Console.WriteLine("После извлечения в стеке {0) элементов:\п"
wordsStack.Count);
System.Collections.IEnumerator myEnumeratorl =
wordsStack.GetEnumerator();
while(myEnumeratorl.MoveNext())
{
Console.WriteLine("{0}", myEnumeratorl.Current);
}
Console.ReadLine();
После запуска программа создает стек и записывает в него 7 текстовых строк фра-
фразы «каждый охотник желает знать, где сидит фазан». Далее программа отображает
на консоли количество элементов и содержимое стека (при помощи итератора). После
этого программа показывает на консоли содержимое верхушки стека, извлекает
из стека два элемента, а затем вновь отображает весь стек:
Всего в стеке 7 элементов:
фазан
сидит
где
знать
желает
охотник
каждый
Первый элемент: фазан
Извлекаем один элемент: фазан
Извлекаем один элемент: сидит
После извлечения в стеке пяти элементов:
где
знать
желает
охотник
каждый
Обратите внимание, что слова исходной фразы отображаются в обратном порядке,
так как они хранились в стеке.
Очередь Queue
Понятие очереди знакомо, наверное, всем, кто хоть раз бывал в магазине. Очередь ра-
работает по принципу «первый вошел — первый вышел».
Для создания очередей в библиотеке классов Microsoft .NET Framework предусмот-
предусмотрен класс Queue. Рассмотрим основные приемы его использования.
Глава 13. Контейнеры для хранения объектов 453
Создание очереди
Для создания очередей вы можете использовать один из четырех перегруженных кон-
конструкторов, предусмотренных в классе Queue.
Простейший конструктор не имеет параметров. Он создает пустую очередь с ха-
характеристиками, заданными по умолчанию:
Queue wordsQueue = new Queue();
Другие конструкторы предназначены для создания очереди на базе существующего
контейнера, очереди с заданным начальным размером и фактором роста. Фактор рос-
роста — это число раз, в которое увеличивается текущая емкость очереди при добавлении
элемента в полностью заполненную очередь.
Добавление элемента в очередь
Чтобы добавить в очередь новый элемент, применяйте метод Enqueue. Ниже мы по-
показали пример использования этого метода:
Queue wordsQueue = new Queue();
wordsQueue.Enqueue("каждый");
wordsQueue.Enqueue("охотник");
wordsQueue.Enqueue("желает") ;
wordsQueue.Enqueue("знать") ;
wordsQueue.Enqueue("где") ;
wordsQueue.Enqueue("сидит") ;
wordsQueue.Enqueue("фазан") ;
При необходимости вы можете добавить в очередь значение null.
Извлечение элементов из очереди
С помощью метода Dequeue программа может извлечь один элемент из очереди:
Console.WriteLine("Извлекаем один элемент: {0}",
wordsQueue.Dequeue());
Как мы уже говорили, вначале из очереди извлекаются те элементы, которые были
туда записаны первыми. После извлечения элемента содержимое очереди сдвигается
на одну позицию.
При попытке извлечения элемента из пустой очереди возникает исключение
InvalidOperationException. Чтобы избежать выполнения операции извлечения
данных из пустой очереди, вы можете контролировать ее текущий размер при помощи
свойства Count:
Console.WriteLine("Всего в очереди @} элементов:\п",
wordsQueue.Count);
454 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Проверка содержимого начала очереди
При помощи метода Peek программа может проверить содержимое начала очереди,
не извлекая хранящийся там элемент:
Console.WriteLine("\пПервый элемент: @}", wordsQueue.Peek());
Метод Peek возвращает элемент, записанный в начале очереди и готовый к «об-
«обслуживанию». Если очередь пустая, при вызове этого метода возникает исключение
InvalidOperationException.
Просмотр очереди с помощью итератора
Содержимое очереди можно просмотреть при помощи итератора класса Sys-
System. Collections . lEnumerator, как это показано ниже:
System.Collections.IEnumerator myEnumerator =
wordsQueue.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLine("{0}", myEnumerator.Current);
}
В отличие от стека элементы очереди будут просматриваться в порядке добавле-
добавления. Процедура просмотра с помощью итератора не изменяет содержимого очереди.
Удаление элементов очереди
Для удаления всех элементов из очереди ваша программа может использовать метод
Clear. Удаление отдельных элементов выполняется при извлечении элементов
из очереди методом Dequeue.
Другие методы и свойства класса Queue
Если очередь (как и любой другой контейнер) создается программой, работающей
в многопоточном режиме, необходимо обеспечить синхронизацию.
Это можно сделать, создав методом Synchonized синхронизированную очередь
либо получив объект синхронизации при помощи свойства SyncRoot. Свойство
IsSynchronized позволяет определить, является ли очередь синхронизированной
или нет.
С помощью метода Contains, имеющего один параметр, вы можете определить,
находится ли заданный элемент в очереди. При вызове нужно передать этому методу
ссылку на элемент. Если такой элемент находится в очереди, метод Contains вернет
значение true, в противном случае — значение false.
При необходимости вы можете скопировать содержимое очереди в другую очередь
(методом Clone) или в одномерный массив класса Array (методом СоруТо).
Глава 13. Контейнеры для хранения объектов 455
Пример программы
В листинге 13.28 мы привели исходный текст программы, выполняющей основные
операции с очередью. Все операции, выполняемые этой программой, были описаны
ранее, поэтому мы оставляем ее вам для самостоятельного изучения.
Листинг 13.28. Файл ch13\QueueDemo\QueueDemoApp.cs
using System;
using System.Collections;
namespace QueueDemo
{
class QueueDemoApp
{
static void Main(string[] args)
{
Queue wordsQueue = new Queue();
wordsQueue.Enqueue("каждый");
wordsQueue.Enqueue("охотник");
wordsQueue.Enqueue ("желает");
wordsQueue.Enqueue("знать");
wordsQueue.Enqueue("где");
wordsQueue.Enqueue("сидит");
wordsQueue.Enqueue("фазан");
Console.WriteLine("Всего в очереди {0} элементов:\n",
wordsQueue.Count);
System.Collections.lEnumerator myEnumerator =
wordsQueue.GetEnumerator();
while(myEnumerator.MoveNext{))
{
Console.WriteLine("{0}", myEnumerator.Current);
Console.WriteLine("\пПервый элемент: {0}", wordsQueue.Peek()) ;
Console.WriteLine("Извлекаем один элемент: {0}",
wordsQueue.Dequeue() ) ;
Console.WriteLine("Извлекаем один элемент: {0}",
wordsQueue.Dequeue());
Console.WriteLine(
"После извлечения в очереди {0} элементов:\n",
wordsQueue.Count);
System.Collections.lEnumerator myEnumeratorl =
wordsQueue.GetEnumerator();
456 А. В. Фролов, Г. В. Фролов. Язык С# Самоучитель
while(myEnumeratorl.MoveNext())
{
Console.WriteLine("{0}", myEnшneratorl.Current)
}
Console.ReadLine();
После запуска программа создает очередь и записывает в нее 7 текстовых строк
фразы «каждый охотник желает знать, где сидит фазан». Далее программа отображает
на консоли количество элементов и содержимое очереди (при помощи итератора). По-
После этого программа показывает на консоли содержимое начала очереди, извлекает
из очереди два элемента, а затем вновь отображает всю очередь:
Всего в очереди 7 элементов:
каждый
охотник
желает
знать
где
сидит
фазан
Первый элемент: каждый
Извлекаем один элемент: каждый
Извлекаем один элемент: охотник
После извлечения в очереди пяти элементов:
желает
знать
где
сидит
фазан
Так как элементы очереди обрабатываются в порядке поступления, при отображе-
отображении содержимого очереди порядок слов исходной фразы не меняется.
Битовый массив BitArray
Последний контейнер, который мы рассмотрим в этой главе, — это битовый мас-
массив BitArray. В отличие от обычных массивов типа bool массивы BitArray
допускают выполнение над собой поразрядных операций, таких, как AND, OR,
NOT и XOR. Для этого в классе BitArray предусмотрены соответствующие ме-
методы.
Глава 13 Контейнеры для хранения объектов 457
Заметим, что, подобно массивам Array, массивы BitArray не допускают изме-
изменения своих размеров после создания. Попытка записи или чтения за пределами мас-
массива BitArray неизбежно приведет к возникновению исключения.
Напомним, что значения логических данных в языке программирования С# пред-
представляются константами true (истина) и false (ложь). Эти значения никак не соот-
соотносятся с числовыми значениями, в частности со значениями 1 и 0.
Создание массива BitArray
Для создания массива BitArray следует воспользоваться одним из конструкторов,
определенных в классе BitArray.
Простейший из этих конструкторов создает массив заданного размера, все ячейки
которого содержат значения false. Размер создаваемого массива передается конст-
конструктору в качестве параметра:
BitArray byteArray = new BitArray(8);
Другие конструкторы позволяют создать битовый массив BitArray на базе су-
существующего массива BitArray, а также массивов чисел bool, byte и int. Есть
также возможность создания массива заданного размера, во всех ячейках которого из-
изначально будут записаны значения true или false, по выбору программиста.
Инициализация ячеек массива
Ячейки массива BitArray проще всего проинициализировать с помощью квадратных
скобок, как это показано ниже:
BitArray byteArray = new BitArray(8);
byteArray[0] = true;
byteArray[1] = false;
byteArray [2] = true,-
byteArray[3] = true;
byteArray[4] = false;
byteArray[5] = false;
byteArray[6] = true;
byteArray[7] = false;
Существуют и другие методы инициализации ячеек массива.
С помощью метода Set All можно записать во все ячейки массива одно и то же
значение, например:
byteArray.SetAll(true) ;
После выполнения этой операции во всех ячейках массива byteArray будет хра-
храниться значение true.
Чтобы установить значение для заданной ячейки массива, воспользуйтесь методом
Set:
byteArray.SetA0, false);
458 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
В качестве первого параметра этому методу передается индекс элемента, а в каче-
качестве второго — новое значение. Таким образом, в приведенном выше примере мы за-
записываем значение false в ячейку массива с индексом 10.
Извлечение значений из массива
Для того чтобы извлечь значение из заданной ячейки массива, вы можете воспользо-
воспользоваться квадратными скобками. Эта процедура вам известна по работе с обычными
массивами. Кроме того, можно извлечь значение из массива BitArray при помощи
метода Get, передав ему индекс нужного элемента.
Просмотр массива
Программа может последовательно просмотреть содержимое всего массива с помо-
помощью итератора System.Collections . IEnumerator.
Это демонстрируется ниже:
System.Collections.IEnumerator myEnumerator =
byteArray.GetEnumerator();
while(myEnumerator.MoveNext())
{
if((bool)myEnumerator.Current)
Console.Write(");
else
Console.Write("} ;
}
Обратите внимание, что для отображения на консоли логических значений в виде
единиц и нулей мы вынуждены выполнять специальное преобразование.
Логические операции над массивами
На наш взгляд, наиболее интересная возможность, предоставляемая классом Bit-
Array, — это выполнение логических операций над содержимым целых битовых
массивов. В табл. 13.1 мы перечислили методы, предназначенные для выполнения по-
поразрядных битовых операций над массивами класса BitArray.
Таблица 13.1. Методы для выполнения поразрядных операций
Операция
и
или
ИСКЛЮЧАЮЩЕЕ ИЛИ
НЕ
Метод
And
Or
Xor
Not
Использование методов для выполнения поразрядных операций демонстрируется
в программе, исходный текст которой представлен в листинге 13.29.
Глава 13. Контейнеры для хранения объектов 459
Листинг 13.29. Файл ch13\BitArrayDemo\BitArrayDemoApp.c$
using System;
using System.Collections;
namespace BitArrayDemo
{
class BitArrayDemoApp
{
static void Main(string!] args)
{
BitArray byteArray = new BitArray(8);
byteArray[0] = true;
byteArray[1] = false;
byteArray[2] = true;
byteArray[3] = true;
byteArray[4] = false;
byteArray[5] = false;
byteArray[6] = true;
byteArray[7] = false;
Console.Write("Число:\t");
System.Collections.IEnumerator myEnumerator =
byteArray.GetEnumerator();
while(myEnumerator.MoveNext())
{
iff(bool)myEnumerator.Current)
Console.Write("l");
else
Console.Write(") ;
Console.Write!"\пМаска:\t") ;
BitArray maskArray = new BitArray(8);
maskArray[0] = true;
maskArray[1] = false;
maskArray[2] = false;
maskArray[3] = false;
maskArray[4] = true;
maskArray [5] = true,-
maskArray[6] = true;
maskArray[7] = true;
System.Collections.IEnumerator myEnumeratorl =
maskArray.GetEnumerator();
460 А. В. Фролов, Г. В. Фролов. Язык С# Самоучитель
while(myEnumeratorl.MoveNext())
{
if((bool)myEnumeratorl.Current)
Console.Write(");
else
Console.WriteC'O") ;
Console.Write("\nAND:\t");
BitArray andArray = new BitArray (byteArray. And (maskArray) )
System.Collections.IEnumerator myEnumerator2 =
andArray.GetEnumerator();
while(myEnumerator2.MoveNext())
{
if((bool)myEnumerator2.Current)
Console.WriteC'l") ;
else
Console.Write(" ) ;
Console.Write("\nOR:\t") ;
BitArray orArray = new BitArray(byteArray.Or(maskArray))
System.Collections.IEnumerator myEnumerator3 =
orArray.GetEnumerator();
while(myEnumerator3.MoveNext())
{
if((bool)myEnumerator3.Current)
Console.WriteC'l") ,-
else
Console.WriteC'O") ;
Console.ReadLine();
Эта программа создает и инициализирует два битовых массива, byteArray
и maskArray, записывая в них различные логические значения:
BitArray byteArray = new BitArray(8);
byteArray[0] = true;
byteArray[1] = false;
byteArray[2] = true;
Глава 13. Контейнеры для хранения объектов 461
byteArray[3] = true;
byteArray[4] = false;
byteArray[5] = false;
byteArray[6] = true;
byteArray[7] = false;
BitArray maskArray = new BitArray(8);
maskArray[O] = true;
maskArray[1] = false;
maskArray[2] = false;
maskArray[3] = false;
maskArray [4] = true;
maskArray[5] = true;
maskArray[6] = true;
maskArray[7] = true;
Исходные значения этих массивов отображаются на консоли при помощи итерато-
итераторов. Вот, например, как отображается массив byteArray:
System.Collections.IEnumerator myEnumerator =
byteArray.GetEnumerator();
while(myEnumerator.MoveNext[))
{
if((bool)myEnumerator.Current)
Console.Write("l");
else
Console.Write{") ;
}
Массив maskArray отображается аналогично.
Далее программа выполняет над массивами byteArray и maskArray логиче-
логическую операцию И, записывая ее результат в новый массив andArray:
Console.Write("\nAND:\t");
BitArray andArray = new BitArray(byteArray.And(maskArray));
System.Collections.IEnumerator myEnumerator2 =
andArray.GetEnumerator();
while(myEnumerator2.MoveNext())
{
if((bool)myEnumerator2.Current)
Console.Write(");
else
Console.Write(");
462 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Результат выполнения операции byteArray .And(maskArray) мы передаем
конструктору класса BitArray, создающему массив andArray. Далее содержимое
массива andArray отображается на консоли.
Аналогичным образом программа выполняет над этими массивами поразрядную
операцию ИЛИ. Вот что появится на консоли после завершения работы нашей про-
программы:
Число:
Маска:
AND:
OR:
10110010
10001111
10000010
10001111
Глава 13 Контейнеры для хранения объектов 463
Глава 14. Файлы и потоки
Большинство программ так или иначе работают с каталогами и файлами. Средства для
работы с файлами предусмотрены практически в любом языке программирования. Эти
средства могут быть встроены непосредственно в язык, а также входить в стандартные
библиотеки функций и классов, поставляющихся вместе с компилятором.
Программы, составленные на языке С#, работают с каталогами и файлами при по-
помощи специально предназначенных для этого классов, входящих в состав библиотеки
классов Microsoft .NET Framework.
Потоки данных и классы
Все операции с файлами выполняются в программах С# с помощью так называемых
потоков данных (data stream). He путайте их с потоками выполнения (thread), о кото-
которых мы рассказывали в гл. 10.
Программы могут выполнять над потоками данных 3 операции:
• запись данных в поток,
• чтение данных из потока,
• позиционирование.
Для потоков, связанных с файлами, определено также такое понятие, как текущая
позиция внутри файла.
Перед началом операций ввода-вывода программа должна открыть поток. При этом
текущая позиция устанавливается на начало файла. При чтении файла или записи
в файл блоков данных текущая позиция сдвигается к концу файла на количество бай-
байтов, равное размеру прочитанного или записанного блока данных. При помощи
средств позиционирования программа может установить текущую позицию в произ-
произвольное место файла. Когда работа с файлом закончена, программа обязательно долж-
должна закрыть соответствующий поток явным образом.
Программа С# может работать с потоками нескольких типов:
• стандартными потоками ввода и вывода,
• потоками, связанными с локальными файлами,
• потоками, связанными с файлами в оперативной памяти.
Рассмотрим кратко классы библиотеки Microsoft .NET Framework, связанные с по-
потоками перечисленных выше типов.
Стандартные потоки
Стандартные потоки обычно связаны с консолью и клавиатурой, но могут быть пере-
перенаправлены в файлы средствами ОС. По своему назначению эти потоки больше всего
напоминают стандартные потоки ввода и вывода, а также стандартный поток вывода
сообщений об ошибках ОС MS-DOS.
~~Шшш 464 =~~~
Практически все программы, исходные тексты которых приводились в нашей кни-
книге, работают со стандартными потоками ввода и вывода. Для вывода данных в стан-
стандартный поток вывода мы применяли методы System.Console.Write и Sys-
System. Console .WriteLine. Ввод данных из стандартного потока выполнялся с по-
помощью метода System.Console.ReadLine.
Базовые классы для работы с файлами и потоками
Количество классов, созданных для работы с файлами, достаточно велико и может при-
привести начинающего программиста в растерянность. Прежде чем мы займемся конкретны-
конкретными классами и приведем примеры приложений, работающих с потоками и файлами, рас-
рассмотрим иерархию классов, предназначенных для организации ввода и вывода.
Основные классы ввода и вывода
Как и все классы в С#, основные классы ввода и вывода произошли от класса Obj ect
(рис. 14.1).
Рис. 14.1. Основные классы для работы с файлами и потоками
Рассмотрим кратко их назначение.
Класс BinaryReader предназначен для чтения блоков данных из потоков ввода
на уровне отдельных байтов. Обычно для чтения объектов из файлов, таких, как стро-
строки и числа, программисты используют другие, более мощные классы.
Класс BinaryWriter служит в качестве низкоуровневого средства записи дан-
данных в потоки вывода.
Класс File предназначен для работы с оглавлениями каталогов. С помощью ста-
статических методов этого класса можно получить список файлов и каталогов, располо-
расположенных в заданном каталоге, создать или удалить каталог, переименовать файл или
каталог, а также выполнить некоторые другие операции.
С помощью статических методов класса Directory программа может получать
списки каталогов и подкаталогов, создавать и удалять каталоги, а также выполнять
над каталогами другие подобные операции.
Класс Path обеспечивает методы и свойства, с помощью которых программы мо-
могут работать с именами и полными путями каталогов.
Глава 14. Файлы и потоки
465
Классы на базе Filesystemlnfo
Рассмотренные в предыдущем разделе классы Directory и File содержат только
статические члены и потому предназначены для работы с файлами и каталогами
без образования экземпляров соответствующих объектов.
Классы Directorylnfo и Filelnfo, напротив, требуют создания экземпляров
объектов, представляющих соответственно каталоги и файлы. Эти классы унаследова-
унаследованы от общего абстрактного базового класса Filesystemlnfo, который, в свою оче-
очередь, является производным от абстрактного базового класса MarshalByRef Obj ect
(рис. 14.2).
Рис. 14.2. Классы на базе Filesystemlnfo
Абстрактный базовый класс MarshalByRef Obj ect обеспечивает возможность
доступа к объектам через границы доменов приложений, что необходимо в тех случа-
случаях, когда приложения выполняют удаленную обработку данных.
Классы для работы с потоками
На базе класса System. 10. Stream создано несколько производных классов, специ-
специально предназначенных для работы с потоками ввода и вывода (рис. 14.3).
Класс System. 10. Stream служит базовым для других классов потокового ввода
и вывода.
Класс FileStream, как это видно из его названия, предназначен для работы
с файлами через потоки ввода и вывода. Он обеспечивает позиционирование внутри
потоков ввода и вывода методом Seek. Это позволяет выполнять прямой доступ
к файлам.
466
А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
FileStream
CryptoStream
Рис. 14.3. Классы для работы с потоками
С помощью класса CryptoStream можно организовать ввод и вывод данных
в зашифрованном виде.
Класс MemoryStream позволяет создавать файлы в оперативной памяти. Доступ
к таким файлам осуществляется очень быстро, так как не требуется работать с мед-
медленными дисковыми устройствами. Обычно файлы в оперативной памяти создаются
для временного хранения данных, потому что после выключения компьютера содер-
содержимое этих файлов исчезнет.
С помощью класса NetworkStream можно осуществлять удаленный ввод и вы-
вывод данных через сетевое соединение.
Класс Buf f eredStream обеспечивает буферизацию при работе с потоками ввода
и вывода. Буферизация операций ввода и вывода в большинстве случаев значительно
ускоряет работу приложений, так как при ее использовании сокращается количество
обращений к системе для обмена данными с внешними устройствами.
Классы для работы с потоками текстовых символов
Специально для работы с потоками текстовых символов UNICODE в библиотеке клас-
классов Microsoft .NET Framework предусмотрены абстрактные классы System.10.Text-
Reader и System. 10 .TextWriter. Первый из этих классов предназначен для по-
потоков ввода, а второй — для потоков вывода (рис. 14.4).
С помощью класса StreamReader, базовым для которого является класс
System. 10.TextReader, программа может читать отдельные байты входного по-
потока символов.
Глава 14. Файлы и потоки
467
StreamReader
StringReader StringWriter StreamWriter
Рис. 14.4. Классы для работы с потоками текстовых символов
Аналогично класс StreamWriter, созданный на базе класса System. 10.Text-
Writer, позволяет писать байты символов в выходной поток.
Классы StringReader и StringWriter, созданные на базе классов Sys-
System. IO.TextReader и System. IO.TextWriter, соответственно предназначены
для чтения и записи текстовых строк string.
Перечисления
При работе с потоками, файлами и каталогами вам также потребуются перечисления
FileAccess, FileMode, FileShare и SeekOrigin. Они содержат определения
констант, нужных для определения режимов создания и открытия файлов, режимов
совместного доступа к файлу, а также позиции при произвольном доступе к файлам.
Все эти константы мы будем рассматривать дальше по мере изложения материала
этой главы.
Работа со стандартными потоками
Приложению С# доступны 3 стандартных потока, которые всегда открыты: стандарт-
стандартный поток ввода, стандартный поток вывода и стандартный поток вывода сообщений
об ошибках.
Вес перечисленные выше потоки определены в классе Console. Вы можете полу-
получить на них ссылки с помощью статических свойств In, Out и Error.
Стандартный поток ввода
Стандартный поток ввода, который можно получить при помощи статического свой-
свойства In, определен как статический объект класса TextReader. Этот класс содержит
методы, предназначенные для ввода отдельных символов, строк и блоков текста.
Вот как программа может получить исылку на стандартный поток ввода:
TextReader trln = Console.In;
468
А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Чтобы ввести данные из стандартного потока ввода (по умолчанию связанного
с клавиатурой компьютера), используйте один из методов, определенных для этого
в классе TextReader.
В предыдущих примерах программ мы использовали метод Console. ReadLine,
осуществляющий ввод символов из стандартного потока ввода. Аналогичный метод
предусмотрен и в классе TextReader:
string s = trln.ReadLine();
Этот оператор вводит текстовую строку с клавиатуры и сохраняет ее в переменной s.
В классе TextReader имеются и другие методы, предназначенные для ввода от-
отдельных символов и блока символов.
Метод Read, не имеющий параметров, читает один символ и возвращает его как
значение типа int. Если символов больше нет, метод возвращает значение -1.
При возникновении ошибок создается исключение IOException, обработку кото-
которого необходимо предусмотреть.
Стандартный поток вывода
Стандартный поток вывода Out создан на базе класса TextWriter, предназначенно-
предназначенного для форматированного вывода данных различного типа с целью их визуального
отображения в виде текстовой строки.
Для того чтобы получить ссылку на стандартный поток вывода, используйте свой-
свойство Console.Out:
TextWriter twOut = Console.Out;
Далее, используя полученную ссылку, можно выводить данные с помощью одного
из методов, определенных в классе TextWriter, например с помощью хорошо из-
известного вам по предыдущим примерам программ метода WriteLine:
twOut.WriteLine("Запись в стандартный поток вывода");
Этот метод аналогичен методу Console. WriteLine.
В классе TextWriter определено несколько реализаций метода Write с пара-
параметрами различных типов. Вот некоторые из них:
public virtual void Write(bool);
public virtual void Write(char);
public virtual void Write(char[]);
public virtual void Write(decimal);
public virtual void Write(double);
public virtual void Write(int);
public virtual void Write(long);
public virtual void Write(object);
public virtual void Write(float);
public virtual void Write(string);
public virtual void Write(uint);
public virtual void Write(ulong);
Глава 14. Файлы и потоки 469
Как видите, вы можете записать в стандартный поток вывода текстовое представ-
представление данных различного типа, в том числе и класса object.
Метод WriteLine аналогичен методу Write, отличаясь лишь тем, что он добав-
добавляет к записываемой в поток строке символ перехода на следующую строку:
public virtual void WriteLine(bool);
public virtual void WriteLine(char);
public virtual void WriteLine(char[]);
public virtual void WriteLine(decimal);
public virtual void WriteLine(double);
public virtual void WriteLine(int);
public virtual void WriteLine(long);
public virtual void WriteLine(object);
public virtual void WriteLine(float);
public virtual void WriteLine(string);
public virtual void WriteLine(uint);
public virtual void WriteLine(ulong);
Реализация метода WriteLine без параметров записывает только символ перехо-
перехода на следующую строку.
Стандартный поток вывода сообщений об ошибках
Стандартный поток вывода сообщений об ошибках Error, так же как и стандартный
поток вывода Out, создан на базе класса TextWriter. Поэтому для записи сообще-
сообщений об ошибках вы можете использовать только что описанные методы Write
иWriteLine.
Вот как программа может получить ссылку на стандартный поток вывода сообще-
сообщений об ошибках:
TextWriter twErr = Console.Error;
По умолчанию сообщения об ошибках выводятся на то же самое устройство
вывода (консоль), на которое выводятся и строки, записываемые в стандартный
поток вывода.
Программа StdStreams
Приложение StdStreams демонстрирует способы работы со стандартными потоками
ввода, вывода и вывода сообщений об ошибках (листинг 14.1).
Листинг 14.1. Файл ch14\StdStreams\StdStreamsApp.cs
using System;
using System.10;
namespace StdStreams
{
class StdStreamsApp
470 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
static void Main(string[] args)
{
TextWriter twOut = Console.Out;
TextWriter twErr = Console.Error;
twOut.WriteLine("Запись в стандартный поток вывода");
twErr.WriteLine(
"Запись в стандартный поток сообщений об ошибках");
TextReader trln = Console.In;
twOut.Write("Введите любую строку: ");
string s = trln.ReadLine();
twOut.WriteLine("Bbi ввели строку {0}", s);
trln.ReadLine();
Получив управление, метод Main нашей программы получает ссылки на стандарт-
стандартный поток вывода и стандартный поток вывода сообщений об ошибках:
TextWriter twOut = Console.Out;
TextWriter twErr = Console.Error;
Далее программа выводит в эти потоки две текстовые строки:
twOut.WriteLine("Запись в стандартный поток вывода");
twErr.WriteLine("Запись в стандартный поток сообщений об ошибках");
После этого наша программа получает ссылку на стандартный поток ввода, кото-
которой по умолчанию связан с клавиатурой:
TextReader trln = Console.In;
Отобразив на экране строку приглашения, программа вводит из стандартного пото-
потока ввода текстовую строку и записывает ее в переменную s:
twOut.Write("Введите любую строку: ");
string s = trln.ReadLine();
Далее введенная строка отображается на консоли следующим образом:
twOut.WriteLine("Вы ввели строку {0}", s);
trln.ReadLine();
Для завершения работы программы необходимо нажать клавишу Enter.
Как видите, приемы работы со стандартными потоками ввода и вывода несложны.
Глава 14. Файлы и потоки 471
Создание потоков, связанных с файлами
Если вам нужно создать входной или выходной поток, связанный с локальным фай-
файлом, содержащим двоичные данные, следует воспользоваться классами Binary-
Writer и BinaryReader из библиотеки Microsoft .NET Framework. Что же касается
чтения из файла или записи в файл текстовых данных, то для решения этой задачи
обычно используются классы StreamReader и StreamWriter.
В любом случае, перед тем как читать данные из файла или записывать их в файл, не-
необходимо выполнить операцию открытия потока, связанного с файлом. Когда работа
с файлом окончена, соответствующий поток необходимо закрыть явным образом.
Обращаем ваше внимание на то, что система сборки мусора, встроенная в среду
исполнения Microsoft .NET Framework, следит за использованием только объектов, на-
находящихся в оперативной памяти, таких, как переменные, массивы, экземпляры объ-
объектов, созданных на базе классов и т. п. Что же касается потоков, связанных с файла-
файлами, то вы должны сами следить за их открытием и закрытием.
Открытие потока FileStream
Для работы с двоичными файлами ваша программа должна вначале создать по-
поток класса FileStream, воспользовавшись соответствующим конструктором,
например:
FileStream fs = new FileStream("myfile.dat", FileMode.CreateNew);
В качестве первого параметра конструктору необходимо передать полный путь
к файлу или имя файла, а в качестве второго — режим открытия потока (табл. 14.1).
Таблица 14.1. Режимы открытия файла FileMode
Режим
Append
Create
CreateNew
Open
Описание
Если файл существует, он открывается. Текущая позиция уста-
устанавливается на конец файла. Если указанного файла нет, то он
создается. Этот режим можно использовать только совместно
с режимом доступа FileAccess .Write.
При попытке чтения из файла, открытого подобным образом,
возникает исключение ArgumentException
ОС должна создать новый файл. Если указанный файл уже
существует, он будет перезаписан
ОС должна создать новый файл. Если указанный файл уже
существует, возникнет исключение lOException
Требуется открыть существующий файл. Необходим доступ
FilelOPermissionAccess . Read. Если требуемый файл
не найден, возникает исключение System. 10. FileNot-
FoundExcepti on
472
А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Режим
OpenOrCreate
Truncate
Описание
Если указанный файл существует, он должен быть открыт.
В противном случае ОС должна создать и открыть указанный файл
Требуется открыть существующий файл. После открытия файл
обрезается до нулевой длины, при этом все ранее хранившиеся
в нем данные пропадают.
При попытке чтения из файла, открытого подобным образом,
возникает исключение
В зависимости от того, для какой цели создается файл, вы можете выбрать тот или
иной режим открытия потока. Например, если поток, связанный с файлом, открывает-
открывается только для чтения, выбирайте режим FileMode.Open. В этом случае будет от-
открыт существующий файл. Если же файл открывается для записи, то вы можете либо
открыть существующий файл, перезаписав его содержимое или дописав в него новые
данные, а также создать новый файл.
В классе FileStream существует несколько конструкторов, позволяющих опре-
определить не только путь к файлу, но и режим его открытия, разрешенный доступ к файлу
и режим совместного использования файла:
FileStream fs = new FileStream(name, FileMode.Open, FileAccess.Read);
FileStream fsl = new FileStream(name, FileMode.Open, FileAccess.Read,
FileShare.Read);
Остановимся на режиме доступа к файлу, передаваемом приведенным выше конст-
конструкторам через третий параметр (табл. 14.2). Режим доступа задается как статическая
константа класса FileAccess.
Таблица 14.2. Режимы доступа FileAccess
Режим
Read
ReadWrite
Write
Тип доступа
Доступ только на чтение
Доступ на чтение и запись
Доступ на запись
При необходимости программа может задать режим совместного использования файла,
когда для одного и того же файла одновременно создается несколько потоков. Этот режим
задается при помощи статических констант класса FileShare (табл. 14.3).
Таблица 14.3. Режимы совместного использования файла FileShare
Режим
Inheritable
None
Read
ReadWrite
Write
Тип доступа
Идентификатор файла может наследоваться дочерним процессом
Запрет совместного использования файла. Любые дополни-
дополнительные запросы на открытие файла будут запрещены то тех
пор, пока файл не будет закрыт
Допускаются дополнительные запросы на открытие файла
для чтения
Допускаются дополнительные запросы на чтение и запись
Допускаются дополнительные запросы на запись
Глава 14. Файлы и потоки
473
Открытие потоков BinaryWriter и BinaryReader
Итак, ваша программа создала поток на базе файла, воспользовавшись для этого клас-
классом FileStream. Далее на базе полученного таким образом потока необходимо соз-
создать потоки классов BinaryWriter и BinaryReader, предназначенные соответст-
соответственно для записи в файл и чтения из файла двоичных данных:
BinaryWriter bw = new BinaryWriter(fs);
BinaryReader br = new BinaryReader(fs);
Работа с потоками BinaryWriter и BinaryReader будет описана чуть позже.
Закрытие потоков
После того как программа завершила работу с потоками, она должна их закрыть.
Для закрытия потоков используйте метод Close. Закрывайте потоки в порядке,
обратном открытию. Если вы вначале открыли поток FileStream, а затем создали
на его базе потоки класса BinaryWriter и BinaryReader, то закрывать потоки
нужно в обратном порядке: вначале закройте потоки BinaryWriter и Binary-
Reader, а затем поток FileStream:
FileStream fs = new FileStream("myfile.dat", FileMode.CreateNew);
BinaryWriter bw = new BinaryWriter(fs);
BinaryReader br = new BinaryReader(fs);
// Работа с потоками
bw.Close();
br.Close();
fs.Close();
Запись двоичных данных
Для записи двоичных данных в поток BinaryWriter предусмотрено несколько пе-
перегруженных методов Write, приведенных ниже:
public virtual void Write(bool);
public virtual void Write(byte);
public virtual void Write(byte[]);
public virtual void Write(char);
public virtual void Write(char[]);
public virtual void Write(decimal);
public virtual void Write(double);
public virtual void Write(short);
public virtual void Write(int);
public virtual void Write(long);
public virtual void Write(sbyte);
public virtual void Write(float);
474 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
public virtual void Write(uint);
public virtual void Write(ulong);
public virtual void Write(string);
public virtual void Write(byte[], int, int);
public virtual void Write(char[], int, int);
Способ применения большинства перечисленных методов интуитивно ясен: для
записи данных того или иного типа в файл вам нужно передать ссылку на экземпляр
данных в качестве параметра методу Write. В зависимости от типа данных компиля-
компилятор выберет нужную перегруженную версию этого метода.
При записи в двоичный поток текстовой строки string она будет предваряться
префиксом, содержащим значение длины строки. Строка будет записана в кодировке
ASCII.
Последние два перегруженных метода Write позволяют записать в поток массив
соответственно двоичных байтов и символов UNICODE. Второй параметр метода за-
задает начальный индекс блока данных в массиве, а третий — размер блока (соответст-
(соответственно в байтах и символах).
Чтение двоичных данных
Чтобы прочитать данные из потока BinaryReader, нужно использовать один из ме-
методов, специально предусмотренных для этого в классе BinaryReader. Мы привели
краткий список этих методов в табл. 14.4.
Таблица 14.4. Методы для чтения данных класса BinaryReader
Метод
ReadBoolean
ReadByte
ReadBytes
ReadChar
ReadChars
ReadDecimal
ReadSingle
ReadDouble
ReadSByte
Readlntl6
Readlnt32
Readlnt64
ReadUIntl6
ReadUInt32
ReadUInt64
ReadString
Тип данных, считываемых из потока
Один объект типа Boolean
1 байт данных
Чтение заданного количества байтов данных
Один текстовый символ
Чтение заданного количества текстовых символов
Десятичное значение размером 16 байт
Значение с плавающей десятичной точкой размером 4 байта
Значение с плавающей десятичной точкой размером 8 байт
Число со знаком размером 1 байт
Число со знаком размером 2 байта
Число со знаком размером 4 байта
Число со знаком размером 8 байт
Число без знака размером 2 байта
Число без знака размером 4 байта
Число без знака размером 8 байт
Текстовая строка с префиксом длины
Глава 14. Файлы и потоки
475
Программа Binary
Программа Binary (листинг 14.2) демонстрирует некоторые приемы работы с двоичными
файлами при помощи классов FileStreara, BinaryWriter и BinaryReader.
Листинг 14.2. Файл ch14\Binary\BinaryApp.cs
using System;
using System.10;
namespace Binary
{
class BinaryApp
{
private const string testFile = "mydata.dat";
static void Main(string[] args)
{
if(File.Exists(testFile))
{
Console.WriteLine("Файл {0} уже существует", testFile);
Console.ReadLine();
return;
}
FileStream fs = new FileStreamttestFile, FileMode.CreateNew) ;
BinaryWriter bw = new BinaryWriter(fs);
bw.Write("Text string");
for(uint i = 0; i < 20; i++)
{
bw.Write((uint)i);
}
bw.Close();
fs.Closef);
fs = new FileStream(testFile, FileMode.Open, FileAccess.Read);
BinaryReader br = new BinaryReader(fs);
string s = br.ReadString();
Console.WriteLine(s);
for (uint i =0; i < 20; i++)
{
Console.Write("{0} ", br.ReadUInt32());
}
476 А. В. Фролов, Г. В Фролов. Язык С#. Самоучитель
br.Close();
fs.Close ();
Console.WriteLine("\пФайл успешно создан");
Console.ReadLine();
В самом начале своей работы программа проверяет существование файла с именем
mydata.dat, используя для этого метод Exists класса File:
if (File.Exists(testFile))
{
Console.WriteLine("Файл {0} уже существует", testFile);
Console.ReadLine();
return;
}
Класс File, предназначенный для работы с файлами и каталогами, мы рассмотрим
позже. Пока же мы только заметим, что данный фрагмент кода проверяет существова-
существование файла mydata.dat в каталоге, который был текущим при запуске нашей программы
на выполнение. Если такой файл там уже существует (например, был создан при пре-
предыдущем запуске этой программы), программа завершает свою работу с сообщением
об ошибке.
Если же в текущем каталоге нет файла с именем mydata.dat, программа создает его, вы-
вызывая для этого конструктор класса Filestream с соответствующими параметрами:
FileStream fs = new FileStream(testFile, FileMode.CreateNew);
Так как мы указали режим создания потока FileMode.CreateNew, то новый
файл будет создан в том случае, если файла с таким именем нет в текущем каталоге.
После создания потока FileStream, связанного с файлом, мы создаем поток bw
класса BinaryWriter, предназначенный для записи в файл двоичных данных:
BinaryWriter bw = new BinaryWriter(fs);
На следующем шаге программа записывает в поток текстовую строку:
bw. Write ("Text string"),-
Далее программа записывает в поток bw 20 чисел типа uint:
for(uint i = 0; i < 20; i++)
{
bw.Write((uint)i);
}
После завершения записи оба потока закрываются методом Close:
bw.Close();
fs.Close();
Глава 14. Файлы и потоки 477
На следующем этапе наша программа считывает содержимое только что записан-
записанного файла и отображает его на консоли.
Для этого она вначале открывает поток fs класса FileStream, указывая, что
файл должен быть открыт только для чтения:
fs = new FileStream(testFile, FileMode.Open, FileAccess.Read);
Режим чтения задается константой FileAccess .Read.
Далее на базе потока f s программа создает поток br класса BinaryReader:
BinaryReader br = new BinaryReader(fs);
Этот поток может быть использован для чтения из файла двоичных данных.
Первым делом программа читает и отображает на консоли текстовую строку, запи-
записанную в файл:
string s = br.ReadString();
Console.WriteLine(s);
Далее она в цикле считывает 20 чисел и также отображает их на консоли:
for (uint i = 0; i < 20; i++)
Console.Write(" {0}
br .ReadUInt32
}
Завершив работу с потоками, программа закрывает их:
br.Close () ;
fs.Close () ;
Далее она выводит на консоль сообщение об успешном создании файла и ждет, ко-
когда пользователь нажмет клавишу Enter, чтобы завершить свою работу:
Console.WriteLine("ХпФайл успешно создан");
Console.ReadLine();
Вот что появится на консоли после запуска программы:
Text string
0 1 2 3 4 5 б 7 8 9 10 11 12 13 14 15 16 17 18 19
Файл успешно создан
Содержимое файла, созданного нашей программой, показано на рис. 14.5.
Я» СЛ ОШЧцвкш ЧЛ ЧЛЛ/т
is - « » <s a - i & i v.
nri i
'as« >
00000000
00000010
00000020
00000030
000000<0
00000050
0B S4 6S 78 74 20 73 74 72 ё? 6£ ь? UG 00 P0 00
01 00 ОС 00 0Г 00 00 00 03 00 00 00 01 00 00 00
0Б 00 ОС 00 06 00 00 DO 07 00 00 DO 06 00 00 00
00 Of 00 0* 00 00 no OS СЭ GO att ЛГ 00 fin 0Э
0D 00 0C 00 OE 00 00 00 OF 0D M 00 1С 00 DO 05
00 0C 00 l; 00 DO DO I3 CS ОС 00
Рис. 14.5. Содержимое созданного двоичного файла
478
А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
Обратите внимание, что в самом начале файла находится байт со значением ОхВ, вслед
за которым идет текстовая строка в кодировке ASCII. Этот байт задает длину строки.
Вслед за строкой идут 20 чисел, каждое из которых занимает 4 байта.
Работа с текстовыми файлами
Хотя рассмотренные в предыдущем разделе потоки FileStream, BinaryWriter
и BinaryReader можно использовать для записи в файлы и чтения из файлов тек-
текстовых строк, лучше прменять специально предназначенные для этого средства. Речь
идет о потоках классов StreamWriter и StreamReader. Эти потоки чрезвычайно
просты в использовании и удобны для работы с текстовыми файлами.
Основные приемы использования потоков StreamWriter и StreamReader де-
демонстрируются в программе, исходный текст которой приведен в листинге 14.3.
Листинг 14.3. Файл ch14\TextFile\TextFileApp.cs
using System;
using System.10;
namespace TextFile
{
class TextFileApp
{
private const string testFile = "mydata.txt";
static void Main{string[] args)
{
if(File.Exists(testFile))
{
Console.WriteLine("Файл {0} уже существует", testFile);
Console.ReadLine();
return;
}
StreamWriter sw = File.CreateText(testFile);
sw.WriteLine("Каждый охотник желает знать, где сидит фазан!");
sw.WriteLine ("Число \"Пи\" равно примерно {0}.", 3.1415926);
sw.Close();
StreamReader sr = File.OpenText(testFile);
while(true)
{
String str = sr.ReadLine();
iffstr == null)
break;
Console.WriteLine(str);
}
Глава 14. Файлы и потоки 479
sr.Close();
Console.WriteLine ("Файл успешно создан");
Console.ReadLine();
Как и предыдущая программа (см. листинг 14.2), наша новая программа сразу по-
после запуска проверяет существование рабочего файла в текущем каталоге:
private const string testFile = "mydata.txt";
if(File.Exists(testFile))
{
Console.WriteLine("Файл {0} уже существует", testFile);
Console.ReadLine();
return;
}
Если файл с именем mydata.txt существует в текущем каталоге, программа завер-
завершает свою работу с сообщением об ошибке. В противном случае программа создает
текстовый файл и открывает поток для работы с ним класса StreamWriter:
StreamWriter sw = File.CreateText(testFile);
Как видите, эта операция выполняется методом CreateText, определенным
в классе File. Аналогичного эффекта можно было бы достичь и с помощью
следующего конструктора класса StreamWriter:
StreamWriter sw = new StreamWriter(testFile, false);
Первый параметр этого конструктора определяет полный путь к открываемому
файлу. Если значение второго параметра равно true, новые данные будут добавлены
к файлу, а если f alse — содержимое файла будет перезаписано.
Для открытия текстового файла на запись вы можете использовать любой из этих
конструкторов. Заметим, что в классе StreamWriter имеются и другие конструкто-
конструкторы, позволяющие, например, задать кодировку текстовых символов, записываемых
в файл.
После того как поток StreamWriter открыт, программа может записывать в него
текстовые строки, пользуясь хорошо известными вам методами Write и WriteLine:
sw.WriteLine("Каждый охотник желает знать, где сидит фазан!");
sw.WriteLine ("Число \"Пи\" равно примерно {0}.", 3.1415926);
Когда запись новых данных в поток завершена, необходимо закрыть поток мето-
методом Close:
sw.Close();
Теперь о чтении данных из текстового файла.
480 А В. Фролов. Г В Фролов. Язык С#. Самоучитель
Прежде всего программа должна открыть поток класса StreamReader, привязав
его к файлу. Это можно сделать методом File. OpenText:
StreamReader sr = File.OpenText(testFile);
Вы можете также открыть поток и привязать его к файлу с помощью конструктора
класса StreamReader:
StreamReader sr = new StreamReader(testFile);
Далее наша программа считывает текстовые строки из файла, вызывая в цикле ме-
метод ReadLine:
while(true)
{
String str = sr.ReadLine();
if(str == null)
break;
Console.WriteLine(str) ;
}
Этот метод возвращает прочитанную строку или значение null при достижении
конца файла.
После завершения работы с потоком StreamReader его следует закрыть методом
Close:
sr.Close();
Таким образом, наша программа записывает две строки в файл, а затем читает их
оттуда и отображает на консоли:
Каждый охотник желает знать, где сидит фазан!
Число "Пи" равно примерно 3,1415926.
Файл успешно создан
Выбор кодировки символов
Исторически сложилось так, что за всю историю существования компьютеров для
представления текстовых символов использовались самые разные кодировки. Когда
ваша программа создает файлы, она должна выбрать какую-то конкретную кодировку
текстовых символов.
Те из вас, кому, как и авторам этой книги, посчастливилось работать на «больших»
компьютерах серии ЕС (аналоги компьютеров IBM 360/370), знакомы с так называе-
называемой кодировкой EBCDIC. В ней представлялись латинские символы, символы кирил-
кириллицы, а также знаки пунктуации и управляющие знаки.
Что же касается современных персональных компьютеров, то для представления
текстовых символов различных алфавитов в них используются кодовые страницы
(Code Pages) и кодировка UNICODE.
Расскажем об этом подробнее.
Глава 14. Файлы и потоки 481
16 Язык С# Самоучитель
Кодовые страницы
До появления ОС Microsoft Windows 95 и Microsoft Windows широко применялась
так называемая модель кодовых страниц. В этой модели каждому символу ставилось
в соответствие число в диапазоне от 0 до 255. Таким образом, для представления одно-
одного символа использовался 1 байт.
Каким способом устанавливается соответствие?
Для ОС MS-DOS было известно по крайней мере несколько таких способов, осно-
основанных на применении различных таблиц кодировок: кодировка ASCII, основная
и альтернативная, а также некоторые другие.
Компания Microsoft стандартизовала таблицы кодировок, назвав их кодовыми
страницами и присвоив каждой странице свой номер. Первые версии MS-DOS работа-
работали с расширенным набором символов IBM, в котором отсутствовали символы кирил-
кириллицы (рис. 14.6).
GO
10
2a
30
48
58
68
88
98
&B
ьв
eB
da
e8
ro
ее
►
e
e
p
g
и
L
л
к
=
81
Э
ч
г
1
й
а
a
Ц
к
i
1
т
р
82
е
t
2
В
R
Ь
£
ff
6
1
т
г
ез
*
!!
I
3
С
S
с
a
А
&
1
^
и
64
♦
41
$
4
D
Т
d
a
К
п
^
-
£
Г
65
*
§
У.
5
Е
и
в
А
й
N
Н
+
J
06
♦
_
Я
6
F
g
t
v
&
а
a
II
h
H
Д.
87
t
'
7
G
U
9
4
u
s
Л
It
w
T
88
D
T
(
8
H
X
h
P
i
1
а
5
B9
о
4
)
9
I
V
i
ё
0
г-
il
It
J
e
8a
S
j
z
j
ё
u
1
r
11
0b
+
К
[
k
i
<j
1
1
Be
9
к
<
L
4
1
t
£
a
В
■
n
Bd
Г
•
-
=
n
1
n
i
¥
i
Л
1
1
2
ве
Л
A
>
N
n
й
R
j
ft
1
e
■
Of
*
T
/
7
0
□
ft
f
■
n
Pi/c. 74.6. Расширенный набор символов IBM
Для того чтобы исправить положение, в свое время было разработано немало ути-
утилит, предназначенных для загрузки символов кириллицы в память видеоадаптера,
а также для переключения раскладок клавиатуры.
В ОС MS-DOS версии 4.01 впервые появилась стандартная кодовая страница с но-
номером 866, содержащая символы кириллицы (рис. 14.7). С тех пор российские пользо-
пользователи компьютеров не могут пожаловаться на то, что компания Microsoft обходит
их своим вниманием, создавая новые ОС и прикладные программы.
482
А В. Фролов, Г. В Фролов. ЯзыкС#. Самоучитель
ее
ie
20
38
40
se
78
88
90
«0
ье
ев
вв
fa
вв
►
в
в
р
я
V
л
Ж
1
01
0
4
1
1
ft
а
Б
с
е
1
т
с
в
02
в
t
2
В
R
В
I
в
1
т
т
е
еэ
*
!!
tt
3
с
S
г
a
г
1
i
У
е
84
♦
$
4
D
I
Я
*
-
05
§
у.
5
Е
и
Е
X
+
X
96
t
*
6
Г
и
X
Ц
1
У
87
1
7
G
U
В
ч
,
1
1
ев
D
8
н
к
и
1
+
еэ
9
i
V
у
й
41
и
j
Ba
!
j
z
z
К
П
г
еь
к
<
л
1
V
1
Be
s
<
L
ч
n
11
В
в
Bd
Г
п
]
>
н
3
л
1
D
ве
Л
>
N
0
■
J
1
■
Bf
?
а
д
П
Я
!
■
Рис. 74.7. Набор символов кодовой страницы 866
Заметим, что для представления символов кириллицы в ОС ШМ OS/2 применяется
кодировка, соответствующая кодовой странице 866. Пользователи ОС Unix и Linux
отдают предпочтение кодировке KOI-8, а пользователи компьютеров Apple Macin-
Macintosh— своей собственной кодировке, несовместимой ни с одной из перечисленных
выше. В Интернете есть немало утилит, предназначенных для перекодирования тек-
текстовых файлов, подготовленных в самых разных кодировках.
Когда появилась ОС Microsoft Windows 3.1, в ней также были предусмотрены ко-
кодовые страницы. К сожалению, кодовые страницы ANSI Microsoft Windows 3.1 были
лишены символов кириллицы (рис. 14.8).
ее
18
20
зе
40
so
те
вв
90
«8
ье
С0
da
ее
1
1
в
в
р
р
1
1
■
0
i
ъ
01
1
1
t
1
А
а
ч
'
1
1
1
к
Я
&
п
02
|
1
'*
2
В
R
г
'
1
(
t
S
6
a
6
83
1
1
«
3
С
S
я
|
1
I
t
К
6
г
6
84
1
1
$
D
I
t
|
1
t
'
i.
0
a
3
05
1
1
X
5
E
U
u
1
1
к
И
A
б
i
г
06
1
1
a
6
т
и
V
1
1
¥
If
б
s
a
07
I
i
7
G
W
u
1
1
I
к
V
»
88
1
1
С
8
H
X
X
1
1
-
t
0
i
■
В9
1
1
)
9
I
V
у
1
1
О
f
0
£
Be
1
1
»
J
z
J
z
1
1
1
1
P
0
t
6
8b
1
1
♦
;
к
с
<
I
I
«
»
p
0
s
а
Be
1
1
<
L
S
|
|
-.
y.
1
и
1
u
Bd
1
1
г
n
i
>
I
I
X
1
f
</
Ob
1
1
>
N
-
1
1
a
я
i
t>
i
\>
Bf
I
I
/
?
а
-
л
1
1
i
1
В
i
У
Рис. 14.8. Кодовая страница ANSI без символов кириллицы
Для ОС Microsoft Windows 3.1 было создано несколько популярных программ русифи-
русификации, устанавливающих шрифты с символами кириллицы и переключатели клавиатур-
клавиатурных раскладок. При этом применялась кодировка ANSI, показанная на рис. 14.9.
Глава 14. Файлы и потоки
483
00
10
20
38
40
50
60
70
80
90
аО
ЬО
сО
ао
ев
00
1
1
0
0
р
р
1
1
•
А
Р
я
Р
01
1
1
!
1
й
Q
а
Ч
'
1
Ё
±
Б
С
б
02
1
1
Z
В
R
Ь
г
*
1
е
>
в
т
в
03
1
1
*
3
с
S
S
1
1
£
•
Г
У
г
У
04
1
1
$
4
D
Т
t
1
1
я
'
д
ф
А
Ф
OS
1
1
X
S
Е
и
и
1
1
V
И
Е
X
с
06
1
1
в
Б
F
V
1
|
1
1
Ж
ц
ж
07
1
1
7
G
и
|
1
1
-
3
ч
3
08
1
1
(
8
Н
X
h
1
1
-
•
И
UI
н
89
1
1
)
9
1
V
i
1
1
о
1
й
щ
й
ва
1
1
к
J
г
J
1
1
1
■
к
ъ
к
оь
1
1
4
:
к
[
к
■1
1
1
«
■ш
л
ы
л
Ос
1
1
<
L
S
1
1
1
1
-
к
и
ь
м
3d
1
1
-
=
п
]
m
>
1
1
К
н
э
н
Ов
1
1
>
N
п
1
1
в
X
0
ю
0
Of
1
1
/
?
0
о
л
1
1
i
П
Я
п
Рис. 14.9. Кодовая страница ANSI с символами кириллицы
Программы MS-DOS, запущенные под управлением ОС Microsoft Windows,
пользовались кодовыми страницами DOS (рис. 14.6 и 14.7), которые получили на-
название страниц оригинальных производителей оборудования — OEM (Original
Equipment Manufacturer).
С появлением русской версии ОС Microsoft Windows 3.1 эта кодировка стала стан-
стандартной. Соответствующая кодовая страница получила номер 1251.
К этому моменту было разработано огромное количество шрифтов True Type
с символами кириллицы, коды которых находились в диапазоне от СО до FF.
Именно эти шрифты и стали популярны, распространяясь в основном пиратскими
способами вместе с программами русификации Microsoft Windows 3.1 или по от-
отдельности.
Недостатки модели кодовых страниц
Хотя на первый взгляд может показаться, что 1 байта вполне достаточно для представ-
представления всех символов, это не всегда так.
Представьте себе, что вам необходимо подготовить документ, содержащий помимо
латинских символов и символов кириллицы, например, греческие и немецкие симво-
символы, а также дополнительные знаки. Очевидно, что, оставаясь в рамках кодовой стра-
страницы 1251 или какой-либо другой, такую задачу решить невозможно. Чтобы как-то
выйти из данной ситуации, приходится дополнительно применять специальные шриф-
шрифты, что усложняет процесс оформления документа.
Заметим также, что в некоторых алфавитах слишком много символов, чтобы их
коды можно было представить 1 байтом. В азиатских версиях Microsoft Windows
применяются многобайтовые наборы символов, где для представления одного
символа используется несколько байтов. Например, в японской, корейской и ки-
китайской версиях Microsoft Windows вы можете столкнуться с 2-байтовыми симво-
символами (набор символов double-byte character set, или DBCS), которые не следует пу-
путать с символами UNICODE.
484
А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Стандарт UNICODE
Стандарт UNICODE был предложен некоммерческой организацией UNICODE Consor-
Consortium, образованной в 1991 г. В этом стандарте для представления каждого символа ис-
используется 16 бит, т. е. 2 байта. С помощью UNICODE можно закодировать очень
большое количество символов из разных алфавитов, благодаря чему отпадает необхо-
необходимость применять кодовые страницы. В документах UNICODE могут встречаться
одновременно латинские и греческие символы, символы кириллицы и технические
символы.
Значения кодов UNICODE разделены на несколько областей.
Область с кодами от 0000 до 007F применяется таким же образом, как и первая по-
половина расширенного набора символов IBM, показанная на рис. 14.6. Далее идут об-
области, в которых расположены греческие символы, символы кириллицы, символы
пунктуации и технические символы, а также зарезервированные области. Для симво-
символов кириллицы, например, выделены коды в диапазоне от 0400 до 0451.
Unicode в Microsoft Windows NT/2000/XP
Ядро Microsoft Windows NT/2000/XP, графический интерфейс GDI этих ОС и файло-
файловая система NTFS реализованы с использованием UNICODE. Тем не менее приложе-
приложения, запущенные в среде Microsoft Windows NT/2000/XP, могут работать и с одной ко-
кодовой страницей ANSI. Она выбирается автоматически в зависимости от того, какая
страна была выбрана пользователем при установке ОС.
Приложения, применяющие кодовую страницу ANSI, перед вызовом некоторых
функций программного интерфейса Microsoft Windows NT выполняют преобразование
кодировки в UNICODE с учетом номера кодовой страницы ANSI. Для того чтобы та-
такое преобразование выполнялось без ошибок, пользователь должен правильно указать
страну в приложении Regional Settings, пиктограмма которого находится в папке Con-
Control Panel. Это также необходимо и для правильной работы программ MS-DOS, запу-
запущенных под управлением Microsoft Windows NT/2000/XP и использующих кодовую
страницу OEM.
Очень важно, что для работы с документами UNICODE требуются шрифты
UNICODE. Один файл такого шрифта содержит несколько наборов символов, соответ-
соответствующих разным областям кодов UNICODE. Разработчик шрифта может разместить
в файле не все, а только некоторые наборы символов.
UNICODE в Microsoft Windows 95
В отличие от Microsoft Windows NT/2000/XP, ядро и графический интерфейс Microsoft
Windows 95 не используют UNICODE, а работают с кодовыми страницами. Тем не ме-
менее в Microsoft Windows 95 предусмотрена возможность динамического переключения
наборов символов, а также клавиатурных раскладок. Это позволяет создавать доку-
документы, содержащие одновременно символы из разных наборов.
В составе Microsoft Windows 95 поставляется набор шрифтов UNICODE. Вы также
можете установить в этой ОС и старые кириллические шрифты, разработанные для
Microsoft Windows 3.1.
Глава 14. Файлы и потоки 485
Кодировка текстовых потоков
После такого краткого введения в кодировки символов мы рассмотрим способы выбо-
выбора кодировки для текстовых потоков, создаваемых в программах С#.
Кодировка символов задается в конструкторе, создающем выходной поток данных.
Например, при создании потока текстовых строк StreamWriter мы можем указать
в третьем параметре конструктора нужную нам кодировку:
StreamWriter sw = new StreamWriter(testFile, false,
System.Text.Encoding.UNICODE);
Кодировка указывается как статическое свойство класса System. Text .En-
.Encoding. Возможные варианты кодировки с именами соответствующих статических
свойств перечислены в табл. 14.5.
Таблица 14.5. Кодировки текстовых потоков
Кодировка
ASCII
UNICODE
UTF7
UTF8
Default
Описание
Кодировка ASCII без символов кириллицы, в которой для пред-
представления текстовых символов используются младшие 7 бит байта
Кодировка UNICODE. Для представления символов используется
16 бит (т. е. 2 байта)
Кодировка UCS Transformation Format. Применяется для представ-
представления символов UNICODE. В ней используются младшие 7 бит
данных
То же, но для представления символов UNICODE в ней использует-
используется 8 бит данных
Системная кодировка ANSI (не путайте ее с кодировкой ASCII).
В этой кодировке для представления каждого символа используется
8 бит данных
Заметим, что кодировка символов может быть задана только при создании потока.
Если поток уже создан, программа может определить текущую кодировку симво-
символов, воспользовавшись для этого свойством Encoding:
Console.WriteLine("Кодировка файла: {0}", sw.Encoding);
Это свойство доступно только для чтения.
В листинге 14.4 мы привели пример программы, создающей текстовый файл в ко-
кодировке UNICODE.
Листинг 14.4. Файл ch14\Encoding\EncodingApp.cs
using System;
using System.10;
namespace Encoding
486 А В Фролов, Г В Фролов Язык С#. Самоучитель
class EncodingApp
{
private const string testFile = "mydata.txt";
static void Main(string[] args)
{
if(File.Exists(testFile))
{
Console.WriteLine("Файл {0} уже существует", testFile);
Console.ReadLine();
return;
}
StreamWriter sw = new StreamWriter(testFile, false.
System.Text.Encoding.UNICODE);
Console.WriteLine("Кодировка файла: {0}", sw.Encoding);
sw.WriteLine("Каждый охотник желает знать, где сидит фазан!");
sw.WriteLine ("Число \"Пи\" равно примерно {0}.", 3.1415926)
sw.Close() ;
StreamReader sr = File.OpenText(testFile);
while(true)
{
String str = sr.ReadLine();
if(str == null)
break;
Console.WriteLine(str) ;
}
sr.Close();
Console.WriteLine ("Файл успешно создан");
Console.ReadLine();
От программы, исходный текст которой был приведен ранее в листинге 14.3,
она отличается только наличием двух строк, приведенных ниже:
StreamWriter sw = new StreamWriter(testFile, false,
System.Text.Encoding.UNICODE);
Console.WriteLine("Кодировка файла: {0}", sw.Encoding);
Глава 14. Файлы и потоки 487
Первая из этих строк задает кодировку символов UNICODE, а вторая -
ст кодировку символов на консоли:
Кодировка файла: System.Text.UNICODEEncoding
Каждый охотник желает знать, где сидит фазан!
Число "Пи" равно примерно 3,1415926.
Файл успешно создан
На рис. 14.10 мы показали содержимое созданного файла в виде дампа.
отобража-
Н Ней Workshop -
В Fie Ed»
шла
N - «
30000000
00000010
00000020
00000030
00000040
00000050
00О0О0БО
00000070
00000080
D0000090
00О0О0АО
Disk
a
»
FF
IF
36
3D
35
44
77
38
20
ЗЕ
39
В nydata-M |
Reedy
[mydiita.lxtl
Options
<s
FF
П4
04
П4
04
04
П4
04
00
04
00
1A
45
35
30
20
30
ЗЯ
*
3F
20
32
Tools
m
П4
A4
П4
nn
П4
П4
no
04
00
00
1
If
3B
4*>
41
37
41
?n
4f
33
36
~
Window:
*!
04
П4
04
04
04
04
04
00
04
00
00
V-
Jb
i?
30
4C
38
30
3B
40
38
2C
2E
■
Help
—
'ica
*
04
ПЛ
П4
П4
04
04
04
П4
04
00
00
-
34
3D
35
?C
34
3D
3E
30
3C
31
0D
pate
-
A4
П4
04
on
04
04
04
04
04
00
uu
4H
in
42
?n
33
21
70
32
35
34
UA
—~~77
liHTi
n<
П4
04
on
04
00
00
04
04
00
00
39
70
31
4?
OD
7?
3D
40
31
(Offset: 00000000
><*
04
П4
on
П4
04
00
no
04
04
00
-I-
1
20
?n
37
14
7П
0Л
IF
3K
3D
35
hie:-
~^
F
!*
nn
nn
П4
П4
nn
00
П4
04
04
00
2S7
■
•
s
D
R
9
I'lfBB
r.#
с
F >
S
0 F
A
0.7
Я А
?.«
.3
2.6
I1
e
R
0
T,
8
.0
9
В
■-
J
1
4
_
5
4
■
>
0
1
|l
1
К
R
В
я
1
7
5
4
п
■ ,
13
ч
в
.1
byt.
-Inlxl
7.
4
j
■
5.
Рис. 14.10. Созданный файл в кодировке UNICODE
Обращаем ваше внимание, что в самом начале файла находятся байты OxFF и OxFE.
Это сигнатура, предназначенная для определения порядка следования байтов в 16-
разрядных символах UNICODE.
Для чего она нужна?
Стандарт UNICODE предназначен для работы на различных компьютерных плат-
платформах. Однако разные процессоры применяют различный порядок следования байтов
в слове: наиболее значимый байт может быть расположен как по старшему, так
и по младшему адресу.
Для правильной обработки текстового файла UNICODE на любой платформе прило-
приложение должно знать порядок следования байтов, использованный в этом файле. Для обо-
обозначения такого порядка и применяется 2-байтовая сигнатура. Ее значение может быть
равно OxFEFF для прямого порядка следования байтов или OxFFEF для обратного.
Кодировка текстовых строк в двоичных потоках
Как мы уже говорили, двоичные потоки создаются с помощью классов FileStream,
BinaryWriter и BinaryReader. С помощью метода Write программа может запи-
записывать в двоичный поток не только числа, но и строки класса string. Соответствующий
пример программы был приведен в разделе «Программа Binary» (см. листинг 14.2).
488
А В Фролов. Г. В. Фролов. Язык С#. Самоучитель
Напомним, что при записи строк string в двоичный поток они снабжаются пре-
префиксом длины, вслед за которым идут байты символов. По умолчанию для представ-
представления символов применяется кодировка ANSI, однако при необходимости программа
может ее изменить.
При создании выходного потока класса BinaryWriter мы можем указать конст-
конструктору необходимую кодировку текстовых символов:
FileStream fs = new FileStream(testFile, FileMode.CreateNew);
BinaryWriter bw = new BinaryWriter(fs, System.Text.Encoding.UNICODE) ;
В данном случае мы указали кодировку UNICODE.
После этого запись строк в поток выполняется как обычно:
bw.Write("Text string");
Для того чтобы правильно прочитать текстовые строки из двоичного потока, при
создании входного потока BinaryReader мы тоже должны указать кодировку:
fs = new FileStream(testFile, FileMode.Open, FileAccess.Read);
BinaryReader br = new BinaryReader(fs, System.Text.Encoding.UNICODE);
После этого можно будет прочитать строку из входного двоичного потока методом
ReadString:
string s = br.ReadString();
Эта техника демонстрируется в программе, исходный текст которой приведен
в листинге 14.5.
Листинг 14.5. Файл ch14\BinaryEncoding\BinaryEncodingApp.cs
using System;
using System.10;
namespace BinaryEncoding
{
class BinaryEncodingApp
{
private const string testFile = "mydata.dat";
static void Main(string[] args)
{
if(File.Exists(testFile))
{
Console.WriteLine("Файл {0} уже существует", testFile);
Console.ReadLine();
return;
)
FileStream fs = new FileStream(testFile, FileMode.CreateNew);
BinaryWriter bw = new BinaryWriter(fs.
System.Text.Encoding.UNICODE);
Глава 14. Файлы и потоки 489
bw.Write("Text string");
for(uint i = 0; i < 2 0; i
{
bw.Write((uint)i);
bw.Close();
fs.Closed ;
fs = new FileStream(testFile, FileMode.Open, FileAccess.Read)
BinaryReader br = new BinaryReader(fs,
System.Text.Encoding.UNICODE);
string s = br.ReadString();
Console.WriteLine(s) ;
for (uint i = 0; i < 20; i++)
{
Console.Write("{0} ", br.ReadUInt32());
br.Close();
fs.Close!) ,-
Console.WriteLine("\пФайл успешно создан");
Console.ReadLine();
Как выглядит текстовая строка UNICODE в созданном нами двоичном файле?
Взгляните на рис. 14.11, где мы показали дамп этого файла.
Н Hex Workshop - [nmiata.clat
Fte E* Cfcfc. Options Took Window He*>
M.lblxl
& *s-
/■ х
У
*B
00000000
ooooooio
00000020
00000030
00000040
00000050
00000060
16
00
00
00
00
00
00
54
69
00
00
00
00
00
00
00
oo
00
00
00
00
65
6E
03
07
0B
OF
13
00
00
00
00
00
00
00
78
67
00
00
00
00
00
00
00
00
00
00
00
00
74
00
04
00
ОС
10
CO
00
00
00
00
00
20
00
00
00
00
oo
00
00
00
00
00
00
73
01
05
09
0D
11
00
00
00
00
00
00
74
00
00
00
00
00
00
00
00
00
00
00
72
02
06
0A
OE
12
_T.e.x.t. s.t.r
.i.n.g
mydatadat
R«dy
Offset; 00000000 Value: 21526
1103 byt«
490
Рис. 14.11. Текст в двоичном потоке имеет кодировку UNICODE
А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Обратите внимание, что первый байт файла содержит длину текстовой строки.
Вслед за этим байтом идут 2-байтовые символы UNICODE.
Если при чтении такого файла средствами класса BinaryReader вы забудете ука-
указать кодировку символов, результат может быть неожиданным:
Text string
О 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Файл успешно создан
В самом деле, так как система не знает заранее, в какой кодировке находятся сим-
символы, она будет использовать кодировку по умолчанию, т. е. ANSI. В результате вме-
вместо одного символа текстовой строки мы увидим символ и пробел.
Если же кодировка указана правильно, программа выведет на консоль строку без
ошибок в исходном виде:
Text string
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Файл успешно создан
Буферизация потоков
Для ускорения операций обмена данными с диском часто применяется механизм бу-
буферизации. Он основан на том, что прежде чем попасть на диск, данные предвари-
предварительно накапливаются в буфере, расположенном в оперативной памяти. Затем, когда
к диску нет обращений, данные из буфера переписываются на диск.
Ускорение достигается за счет того, что фактически программа пишет данные не на
диск, а в оперативную память, обладающую высоким быстродействием. Во время про-
простоев компьютера, когда он не занят другой работой, данные переписываются на диск.
Разумеется, буферизация дает ускорение работы программ не всегда, а только в тех
случаях, когда программа периодически обращается к одним и тем же фрагментам
файла или выполняет последовательное чтение или запись файла.
Буферизация двоичных потоков
Напомним, что двоичные потоки создаются при помощи классов FileStream,
BinaryWriter и BinaryReader. Потоки FileStream по умолчанию являются
буферизованными. Для задания размера буфера мы должны использовать соответст-
соответствующий перегруженный конструктор:
FileStream fs = new FileStream(testFile, FileMode.CreateNew,
FileAccess.Write, FileShare.Read, 10000);
Через первый параметр этому конструктору передается путь к файлу, с которым
связывается поток. Второй параметр задает режим открытия файла. Третий и четвер-
четвертый параметр определяют соответственно режим доступа к файлу и режим совместно-
совместного использования файла.
Что же касается размера буфера, то он задастся последним, пятым параметром
и указывается в байтах.
Глава 14. Файлы и потоки 491
Создав таким способом поток класса FileStream, необходимо образовать на его
основе поток BinaryWriter:
BinaryWriLer bw = new BinaryWriter(fs);
Далее этот поток можно использовать для записи как обычно, добавляя в него дан-
данные методом Write.
Если вам нужен выходной буферизованный двоичный поток, то он создается так:
fs = new FileStream(testFile, FileMode.Open, FileAccess.Read,
FileShare.Read, 10000);
В дальнейшем на базе этого потока можно создать входной поток класса Binary-
Reader:
BinaryReader br = new BinaryReader(fs);
Работа с буферизованными двоичными потоками данных демонстрируется в про-
программе, исходный текст которой приведен в листинге 14.6. Мы оставляем ее вам
на самостоятельное изучение.
Листинг 14.6. Файл ch14\BinaryBuffered\BinaryBufferedApp.cs
using System;
using System.10;
namespace BinaryBuffered
{
class BinaryBufferedApp
{
private const string testFile = "mydata.dat";
static void Main(string[] args)
{
if(File.Exists(testFile))
{
Console.WriteLine("Файл {0} уже существует", testFile);
Console.ReadLine ();
return;
}
FileStream fs = new FileStream(testFile, FileMode.CreateNew,
FileAccess.Write, FileShare.Read, 10000);
BinaryWriter bw = new BinaryWriter(fs);
bw.Write("Text string");
for(uint i = 0; i < 20; i++)
{
bw.Write((uint)i) ;
]
492 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
bw.Close();
fs.Closed ;
fs = new FileStream{testFile, FileMode.Open, FileAccess.Read,
FileShare.Read, 10000);
BinaryReader br = new BinaryReader(fs);
string s = br.ReadString();
Console.WriteLine(s);
for (uint i = 0; i < 20; i++)
{
Console.Write("{0} ", br.ReadUInt32());
br.Close();
fs.Closed ;
Console.WriteLine("\пФайл успешно создан")
Console.ReadLine();
Буферизация текстовых потоков
Потоки, связанные с текстовыми файлами, также можно буферизовать. Для этого
нужно воспользоваться соответствующими перегруженными конструкторами в клас-
классах StreamWriter и StreamReader.
Вот как создается выходной буферизованный текстовый поток:
StreamWriter sw = new StreamWriter(testFile, false,
System.Text.Encoding.UNICODE, 10000);
Первые З параметра конструктора класса StreamWriter задают соответственно
путь к файлу, флаг добавления новых данных в файл и кодировку данных. Размер бу-
буфера определяется в байтах при помощи четвертого параметра.
Входной буферизованный поток данных создается при помощи следующего конст-
конструктора класса StrearnReader:
StreamReader sr = new StreamReader(testFile,
System.Text.Encoding.UNICODE, true, 10000);
Размер буфера задается последним, четвертым параметром конструктора.
Обратите внимание на третий параметр конструктора класса StreamReader. Ес-
Если этот параметр равен true, при открытии файла его тип будет автоматически опре-
определяться по сигнатуре, т. е. по начальным байтам файла. Напомним, что текстовые
Глава 14. Файлы и потоки 493
файлы в кодировке UNICODE содержат 2-байтовую сигнатуру, определяющую поря-
порядок следования остальных байтов файла. Об этом мы рассказывали выше в разделе
«Кодировка текстовых потоков».
Полный пример программы, работающей с буферизованными текстовыми потока-
потоками, приведен в листинге 14.7.
Листинг 14.7. Файл ch14\TextFileBuffered\TextFileBuffereclApp.cs
using System;
using System. 10,-
namespace TextFileBuffered
{
class TextFileBufferedApp
{
private const string testFile = "mydata.txt";
static void Main(string[] args)
{
if(File.Exists(testFile))
{
Console.WriteLine("Файл {0} уже существует", testFile);
Console.ReadLine();
return;
)
StreamWriter sw = new StreamWriter(testFile, false,
System.Text.Encoding.UNICODE, 10000);
sw.WriteLine("Ka>iyibii5 охотник желает знать, где сидит фазан!");
sw.WriteLine ("Число \"Пи\" равно примерно {0}.", 3.1415926);
sw.Close();
StreamReader sr = new StreamReader(testFile,
System.Text.Encoding.UNICODE, true, 10000);
while(true)
{
String str = sr.ReadLine();
if(str == null)
break;
Console.WriteLine(str);
}
sr.Close();
Console.WriteLine ("Файл успешно создан");
Console.ReadLine();
494 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Принудительный сброс буферов
Еще один важный момент связан с буферизованными потоками. Как мы уже говорили,
буферизация ускоряет работу приложений с потоками, так как при ее использовании
сокращается количество обращений к системе ввода-вывода. Вы можете постепенно,
в течение дня, добавлять в поток данные по 1 байту, и только к вечеру эти данные бу-
будут физически записаны в файл на диске.
Во многих случаях, однако, приложение должно, не отказываясь совсем от буфери-
буферизации, выполнять принудительную запись буферов в файл. Это можно сделать с по-
помощью метода Flush.
Данный метод определен во всех классах, имеющих отношение к буферизованным
потокам. Вот как он используется с буферизованными текстовыми потоками данных:
StreamWriter sw = new StreamWriter(testFile, false,
System.Text.Encoding.UNICODE, 10000);
sw.WriteLine ("Каждый охотник желает знать, где сидит фазан!");
sw.WriteLine ("Число \"Пи\" равно примерно {0}.", 3.1415926);
sw.Flush();
sw.Close();
В этом фрагменте кода мы создали выходной буферизованный поток данных, запи-
записали в него две строки методом WriteLine, а затем сбросили буферы методом
Flush. Закончив работу с потоком, мы его закрыли методом Close.
Потоки в оперативной памяти
ОС Microsoft Windows 95 и Microsoft Windows NT/2000/XP предоставляют на уровне
своего программного интерфейса Win32 возможность работать с оперативной памя-
памятью как с файлом. Это очень удобно во многих случаях, в частности, файлы, отобра-
отображаемые на память, можно использовать для передачи данных между одновременно
работающими задачами и процессами. Подробнее об этом вы можете прочитать в 27-м
томе «Библиотеки системного программиста», который называется «Операционная
система Microsoft Windows NT для программиста. Часть 2» [12].
При создании программ С# вы также можете работать с объектами оперативной
памяти как с файлами, а точнее говоря, как с потоками.
Ранее мы отмечали, что в библиотеке классов Microsoft .NET Framework есть класс
MemoryStream, специально предназначенный для создания потоков в оперативной
памяти. Рассмотрим основные приемы его использования.
Создание потока
Для создания потоков в оперативной памяти в классе MemoryStreara предусмотрено
несколько конструкторов.
Простейший конструктор не имеет параметров и позволяет создать поток с буфе-
буфером, имеющим нулевой начальный размер:
MemoryStream ras = new MemoryStreara();
Глава 14 Файлы и потоки 495
По мерс добавления данных в этот поток размер буфера автоматически увеличивается.
Вы можете создать поток Memory-Stream на базе массива байтов, как это показа-
показано ниже:
bytet] data = new byte []
{
0, 1, 2, 3, 4, 5, б, 7, 8, 9
};
MemoryStream ms = new MemoryStream(data);
Созданный таким способом поток будет содержать данные массива. Заметим, что
размер такого потока не может быть увеличен (однако при необходимости его можно
уменьшить методом SetLength).
Предусмотрен также вариант конструктора, позволяющий задать начальный размер
буфера потока, который будет увеличиваться при необходимости по мере добавления
в поток новых данных:
MemoryStream ms = new MemoryStreamA000) ;
Здесь мы создали поток с начальным размером буфера, равным 1000 байт.
При необходимости можно создать поток, доступный только для чтения. Это мож-
можно сделать с помощью следующего конструктора:
byte[] data = new byte []
{
0, 1, 2, 3, 4, 5, б, 7, 8, 9
};
MemoryStream msReadOnly = new MemoryStream(data, false);
В данном случае поток msReadOnly будет доступен только для чтения.
При необходимости можно создать поток не из всего массива, а только из его час-
части. Для этого нужно указать конструктору начальный индекс и размер блока данных,
из которого будет образован массив:
byte[] data = new byte []
{
0, 1, 2, 3, 4, 5, б, 7, 8, 9
};
MemoryStream ms = new MemoryStream(data, 1, 5);
MemoryStream msReadOnly = new MemoryStream(data, 1, 5, false);
Здесь на базе части массива data мы создаем два потока— ms и msReadOnly.
Первый из них доступен для чтения и записи, а второй — только для чтения.
Чтение данных
Если вы создали поток MemoryStream на базе проинициализированного массива
байтов, то можете читать из него данные методами Read и ReadByte.
Первый из них читает блок данных, заданных смещением от начала потока и размером,
возвращая прочитанные данные в виде массива. Текущая позиция продвигается вперед на
количество байтов, равное размеру прочитанного блока. Второй метод (ReadByte) чита-
читает из потока 1 байт, продвигая текущую позицию вперед на 1 байт.
496 А. В. Фролов, Г В. Фролов. Язык С# Самоучитель
Техника чтения данных из потока MemoryStream демонстрируется в программе,
исходный текст которой приведен в листинге 14.8.
Листинг 14.8. Файл ch14\MemoryStreamDemo\MemoryStreamDemoApp.cs
using System;
using System.IO;
namespace MemoryStreamDemo
{
class MeraoryStreamDemoApp
{
static void Main(string[] args)
{
byte[] data = new byte []
{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
>;
byte[] dataCopy = new byte [10];
MemoryStream ms = new MemoryStream(data);
ms.Read(dataCopy, 0, 9);
foreach (byte bt in dataCopy)
Console.Write(bt);
ms.Close();
Console.ReadLine();
Массив data, на базе которого будет создаваться поток MemoryStream, проини-
циализирован целыми числами от 0 до 9, как это показано ниже:
byte[] data = new byte []
{
0, 1, 2, 3, 4, 5, б, 7, 8, 9
);
Кроме того, в программе определен неинициализированный массив байтов data-
Copy размером в 10 ячеек:
byte[] dataCopy = new byte [10];
Вначале наша программа создает поток ms на базе массива data:
MemoryStream ms = new MemoryStream(data);
Глава 14. Файлы и потоки 497
Далее она читает содержимое всего потока в массив dataCopy:
ms.Read(dataCopy, 0, 9);
Вслед за этим содержимое массива dataCopy, проинициализированного содер-
содержимым потока, отображается на консоли:
foreach (byte bt in dataCopy)
Console.Write(bt);
Завершив работу с потоком ms, программа закрывает его методом Close:
ms.Close () ;
Запись данных
Для записи данных в поток MemoryStream вы можете использовать методы Write и
WriteByte. Первый из этих методов записывает в поток 1 байт данных, а второй —
массив байтов или часть этого массива.
В листинге 14.9 мы привели пример программы, которая сначала записывает дан-
данные в поток MemoryStream по байтам, а затем читает данные из этого потока и ото-
отображает их на консоли.
Листинг 14.9. Файл ch14\MemoryStreamDemo1\MemoryStreamDemo1App.cs
using System;
using System.IO;
namespace MemoryStreamDemol
{
class MemoryStreamDemolApp
{
static void Main(string[] args)
{
MemoryStream ms = new MemoryStream();
for(byte i = 0; i < 10; i++)
{
ms.WriteByte(i);
ms .Flush();
ms.Seek@, SeekOrigin.Begin);
while(true)
{
int data = ms .ReadByte();
if(data == -1)
break;
Console.Write("{0} ", data);
498 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
ms.Close();
Console.ReadLine{)
Первым делом программа создает поток с помощью простейшего конструктора без
параметров:
MemoryStream ms = new MemoryStream();
Как мы уже говорили, для хранения данных такого потока в оперативной памяти
создается буфер нулевой длины. Размер этого буфера будет автоматически увеличи-
увеличиваться по мере записи в поток новых данных.
Далее наша программа в цикле записывает в поток 10 чисел, пользуясь для этого
методом WriteByte:
for(byte i = 0; i < 10; i++)
{
ms.WriteByte(i);
}
Далее, для того чтобы из потока можно было читать, нам необходимо сбросить бу-
буфер методом Flash, а также переместить текущую позицию в начало потока:
ms.Flush();
ms.Seek@, SeekOrigin.Begin);
Подробнее о позиционировании внутри потока мы расскажем позже, а сейчас заме-
заметим только, что методу Seek передаются два параметра. Первый параметр указывает
смещение в байтах, а второй — место в потоке, от которого отсчитывается это смеще-
смещение. Константа SeekOrigin. Begin задает смещение относительно начала потока.
Заметим, что, если бы мы просто закрыли поток, его данные были бы для нас поте-
потеряны, и мы не смогли бы их прочитать. Однако после сброса буфера и установки те-
текущей позиции на начало потока данные доступны для чтения.
Наша программа считывает их побайтно, вызывая для этого метод ReadBy te:
while(true)
{
int data = ms.ReadByte();
if(data == -1)
break;
Console.Write("{0} ", data);
>
Этот метод возвращает значение считанного байта, преобразованное в тип int,
или значение -1, если был достигнут конец потока.
После отображения всех данных, хранящихся в потоке, мы закрываем поток мето-
методом Close:
ms.Close();
Глава 14. Файлы и потоки 499
Доступ к буферу потока MemoryStream
Как вы уже знаете, поток MemoryStream может быть сформирован на базе предвари-
предварительно проинициализированного массива байтов или путем последовательной записи
в него данных методами Write и WriteByte. При этом данные фактически будут
храниться во внутреннем буфере потока.
С помощью метода GetBuf fer вы можете получить доступ к буферу потока. Этот
метод возвращает ссылку на массив байтов, представляющий собой текущее содержи-
содержимое буфера.
Метод ТоАггау позволяет преобразовать поток (даже закрытый методом Close)
в массив байтов.
Программа, исходный текст которой представлен в листинге 14.10, демонстрирует
использование упомянутых выше методов.
Листинг 14.10. Файл ch14\GetBufferApp\GetBufferApp.cs
using System;
using System.10;
namespace GetBuffer
{
class GetBufferApp
{
static void Main(string[] args)
{
MemoryStream ms = new MemoryStream();
for(byte i = 0; i < 10; i++)
{
ms . WriteByte (i) ;
}
ms.Flush();
byte[] buf = ms.GetBuffer();
foreach (byte bt in buf)
Console.Write(bt) ;
Console.WriteLine();
ms .Close ();
byte[] bufl = ms.ToArray();
foreach (byte bt in bufl)
Console.Write(bt);
Console.ReadLine();
500 А. В. Фролов. Г. В. Фролов. Язык С#. Самоучитель
Получив управление, программа создаст поток в памяти и записывает в него 10 це-
целых чисел. После этого происходит сброс буфера:
MemoryStream ms = new MemoryStream();
for(byte i = 0; i < 10; i++)
{
ms.Wr i teByte(i);
}
ms .Flush{);
Далее наша программа получает доступ к буферу открытого потока, вызывая метод
GetBuf f er:
byte[] buf = ms.GetBuffer();
В результате в массив buf будет записано содержимое текущего буфера потока.
Программа отображает его на консоли с помощью простого цикла:
foreach (byte bt in buf)
Console.Write(bt);
Заметим, что, хотя мы записали в поток всего 10 байт, на консоль будут выведены
не только записанные числа, но и дополнительные нули. Это получается потому,
что по умолчанию размер буфера превышает 10 байт. Метод GetBuf fer возвращает
нам полное содержимое буфера потока, изначально проинициализированного нулевы-
нулевыми значениями.
После вывода содержимого буфера потока на консоль наша программа закрывает
поток:
ms.Close();
Несмотря на то что поток закрыт, хранящиеся в нем данные все еще могут быть
доступны. С помощью метода ТоАггау программа может преобразовать закрытый
поток в массив байтов, как это показано ниже:
byteU bufl = ms . ТоАггау ();
Далее содержимое полученного таким способом массива отображается на консоли:
foreach (byte bt in bufl)
Console.Write(bt) ;
Обратите внимание, что во второй раз будет выведено только 10 чисел:
012345678900000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000
0123456789
Дело в том, что метод ТоАггау преобразует только тс данные, которые были за-
записаны в поток, а не весь буфер потока.
Глава 14. Файлы и потоки 501
Потоки на базе строк string
Рассказывая о потоках в оперативной памяти, следует упомянуть и потоки, создавае-
создаваемые на базе текстовых строк класса string.
Чтобы создать входной поток на базе строки string, вам потребуется класс
StringReader.
Создав такой поток с помощью конструктора класса StringReader, программа
сможет читать из него отдельные символы или блоки символов с помощью перегру-
перегруженных методов Read.
Использование этих методов демонстрируется в программе, исходный текст кото-
которой представлен в листинге 14.11.
Листинг 14.11. Файл ch14\StringReaderDemo\StringReaderDemoApp.cs
using System;
using System.10;
namespace StringReaderDemo
class StringReaderDemoApp
static void Main(string[] args)
string str = "Каждый охотник желает знать, где сидит фазан";
StringReader sr = new StringReader(str);
int ch;
while(true)
ch = sr.Read();
if(ch == -1)
break;
Console.Write((char)ch) ;
Console.WriteLine();
sr.Close();
sr = new StringReader(str);
char[] b = new char[str.Length];
sr.Read(b, 0, 15) ;
Console.WriteLine(b);
502 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
sr.Close();
Console.ReadLine();
В этой программе мы определили текстовую строку str класса string, а затем
создали на ее основе поток sr класса StringReader:
string str = "Каждый охотник желает знать, где сидит фазан";
StringReader sr = new StringReader(str);
Далее мы в цикле считываем символы из потока и отображаем их на консоли:
int ch;
while(true)
{
ch = sr.Read();
if(ch == -1)
break;
Console.Write((char)ch);
}
Заметим, что метод Read возвращает значение int, а не char. Поэтому перед
отображением прочитанного символа на консоли мы выполняем необходимое явное
преобразование типов.
Завершив работу с потоком, мы закрываем его обычным образом с помощью мето-
метода Close:
sr.Close();
Далее в программе демонстрируется чтение блока символов.
Для этого мы снова открываем поток:
sr = new StringReader(str);
Используя перегруженный метод Read, мы читаем первые 15 символов из потока
в массив data:
chart] data = new char[str.Length];
sr.Read(data, 0, 15);
Далее программа отображает на консоли содержимое массива и закрывает поток:
Console.WriteLine(data);
sr.Close ();
Вот что появится на консоли, после того как программа завершит свою работу:
Каждый охотник желает знать, где сидит фазан
Каждый охотник
Глава 14. Файлы и потоки 503
Потоки класса StringBuilder
С помощью класса StringBuilder вы можете создавать потоки на базе текстовых
строк, которые затем поддаются изменению. Например, создав такой поток, програм-
программа может вставлять в произвольное место потока новые текстовые строки или добав-
добавлять их в конец потока, а также удалять и замещать фрагменты строк.
Основные приемы работы с потоками класса StringBuilder демонстрируются
в программе, исходный текст которой представлен в листинге 14.12.
Листинг 14.12. Файл ch14\StringBuilderDemo\StringBuilderDemoApp.cs
using System;
using System.10;
using System.Text;
namespace StringBuilderDemo
{
class StringBuilderDemoApp
{
static void Main(string[] args)
{
StringBuilder sb = new StringBuilder("охотник желает");
sb.Insert(O, "Каждый ");
sb.Append(" знать, где сидит");
StringWriter sw = new StringWriter(sb);
char[] data =
{
1 ', 'ф', 'а1, 'з','а', 'н', '.'
};
sw.Write(data) ;
Console.WriteLine(sb) ;
sw.Close();
Console.ReadLine();
Прежде всего наша программа создает поток StringBuilder с помощью конст-
конструктора и записывает в него фрагмент фразы про охотника:
StringBuilder sb = new StringBuilder("охотник желает");
Далее, пользуясь методами Insert и Append, программа вставляет один фраг-
фрагмент фразы в начало потока, а другой — в конец потока:
sb.Insert@, "Каждый ");
sb.Append(" знать, где сидит");
504 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
При необходимости вы можете воспользоваться методами Remove и Replace со-
соответственно для удаления из потока фрагмента строки и для замены его другим
фрагментом.
Методу Remove в качестве первого параметра нужно передать начальный ин-
индекс удаляемого фрагмента, а в качестве второго — длину удаляемого фрагмента
в байтах:
public Remove(int index, int size);
Что же касается метода Replace, то определено его несколько перегруженных ва-
вариантов.
Следующие два метода заменяют в потоке соответственно все заданные символы
и строки другими:
public Replace(char chl, char ch2);
public Replace(string strl, string str2);
Эти методы делают замену во всем потоке.
Следующие два метода позволяют ограничить область замены блоком, начало ко-
которого задано параметром index, а размер — параметром size:
public Replace(char chl, char ch2, int index, int size);
public Replace(string strl, string str2, int index, int size);
Вернемся, однако, к описанию нашей программы.
На следующем шаге программа создает поток StringWriter на базе потока
StringBuilder:
StringWriter sw = new StringWriter(sb);
Пользуясь этим потоком с помощью метода Write, наша программа дописывает
в конец фразы последнее слово:
char[] data =
{
1 ', 'ф1, 'а', 'з', 'а1, 'н1 ,
sw.Write(data);
Далее содержимое потока выводится на консоль, после чего поток закрывается ме-
методом Close:
Console.WriteLine(sb);
sw.Close ();
В классе StringWriter определено множество перегруженных методов Write,
позволяющих записывать в поток данные практически всех встроенных типов. Кроме
того, предусмотрены аналогичные методы WriteLine, добавляющие после записи
символ перевода строки.
Глава 14. Файлы и потоки 505
Управление каталогами
В библиотеке классов Microsoft .NET Framework предусмотрено мощное средство ра-
работы с каталогами — класс Directory. С помощью статических методов этого клас-
класса программа может выполнять такие действия, как получение списка логических дис-
дисков, определение текущего каталога, просмотр содержимого каталогов, создание
и удаление каталогов, проверка их существования и пр.
Список логических дисков
Чтобы получить список всех логических дисков, имеющихся в системе, воспользуй-
воспользуйтесь статическим методом Directory. GetLogicalDrives:
string!] drives = Directory.GetLogicalDrives();
foreach(string s in drives)
Console.Write{"{0} ", s);
Этот метод не имеет параметров. После выполнения он возвращает ссылку на мас-
массив текстовых строк вида «С: \» с обозначениями всех доступных логических диско-
дисковых устройств.
Текущий каталог
С помощью метода Directory. GetCurrentDirectory программа может опреде-
определить полный путь к текущему каталогу:
string currentDir = Directory.GetCurrentDirectory();
Console.WriteLineСХпТекущий каталог: {0}", currentDir);
Метод возвращает этот путь в виде текстовой строки класса string.
При необходимости программа может определить диск, на котором находится те-
текущий или любой другой каталог. Для этого она должна воспользоваться методом
Directory.GetDirectoryRoot, передав ему в качестве параметра путь к нужно-
нужному каталогу:
string directoryRoot = Directory.GetDirectoryRoot(currentDir);
Console.WriteLine("Корень диска с текущим каталогом: {0}",
directoryRoot);
Просмотр содержимого каталога
Для получения списка содержимого любого заданного каталога используйте метод
Directory.GetFileSystemEntries:
string[] drive_c_Dirs = Directory.GetFileSystemEntries("с:W);
foreach(string s in drive_c_Dirs)
Console.WriteLine("{0} ", s);
506 А. В. Фролов. Г. В. Фролов. Язык С#. Самоучитель
В качестве параметра этому методу нужно передать путь к интересующему вас ка-
каталогу. Метод возвращает массив строк string, каждая из которых содержит имя ка-
каталога или файла, расположенного в указанном каталоге.
В программе, исходный текст которой приведен в листинге 14.13, мы продемонст-
продемонстрировали использование метода Directory.GetFileSystemEntries, а также
других описанных ранее методов.
Листинг 14.13. Файл ch14\Drivelnfo\DrivelnfoApp.cs
using System;
using System.10;
namespace Drivelnfo
{
class DrivelnfoApp
{
static void Main(string[] args)
{
Console.WriteLine("Список логических дисков: ");
stringU drives = Directory.GetLogicalDrives();
foreach(string s in drives)
Console.Writef"{0} *, s);
string currentDir = Directory.GetCurrentDirectory();
Console.WriteLine("\пТекущий каталог: {0}", currentDir);
string directoryRoot = Directory.GetDirectoryRoot(currentDir);
Console.WriteLine("Корень диска с текущим каталогом: {0}",
directoryRoot);
Console.WriteLine("Содержимое диска С: ");
stringП drive_c_Dirs =
Directory.GetFileSystemEntries("с:\\");
foreach(string s in drive_c_Dirs)
Console.WriteLine("{0} ", s) ;
Console.ReadLineO ;
После запуска она получает и выводит на консоль список логических дисков, пол-
полный путь к текущему каталогу, обозначение корневого каталога диска, на котором на-
находится текущий каталог, а также содержимое диска С:. Ниже мы показали в сокра-
щенном виде информацию, отображаемую нашей программой на консоли:
Глава 14. Файлы и потоки 507
Список логических дисков:
А:\ С:\ D:\ E:\ F:\ G:\ H:\ I:\ J:\ K:\ L: \ M:\
Текущий каталог: Н:\[Beginner C# Book]\src\chl4\DriveInfo\bin\Debug
Корень диска с текущим каталогом: Н:\
Содержимое диска С:
с:\TEMP
c:\Program Files
с:\WINDOWS
c:\My Documents
с:\MSDOS.SYS
с:\AUTOEXEC.BAT
С:\COMMAND.COM
Создание каталога
Чтобы создать каталог, используйте метод Directory. CreateDirectory:
string dirName = "temp\\dir";
Directory.CreateDirectory(dirName);
Console.WriteLine("Каталог {0} создан", dirName);
В качестве параметра этому методу нужно передать полный путь к создаваемому
каталогу. Учтите, что данный метод позволяет не только создать один каталог, но
и построить цепочку вложенных каталогов. В приведенном выше примере создается
каталог temp, а также вложенный в него каталог с именем dir.
Удаление каталога
Чтобы удалить каталог, вам необходим метод Directory. Delete. Существует два
перегруженных варианта этого метода. Первый из них имеет один параметр и позво-
позволяет удалять только пустые каталоги. С помощью второго можно удалять непустые
деревья каталогов вместе с подкаталогами и файлами.
Ниже мы показали способ удаления дерева каталогов:
Directory.Delete(dirName, true);
Console.WriteLine("Каталог {0} удален", dirName);
В качестве второго параметра методу Directory .Delete передастся флаг ре-
рекурсивного удаления. Если он равен true, то метод рекурсивно удаляет все содержи-
содержимое каталога. В том случае, когда значение этого флага равно false, при попытке
удаления непустого каталога возникает исключение IOException.
Получение информации о каталоге
Методы класса Directory позволяют получать различную информацию о каталоге.
Например, с помощью метода Directory.GetCreationTime нетрудно опреде-
определить дату и время создания каталога:
DateTime dt = Directory.GetCreationTime(dirName);
Console.Write(
"Дата и время создания каталога: {0}-{1}-{2} {3}:D):{5}",
dt.Day, dt.Month, dt.Year, dt.Hour, dt.Minute, dt.Second);
508 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Информация о дате и времени возвращается в виде ссылки на объект класса
DateTime. Пользуясь свойствами этого объекта, нетрудно извлечь отдельные компо-
компоненты даты, такие, как число, месяц, год, час, минута и секунда создания файла.
Аналогично с помощью методов GetLastAccess и GetLastWrite класса
Directory можно узнать, когда программы в последний раз обращались к каталогу
и когда они выполняли в него запись.
С помощью метода Directory .GetParent программа может получить подроб-
подробную информацию о родительском каталоге любого заданного каталога:
Directorylnfо di = Directory.GetParent(dirName);
Console.WriteLine("\пРодительский каталог {0}:", di.Parent);
Эта информация возвращается методом Directory.GetParent в виде ссылки
на объект класса Directorylnf о. Свойства этого класса позволяют получить такую ин-
информацию, как имя каталога и имя родительского каталога, атрибуты каталога, дата
и время создания, а также изменения каталога, проверить существование каталога и т. д.
В листинге 14.14 мы показали применение некоторых описанных выше методов
для создания и удаления каталога, а также для получения справочной информации
о созданном каталоге.
Листинг 14.14. Файл ch14\Dirlnfo\DirlnlbApp.cs
using System;
using System.10;
namespace Dirlnfo
{
class DirlnfoApp
{
static void Main(string!] args)
{
string dirName = " tempWdir";
Directory.CreateDirectory{dirName);
Console.WriteLine("Каталог {0} создан", dirName);
DateTime dt = Directory.GetCreationTime(dirName);
Console.Write(
"Дата и время создания каталога: {0}-{1}-{2} {3}:{4}:{5}",
dt.Day, dt.Month, dt.Year, dt.Hour, dt.Minute, dt.Second);
Directorylnfo di = Directory.GetParent(dirName);
Console.WriteLine("\пРодительский каталог {0}:", di.Parent);
Directory.Delete(dirName, true);
Console.WriteLine("Каталог {0} удален", dirName);
Console.ReadLine();
Глава 14. Файлы и потоки 509
Вот что эта программа выводит на консоль во время своей работы:
Каталог temp\dir создан
Дата и время создания каталога: 7-8-2002 11:20:28
Родительский каталог Debug:
Каталог temp\dir удален
Таким образом, класс Directory предоставляет в распоряжение разработчика
программ мощное средство управления каталогами. Что же касается управления фай-
файлами, то такие средства тоже предусмотрены в рамках класса File.
Управление файлами
В предыдущих разделах этой главы мы рассмотрели классы, предназначенные для
чтения и записи потоков, а также для работы с каталогами. При создании программ
часто возникает необходимость выполнения и таких операций, как определение атри-
атрибутов файла, создание или удаление каталогов, удаление файлов, получение списка
всех файлов в каталоге и т. д.
В то время как практически все операции с каталогами можно выполнить при по-
помощи статических членов класса Directory, для выполнения операций с файлами
используется класс File. Этот класс содержит только статические члены, поэтому
вам не придется создавать объекты класса File.
Проверка существования файла или каталога
С помощью метода Exists вы можете проверить существование файла или каталога.
Если файл существует, метод возвращает значение true, в противном случае ■— зна-
значение false.
Этот метод можно применять перед созданием потоков на базе файлов, если вам
нужно избежать случайной перезаписи существующего файла.
Вот пример использования метода File. Exists:
string testFile = "temp.txt";
if(File.Exists(testFile))
{
Console.WriteLine("Файл {0} уже существует", testFile);
return;
}
С такой конструкцией вы уже встречались в примерах программ, приведенных
в этой главе. Если файл testFile существует, то программа возвращает управление
оператором return, не предпринимая никаких других действий.
Создание файла
В классе File предусмотрены два метода, предназначенных для создания файлов.
Первый из этих методов с именем CreateText создает текстовый файл и откры-
открывает связанный с ним поток streamWriter:
string testFile = "temp.txt";
StreamWriter sw = File.CreateText(testFile);
510 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Мы уже работали с такими потоками ранее в этой главе.
Другой метод с именем Create создает файл и открывает для него поток класса
FileStream. Этот метод удобен для работы с двоичными файлами.
Удаление файла
Удалить файл так же просто, как и создать. Для этого ваша программа должна исполь-
использовать метод Delete, как это показано ниже:
string testFile = "temp.txt";
File.Delete(testFile) ;
Console.WriteLine("Файл {0} удален", testFile);
Определение времени создания файла
Чтобы определить время создания файла, используйте метод File.GetCrea-
tionTime, как это показано ниже:
string testFile = "temp.txt";
DateTime dt = File.GetCreationTime(testFile);
Console.Write(
"Дата и время создания файла: {0}-{1}-{2} {3} : {4} : {5}",
dt.Day, dt.Month, dt.Year, dt.Hour, dt.Minute, dt.Second);
Дата и время создания файла возвращаются в виде ссылки на объект класса
DateTime, о котором мы уже рассказывали ранее при описании класса Directory.
Определение времени доступа и изменения файла
В классе File определены методы GetLastAccessTime и GetLastWriteTime.
Первый из них позволяет определить время и дату последнего доступа к файлу, а вто-
второй — время и дату последнего изменения файла. Оба метода возвращают ссылки
класса DateTime.
Определение атрибутов файлов и каталогов
С помощью класса File нетрудно определить атрибуты файлов и каталогов, восполь-
воспользовавшись статическим методом FileAttributes. В качестве параметра этому ме-
методу необходимо передать путь к файлу или каталогу, атрибуты которых подлежат
определению:
Console.Write("\пАтрибуты файла: ");
FileAttributes fa = File.GetAttributes{testFile);
Console.WriteLine(fa) ;
В качестве параметра методу нужно передать полный путь к файлу. После выпол-
выполнения метод возвращает перечисление класса FileAttributes. Возможные значе-
значения и их описание приведены в табл. 14.6.
Глава 14. Файлы и потоки 511
Таблица 14.6. Кодировки текстовых потоков
Имя
Archive
Compressed
Device
Directory
Encrypted
Hidden
Normal
NotContentIndexed
Offline
Readonly
ReparsePoint
SparseFile
System
Temporary
Описание
Состояние архивирования. Используется для отметки
файлов, выгруженных при создании резервной копии
Файл упакован
Зарезервировано
Указанный объект является каталогом
Файл или каталог зашифрован
Скрытый файл или каталог
Файл имеет стандартный набор атрибутов
Файл не индексируется сервисом индексирования ОС (та-
(такое индексирование применяется для ускорения поиска)
Файл находится в состоянии offline
Только читаемый файл
Файл содержит точку монтирования файла или каталога
Файл, в котором большие зоны нулевых данных хранят-
хранятся в упакованном виде
Системный файл
Временный файл
В листинге 14.15 мы приведи исходный текст программы, демонстрирующей опи-
описанные выше приемы работы с методами класса File.
Листинг 14.15. Файл ch14\Filelnfo\FilelnfoApp.cs
using System;
using System.10;
namespace Filelnfo
class FilelnfoApp
static void Main(string[] args)
string testFile = "temp.txt";
if(File.Exists(testFile))
Console.WriteLine(
"Файл {0} уже существует, перезаписываем...
", testFile)
512
StreamWriter sw = File.CreateText(testFile);
sw.WriteLine ("Каждый охотник желает знать, где сидит фазан!");
А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
sw.WriteLine("Число \"Пи\" равно примерно {0}.", 3.1415926);
sw.Close();
DateTime dt = File.GetCreationTime(testFile);
Console.Write(
"Дата и время создания файла: {0}-{1}-{2} {3}:{4}:{5}",
dt.Day, dt.Month, dt.Year, dt.Hour, dt.Minute, dt.Second);
Console.Write("\пАтрибуты файла: ");
FileAttributes fa = File.GetAttributes(testFile);
Console.WriteLine(fa);
File.Delete(testFile);
Console.WriteLine{"Файл {0} удален", testFile);
Console.ReadLine();
Эта программа создает файл, отображает его атрибуты на консоли, а затем удаляет
возданный файл:
Дата и время создания файла: 7-8-2002 12:7:42
Атрибуты файла: Archive
Файл temp.txt удален
Изменение атрибутов файлов и каталогов
С помощью методов SetAttributes, SetCreationTime, SetLastAccessTime
и SetLastWriteTime программа может изменить атрибуты файлов и каталогов.
Переименование или перемещение файлов
Для переименования или перемещения файла вы можете использовать метод Move,
определенный в классе File.
Этот метод имеет два параметра, первый из которых содержит старое имя
(или полный путь) файла, а второй — новое имя или новый полный путь.
Произвольный доступ к файлам
В ряде случаев, например при создании системы управления базой данных, требуется
обеспечить произвольный доступ к файлу. Рассмотренные нами ранее методы работы
с потоками ввода и вывода пригодны лишь для последовательного доступа. Между
тем в некоторых рассмотренных нами потоковых классах предусмотрен метод Seek,
позволяющий программам выполнять позиционирование внутри файла.
Глава 14 Файлы и потоки 513
17 Язык С# Самоучитель
Проверка возможности позиционирования
Заметим, что средства позиционирования (т. е. средства изменения текущей позиции)
предусмотрены не во всех потоках. С помощью метода CanSeek программа может
определить, есть ли такие средства в потоках того или иного класса.
Например, поток класса FileStream, с помощью которого мы работали с двоич-
двоичными файлами, такими средствами обладает, а потоки класса TextReader и Text-
Writer, с помощью которых мы работали с текстовыми файлами, — нет.
Соответственно в классе FileStream определен метод Seek, с помощью которо-
которого программа может выполнять позиционирование.
Что же касается потоков BinaryReader и BinaryWriter, предназначенных для
работы с двоичными потоками и позволяющих читать и писать переменные различных
типов, то с ними ситуация, на наш взгляд, довольно странная. В классе Binary-
Writer определен метод Seek, а в классе BinaryReader— нет. Поэтому произ-
произвольный доступ к двоичным файлам на чтение возможен только средствами класса
FileStream. К сожалению, класс FileStream позволяет читать из потока только
байты и массивы байтов, но не данные других типов.
Метод Seek
Метод Seek позволяет установить текущую позицию внутри потока. Он имеет два па-
параметра, первый из которых (параметр offset) позволяет задать относительную по-
позицию, а второй (параметр origin) — способ применения этой позиции:
public override long Seek(long offset, SeekOrigin origin);
Относительная позиция offset задается в байтах. Что же касается параметра
origin, то она задается с помощью перечисления SeekOrigin. Возможные значе-
значения для этого параметра представлены в табл. 14.7.
Таблица 14.7. Способ позиционирования методом Seek
Имя
Begin
Current
End
Способ позиционирования
Новая текущая позиция в потоке устанавливается рав-
равной значению, заданному параметром of f set, и от-
считывается от начала потока
То же, но новая позиция отсчитывается от текущей, ко-
которая была установлена до вызова метода Seek
Новая текущая позиция отсчитывается от конца файла
В любой момент времени программа может определить текущую позицию внутри
потока, обратившись для этого к свойству Position.
Заметим, что если поток может работать со средствами позиционирования, то, из-
изменяя значение свойства Position, программа может изменять текущую позицию
514 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
в потоке. Это возможно потому, что для таких потоков свойство Position доступно
не только для чтения, но и для записи.
С помощью свойства Length вы можете определить текущую длину потока
(т. е. файла, с которым связан поток).
Приложение DirectFileAccess
Для иллюстрации способов прямого доступа к файлам мы подготовили приложение
DirectFileAccess, в котором создается небольшая база данных. Эта база данных состоит
из двух файлов: файла данных и файла индекса.
В файле данных хранятся записи, состоящие из двух полей — текстового и число-
числового. Текстовое поле с названием name хранит строки, а числовое поле с названием
account — значения типа int.
Дамп файла данных, создаваемого при первом запуске программы DirectFileAccess,
приведен на рис. 14.12.
Н Нсн Workshop - Idbtesl.dat]
Не Е* И* Options Tools Widow Help
|cm
Jjigis11|T i t r I
Т
v-
00000000
ooooooio
00000020
Р6 49 76 61 6Е 6F 76 Е8 03 00 00 06 50 65 74 72
6F 7S DO 07 00 00 08 53 69 64 6F 72 6F 66 66 B8
OB 00 DO
^.Ivanov Petr
OV Sidoroff .
В dbKatdal f
Ready
€ffsat: 0000OD00
Рис. 14.12. Дамп файла данных
Из этого дампа видно, что после первого запуска приложения в файле данных име-
имеется несколько записей (табл. 14.8).
Таблица 14.8. Записи в файле данных
Номер записи
0
1
2
Смещение в файле данных
0
11
22
Поле пате
Ivanov
Petrov
Sidoroff
Поле account
1000
2000
3000
При последующих запусках программы каждый раз в файл данных будут добав-
добавляться приведенные выше записи.
Так как поле name имеет переменную длину, для обеспечения возможности прямо-
прямого доступа к записи по ее номеру необходимо где-то хранить смещения всех записей.
Мы это делаем в файле индексов, дамп которого на момент после первого запуска
приложения представлен на рис. 14.13.
Глава 14 Файлы и потоки
515
H Hex Workshop -
Hi Hie E* Disk Options Tosb Whdow Нф
В S I I I ■
- I & \v-
!*H
00000000 pO 00 00 00 OCl 00 00 00 0B 00 00 00 00 00 00 00 _
00000010 16 00 00 00 00 00 00 00
.B obtoatkh
Rudy
tOftebOQOOOOOO
!24 bytes
Puc. 74.13. Дамп файла индекса
Файл индексов хранит 8-байтовые смещения записей файла данных в формате
long. Зная номер записи, можно легко вычислить смещение в файле индексов, по ко-
которому хранится смещение нужной записи в файле данных. Если извлечь это смеще-
смещение, то можно выполнить позиционирование в файле данных с целью чтения нужной
записи. Именно это и делает наша программа.
После добавления трех записей в базу данных приложение извлекает 3 записи
в обратном порядке, т. е. сначала запись с номером 2, затем с номером 1 и, наконец,
с номером 0. Извлеченные записи отображаются в консольном окне:
> 3000, Sidoroff
> 2000, Petrov
> 1000, Ivanov
Исходные тексты программы DirectFileAccess приведены в листинге 14.16.
Листинг 14.16. Файл ch14\DirectFileAccess\DirectFileAccessApp.cs
using System;
using System.IO;
namespace DirectFileAccess
{
// =========================================================
// Класс SimpleDBMS
// Простейшая база данных
class SimpleDBMS
{
// Файл индексов
string indexFileName;
FileStream fsldx,-
BinaryWriter bwldx;
BinaryReader brldx;
// Файл данных
string dataFileName;
FileStream fsData;
EinaryWriter bwData;
BinaryReader brData;
516
А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
// Значение указателя на текущую запись
long idxFilePointer = 0;
/ / ,
// SimpleDBMS
// Конструктор. Сохраняет имена файлов
//
public SimpleDBMS(String indexFile, String dataFile)
{
indexFileName = indexFile;
dataFileName = dataFile;
/ / , ,
// AddRecord
// Добавление записи в базу данных
//
public void AddRecord(String name, int account)
{
try
{
fsData = new FileStream(dataFileName,
FileMode.OpenOrCreate) ;
// Устанавливаем текущую позицию в файле данных
//на конец файла
fsData.Seek@, SeekOrigin.End);
// Получаем смещение позиции в файле данных,
II ъ которой будет добавлена новая запись
idxFilePointer = fsData.Position;
// Сохраняем это смещение в конце файла индексов
fsldx = new FileStream(indexFileName,
FileMode.OpenOrCreate);
bwldx = new BinaryWriter(fsldx);
bwldx.Seek@, SeekOrigin.End);
bwldx.Write(idxFilePointer);
bwIdx.CloseO ;
fsldx.Closed ;
// Сохраняем в файле дайнных два поля новой записи
bwData = new BinaryWriter(fsData);
bwData.Seek@, SeekOrigin.End);
bwData.Write(name);
bwData.Write(account);
Глава 14. Файлы и потоки 517
bwData.Close() ;
fsData.Close() ;
catch(Exception ioe)
Console.WriteLine(ioe.ToString()) ;
// GetRecordByNumber
// Извлечение записи по ее порядковому номеру
II ,
public string GetRecordByNumber(long nRec)
// Строка, в которой будет сохранена извлеченная запись
string sRecord = "<empty>";
try
{
// Значение поля account
int account;
// Значение поля name
string str = null;
// Вычисляем смещение в файле индексов по
// порядковому номеру записи
fsldx = new FileStream(indexFileName, FileMode.Open);
brldx = new BinaryReader{fsldx);
// Пропускаем нужное количество байтов
for(long i = 0; i < nRec * 8; i++)
brldx.ReadByte();
// Извлекаем из файла индексов смещение записи
// в файле данных
idxFilePointer = brldx.Readlnt32();
brldx.CloseO ;
fsldx.Close() ;
// Выполняем позиционирование на нужную запись
//в файле данных
fsData = new FileStream(dataFileName, FileMode.Open);
brData = new BinaryReader(fsData);
// Пропускаем нужное количество байтов
for(long i = 0; i < idxFilePointer; i++)
brData.ReadByte();
// Извлекаем поля записи
str = brData.ReadString();
account = brData.Readlnt32();
518 А В. Фролов, Г. В. Фролов. Язык С# Самоучитель
brData.Close();
fsData.Close();
// Объединяем значения полей в текстовую строку
sRecord = String.Format("> {0}, {1}", account, str);
catch(Exception ioe)
Console.WriteLine(ioe.ToString());
// Возвращаем извлеченную запись
return sRecord;
}
}
class DirectFileAccessApp
static void Main(string[] args)
try
// Создаем новую базу данных
SimpleDBMS db = new SimpleDBMS("dbtest.idx",
"dbtest.dat");
// Добавляем в нее 3 записи
db.AddRecord("Ivanov", 1000);
db.AddRecord("Petrov", 2000);
db.AddRecord("Sidoroff", 3000);
// Получаем и отображаем содержимое первых трех
// записей с номерами 2, 1 и О
Console.WriteLine(db.GetRecordByNumberB));
Console.WriteLine(db.GetRecordByNumberA));
Console.WriteLine(db.GetRecordByNumber@));
// После ввода любой строки завершаем работу программы
Console.ReadLine();
catch(Exception ioe)
Console.WriteLine(ioe.ToString());
Для работы с базой данных мы создали класс SimpleDBMS, определив в.нем кон-
конструктор, методы для добавления записей и извлечения записей по их порядковому
номеру.
Глава 14. Файлы и потоки 519
Сразу после запуска метод Main программы DirectFileAccess создает базу данных,
передавая конструктору имена файла индекса dbtest.idx и файла данных dbtest.dal:
SimpleDBMS db = new SimpleDBMS("dbtest.idx", "dbtest.dat");
После этого с помощью метода AddRecord, определенного в классе SimpleDBMS,
в базу добавляются 3 записи, состоящие из текстового и числового полей:
db.AddRecord("Ivanov", 1000);
db.AddRecord("Petrov", 2 000);
db.AddRecord{"Sidoroff», 3000);
Сразу после добавления записей приложение извлекает 3 записи с номерами 2, 1
и 0, вызывая для этого метод GetRecordByNumber, также определенный в классе
SimpleDBMS:
System.out.println(db.GetRecordByNumberB));
System.out.println(db.GetRecordByNumberA));
System.out.println(db.GetRecordByNumber@));
Извлеченные записи отображаются на системной консоли.
Класс SimpleDBMS
Рассмотрим теперь класс SimpleDBMS. В этом классе определены поля для работы
с файлом индексов, с файлом данных, а также поле указателя на текущую запись:
// Файл индексов
string indexFileName;
FileStream fsldx;
BinaryWriter bwldx;
BinaryReader brldx;
// Файл данных
string dataFileName;
FileStream fsData;
BinaryWriter bwData;
BinaryReader brData;
// Значение указателя на текущую запись
long idxFilePointer = 0;
Имена файлов индекса и данных хранятся соответственно в полях indexFile-
indexFileName и dataFileName.
Поля fsldx и fsData являются объектами класса FileStream. Они создаются
соответственно для файла индексов и файла данных.
Непосредственное чтение файла индекса и данных выполняется средствами класса
BinaryReader. Для хранения ссылок на потоки этого класса в классе SimpleDBMS
предусмотрены поля brldx и brData.
Что же касается записи в файл индекса и даты, то она выполняется средствами
класса BinaryWriter. Для хранения ссылок на соответствующие потоки в классе
SimpleDBMS имеются поля bwldx и bwData.
520 А В. Фролов, Г В Фролов. Язык С#. Самоучитель
Конструктор класса SimplcDBMS
Конструктор класса SimpleDBMS выглядит достаточно просто. Все, что он делает,
это сохраняет имена файлов индекса и данных в соответствующих полях класса:
public SimpleDBMS(String indexFile, String dataFile)
{
indexFileName = indexFile;
dataFileName = dataFile;
Добавление новой записи
Метод AddRecord добавляет новую запись в конец файла данных, а смещение этой
записи — в конец файла индекса. Поэтому перед началом записи текущая позиция
обоих указанных потоков устанавливается на конец файла.
Вначале метод AddRecord создает поток на базе файла данных:
fsData = new FileStream(dataFileName, FileMode.OpenOrCreate);
Далее текущая позиция потока устанавливается на конец файла данных, так как мы
будем добавлять запись в конец файла:
fsData.Seek@, SeekOrigin.End);
Для установки текущей позиции мы применили метод Seek из класса
FileStream, передав ему в качестве первого параметра (задающего смещение) нуле-
нулевое значение, а в качестве второго параметра (задающего способ отсчета смещения) —
значение SeekOrigin. End. В результате текущая позиция будет установлена на ко-
конец потока.
Перед тем как добавлять новую запись в файл данных, метод AddRecord опреде-
определяет текущую позицию в файле данных (в данном случае это позиция конца файла):
idxFilePointer = fsData.Position;
Данную позицию (смещение записи в файле данных) необходимо записать в файл
индекса. Для этого наша программа открывает поток, связанный с файлом индекса:
fsldx = new FileStream(indexFileName, FileMode.OpenOrCreate);
Запись будет выполняться методом Write класса BinaryWriter, поэтому про-
программа создает соответствующий поток:
bwldx = new BinaryWriter(fsldx);
Далее программа устанавливает текущую позицию потока на конец файла индек-
индексов, записывает туда смещение записи в файле данных и закрывает оба потока, свя-
связанных с файлом индексов:
bwldx.Seek@, SeekOrigin.End);
bwldx.Write(idxFilePointer);
bwldx.Close();
fsldx.Close();
Глава 14. Файлы и потоки 521
Теперь нужно сохранить новую запись в файле данных.
Для этого наша программа открывает поток BinaryWriter, связанный с файлом
данных, и устанавливает текущую позицию на конец файла:
bwData = new BinaryWriter(fsData) ,•
bwData.Seek@, SeekOrigin.End);
Далее программа последовательно сохраняет в этом потоке имя паше и числовое
значение account (т. е. содержимое полей добавляемой записи):
bwData.Write(name);
bwData.Write(account);
После выполнения этой операции потоки, связанные с файлом данных, закрываются:
bwData.Close();
fsData.Close();
Извлечение записи по ее номеру
Метод GetRecordByNumber позволяет извлечь произвольную запись из файла дан-
данных по ее порядковому номеру.
Напомним, что смещения всех записей хранятся в файле индексов и имеют одина-
одинаковую длину 8 байт. Пользуясь этим, метод GetRecordByNumber вычисляет смеще-
смещение в файле индекса простым умножением порядкового номера записи на длину пере-
переменной типа long, т. е. на 8 байт, а затем выполняет позиционирование:
fsldx = new FileStream(indexFileName, FileMode.Open);
brldx = new BinaryReader(fsldx);
// Пропускаем нужное количество байтов
for(long i = 0; i < nRec * 8; i++)
brldx.ReadByte();
К сожалению, в классе BinaryReader метод Seek не предусмотрен. Поэтому
для позиционирования на нужную запись нам пришлось прочесть в цикле необходи-
необходимое количество байтов «вхолостую».
После этого метод GetRecordByNumber извлекает из файла индексов смещение
нужной записи в файле данных, вызывая для этого метод Readlnt32:
idxFilePointer = brldx.Readlnt32();
Затем программа выполняет позиционирование на нужную запись в файле данных,
пропуская необходимое количество байтов:
fsData = new FileStream(dataFileName, FileMode.Open);
brData = new BinaryReader(fsData);
// Пропускаем нужное количество байтов
for(long i = 0; i < idxFilePointer; i++)
brData.ReadByte();
522 А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Поля записи читаются из файла данных в два приема. Вначале читается строка тек-
текстового поля, а затем — численное значение. Для этого вызываются соответственно
методы ReadString и Readlnt3 2:
str = brData.ReadString();
account = brData .Readlnt32 (),-
Полученные значения полей объединяются в текстовой строке и записываются
в переменную sRecord:
sRecord = String.Format("> {0}, Cl}", account, str);
Содержимое этой переменной метод GetRecordByNumber возвращает в качестве
извлеченной строки записи базы данных:
return sRecord;
Обработка исключений
При работе с файлами и каталогами, а также при работе с потоками средствами клас-
классов, описанных в этой главе, могут возникать исключения, такие как FileNot-
FoundException, SecurityException, IOException и другие. В только что
рассмотренной программе (листинг 14.16) мы использовали обработку исключений.
Для того чтобы ваша программа, обращающаяся к файлам и потокам, вела себя пред-
предсказуемым образом, мы советуем вам всегда предусматривать обработку исключений.
Почему при работе с каталогами, файлами и потоками могут возникать исключения?
Причины возникновения исключений могут быть самыми разными. Они могут
быть связаны с ошибками разного рода, а также могут возникать при выполнении
«штатных» операций, таких, например, как попытка чтения за концом потока.
Например, если программа пытается создать каталог, но у пользователя, запустив-
запустившего эту программу, нет на это прав, возникает исключение SecurityException.
Если же диск, на котором создается каталог, доступен только для чтения, появится ис-
исключение IOException.
Исключение SecurityException возникает также при попытке открыть файл,
для которого запрещен доступ. Например, если файл можно только читать, а он от-
открывается для записи, возникнет исключение SecurityException. Если файл не
может быть открыт для записи по каким-либо другим причинам, возникает исключе-
исключение IOException.
Исключение FileNotFoundException возникает при попытке открыть входной
поток данных для несуществующего файла, т. е. когда файл не найден.
Что же касается метода BinaryReader .ReadString, считывающего из двоич-
двоичного потока текстовые строки, то при достижении конца потока возникает исключение
EndOfStreamException.
Более подробную информацию об исключениях, возникающих при работе с ката-
каталогами, файлами и потоками, вы найдете в справочной системе Microsoft Visual Studio
.NET или в библиотеке MSDN (http://msdn.microsoft.com).
Глава 14. Файлы и потоки 523
Приложение 1. Зарезервированные
ключевые слова С#
abstract
as
base
bool
break
byte
case
catch
char
checked
class
const
continue
decimal
default
delegate
do
double
else
enum
event
explicit
extern
false
finally-
fixed
float
for
foreach
goto
if
implicit
in
int
interface
internal
is
lock
long
namespace
new
null
object
operator
out
override
params
private
protected
public
readonly
ref
return
sbyte
sealed
short
sizeof
stackalloc
static
string
struct
switch
this
throw
true
try
typeof
uint
ulong
unchecked
unsafe
ushort
using
virtual
void
volatile
while
524
Приложение 2. Имена и коды
национальных культур
Национальный язык, страна или район
не учитывается
Аджарский
Аджарский (Азербайджан, кириллица)
Аджарский (Азербайджан, латиница)
Албанский
Албанский (Албания)
Английский
Английский (Австралия)
Английский (Белиз)
Английский (Зимбабве)
Английский (Ирландия)
Английский (Канада)
Английский (Карибы)
Английский (Новая Зеландия)
Английский (Объединенное Королевство)
Английский (Соединенные Штаты Америки)
Английский (Тринидад и Тобаго)
Английский (Филиппины)
Английский (Южная Африка)
Английский (Ямайка)
Арабский
Арабский (Альгерия)
Арабский (Бахрейн)
Арабский (Египет)
Арабский (Иордания)
Арабский (Ирак)
Арабский (Йемен)
Арабский (Катар)
Арабский (Кувейт)
Арабский (Ливан)
Имя культуры
"" (пустая строка)
az
Cy-az-AZ
Lt-az-AZ
sq
sq-AL
en
en-AU
en-BZ
en-ZW
en-IE
en-CA
en-CB
en-NZ
en-GB
en-US
en-TT
en-PH
en-ZA
en-JM
ar
ar-DZ
ar-BH
ar-EG
ar-JO
ar-IQ
ar-YE
ar-QA
ar-KW
ar-LB
Код культуры
0x007F
0x002C
0x082C
0x042C
0x001С
0x04lC
0x0009
0x0C09
0x2809
0x3009
0x1809
0x1009
0x2409
0x1409
0x0809
0x0409
0x2C09
0x3409
0xlC09
0x2009
0x0001
0x1401
0x3C01
OxOCOl
0x2C01
0x0801
0x2401
0x4001
0x3401
0x3001
/ИМСШК0И
525
Национальный язык, страна или район
Арабский (Ливия)
Арабский (Марокко)
Арабский (Объединенный Арабские Эмираты)
Арабский (Оман)
Арабский (Саудовская Аравия)
Арабский (Сирия)
Арабский (Тунис)
Армянский
Армянский (Армения)
Африканский
Африканский (Южная Африка)
Баскский
Баскский
Белорусский
Белорусский (Беларусь)
Болгарский
Болгарский (Болгария)
Венгерский
Венгерский (Венгрия)
Вьетнамский
Вьетнамский (Вьетнам)
Греческий
Греческий (Греция)
Грузинский
Грузинский (Грузия)
Датский
Датский (Дания)
Индонезийский
Индонезийский (Индонезия)
Исландский
Исландский (Исландия)
Испанский
Испанский (Аргентина)
Испанский (Боливия)
Испанский (Венесуэла)
Испанский (Гватемала)
Имя культуры
ar-LY
аг-МА
аг-АЕ
аг-ОМ
ar-SA
ar-SY
ar-TN
hy
hy-AM
af
af-ZA
cu
eu-ES
be
be-BY
bg
bg-BG
hu
hu-HU
vi
vi-VN
el
el-GR
ka
ka-GE
da
da-DK
id
id-ID
is
is-IS
es
es-AR
es-BO
es-VE
cs-GT
Код культуры
0x1001
0x1801
0x3801
0x2001
0x0401
0x2801
OxlCOl
Ox002B
0x042B
0x0036
0x0436
OxO02D
0x042D
0x0023
0x0423
0x0002
0x0402
OxOOOE
0x040E
0x002A
0x042A
0x0008
0x0408
0x0037
0x0437
0x0006
0x0406
0x0021
0x0421
OxOOOF
0x040F
OxOOOA
0x2C0A
0x400A
0x200A
OxlOOA
526
А В. Фролов, Г В. Фролов. Язык С#. Самоучитель
Национальный язык, страна или район
Испанский (Гондурас)
Испанский (Доминиканская Республика)
Испанский (Испания)
' Испанский (Колумбия)
Испанский (Коста-Рика)
Испанский (Мексика)
Испанский (Никарагуа)
Испанский (Панама)
Испанский (Парагвай)
Испанский (Перу)
Испанский (Пуэрто-Рико)
Испанский (Сальвадор)
Испанский (Уругвай)
Испанский (Чили)
Испанский (Эквадор)
Итальянский
Итальянский (Италия)
Итальянский (Швейцария)
Казахский
Казахский (Казахстан)
Каталанский
Каталанский
Киргизский
Киргизский (Казахстан)
Китайский (Гонгконг)
Китайский (Китай)
Китайский (Маасу)
Китайский (Сингапур)
Китайский (Тайвань)
Китайский (традиционный)
Китайский (упрощенный)
Корейский
Корейский (Корея)
Кункаи
Кункаи (Индия)
Латвийский
Имя культуры
es-HN
es-DO
es-ES
es-CO
es-CR
es-MX
es-NI
es-PA
es-PY
es-PE
es-PR
es-SV
es-UY
es-CL
es-EC
it
it-IT
it-CH
kk
kk-KZ
ca
ca-ES
ky
ky-KZ
zh-HK
zh-CN
zh-MO
zh-SG
zh-TW
zh-CHT
zh-CHS
ko
ko-KR
kok
kok-IN
lv „
Код культуры
0x480A
OxlCOA
OxOCOA
0x24OA
0x140A
0x08OA
0x4C0A
0xl80A
ОхЗСОА
0x280A
Ox500A
0x440A
0x380A
0x340A
ОхЗООА
0x0010
0x0410
0x0810
0x003F
0x043F
0x0003
0x0403
0x0040
0x0440
0x0C04
0x0804
0x1404
0x1004
0x0404
0x7C04
0x0004
0x0012
0x0412
0x0057
0x0457
0x0026
Приложение 2. Имена и коды национальных культур
527
Национальный язык, страна или район
Латвийский (Латвия)
Литовский
Литовский (Литва)
Македонский
Македонский
Малайский
Малайский (Бруней)
Малайский (Малайзия)
Марати
Марати (Индия)
Монгольский
Монгольский (Монголия)
Немецкий
Немецкий (Австрия)
Немецкий (Германия)
Немецкий (Лихтенштейн)
Немецкий (Люксембург)
Немецкий (Швейцария)
Нидерландский
Нидерландский (Бельгия)
Нидерландский (Нидерланды)
Норвежский
Норвежский
Норвежский
Польский
Польский (Польша)
Португальский
Португальский (Бразилия)
Португальский (Португалия)
Румынский
Румынский (Румыния)
Русский
Русский (Россия)
Санскрит
Санскрит (Индия)
Сербский (Сербия, кириллица)
Имя культуры
lv-LV
It
lt-LT
mk
mk-MK
ms
ms-BN
ms-MY
mr
mr-IN
mn
mn-MN
de
de-AT
de-DE
de-LI
de-LU
de-CH
nl
nl-BE
nl-NL
no
nb-NO
nn-NO
Pi
pl-PL
Pt
pt-BR
pt-PT
ro
ro-RO
ru
ru-RU
sa
sa-IN
Cy-sr-SP
Код культуры
0x0426
0x0027
0x0427
0x002F
0x042F
ОхООЗЕ
0x083E
0x043E
OxOO4E
0x044E
0x0050
0x0450
0x0007
0x0C07
0x0407
0x1407
0x1007
0x0807
0x0013
0x0813
0x0413
0x0014
0x0414
0x0814
0x0015
0x0415
0x0016
0x0416
0x0816
0x0018
0x0418
0x0019
0x0419
0x004F
0x044F
OxOClA
528
А В. Фролов. Г. В. Фролов. Язык С# Самоучитель
Национальный язык, страна или район
Сербский (Сербия, латиница)
Сирийский
Сирийский (Сирия)
Словацкий
Словацкий (Словакия)
Словенский
Словенский (Словения)
Тайский
Тайский (Таиланд)
Тамил
Тамил (Индия)
Татарский
Татарский (Россия)
Турецкий
Турецкий (Турция)
Узбекский
Узбекский (Узбекистан, кириллица)
Узбекский (Узбекистан, латиница)
Украинский
Украинский (Украина)
Урду
Урду (Пакистан)
Фарси
Фарси (Иран)
Финский
Финский (Финляндия)
Французский
Французский (Бельгия)
Французский (Канада)
Французский (Люксембург)
Французский (Монако)
Французский (Франция)
Французский (Швейцария)
Хинди
Хинди (Индия)
Хорватский
Имя культуры
Lt-sr-SP
syr
syr-SY
sk
sk-SK
si
sl-SI
th
th-TH
ta
ta-IN
tt
tt-RU
tr
tr-TR
uz
Cy-uz-UZ
Lt-uz-UZ
uk
uk-UA
ur
ur-PK
fa
fa-IR
fi
fi-FI
fr
fr-BE
fr-CA
fr-LU
fr-MC
fr-FR
fr-CH
hi
hi-IN
hr
Код культуры
0x081A
Ox005A
0x045A
OxOOlB
0x041B
0x0024
0x0424
OxOOlE
0x041E
0x0049
0x0449
0x0044
0x0444
OxOOlF
OxO41F
0x0043
0x0843
0x0443
0x0022
0x0422
0x0020
0x0420
0x0029
0x0429
OxOOOB
0x040B
0x00ОС
0x080C
OxOCOC
0xl40C
0xl80C
0x040C
OxlOOC
0x0039
0x0439
OxOOlA
Приложение 2 Имена и коды национальных культур
18 Язык С# Самоучитель
529
Национальный язык, страна или район
Хорватский (Хорватия)
Чехословацкий
Чехословацкий (Чехословацкая Республика)
Шведский
Шведский (Финляндия)
Шведский (Швеция)
Эстонский
Эстонский (Эстония)
Японский
Японский (Япония)
Имя культуры
hr-HR
CS
cs-CZ
sv
sv-FI
sv-SE
et
et-EE
ja
ja-JP
Код культуры
0x041A
0x0005
0x0405
OxOOlD
0x081D
0x041D
0x0025
0x0425
0x0011
0x0411
530
А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Библиография
1. Страуструп Б. Язык программирования C++. СПб.; М.: «Невский Диалект»: «Изд-
во БИНОМ», 1999.
2. Фролов А. В., Фролов Г. В. Создание Web-приложений: Практ. руководство.
М.: Издат.-торг. дом «Рус. Редакция», 2001.
3. Фролов А. В., Фролов Г. В. Практика применения PERL, PHP, Apache и MySQL
для активных Web-сайтов. М.: Издат.-торг. дом «Рус. Редакция», 2002.
4. Петцольд Ч. Код. М.: Издат.-торг. дом «Рус. Редакция», 2001.
5. Шарова И. X. Зоология беспозвоночных. М.: Гуманит. изд. центр «ВЛАДОС»,
1999.
6. Некрасов Б. В. Учебник общей химии. М.: Химия, 1981.
7. Рывкин А. А. и др. Справочник по математике. М.: Высш. шк., 1975.
8. Роджерсон Д. Основы СОМ. М.: Издат.-торг. дом «Рус. Редакция», 2000.
9. Армстронг Т. ActiveX: создание Web-приложений. Киев: Издат. Гр. BHV, 1998.
10. Фролов А. В., Фролов Г. В. Программирование для Windows NT. Ч. 1 М.: Диалог-
МИФИ, 1996. (Биб-ка системного программиста; Т. 26).
11. Фридл Дж. Регулярные выражения. Библиотека программиста. СПб.: Питер, 2001.
12. Фролов А. В., Фролов Г. В. Программирование для Windows NT. Ч. 2 М.: Диалог-
МИФИ, 1997. (Библ-ка системного программиста; Т. 27).
13. Фролов А. В., Фролов Г. В. Всемирная паутина. Ваш спутник в Интернете. М.: Из-
Издат.-торг. дом «Рус. Редакция», 2000.
14. Фролов А. В., Фролов Г. В. Всемирная паутина. Электронная почта. М.: Издат.-
торг. дом «Рус. Редакция», 2000.
15. Фролов А. В., Фролов Г. В. Всемирная паутина. Интернет-тусовка. М.: Издат.-
торг. дом «Рус. Редакция», 2000.
16. Фролов А. В., Фролов Г. В. Базы данных в Интернете: практическое руководство
по созданию Web-приложений с базами данных. М.: Издат.-торг. дом «Рус. Редак-
Редакция», 2000.
17. Фролов А. В., Фролов Г. В. Сервер Web своими руками. М.: Диалог-МИФИ, 1997.
(Библ-ка системного программиста; Т. 29).
ДИМСШКВИ 531
18. Фролов А. В., Фролов Г. В. Сценарии JavaScript в активных страницах Web.
М.: Диалог-МИФИ, 1998. (Библ-ка системного программиста; Т. 34).
19. Фролов А. В., Фролов Г. В. Грузите файлы на Web-сервер браузерами. //Открытые
системы, МИР ПК. № 3 — 4. 1999.
20. Фролов А. В., Фролов Г. В. Немного Java— и страница ожила. //Открытые систе-
системы, МИР ПК. № 12. 1998. № 1. 1999.
21. Фролов А. В., Фролов Г. В. Активный сервер Web: расширения CGI. //Открытые
системы, МИР ПК. № 8., 1997.
22. Дилип Н. Стандарты и протоколы Интернета. М.: Издат.-торг. дом «Рус. Редак-
Редакция», 1999.
23. Пройдаков Э. М., Теплицкий Л. А. Англо-русский толковый словарь по вычисли-
вычислительной технике, Интернету и программированию. М.: Издат.-торг. дом «Рус. Ре-
Редакция», 2000.
532 А В. Фролов, Г. В Фролов. Язык С#. Самоучитель
Предметный указатель
.NET Common Type System 192
as- 195,269
ASCII- 368,475,482,486
ASP- 13,14
assembly- 36,134
attribute- 318
«умные» поля • 207
Abort- 326
abstract- 174,217,222
accessor • 208
Active Server Pages ■ 13
ActiveX- 5,17,19,20,318
Add- 408,433
And- 459
AND- 73,457
ANSI- 24,43,483,486
apartment threading 319
apartments • 319
API- 14,21,287,314
Append- 472,504
Apple- 313
application domains • 19, 315
Application Program Interface 14,287,314
Archive- 512
ArgumentNullException • 407
ArgumentOutOfRangeException ■ 407,410
array • 225
Array- 227,411
Array.BinarySearch ■ 248,249
Array .Reverse • 247
Array.Sort • 247
ArrayList- 406,407
ArrayList.Capacity • 411
ArrayList.Count • 411
в
base- 122,149
Basic- 3,5,8,9,10,12,13,14,16,22
Begin- 514
binary • 68
BinaryReader ■ 465,472,474,475,479,488,
514
BinaryReader.ReadString • 523
BinarySearch • 424
BinaryWriter ■ 465,472,474,479,488,514
BitArray- 406,457
boob 50,88
Borland- 23
Borland C++Builder ■ 22
Borland Delphi- 7,8,16,22
Borland Java Builder • 10
Borland SideKick 312
Borland Turbo Pascal 22
boxing ■ 194
break- 82,95,96,102
BufferedStream ■ 467
byte- 46,88
CanSeek- 514
case- 92
catch ■ 89, 288,290,297, 307
CGI- 13
char- 49,88
checked- 191,296
533
child- 124
class- 113
class member- 114
Clear- 436,448,451,455
Clone- 452,455
Close- 474,500
CLR- 15,18,19
CLS- 16
Code Pages)- 481
collection • 406
COM- 14,17,19,318
Common Language Runtime • 15,18
Common Language Specification ■ 16
Compare ■ 383
CompareTo- 423
Component Object Model • 14,318
Compressed- 512
computer • 40
Console- 32,468
Console.Out- 469
Console.ReadLine ■ 469
Console. WriteLine ■ 386
const- 141
Contains • 455
ContainsKey • 435
ContainsValue • 435
continue- 82,97,102
Convert- 88
cooperative multi-threading • 312
CopyTo- 452,455
Count- 410
Create- 472
CreateNew- 472
critical sections • 343
CryptoStream ■ 467
csc.exe • 33
CTS- 192
culture ■ 385
Current- 409,514
сортированный список• 441
D
data stream • 464
DateTime- 511
DBCS- 484
decimal- 49,88
default- 92
Default- 486
default constructor ■ 147
delegate- 25,316,317
Dequeue • 454,455
derived • 124
Device- 512
Directory- 465,466,506,512
Directory .CreateDirectory • 508
Directory.Delete • 508
Directory.GetCreationTime • 508
Directory.GetCurrentDirectory ■ 506
Directory .GetDirectoryRoot • 506
Directory .GetFileSystemEntries • 506
Directory.GetLogicalDrives • 506
Directory .GetParent ■ 509
Directorylnfo • 466,509
DLL- 17,19
do- 82,95,98,231
DotGNU- 18
DotGNUPortable.NET- 23
double- 48,88
double-byte character se t484
downcast • 195
Dynamic Load Libraries • 17
E
early binding • 172
EBCDIC- 481
Encoding ■ 486
Encrypted- 512
End- 514
EndOfStreamException • 523
Enqueue■ 454
enum • 50
Equals- 180
Error- 468,470
escape-последовательность • 52
event- 317,358
event handler • 359
534
А В. Фролов, Г. В. Фролов. Язык С# Самоучитель
events driven program 358
events publishing • 359
events subscribing 359
Exception ■ 297
Exists- 510
explicit • 200
exception • 288
GetKey- 444
GetLastAccess ■ 509
GetLastAccessTime • 511
GetLastWritc • 509
GetLastWriteTime ■ 511
GetLowerBound • 245, 247
GetType- 183
GetUpperBound • 245,247
goto- 82,102
false- 50,78
FAR- 33
File- 465,466,510
Filc.Exists- 510
File.GetCreationTime ■ 511
FileAccess- 468,473
File Attributes ■ 511
Filelnfo- 466
FileMode- 468
FileNotFoundException ■ 523
FileShare- 468,473
FilcStream • 466,472,474,479,488,514
Filesystemlnfo • 466
Finalize- 183
finally- 288,307
fixed- 372
FixedSize- 420
float- 48,88
Flush- 495
for- 82,95,231,339
forcach- 82,95,99,231,233
format string • 387
free threading • 319
FrecBSD- 11
FTP- 310
garbage collection 15
get- 208,209,241,277
GetBuffer- 500
GetBylndex • 444
GetEnumerator • 409,433
GetHashCode ■ 180
H
Hashtable- 406,430
Hidden- 512
Highest- 337
HTML- 11,310
Hyper Text Markup Language • 11
IBM 360/370- 18,481
IBM OS/2- 312
IBM OS/2 Warp- 313
IBMVM- 18
IComparable- 423
if- 82,83,92
implicit- 200
In- 468
include-файлы ■ 26
indexer- 238
IndexOfKey- 445
IndexOfValue • 445
InfoEventArgs • 361
Inheritable • 473
inner exception • 303
Insert- 377,504
InstallShield • 20
instance member • 137
int- 47,88
Intel 80286- 313
Intel 8080- 38
interface ■ 250
internal • 134
InvalidOperationException • 454,455
Предметный указатель
535
IOExccption - 469, 508, 523
is- 195,269
IsReadOnly- 418
IsSynchronized • 428,451,455
Item- 433
J
jagged- 228
Java- 5,10,11,12,16,19,21,23,24
JavaScript- 12
JIT- 19
join • 341
Join- 341
JScript- 13
JScript.NET- 14
к
XV
Key■ 433
L
late binding • 172
left value- 60
Length- 245,515
T " A £L 1Л • i \ A in лч лпл
Linux ■ 4, 6, 10, 11, 14, 18,23,283
LiveScript- 12
load factor ■ 432
lock- 344,429
lone • 47 88
Lowest ■ 337
lvalue- 60
M
Macintosh ■ 313
Main- 32
Managed C++, • 14
managed code ■ 18
Managed Extensions • 17
manifest ■ 20
MarshalByRefObject • 466
MemoryStream ■ 467,495
MFC- 4,7,17,19,20,21
Microsoft .NET Framework ■ 14,15,128,192,
285
Microsoft .NET Framework SDK • 26, 32
Microsoft C#- • 14
Microsoft FrontPage • 12
Microsoft Intermediate Language • 16
Microsoft Internet Explorer • 13
Microsoft Visual Basic • 10,13,22
Microsoft Visual Basic .NET • 10
Microsoft Visual J# .NET • 14
Microsoft Visual Studio .NET • 14,22,26, 33
Microsoft Windows Scripting Host • 13
Microsoft Word • 33
Monitor- 348
Move- 513
MoveNext ■ 433
MSDN- 349
MSDNLibrary- 33
MS-DOS- 9,18,43,312,482
MSIL- 16,18,19,46
MTA- 319
multi-threaded- 310
multi-threaded apartments • 319
Mutex- 345,347
NI
N
namespace ■ 30
Netscape Communication Corporation ■ 12
Netscape Navigator • 13
NetworkStream ■ 467
new ■ 146,172
None- 473
Normal- 512
Not- 459
NOT- 73,457
NotContentlndexed ■ 512
Notepad ■ 32
NotSupportedException • 408
Novell NetWare • 313
null- 54,297
536
А В. Фролов, Г В Фролов Язык С# Самоучитель
о
Oak- 5,10
Object- 126,180,465
OEM- 484
Offline- 512
Open- 472
OpenOrCreate • 473
operator- 200
Or- 459
OR- 73,457
Original Equipment Manufacturer • 484
out- 156,158
Out- 468,469,470
override • 170,172,174,222
PadLeft- 380
PadRight- 380
parent- 122
Parse- 403
Pascal • 4,5,7,8,17,18,20,22,42,45,73,
315,368
Path- 465
peek- 450
Peek- 455
Perl- 13,16,368
PHP- 14
PHP Hypertext Preprocessor ■ 14
pop- 450
Pop- 450
Position- 514
preemptive mult-threading • 313
printf- 386
priority ■ 337
private- 130
process- 314
project • 35
properties- 205,237
protected • 133
public ■ 114,116, 132, 147,206, 241
push- 450
Push- 450
Queue- 406,454
Quicksort- 423
R
RAD- 7
Rapid Application Development ■ 7
Read- 473,496,502
ReadBoolean- 475
ReadByte- 475,496
ReadBytes- 475
ReadChar- 475
ReadChars- 475
ReadDecimal- 475
ReadDouble- 475
Readlntl6- 475
Readlnt32- 475
Readlnt64- 475
ReadLine- 32,88
readonly- 152,361
Readonly- 418,512
ReadSByte- 475
ReadSingle- 475
ReadString- 475
ReadUIntl6- 475
ReadUInt32- 475
ReadUInt64- 475
ReadWrite- 473
ref- 156,158
Regex- 368
regular expression • 368
Remove- 378,436,505
RemoveAt- 448
ReparsePoint ■ 512
Replace- 377,505
return- 82,102,209
Reverse • 424
Предметный указатель
537
sandbox ■ 10
sbyte- 47,88
sealed ■ 159
SecurityException • 523
Seek- 466,513,514
SeekOrigin- 468
set- 208,241,277
SetAttributes • 513
SetBylndex- 446
SetCreationTime ■ 513
SetLastAccessTime ■ 513
SetLastWriteTime ■ 513
SetValue- 245
short- 47,88
Single Thread Apartments • 318
single-threaded- 310
single-threaded apartment ■ 35,319
Sleep- 330
smart fields • 207
solution ■ 35
Sort- 421
SortedList- 406,441,442
SparseFile- 512
Split- 382
sprintf- 386
STA- 35,318,319
stack- 449
Stack- 406
Standard Template Library • 17
Start- 342
STAThread- 318,319
static- 32,140,151,174,222
STL- 17
StreamReader ■ 467,472,479,493
StreamWriter • 468,472,479,486,493
string ■ 49
String.Concat • 375
String.Format • 386
String.Join • 381
StringBuilder ■ 368,504
StringReader ■ 468,502
StringWriter ■ 324,468
Substring • 376
Sun Microsystems ■ 10,11
switch- 82,91
Synchonized • 451,455
Synchronized • 426,427,438,449
SyncRoot ■ 428,438,449,451,455
System- 30,512
System.ArithmeticException • 297
System.ArrayTypeMismatchException • 297
System.Collections.IDictionaryEnume-
rator- 433
System.Collections.ffinumerator ■ 409,451,
455,459
System.Console.Write ■ 465
System.Console.WriteLine • 465
System.DateTime • 361
System.DivideByZeroException • 285,292,
297
System.Exception ■ 289,294
System.Exception.Message ■ 290,294
System.Exception.Source • 290,294
System.Exception.StackTrace ■ 290,294
System.FormatException • 403,405
System.Globalization.Culturelnfo ■ 385
System.IndexOutOfRangeException • 293,
297
System.InvalidCastException ■ 293,297
System.IO.Stream ■ 466
System.IO.TextReader • 467,468
SystemJO.TextWriter ■ 467,468
System.MulticastNotSupportedExcep-
tion- 297
System.NotSupportedException • 419
System.NullReferenceException • 297
Systcm.Object • 183
System.OutOfMemoryException ■ 298
System.OverflowException • 192,296,298
System.StackOverflowException ■ 298
System.String ■ 368,369
System.Thread • 315
System.Threading • 315
System.Threading.ThreadAbortExcep-
tion • 326
System.Threading.Timeout.Infinite • 330
System.TypelnitializationException ■ 298
538
А В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
TCP/IP- 17
Temporary- 512
TextReader- 468,469,514
TextWriter- 469,514
this- 149,151,163,241
thread- 314,464
Thread- 326
ThreadAbort ■ 330
ThreadJoin- 341
Thread.Resume ■ 333
Thread.Sleep • 321,339
throw- 288
TimeSpan- 330
ToArray- 500
ToBoolean- 88
ToByte- 88
ToChar- 88
ToDecimal- 88
ToDouble- 88
Tolntl6- 88
Tolnt32- 88
Tolnt64- 88
ToLower- 380
ToSbyte- 88
ToSingle- 88
ToString- 183
ToUintl6- 88
ToUlnt32- 88
ToUInt64- 88
ToUpper- 380
Trim- 379
TrimEnd- 379
TrimStart- 379
true- 50,78
Truncate- 473
try- 89,288,297,307
unary • 68
unchecked- 296
UNICODE • 24,44,49,52,293, 368, 380,
481,485,486
unsafe- 372
unsafe code ■ 25
upcast- 195
ushort- 46,88
using- 30,120,316
UTF7- 486
UTF8- 486
value- 209,241
Value- 433
VBScript- 13
verbatim • 54
virtual • 170,172, 174, 217, 222
Visual Perl • 16
VMWare- 18
void- 32,110,297
W
Web- 310
Web-браузер • 11
while- 82,95,98,231
white space • 379
Win32- 346
Windows Forms • 319
Write- 88,470,473,474,488,498,505
WriteByte- 498
WriteLine • 32,58,71, 88,469,470, 505
WSH- 13
U
UCS Transformation Format • 486
uint- 46,88
ulong- 46,88
XML- 17
Xor- 459
XOR- 73,457
Предметный указатель
539
абстрактное свойство 221
абстрактный класс • 172
активные серверные страницы 13
апартаменты • 319
аргументы запуска программы ■ 35
арифметико-логическое устройство ■ 38
ассемблер • 5
атрибут- 35,318
виртуальные устройства ввода-выво-
ввода-вывода- 18
виртуальный диск • 18
виртуальный метод ■ 170
виртуальный процессор 18
внутреннее исключение • 303
восходящее приведение типов ■ 195
выделение тысяч ■ 394
выражение • 55
вытесняющая многопоточность ■ 313
вытесняющая мулътизадачноетъ • 313
базовый класс • 122
базовый тип перечисления 57
байт • 39
байт-код ■ 10, 16
безопасное преобразование типов • 199
безопасный указатель • 317
белый пробел 379
библиотека динамической загрузки ■ 9
библиотека динамической компонов-
компоновки • 17
библиотека классов • 15,17
библиотека стандартных функций 6
библиотеки функций • 17
библиотеки шаблонов ■ 17
бинарная операция • 60
бинарный оператор ■ 177
бит- 39
битовый массив • 457
браузер ■ 11
буквальный литерал • 54
буфер потока • 500
буферизация • 491
В
виртуальная машина • 18
виртуальная машина Java • 19
виртуальная оперативная память ■ 18
виртуальная функция • 172
виртуальные ресурсы компьютера ■ 18
гигабайт- 40
глобальная переменная ■
141
Д
данные - 38
двухбайтовые символы 484
декремент ■ 69
делегат- 25,316,350
деление по модулю ■ 67
деньги ■ 49
дерево иерархии классов ■ 125
деструктор- 151
дизайнер Web-форм ■ 12
динамическая инициализация масси-
массива- 226
динамический документ HTML • 13
домен приложений 315
домены приложений ■ 19
дословный литерал • 54
дочерний класс • 124
драйвер ■ 5
Ё
ЕС- 481
540
А В. Фролов, Г. В Фролов. Язык С# Самоучитель
закрытый конструктор • 148
запись в стек • 450
значение • 55
значение в левой части • 60
И
И- 73
извлечение из стека • 450
изменение приоритетов потоков • 337
ИЛИ- 73
имя переменной • 41
индекс- 99,225
индексатор- 238,279,282
индексаторы ■ 274
инициализатор • 56
инициализатор конструктора ■ 149
инкапсуляция ■ 117
инкремент ■ 69
интерпретатор языка Basic ■ 9
интерфейс ■ 124,250
интерфейс программирования • 14
ИСКЛЮЧАЮЩЕЕ ИЛИ- 73
исключение • 67,89,192,285,288
итерационные операторы ■ 82,95
К
каталог • 506
килобайт • 40
кириллица• 43
класс- 25,30,113
классификатор доступа ■ 114
клиентский сценарий • 12
ключевые слова С# 41
код возврата- 21
кодирование символов 43
кодировка • 481
кодовые страницы 481
коды машинных команд ■ 19
команда• 38
комбинированный интерфейс 263
комментарии • 35,46
компилятор just-in-time ■ 19
комплексное число • 175
компьютер • 40
конкатенация ■ 62
конкатенация строк ■ 375
консольная программа • 32
консольное окно ■ 32
конструктор • 114,147
конструктор по умолчанию •
контейнер • 99,126,406
критическая секция • 343
культура• 385
147
Л
литерал • 51
логическая переменная • 50
логический литерал ■ 54
логический оператор • 78
локальная переменная ■ 107
М
макрокоманда - 26
массив- 25,99,225,282,406
массивы массивов - 228
мастер проекта • 36
матрица • 227
машинная команда • 5,19
мегабайт ■ 40
мейнфрейм • 18
метка ■ 103
метод- 108,110
методы передачи параметров 155
многомерный массив ■ 227,234,242
многопоточная модель ■ 319
многопоточность■ 310
множественное наследование • 124
моделирование• 7
модель компонентных объектов 14, 318
модель многопоточности • 318
модель разделенных потоков ■ 319
модель свободных потоков • 319
модификатор доступа • 129
Предметный указатель
541
н
набор данных • 406
набор регистров 38
набор символов ШМ • 485
наследование ■ 122,269
научный формат • 392
НЕ- 73
небезопасный код • 25
недопустимое значение индекса • 102
незначащий пробел 50
незначащий символ • 379
неявное приведение типов • 187
нисходящее приведение типов • 195
О
обработка исключений ■ 21,67
обработчик события ■ 359
объект• 108
объект Mutex • 345
объектно-ориентированная обработка
исключений • 21
объектно-ориентированный подход 6
объявление индексатора • 240
объявление интерфейсов • 251
однозадачная модель разделенных пото-
потоков • 35
одномерный массив • 225
однопоточная модель ■ 319
однопоточность • 310
ожидание завершения работы задачи • 341
ООП- 108
операнд • 55
оперативная память • 40
оператор • 55
оператор выбора • 91
оператор отношения 79
оператор приведения типа ■ 190
оператор проверки • 86
оператор структурного программирова-
программирования • 6
операторы безусловного перехода • 82, 102
операторы выбора ■ 82
операция дополнения • 76
оптимизирующий компилятор • 6
остаток от деления ■ 84
отладка программ • 59
очередь • 406,453
ошибка • 284
П
пакетный режим работы ■ 358
перегруженный конструктор ■ 149
перегрузка методов ■ 144
перегрузка операторов • 174,177
передача параметров по ссылке • 156
переключательная многопоточность • 312
переключательная мультизадач-
ность- 312
переменная- 41
переменная цикла • 96
переопределение операций • 62
перечисления • 50
песочница- 10
планировщик ■ 336
подписка на события 359
подстрока ■ 376
позднее связывание 172
поиск• 248
поле класса- 113
полиморфизм • 161
поразрядные операторы • 73
поразрядный сдвиг ■ 73,76
поток- 314
поток в оперативной памяти 495
поток выполнения • 464
поток данных • 464
потоки на базе строк string • 502
преобразование типов • 187
препроцессор гипертекста РНР • 14
прерывания системного таймера ■ 336
приведение типов • 187
приоритет • 337
приоритет операторов • 80
приоритеты задач • 337
присваивание • 55
542
А В Фролов, Г. В. Фролов. Язык С# Самоучитель
проверка значения параметров • 285
проверка кода возврата • 287
проверка содержимого верхушки сте-
стека- 450
программирование в машинных кодах ■ 38
программный интерфейс ■ 287
программы, управляемые событиями 358
проект• 35
производный класс • 122,124
промежуточный язык • 16
пространство имен ■ 30,120
процедура доступа • 208
процедурное программирование • 6
процент• 399
процесс- 314
процессор • 5
псевдонимы ■ 192
публикация событий 359
пустой оператор ■ 106
разделы- 319
размерный тип данных • 25
разряд • 39
раннее связывание 172
реализация интерфейса • 251
регулярное выражение • 368
резидентная программа • 312
решение • 35
родительский класс • 122
сборка • 19,36,134
свойства- 205,237,274
серверные сценарии РНР 14
серверный сценарий ■ 13
символ новой строки • 50
символы алфавита • 42
символьный литерал UNICODE • 53
синхронизация задач • 340
синхронизированный массив • 427
система быстрой разработки приложе-
приложений ■ 7
система исполнения программ Common
Language Runtime ■ 15
система сборки мусора 15,21, 151
системы виртуальных машин • 18
слияние строк 62
словарь • 406,430
событие- 25,274,317,358
совместная многопоточность • 312
сортированный список • 406
сортировка • 248,421
составной оператор • 72,107
список инициализации массива ■ 226
ссылка на объект • 115
ссылочный тип данных • 25
стандартная библиотека шаблонов • 17
стандартные классы исключений С# • 297
стандартные потоки • 464
стандартный поток ввода 468
стандартный поток вывода 468
стандартный поток вывода сообщений
об ошибках - 468
статическая инициализация массива ■ 226
статический конструктор • 151
статический метод • 32
статический член класса ■ 138
статическое поле класса ■ 139
статическое свойство 222
стек- 406,449
страница 1251 • 484
страница оригинальных производителей
оборудования OEM ■ 484
строка формата ■ 387
строковый литерал ■ 53
структура • 25
структура программы ■ 104
схемы адресации • 38
сценарий JavaScript ■ 12
таблица приоритетов операторов С# • 80
текущая позиция внутри файла • 464
терабайт ■ 40
тернарный оператор ■ 86
Предметный указатель
543
указатель- 7,25,350
унарная операция • 61
унарный оператор • 68,177,178
универсальный формат ■ 395
упаковка■ 194
управляемый код • 18
управляющее устройство • 38
управляющие операторы ■ 82
условный оператор ■ 79, 82
Ф
файл ■ 464
файл манифеста • 20
фактор загрузки • 432
фоновый процесс сборки мусора 21
функция обратного вызова ■ 317, 3 50
центральный процессор 38
цикл • 231
член класса • 114
член экземпляра класса ■
137
Ш
шаблоны • 397
шестнадцатеричное представление чи-
чисел ■ 40
ширина поля вывода • 3 89
элемент управления ActiveX • 14
хеш-код- 180,433
Ц
целочисленный литерал • 51
Я
явное преобразование типов • 195
явное приведение типов • 187
язык ассемблера ■ 38
язык разметки гипертекста ■ 11
язык сценариев • 12
544
А В Фролов, Г. В Фролов. Язык С#. Самоучитель
Оглавление
ВВЕДЕНИЕ.
от ассемблера к с# 3
Классические языки программирования 5
Ассемблер 5
С 6
C++ 6
Pascal 7
Basic 8
Java 10
Языки для создания Интернет-приложений 11
html и
JavaScript 12
JScript 13
VBScript 13
Perl 13
PHP 14
Новые технологии Microsoft .NET 14
Платформа Microsoft .NET Framework 15
Совмещение разных языков программирования 16
Интегрирование с ранее созданными проектами 16
Библиотека классов Microsoft .NET Framework 17
Виртуальная машина CLR 18
Домены приложений 19
Компилятор JIT 19
Сборки 19
Упрощение отладки программ С# 20
Программирование на С# для Microsoft Windows 21
Проект DotGNU, или С# для Linux 23
Отличия С# от C++ 23
Классы и наследование 23
Интерфейсы 24
545
Типы данных 24
Указатели 25
Массивы 25
Структуры 25
Операторы и ключевые слова 25
Директивы препроцессора 26
ЧЕМ ТРАНСЛИРОВАТЬ ПРОГРАММЫ С# 26
Условные обозначения в книге 27
Благодарности 28
Как связаться с авторами книги 28
ГЛАВА 1. БАЗОВЫЕ ПОНЯТИЯ И ОПРЕДЕЛЕНИЯ 29
Первая программа на языке С# 29
Исходный текст простейшей программы 29
Пространство имен System 30
Определение собственного пространства имен 30
Класс HelloApp 31
Метод Main 31
Трансляция программы при помощи .NET Framework SDK 32
Использование Microsoft Visual Studio .NET 33
Создание нового проекта 33
Проекты и решения 35
Изменение проекта 36
Элементарные типы данных 38
Бит 39
Байт 39
Числовые типы данных 40
Текстовые символы и строки 42
Обозначение типов данных в С# 45
Числа без знака 45
Числа со знаком 47
Числа с плавающей точкой 48
Числа для финансистов 48
Текстовые символы 49
Текстовые строки 49
Логический тип данных 50
Перечисления 50
5Л6 А. В Фролов, Г. В. Фролов. Язык С#. Самоучитель
i
Литералы 51
Целочисленные литералы 51
Литералы с плавающей точкой 52
Символьные литералы 52
Строковые литералы 53
Логические литералы 54
Литерал null 54
Базовые выражения и операторы С# 55
Инициализация переменных и оператор присваивания 55
Инициализация перечислений 56
Проверка результата инициализации 57
Значение в левой части 60
Математические операторы 60
Сложение 60
Вычитание 62
Умножение 64
Деление 65
Вычисление остатка при целочисленном делении 67
Унарные операторы 68
Унарный плюс 68
Унарный минус 68
Инкремент и декремент 69
Унарное логическое отрицание 69
Унарная поразрядная операция дополнения 70
Преобразование типа выражения 70
Пример использования унарных операторов 70
Составные операторы 72
Поразрядные операторы 73
Поразрядное логическое И 74
Поразрядное логическое ИЛИ. 74
Поразрядное логическое ИСКЛЮЧАЮЩЕЕ ИЛИ. 75
Унарная поразрядная операция дополнения 76
Поразрядный сдвиг 76
Пример использования поразрядных операторов 77
Логические операторы 78
Операторы отношения 79
Приоритеты операторов 79
ГЛАВА 2. УПРАВЛЯЮЩИЕ ОПЕРАТОРЫ 82
УСЛОВНЫЙ ОПЕРАТОР 83
Простой условный оператор 83
Оглавление 547
Вложенный условный оператор 84
Оператор проверки 86
Множественный выбор 86
Применение логических операций 90
Оператор выбора 91
Примеры применения оператора выбора 92
Объединение меток case 94
Пропущенный break 94
Итерационные операторы 95
Оператор for 95
Прерывание цикла 96
Возобновление цикла 97
Оператор while 98
Оператор do 98
Оператор foreach 99
Пример использования итерационных операторов 100
Операторы безусловного перехода 102
Операторы break и continue 102
Оператор return 102
Оператор goto 102
Организация цикла с помощью goto 102
Применение goto в операторе выбора switch 104
Другие применения оператора goto 106
Пустой оператор 106
Составной оператор 107
ГЛАВА 3. ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ
ПРОГРАММИРОВАНИЕ 108
Первые шаги к ООП 108
Программная модель телевизора 109
Данные 109
Методы 110
Объединяем все вместе 112
Создание объектов класса TelevisionSet 115
Вызов методов класса //5
Обращение к полям класса 116
Пример программы 117
548 А. В. Фролов, Г. В. Фролов. Язык С#. Самоучитель
Наследование 121
Базовый класс 122
Производный класс 123
Множественное наследование 124
Представление иерархии классов 125
Пример программы 126
Маскирование методов базового класса 128
Модификаторы доступа 129
Модификатор private 130
Модификатор public 132
Модификатор protected 133
Модификатор internal 134
Пример программы 135
Статические члены класса 137
Статические поля класса 139
Статические константы 141
Статические методы класса 142
Перегрузка методов 144
Конструктор 146
Конструктор по умолчанию 147
Конструкторы и наследование 148
Инициализатор конструктора base 149
Инициализатор конструктора this 151
Статический конструктор 151
Деструктор 151
Еще о классах и полях 152
Поля readonly 152
Передача параметров по ссылке 154
Запрет наследования классов 159
ГЛАВА 4. ПОЛИМОРФИЗМ 161
Применение полиморфизма 161
Применение классов 161
Попытка обобщения с помощью наследования 164
Виртуальные методы 167
Раннее и позднее связывание 172
Оглавление 549
Абстрактные классы 172
Перегрузка операторов 174
Краткая теория комплексных чисел 175
Сравнение 175
Сложение 175
Вычитание 175
Умножение 175
Деление 176
Класс для представления комплексных чисел 176
Перегрузка бинарных операторов 177
Перегрузка унарных операторов 178
Перегрузка операторов сравнения 179
Пример программы 180
Класс System.Object 183
ГЛАВА 5. ПРЕОБРАЗОВАНИЕ ТИПОВ ОБЪЕКТОВ 187
Неявное преобразование числовых типов 187
Числа без знака 188
Числа со знаком 189
Текстовые символы char 189
Числа с плавающей точкой 190
Явное преобразование числовых типов 190
Проверка преобразования числовых типов 191
Преобразования типов и классы 192
Псевдонимы типов данных 192
Приведение производных и базовых классов 194
Операторы is и as 195
Нестандартное преобразование 199
ГЛАВА 6. СВОЙСТВА ОБЪЕКТОВ 205
Объявление свойства 207
Процедура доступа set 208
Процедура доступа get 209
Свойства только для чтения и только для записи 209
Пример программы 210
550 А В Фролов. Г. В. Фролов. Язык С#. Самоучитель
Наследование свойств 217
Статические свойства 222
ГЛАВА 7. МАССИВЫ И ИНДЕКСАТОРЫ 225
Типы массивов 225
Одномерные массивы 225
Многомерные массивы 227
Массивы массивов 228
Пример программы 229
Массивы и циклы 231
Обработка одномерного массива чисел 231
Обработка одномерного массива строк 232
Использование оператора foreach 233
Многомерный массив объектов класса String 234
Несимметричный массив объектов класса String 236
Индексаторы 237
Объявление индексатора 240
Индексаторы многомерных массивов 242
Дополнительные операции с массивами в С# 245
Определение размера массива 245
Сортировка и реверсирование массивов 247
Поиск в массиве 248
ГЛАВА 8. ИНТЕРФЕЙСЫ 250
Применение интерфейсов 250
Объявление интерфейса 251
Реализация интерфейса 251
Вызов методов интерфейса 253
Пример программы 253
Проверка реализации интерфейса 255
Комбинированные интерфейсы 263
Интерфейсы и наследование классов 269
Свойства в интерфейсах 274
Индексаторы в интерфейсах 279
Оглавление 551
ГЛАВА 9. ОБРАБОТКА ИСКЛЮЧЕНИЙ 283
Классические способы обработки ошибок 284
Предварительная проверка параметров 285
Проверка кодов возврата функций и методов 285
Применение механизма исключений 288
Блоки try-catch 288
Использование нескольких блоков catch 290
Исключение при арифметическом переполнении 294
Стандартные классы исключений 297
Создание исключений 298
Создание исключений класса Exception 298
Новый класс на базе класса Exception 300
Конструкторы класса Exception 303
Передача исключения для повторной обработки 304
Применение блока finally 307
ГЛАВА 10. МНОГОПОТОЧНОСТЬ 310
Виды многопоточности 312
Переключательная многопоточность 312
Совместная многопоточность 312
Вытесняющая многопоточность 313
Процессы, потоки и приоритеты 314
Процесс 314
Поток 314
Домен приложения AppDomain 315
Примеры многопоточных программ 315
Создание и запуск потока класса Thread 315
Использование делегатов 317
Модели многопоточности 318
Завершение работы созданного потока 319
Потоки и классы 323
Управление потоками 326
Аварийное завершение потока 326
552 А. В. Фролов. Г. В. Фролов. Язык С#. Самоучитель
Пауза в работе потока 330
Приостановка и возобновление работы 333
Управление приоритетами потоков 336
Синхронизация потоков 340
Ожидание завершения потока 341
Критические секции 343
Применение ключевого слова lock 344
Использование класса Mutex 345
Использование класса Monitor 348
ГЛАВА 11. ДЕЛЕГАТЫ И СОБЫТИЯ 350
Использование делегатов 351
Статические делегаты 353
Массивы делегатов 356
Обработка событий 358
Публикация событий 359
Создание события 361
Подписка на события 363
Программа с обработкой событий 364
ГЛАВА 12. РАБОТА С ТЕКСТОВЫМИ СТРОКАМИ 368
Применение класса System.String 369
Создание строк 369
Преобразование массива символов в строку 369
Создание строки на базе фрагмента массива 370
Заполнение строки символом 370
Небезопасные конструкторы в классе System.String 371
Копирование и клонирование строк 373
Конкатенация строк 375
Извлечение подстроки 376
Вставка подстроки 377
Замена символов и строк 377
Удаление символов из строки 378
Удаление незначащих пробелов 379
Преобразование к верхнему и нижнему регистру 380
Выравнивание по левому и правому краю поля 380
Объединение массива строк 381
Разбор строки 382
Сравнение строк 383
Оглавление 553
Форматирование текстовых строк 386
Представление целых чисел 387
Автоматическое форматирование 388
Представление чисел в шестиадцатеричном формате 388
Определение ширины поля вывода 389
Выравнивание числа внутри поля вывода 389
Представление чисел с фиксированной десятичной точкой 390
Формат по умолчанию 391
Указание количества знаков после десятичной точки 391
Ограничение ширины поля вывода 392
Извлечение целой части числа 392
Избыточная точность при выводе 392
Представление чисел в научном формате 392
Выделение тысяч при отображении больших чисел 394
Универсальный формат для представления чисел 395
Формат для представления денежных сумм 396
Использование шаблонов при форматировании 397
Форматирование целых чисел 397
Форматирование чисел с плавающей десятичной точкой 398
Форматирование чисел с процентами 399
Форматирование с учетом знака чисел 400
Создание новых форматов 401
Преобразование текстовых строк в числа 403
ГЛАВА 13. КОНТЕЙНЕРЫ ДЛЯ ХРАНЕНИЯ
ОБЪЕКТОВ 406
Контейнеры в библиотеке классов .NET Framework 406
Массив ArrayList 407
Создание массива 407
Добавление элементов в массив 408
Чтение элементов массива 409
Использование итераторов 409
Использование индексов 409
Изменение элементов массива 410
Емкость и текущий размер массива 411
Объединение массивов 412
Удаление элементов из массива 414
Запрет записи данных в массив 417
554 А. В. Фролов, Г В. Фролов. Язык С#. Самоучитель
Ограничение размера массива 419
Сортировка 421
Обратное расположение элементов массива 423
Поиск в массиве 424
Работав многопоточном режиме 426
Применение метода Synchronized 426
Свойство IsSynchronized 428
Свойство SyncRoot 428
Недостатки массивов ArrayList 429
Словарь Hashtable 430
Создание словаря 430
Конструктор без параметров 430
Копирование словаря 431
Указание начальной емкости словаря 432
Другие конструкторы 432
Добавление новых элементов 433
Чтение содержимого словаря 433
Поиск по ключу 433
Предварительная проверка содержимого словаря 434
Удаление элементов из словаря 436
Емкость и текущий размер словаря 438
Словари и многопоточность 438
Сортированный список SortedList 441
Создание и наполнение списка 442
Извлечение данных из списка 442
Определение индекса по ключу и значению 444
Изменение значения по индексу 446
Удаление элементов списка 448
Работа в многопоточном режиме 449
Стек Stack 449
Создание стека 450
Добавление элементов в стек 450
Извлечение элементов из стека 450
Проверка содержимого верхушки стека 451
Просмотр стека с помощью итератора 451
Удаление элементов стека 451
Другие методы и свойства класса Stack 451
Пример программы 452
Оглавление 555
Очередь Queue 453
Создание очереди 454
Добавление элемента в очередь 454
Извлечение элементов из очереди 454
Проверка содержимого начала очереди 455
Просмотр очереди с помощью итератора 455
Удаление элементов очереди 455
Другие методы и свойства класса Queue 455
Пример программы 456
Битовый массив Bit Array 457
Создание массива BitArray 458
Инициализация ячеек массива 458
Извлечение значений из массива 459
Просмотр массива 459
Логические операции над массивами 459
ГЛАВА 14. ФАЙЛЫ И ПОТОКИ 464
Потоки данных и классы 464
Стандартные потоки 464
Базовые классы для работы с файлами и потоками 465
Основные классы ввода и вывода 465
Классы на базе Filesystemlnfo 466
Классы для работы с потоками 466
Классы для работы с потоками текстовых символов 467
Перечисления 468
Работа со стандартными потоками 468
Стандартный поток ввода 468
Стандартный поток вывода 469
Стандартный поток вывода сообщений об ошибках 470
Программа StdStreams 470
Создание потоков, связанных с файлами 472
Открытие потока FileStream 472
Открытие потоков BinaryWriter и BinaryReader 474
Закрытие потоков 474
Запись двоичных данных 474
Чтение двоичных данных 475
Программа Binary 476
Работа с текстовыми файлами 479
556 А В. Фролов, Г В. Фролов. Язык С#. Самоучитель
Выбор кодировки символов 481
Кодовые страницы 482
Недостатки модели кодовых страниц 484
Стандарт UNICODE 485
Unicode в Microsoft Windows NT/2000/XP 485
UNICODE в Microsoft Windows 95 485
Кодировка текстовых потоков 486
Кодировка текстовых строк в двоичных потоках 488
Буферизация потоков 491
Буферизация двоичных потоков 491
Буферизация текстовых потоков 493
Принудительный сброс буферов 495
ПОТОКИ В ОПЕРАТИВНОЙ ПАМЯТИ 495
Создание потока 495
Чтение данных 496
Запись данных 498
Доступ к буферу потока MemoryStream 500
Потоки на базе строк string 502
Потоки класса StringBuilder 504
Управление каталогами 506
Список логических дисков 506
Текущий каталог 506
Просмотр содержимого каталога 506
Создание каталога 508
Удаление каталога 508
Получение информации о каталоге 508
Управление файлами 510
Проверка существования файла или каталога 510
Создание файла 510
Удаление файла 511
Определение времени создания файла 511
Определение времени доступа и изменения файла 511
Определение атрибутов файлов и каталогов 511
Изменение атрибутов файлов и каталогов 513
Переименование или перемещение файлов 513
Произвольный доступ к файлам 513
Проверка возможности позиционирования 514
Метод Seek 514
Оглавление 557
Приложение DirectFileAccess 515
Класс SimpleDBMS 520
Конструктор класса SimpleDBMS 521
Добавление новой записи 521
Извлечение записи по ее номеру 522
Обработка исключений 523
ПРИЛОЖЕНИЕ 1. ЗАРЕЗЕРВИРОВАННЫЕ
КЛЮЧЕВЫЕ СЛОВА С# 524
ПРИЛОЖЕНИЕ 2. ИМЕНА И КОДЫ НАЦИОНАЛЬНЫХ
КУЛЬТУР 525
БИБЛИОГРАФИЧЕСКИЯ 531
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ 533
558 А В Фролов, Г. В. Фролов. Язык С#. Самоучитель
ааашашашшашашашашшшшашшшшшшаа
e-mail: dialog@bitex.ru
http://www.bitex.ru/~dialog
320-43-77, 320-43-55
факс: 320-31-33
шошЕашсзшшЕзшсасзсзшшшшшсэшшшшшшсэшшса
Предлагает литературу по программированию
и вычислительной технике
Авраамова О. Д. Язык VRML. Практическое руководство
Архипенков С. Я. Аналитические системы на базе Oracle Express OLAP
Архипенков С. Я., Голубев Д. В., Максименко О. Б. ХРАНИЛИЩА ДАННЫХ
Баженова И. Ю. VISUAL C++ 6.0
Бартеньев О. В. ФОРТРАН ДЛЯ ПРОФЕССИОНАЛОВ.
Математическая библиотека IMSL. В 3-х частях
Бартеньев О. В. 1С:ПРЕДПРИЯТИЕ: программирование для всех
Березин Б. И., Березин С. Б. Начальный курс С и C++
Ватолин Д., Ратушняк А., Смирнов М., Юкин В. МЕТОДЫ СЖАТИЯ ДАННЫХ.
Устройство архиваторов, сжатие изображений и видео.
Вовк Е. Т. PAGEMAKER 6.5. Самоучитель,- 2-е изд.
Гусева А. И. Учимся программировать: PASCAL 7.O. Задачи и методы их решения. -
2-е изд., перераб. и доп.
Гусева А. И. УЧИМСЯ ИНФОРМАТИКЕ. - 2-е изд., дополн. и испр.
Гусева А. И. СЕТИ И МЕЖСЕТЕВЫЕ КОММУНИКАЦИИ. Windows 2000
Епанешников А., Епанешников В. Программирование в среде TURBO PASCAL 7.0. -
4-е изд., испр. и дополн.
Епанешников А., Епанешников В. DELPHI 4. Среда разработки: Учебное пособие
Епанешников А., Епанешников В. DELPHI. Проектирование СУБД
Лукин С. Н. ТУРБО-ПАСКАЛЬ 7.0. Самоучитель для начинающих. -
2-е изд., испр. и дополн.
Лукин С. Н. VISUAL BASIC. Самоучитель для начинающих
Маклаков С. В. МОДЕЛИРОВАНИЕ БИЗНЕС-ПРОЦЕССОВ С BPWIN 4.0
Низаметдинова Н. Н. СОВРЕМЕННАЯ РУССКАЯ ПУНКТУАЦИЯ
Пильщиков В. Н. Программирование на языке АССЕМБЛЕРА
По.шщук В., Пол и щук A. AUTOCAD 2002
а □ а а а а а а а а з з =j 559 аоысзсасзсзсасзшсзсза
E-mail: dialog@bitex.ru. Http://www.bitex.ru/~dialog.
Тел.: 320-43-77, 320-43-55
Рудаков П. И., Финогснов К. Г. ЯЗЫК АССЕМБЛЕРА: уроки программирования
Хейфец А. Л. ИНЖЕНЕРНАЯ КОМПЬЮТЕРНАЯ ГРАФИКА. AutoCAD
Цисарь И. Ф., Нейман В. Г. КОМПЬЮТЕРНОЕ МОДЕЛИРОВАНИЕ ЭКОНОМИКИ
Шикин А. В., Боресков А. В. КОМПЬЮТЕРНАЯ ГРАФИКА. Полигональные модели
Книги по MATLAB
ШВпАкеты :
прикладных1
проф/ \мм
йШпДкеты
ПРИКЛ£ДДНЫХ:
проф/\мм i
Потемкин В. Г. Инструментальные средства
MATLAB 5.x
Потемкин В. Г., Медведев В. С.
Control System Toolbox. MATLAB 5 для студентов
Лавров К. Н., Цыплякова Т. П.
Финансовая аналитика. MATLAB 6.
(Под общ. ред. к. т. н. В. Г. Потемкина)
Потемкин В. Г., Медведев В. С.
Нейронные сети. MATLAB б
Новые книги
ф Разработка бизнес-приложений в экономике
на базе MS Excel. Под ред. к. т. н. А. И. Афоничкина
фФедоров А., Елманова Н.
Введение в OLAP-технологии Microsoft
ффиногенов К. Г.
WIN32. Основы программирования. Учебный курс
560