Спасибо, что скачали книгу в бесплатной электронной библиотеке BooksCafe.Net
Все книги автора
Эта же книга в других форматах
Приятного чтения!
C# 4.0 полное руководство - 2011
Герберт Шилдт

ПОЛНОЕ
РУКОВОДСТВО
Su P#
A n
Reference В HERBERT SCHILDT 
ПОЛНОЕ РУКОВОДСТВО
С#4.0 ГЕРБЕРТ ШИЛДТ
Москва • Санкт-Петербург • Киев2011 ББК 32.973.26-018.2.75 Ш 57 УДК 681.3.07 Издательский дом "Вильямс" Зав. редакциейС.Н. ТригубПеревод с английского и редакцияИ.В. Берштейна По общим вопросам обращайтесь в Издательский дом "Вильямс" по адресу:info@williamspublishing.com,http://www.williamspublishing.com Шилдт, Герберт. Ш57 C# 4.0: полное руководство. : Пер. с англ. — М. : ООО "И.Д. Вильямс", 2011. — 1056 с.: ил. — Парал. тит. англ. ISBN 978-5-8459-1684-6 (рус.) ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства McGrow-Hill Higher Ed. Authorized translation from the English language edition published by McGraw-Hill Companies, Copyright © 2010 All rights reserved. Except as permitted under the Copyright Act of 1976, no part of this publication may be reproduced or distributed in any form or by any means, or stored in a database or retrieval system, without the prior written permission of publisher, with the exception that the program listings may be entered, stored, and executed in a computer system, but they may not be reproduced for publication. Russian language edition published by Williams Publishing House according to the Agreement with R&I Enterprises International, Copyright © 2011 Научно-популярное издание Герберт Шилдт C# 4.0: полное руководство
Литературный редакторЕ.П. Перестюк ВерсткаА.В. ЧернокозинскаяХудожественный редакторС А. ЧернокозинскийКорректорАЛ. Гордиенко Подписано в печать 17.09.2010. Формат 70x100/16. Гарнитура Times. Печать офсетная. Уел. печ. л. 85,14. Уч.-изд. л. 51,55. Тираж 1500 экз. Заказ № 24007. Отпечатано по технологии CtP в ОАО "Печатный двор" им. А. М. Горького 197110, Санкт-Петербург, Чкаловский пр., 15. ООО "И. Д. Вильямс", 127055, г. Москва, ул. Лесная, д. 43, стр. 1 © Издательский дом "Вильямс", 2011 © by The McGraw-Hill Companies, 2010 ISBN 978-5-8459-1684-6 (рус.) ISBN 0-07-174116-Х (англ.) Оглавление
Содержание
Краткий обзор элементов C#
Точка с запятой и оформление исходного текста программы
Управляющие последовательности символов
Операторы
Поразрядные составные операторы присваивания
Объявление управляющих переменных в цикле for
Добавление метода в класс Building
Массивы и строки
Строки
Подробнее о методах и классах
Возврат объектов из методов
Необязательные аргументы и неоднозначность
Перегрузка операторов
Операторы преобразования
Индексаторы и свойства
Применение индексаторов и свойств
Наследование
Порядок вызова конструкторов
Упаковка и распаковка
Интерфейсы, структуры и перечисления
Инициализация перечисления
Обработка исключительных ситуаций
Получение производных классов исключений
Применение средств ввода-вывода
Использование класса FileStream для копирования файла
Применение классов StringReader и StringWriter
Делегаты, события и лямбда-выражения
События
Пространства имен, препроцессор и сборки
Директива #еггог
Получение типов данных из сборок
Обобщения
// ...
Сравнение экземпляров параметра типа
// ...
Применение вложенных операторов from
Формирование запросов с помощью методов запроса
Небезопасный код, указатели, обнуляемые типы и разные ключевые слова
Частичные методы
Библиотека C#
Структуры типов данных с плавающей точкой
Окончание табл. 21.9
Сортировка и поиск в массивах
Класс Tuple
Сцепление строк ,
Заполнение и обрезка строк
Определение пользовательского формата даты и времени
Свойство IsBackground
Семафор
Многопоточное программирование. Часть вторая: библиотека TPL
Другие средства организации задач
Вопросы эффективности PLINQ
Сортировка и поиск в коллекции типа ArrayList
Специальные коллекции
Класс DictionaryCTKey, TValue>
Сохранение объектов, определяемых пользователем классов, в коллекции
Создание именованного итератора
Исключения, генерируемые методом GetResponseStream ()
Дескрипторы XML-комментариев
п
Оглавление
Содержание Глава 20. Небезопасный код, указатели, обнуляемые типы и разные ключевые слова 681
06 авторе
Герберт Шилдт (Herbert Schildt) является одним из самых известных специалистов по языкам программирования С#, C++, С и Java. Его книги по программированию изданы миллионными тиражами и переведены с английского на все основные иностранные языки. Его перу принадлежит целый ряд популярных книг, в том числеПолный справочник по Java,Полный справочник поC++,Полный справочник поС (все перечисленные книги вышли в издательстве "Вильямс" в 2007 и 2008 гг.). Несмотря на то что Герберт Шилдт интересуется всеми аспектами вычислительной техники, его основная специализация — языки программирования, в том числе компиляторы, интерпретаторы и языки программирования роботов. Он также проявляет живой интерес к стандартизации языков. Шилдт окончил Иллинойский университет и имеет степени магистра и бакалавра. Связаться с ним можно, посетив его веб-сайт по адресуwww.HerbSchildt.com. N
0 научном редакторе
Майкл ХоварД (Michael Howard) работает руководителем проекта программной защиты в группе техники информационной безопасности, входящей в подразделение разработки защищенных информационных систем (TwC) корпорации Microsoft, где он отвечает за внедрение надежных с точки зрения безопасности методов проектирования, программирования и тестирования информационных систем в масштабах всей корпорации. Ховард является автором методики безопасной разработки (Security Development Lifecycle — SDL) — процесса повышения безопасности программного обеспечения, выпускаемого корпорацией Microsoft. Свою карьеру в корпорации Microsoft Ховард начал в 1992 году, проработав два первых года с ОС Windows и компиляторами в службе поддержки программных продуктов (Product Support Services) новозеландского отделения корпорации, а затем перейдя в консультационную службу (Microsoft Consulting Services), где он занимался клиентской поддержкой инфраструктуры безопасности и помогал в разработке заказных проектных решений и программного обеспечения. В 1997 году Ховард переехал в Соединенные Штаты и поступил на работу в отделение Windows веб-службы Internet Information Services, представлявшей собой веб-сервер следующего поколения в корпорации Microsoft, прежде чем перейти в 2000 году к своим текущим служебным обязанностям. Ховард является редактором журналаIEEE Security & Privacy,часто выступает на конференциях, посвященных безопасности программных средств, и регулярно пишет статьи по вопросам безопасного программирования и проектирования программного обеспечения. Он является одним из авторов шести книг по безопасности информационных систем. Благодарности
Особая благодарность выражается Майклу Ховарду за превосходное научное редактирование книги. Его знания, опыт, дельные советы и предложения оказались неоценимыми. Предисловие
эффективности и переносимости разрабатываемых ими программ. Они не менее требовательны к применяемым инструментальным средствам и особенно к языкам программирования. Существует немало языков программирования, но лишь немногие из них действительно хороши. Хороший язык программирования должен быть одновременно эффективным и гибким, а его синтаксис — кратким, но ясным. Он должен облегчать создание правильного кода, не мешая делать это, а также поддерживать самые современные возможности программирования, но не ультрамодные тенденции, заводящие в тупик. И наконец, хороший язык программирования должен обладать еще одним, едва уловимым качеством: вызывать у нас такое ощущение, будто мы находимся в своей стихии, когда пользуемся им. Именно таким языком и является С#.
рограммисты — люди требовательные, постоянно ищущие пути повышения производительности, Язык C# был создан корпорацией Microsoft для поддержки среды .NET Framework и опирается на богатое наследие в области программирования. Его главным разработчиком был Андерс Хейльсберг (Anders Hejlsberg) — известнейший специалист по программированию. C# происходит напрямую от двух самых удачных в области программирования языков: С и C++. От языка С он унаследовал синтаксис, многие ключевые слова и операторы, а от C++ — усовершенствованную объектную модель. Кроме того, C# тесно связан с Java — другим не менее удачным языком. Имея общее происхождение, но во многом отличаясь, C# и Java похожи друг на друга как близкие, но не кровные родственники. В обоих языках поддерживается распределенное программирование и применяется промежуточный код для обеспечения безопасности и переносимости, но отличия кроются в деталях реализации. Кроме того, в обоих языках предоставляется немало возможностей для проверки ошибок при выполнении, обеспечения безопасности и управляемого исполнения, хотя и в этом случае отличия кроются в деталях реализации. Но в отличие от Java, язык C# предоставляет доступ к указателям — средствам программирования, которые поддерживаются в C++. Следовательно, C# сочетает в себе эффективность, присущую C++, и типовую безопасность, характерную для Java. Более того, компромиссы между эффективностью и безопасностью в этом языке программирования тщательно уравновешены и совершенно прозрачны. На протяжении всей истории вычислительной техники языки программирования развивались, приспосабливаясь к изменениям в вычислительной среде, новшествам в теории языков программирования и новым тенденциям в осмыслении и подходе к работе программистов. И в этом отношении C# не является исключением. В ходе непрерывного процесса уточнения, адаптации и нововведений C# продемонстрировал способность быстро реагировать на потребности программистов в переменах. Об этом явно свидетельствуют многие новые возможности, введенные в C# с момента выхода исходной версии 1.0 этого языка в 2000 году. Рассмотрим для примера первое существенное исправление, внесенное в версии C# 2.0, где был введен ряд свойств, упрощавших написание более гибкого, надежного и быстро действующего кода. Без сомнения, самым важным новшеством в версии C# 2.0 явилось внедрение обобщений. Благодаря обобщениям стало возможным создание типизированного, повторно используемого кода на С#. Следовательно, внедрение обобщений позволило основательно расширить возможности и повысить эффективность этого языка. А теперь рассмотрим второе существенное исправление, внесенное в версии C# 3.0 . Не будет преувеличением сказать, что в этой версии введены свойства, переопределившие саму суть C# и поднявшие на новый уровень разработку языков программирования. Среди многих новых свойств особенно выделяются два следующих: LINQ и лябмда-выражения. Сокращение LINQ означаетязык интегрированных запросов.Это языковое средство позволяет создавать запросы к базе данных, используя элементы С#. А лябмда-выражения — это синтаксис функционалов с помощью лямбда-оператора =>, причем лябмда-выражения часто применяются в LINQ-выражениях. И наконец, третье существенное исправление было внесено в версии C# 4.0, описываемой в этой книге. Эта версия опирается на предыдущие и в то же время предоставляет целый ряд новых средств для рационального решения типичных задач программирования. В частности, в ней внедрены именованные и необязательные аргументы, что делает более удобным вызов некоторых видов методов; добавлено ключевое слово dynamic, упрощающее применение C# в тех случаях, когда тип данных создается во время выполнения, например, при сопряжении с моделью компонентных объектов (СОМ) или при использовании рефлексии; а средства ковариантности и контравариантности, уже поддерживавшиеся в С#, были расширены с тем, чтобы использовать параметры типа. Благодаря усовершенствованиям среды .NET Framework, представленной в виде библиотеки С#, в данной версии поддерживается параллельное программирование средствами TPL (Task Parallel Library — Библиотека распараллеливания задач) и PLINQ (Parallel LINQ — Параллельный язык интегрированных запросов). Эти подсистемы упрощают создание кода, который мае- Предисловие 27
штабируется автоматически для более эффективного использования компьютеров с многоядерными процессорами. Таким образом, с выпуском версии C# 4.0 появилась возможность воспользоваться преимуществами высокопроизводительных вычислительных платформ. Благодаря своей способности быстро приспосабливаться к постоянно меняющимся потребностям в области программирования C# по-прежнему остается живым и новаторским языком. А следовательно, он представляет собой один из самых эффективных и богатых своими возможностями языков в современном программировании. Это язык, пренебречь которым не может позволить себе ни один программист. И эта книга призвана помочь вам овладеть им. Структура книги
В этой книге описывается версия 4.0 языка С#. Она разделена на две части. В части I дается подробное пояснение языка С#, в том числе новых средств, внедренных в версии 4.0. Это самая большая часть книги, в которой описываются ключевые слова, синтаксис и средства данного языка, а также операции ввода-вывода и обработки файлов, рефлексия и препроцессор. В части II рассматриваемся библиотека классов С#, которая одновременно является библиотекой классов для среды .NET Framework. Эта библиотека довольно обширна, но за недостатком места в этой книге просто невозможно описать ее полностью. Поэтому в части II основное внимание уделяется корневой библиотеке, которая находится в пространстве именSystem.Кроме того, в этой части рассматриваются коллекции, организация многопоточной обработки, сетевого подключения к Интернету, а также средства TPL и PLINQ. Это те части более обширной библиотеки классов, которыми пользуется всякий, программирующий на языке С#. Книга для всех программирующих
Для чтения этой книги вообще не требуется иметь опыт программирования. Если вы уже знаете C++ или Java, то сможете довольно быстро продвинуться в освоении излагаемого в книге материала, поскольку у C# имеется немало общего с этими языками. Даже если вам не приходилось программировать прежде, вы сможете освоить С#, но для этого вам придется тщательно проработать примеры, приведенные в каждой главе книги. Необходимое программное обеспечение
Для компилирования и выполнения примеров программ на C# 4.0, приведенных в этой книге, вам потребуется пакет Visual Studio 2010 (или более поздняя версия). Код, доступный в Интернете
Не забывайте о том, что исходный код для примеров всех программ, приведенных в этой книге, свободно доступен для загрузки по адресуwww .mhprofessional. com. Что еще почитать
Эта книга — своеобразный "ключ" к целой серии книг по программированию, написанных Гербертом Шилдтом. Ниже перечислены другие книги, которые могут представлять для вас интерес. Для изучения языка программирования Java рекомендуются следующие книги. Полный справочник по Java(ИД " Вильямс", 2007 г.) ]ava: руководство для начинающих(ИД " Вильямс", 2008 г.) SWING: руководство для начинающих(ИД "Вильямс", 2007 г.) Искусство программирования на Java(ИД "Вильямс", 2005 г.) Java. Методики программирования Шилдта(ИД "Вильямс", 2008 г.) Для изучения языка программирования C++ особенно полезными окажутся следующие книги. Полный справочник поC++ (ИД "Вильямс", 2007 г.) C++.Руководство для начинающих(ИД "Вильямс", 2005 г.) STL Programming From the Ground Up Искусство программирования наС++ С++.Методики программирования Шилдта(ИД "Вильямс", 2009 г.) Если же вы стремитесь овладеть языком С, составляющим основу всех современных языков программирования, вам будет интересно прочитать книгу Полный справочник по С(ИД "Вильямс", 2007 г.) От издательства
Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые другие замечания, которые вам хотелось бы высказать в наш адрес. Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумажное или электронное письмо, либо просто посетить наш Web-сервер и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравится или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас. Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию последующих книг. Наши координаты: E-mail: info0williamspublishing. com WWW: http://www.williamspublishing.com Информация для писем из: России: 127055, г. Москва, ул. Лесная, д. 43, стр. 1 Украины: 03150, Киев, а/я 152 ЧАСТЬ 1 Язык C# В части I рассматриваются отдельные элементы языка С#, в том числе ключевые слова, синтаксис и операторы. Описывается также ряд основополагающих методов программирования, тесно связанных с языком С#, включая организацию ввода-вывода и рефлексию. ГЛАВА 1
Создание C# ГЛАВА 2
Краткий обзор элементов C# ГЛАВА 3
Типы данных, литералы и переменные ГЛАВА 4
Опера I оры ГЛАВА 5
Управляющие операторы ГЛАВА 6
Введение в классы, объекты и методы ГЛАВА 7
Массивы и строки ГЛАВА 8
Подробнее о методах и классах ГЛАВА 9
Перегрузка операторов ГЛАВА 10
Индексаторы и свойства ГЛАВА 11
Наследование ГЛАВА 12
Интерфейсы, структуры и перечисления ГЛАВА 13
Обработка исключительных ситуаций ГЛАВА 14
Применение средств ввода-вывода ГЛАВА 15
Делегаты, события и лямбда-выражения ГЛАВА 16
Пространства имен, препроцессор и сборки ГЛАВА 17
Динамическая идентификация типов, рефлексия и атрибуты ГЛАВА 18
Обобщения ГЛАВА 19
LINQ ГЛАВА 20
Небезопасный код, указатели, обнуляемые типы и разные ключевые слова Глава 1 Создание C# О является основным языком разработки программ на платформе .NET корпорации Microsoft. В нем удачно сочетаются испытанные средства программирования с самыми последними новшествами и предоставляется возможность для эффективного и очень практичного написания программ, предназначенных для вычислительной среды современных предприятий. Это, без сомнения, один из самых важных языков программирования XXI века. Назначение этой главы — представить C# в его историческом контексте, упомянув и те движущие силы, которые способствовали его созданию, выработке его конструктивных особенностей и определили его влияние на другие языки программирования. Кроме того, в этой главе поясняется взаимосвязь C# со средой .NET Framework. Как станет ясно из дальнейшего материала, C# и .NET Framework совместно образуют весьма изящную среду программирования. Генеалогическое дерево C#
Языки программирования не существуют в пустоте. Напротив, они тесно связаны друг с другом таким образом, что на каждый новый язык оказывают в той или иной форме влияние его предшественники. Этот процесс сродни перекрестному опылению, в ходе которого свойства одного языка приспосабливаются к другому языку, полезные нововведения внедряются в существующий контекст, а устаревшие конструкции удаляются. Таким путем развиваются языки программирования и совершенствуется искусство программирования. И в этом отношении C# не является исключением. У языка программирования C# "богатое наследство". Он является прямым наследником двух самых удачных языков программирования: С и C++. Он также имеет тесные родственные связи с еще одним языком: Java. Ясное представление об этих взаимосвязях имеет решающее значение для понимания С#. Поэтому сначала определим, какое место занимает C# среди этих трех языков. Язык С - начало современной эпохи программирования
Создание С знаменует собой начало современной эпохи программирования. Язык С был разработан Деннисом Ритчи (Dennis Ritchie) в 1970-е годы для программирования на мини-ЭВМ DEC PDP-11 под управлением операционной систему Unix. Несмотря на то что в ряде предшествовавших языков, в особенности Pascal, был достигнут значительный прогресс, именно С установил тот образец, которому до сих пор следуют в программировании. Язык С появился в результате революции вструктурном программированиив 1960-е годы. До появления структурного программирования писать большие программы было трудно, поскольку логика программы постепенно вырождалась в так называемый "макаронный" код — запутанный клубок безусловных переходов, вызовов и возвратов, которые трудно отследить. В структурированных языках программирования этот недостаток устранялся путем ввода строго определенных управляющих операторов, подпрограмм с локальными переменными и других усовершенствований. Благодаря применению методов структурного программирования сами программы стали более организованными, надежными и управляемыми. И хотя в то время существовали и другие структурированные языки программирования, именно в С впервые удалось добиться удачного сочетания эффективности, изящества и выразительности. Благодаря своему краткому, но простому синтаксису в сочетании с принципом, ставившим во главу угла программиста, а не сам язык, С быстро завоевал многих сторонников. Сейчас уже нелегко представить себе, что С оказался своего рода "струей свежего воздуха", которого так не хватало программистам. В итоге С стал самым распространенным языком структурного программирования в 1980-е годы. Но даже у такого достойного языка, как С, имелись свои ограничения. К числу самых труднопреодолимых его ограничений относится неспособность справиться с большими программами. Как только проект достигает определенного масштаба, язык С тут же ставит предел, затрудняющий понимание и сопровождение программ при их последующем разрастании. Конкретный предел зависит от самой программы, программиста и применяемых инструментальных средств, тем не менее, всегда существует "порог", за которым программа на С становится неуправляемой. Появление ООП и C++
К концу 1970-х годов масштабы многих проектов приблизились к пределам, с которыми уже не могли справиться методики структурного программирования вообще и язык С в частности. Для решения этой проблемы было открыто новое направление в программировании — так называемое объектно-ориентированное программирование (ООП). Применяя метод ООП, программист мог работать с более "крупными" программами. Но главная трудность заключалась в том, что С, самый распространенный в то время язык, не поддерживал ООП. Стремление к созданию объектно-ориентированного варианта С в конечном итоге привело к появлению C++. Язык C++ был разработан в 1979 году Бьярне Страуструпом (Bjarne Stroustrup), ра-бртавшим в компании Bell Laboratories, базировавшейся в Мюррей-Хилл, шт. Нью-Джерси. Первоначально новый язык назывался "С с классами", но в 1983 году он был переименован в C++. Язык С полностью входит в состав C++, а следовательно, С служит основанием, на котором зиждется C++. Большая часть дополнений, введенных Страуструпом, обеспечивала плавный переход к ООП. И вместо того чтобы изучать совершенно новый язык, программирующему на С требовалось лишь освоить ряд новых свойств, чтобы воспользоваться преимуществами методики ООП. В течение 1980-х годов C++ все еще оставался в тени, интенсивно развиваясь, но к началу 1990-х годов, когда он уже был готов для широкого применения, его популярность в области программирования заметно возросла. К концу 1990-х годов он стал наиболее широко распространенным языком программирования и в настоящее время по-прежнему обладает неоспоримыми преимуществами языка разработки высокопроизводительных программ системного уровня. Важно понимать, что разработка C++ не была попыткой создать совершенно новый язык программирования. Напротив, это была попытка усовершенствовать уже существовавший довольно удачный язык. Такой подход к разработке языков программирования, основанный на уже существующем языке и совершенствующий его далее, превратился в упрочившуюся тенденцию, которая продолжается до сих пор. Появление Интернета и Java
Следующим важным шагом в развитии языков программирования стала разработка Java. Работа над языком Java, который первоначально назывался Oak (Дуб), началась в 1991 году в компании Sun Microsystems. Главной "движущей силой" в разработке Java был Джеймс Гослинг (James Gosling), но не малая роль в работе над этим языком принадлежит также Патрику Ноутону (Patrick Naughton), Крису Уорту (Chris Warth), Эду Фрэнку (Ed Frank) и Майку Шеридану (Mike Sheridan). Java представляет собой структурированный, объектно-ориентированный язык с синтаксисом и конструктивными особенностями, унаследованными от C++. Нововведения в Java возникли не столько в результате прогресса в искусстве программирования, хотя некоторые успехи в данной области все же были, сколько вследствие перемен в вычислительной среде. До появления на широкой арене Интернета большинство программ писалось, компилировалось и предназначалось для конкретного процессора и операционной системы. Как известно, программисты всегда стремились повторно использовать свой код, но, несмотря на это, легкой переносимости программ из одной среды в другую уделялось меньше внимания, чем более насущным задачам. Тем не менее с появлением Интернета, когда в глобальную сеть связывались разнотипные процессоры и операционные системы, застаревшая проблема переносимости программ вновь возникла с неожиданной остротой. Для решения проблемы переносимости потребовался новый язык, и им стал Java. Самым важным свойством (и причиной быстрого признания) Java является способность создавать межплатформенный, переносимый код, тем не менее, интересно отметить, что первоначальным толчком для разработки Java послужил не Интернет, а потребность в независящем от платформы языке, на котором можно было бы разрабатывать программы для встраиваемых контроллеров. В 1993 году стало очевидно, что вопросы межплатформенной переносимости, возникавшие при создании кода для встраиваемых контроллеров, стали актуальными и при попытке написать код для Интернета. Напомним, что Интернет — это глобальная распределенная вычислительная среда, в которой работают и мирно "сосуществуют" разнотипные компьютеры. И в итоге оказалось, что теми же самыми методами, которыми решалась проблема переносимости программ в мелких масштабах, можно решать аналогичную задачу в намного более крупных масштабах Интернета. Переносимость программ на Java достигалась благодаря преобразованию исходного кода в промежуточный, называемыйбайт-кодом.Этот байт-код затем выполнялся виртуальной машиной Java (JVM) — основной частью исполняющей системы Java. Таким образом, программа на Java могла выполняться в любой среде, для которой была доступна JVM. А поскольку JVM реализуется относительно просто, то она сразу же стала доступной для большого числа сред. Применением байт-кода Java коренным образом отличается от С и C++, где исходный код практически всегда компилируется в исполняемый машинный код, который, в свою очередь, привязан к конкретному процессору и операционной системе. Так, если требуется выполнить программу на С или C++ в другой системе, ее придется перекомпилировать в машинный код специально для данной вычислительной среды. Следовательно, для создания программы на С или C++, которая могла был выполняться в различных средах, потребовалось бы несколько разных исполняемых версий этой программы. Это оказалось бы не только непрактично, но и дорого. Изящным и рентабельным решением данной проблемы явилось применение в Java промежуточного кода. Именно это решение было в дальнейшем приспособлено для целей языка С#. Как упоминалось ранее, Java происходит от С и C++. В основу этого языка положен синтаксис С, а его объектная модель получила свое развитие из C++. И хотя код Java не совместим с кодом С или C++ ни сверху вниз, ни снизу вверх, его синтаксис очень похож на эти языки, что позволяет большому числу программирующих на С или C++ без особого труда перейти на Java. Кроме того, Java построен по уже существующему образцу, что позволило разработчикам этого языка сосредоточить основное внимание на новых и передовых его свойствах. Как и Страуструпу при создании C++, Гослингу и его коллегам не пришлось изобретать велосипед, т.е. разрабатывать Java как совершенно новый язык. Более того, после создания Java языки С и C++ стали признанной основой, на которой можно разрабатывать новые языки программирования. Создание C#
Несмотря на то что в Java успешно решаются многие вопросы переносимости программ в среде Интернета, его возможности все же ограничены. Ему, в частности, недостаетмежъязыковой возможности взаимодействия,называемой такжемногоязыковым программированием.Это возможность кода, написанного на одном языке, без труда взаимодействовать с кодом, написанным на другом языке. Межъязыковая возможность взаимодействия требуетсядляпостроения крупных, распределенных программных систем. Она желательна также для создания отдельных компонентов программ, поскольку наиболее ценным компонентом считается тот, который может быть использован в самых разных языках программирования и в самом большом числе операционных сред. Другой возможностью, отсутствующей в Java, является полная интеграция с платформой Windows. Несмотря на то что программы на Java могут выполняться в среде Windows, при условии, что установлена виртуальная машина Java, среды Java и Windows не являются сильно связанными. А поскольку Windows является самой распространенной операционной системой во всем мире, то отсутствие прямой поддержки Windows является существенным недостатком Java. Для удовлетворения этих и других потребностей программирования корпорация Microsoft разработала в конце 1990-х годов язык C# как часть общей стратегии .NET. Впервые он был выпущен в виде альфа-версии в середине 2000 года. Главным разработчиком C# был Андерс Хейльсберг — один из ведущих в мире специалистов по языкам программирования, который может похвалиться рядом заметных достижений в данной области. Достаточно сказать, что в 1980-е годы он был автором очень удачной и имевшей большое значение разработки — языка Turbo Pascal, изящная реализация которого послужила образцом для создания всех последующих компиляторов. Язык C# непосредственно связан с С, C++ и Java. И это не случайно. Ведь это три самых широко распространенных и признанных во всем мире языка программирования. Кроме того, на момент создания C# практически все профессиональные программисты уже владели С, C++ или Java. Благодаря тому что C# построен на столь прочном и понятном основании, перейти на этот язык из С, C++ или Java не представляло особого труда. А поскольку и Хейльсбергу не нужно (да и нежелательно) было изобретать велосипед, то он мог сосредоточиться непосредственно на усовершенствованиях и нововведениях в С#. На рис. 1.1 приведено генеалогическое дерево С#. Предком C# во втором поколении является С, от которого он унаследовал синтаксис, многие ключевые слова и операторы. Кроме того, C# построен на усовершенствованной объектной модели, определенной в C++. Если вы знаете С или C++, то будете чувствовать себя уютно и с языком С#. 
Рис. 1.1. Генеалогическое дерево C#
Родственные связи C# и Java более сложные. Как пояснялось выше, Java также происходит от С и C++ и обладает общим с ними синтаксисом и объектной моделью. Как и Java, C# предназначен для получения переносимого кода, но C# не происходит непосредственно от Java. Напротив, C# и Java — это близкие, но не кровные родственники, имеющие общих предков, но во многом отличающиеся друг от друга. Впрочем, если вы знаете Java, то многие понятия C# окажутся вам знакомыми. С другой стороны, если вам в будущем придется изучать Java, то многие понятия, усвоенные в С#, могут быть легко распространены и на Java. В C# имеется немало новых средств, которые будут подробно рассмотрены на страницах этой книги, но самое важное из них связано со встроенной поддержкой программных компонентов. В действительности C# может считаться компонентноориентированным языком программирования, поскольку в него внедрена встроенная поддержка написания программных компонентов. Например, в состав C# входят средства прямой поддержки таких составных частей программных компонентов, как свойства, методы и события. Но самой важной компонентно-ориентированной особенностью этого языка, вероятно, является возможность работы в безопасной среде многоязыкового программирования. Развитие C#
С момента выпуска исходной версии 1.0 развитие C# происходило быстро. Вскоре после версии 1.0 корпорация Microsoft выпустила версию 1.1, в которую было внесено немало корректив, но мало значительных возможностей. Однако ситуация совершенно изменилась после выпуска версии C# 2.0. Появление версии 2.0 стало поворотным моментом в истории развития С#, поскольку в нее было введено много новых средств, в том числе обобщения, частичные типы и анонимные методы, которые основательно расширили пределы возможностей и область применения этого языка, а также повысили его эффективность. После выпуска версии 2.0 "упрочилось" положение С#. Ее появление продемонстрировало также приверженность корпорации Microsoft к поддержке этого языка в долгосрочной перспективе. Следующей значительной вехой в истории развития C# стал выпуск версии 3.0. В связи с внедрением многих новых свойств в версии C# 2.0 можно было ожидать некоторого замедления в развитии С#, поскольку программистам требовалось время для их освоения, но этого не произошло. С появлением версии 3.0 корпорация Microsoft внедрила ряд новшеств, совершенно изменивших общее представление о программировании. К числу этих новшеств относятся, среди прочего, лямбда-выражения, язык интегрированных запросов (LINQ), методы расширения и неявно типизированные переменные. Конечно, все эти новые возможности очень важны, поскольку они оказали заметное влияние на развитие данного языка, но среди них особенно выделяются две: язык интегрированных запросов (LINQ) и лямбда-выражения. Язык LINQ и лямбда-выражения вносят совершенно новый акцент в программирование на C# и ещеглубжеподчеркивают его ведущую роль в непрекращающейся эволюции языков программирования. Текущей является версия C# 4.0, о которой и пойдет речь в этой книге. Эта версия прочно опирается на три предыдущие основные версии С#, дополняя их целым рядом новых средств. Вероятно, самыми важными среди них являются именованные и необязательные аргументы. В частности, именованные аргументы позволяют связывать аргумент с параметром по имени. А необязательные аргументы дают возможность указывать для параметра используемый по умолчанию аргумент. Еще одним важным новым средством является типdynamic,применяемый для объявления объектов, которые проверяются на соответствие типов во время выполнения, а не компиляции. Кроме того, ковариантность и контравариантность параметров типа поддерживается благодаря новому применению ключевых словinиout.Тем, кто пользуется моделью СОМ вообще и прикладными интерфейсами Office Automation API в частности, существенно упрощен доступ к этим средствам, хотя они и не рассматриваются в этой книге. В целом, новые средства, внедренные в версии C# 4.0, способствуют дальнейшей рационализации программирования и повышают практичность самого языка С#. Еще два важных средства, внедренных в версии 4.0 и непосредственно связанных с программированием на С#, предоставляются не самим языком, а средой .NET Framework 4.0. Речь идет о поддержке параллельного программирования с помощью библиотеки распараллеливания задач (TPL) и параллельном варианте языка интегрированных запросов (PLINQ). Оба эти средства позволяют существенно усовершенствовать и упростить процесс создания программ, в которых применяется принцип параллелизма. И то и другое средство упрощает создание многопоточного кода, который масштабируется автоматически для использования нескольких процессоров, доступных на компьютере. В настоящее время широкое распространение получили компьютеры с многоядерными процессорами, и поэтому возможность распараллеливать выполнение кода среди всех доступных процессоров приобретает все большее значение практически для всех, кто программирует на С#. В силу этого особого обстоятельства средства TPL и PLINQ рассматриваются в данной книге. Связь C# со средой .NET Framework
Несмотря на то что C# является самодостаточным языком программирования, у него имеется особая взаимосвязь со средой выполнения .NET Framework. Наличие такой взаимосвязи объясняется двумя причинами. Во-первых, C# первоначально предназначался для создания кода, который должен выполняться в среде .NET Framework. И во-вторых, используемые в C# библиотеки определены в среде .NET Framework. На практике это означает, что C# и .NET Framework тесно связаны друг с другом, хотя теоретически C# можно отделить от среды .NET Framework. В связи с этим очень важно иметь хотя бы самое общее представление о среде .NET Framework и ее значении для С#. 0 среде NET Framework
Назначение .NET Framework — служить средой для поддержки разработки и выполнения сильно распределенных компонентных приложений. Она обеспечивает совместное использование разных языков программирования, а также безопасность, переносимость программ и общую модель программирования для платформы Windows. Что же касается взаимосвязи с С#, то среда .NET Framework определяет два очень важных элемента. Первым из них являетсяобщеязыковая среда выполнения(Common Language Runtime — CLR). Это система, управляющая выполнением программ. Среди прочих преимуществ — CLR как составная часть среды .NET Framework поддерживает многоязыковое программирование, а также обеспечивает переносимость и безопасное выполнение программ. Вторым элементом среды .NET Framework являетсябиблиотека классов.Эта библиотека предоставляет программе доступ к среде выполнения. Так, если требуется выполнить операцию ввода-вывода, например вывести что-нибудь на экран, то для этой цели используется библиотека классов .NET. Для тех, кто только начинает изучать программирование, понятиеклассаможет оказаться незнакомым. Оно подробно разъясняется далее в этой книге, а пока достаточно сказать, что класс — это объектно-ориентированная конструкция, помогающая организовать программы. Если программа ограничивается средствами, определяемыми в библиотеке классов .NET, то такая программа может выполняться везде, где поддерживается среда выполнения .NET. А поскольку в C# библиотека классов .NET используется автоматически, то программы на C# заведомо оказываются переносимыми во все имеющиеся среды .NET Framework. Принцип действия CLR
Среда CLR управляет выполнением кода .NET. Действует она по следующему принципу. Результатом компиляции программы на C# является не исполняемый код, а файл, содержащий особого рода псевдокод, называемыйMicrosoft Intermediate Language,MSIL (промежуточный язык Microsoft). Псевдокод MSIL определяет набор переносимых инструкций, не зависящих от конкретного процессора. По существу, MSIL определяет переносимый язык ассемблера. Следует, однако, иметь в виду, что, несмотря на кажущееся сходство псевдокода MSIL с байт-кодом Java, это все же разные понятия. Назначение CLR — преобразовать промежуточный код в исполняемый код по ходу выполнения программы. Следовательно, всякая программа, скомпилированная в псевдокод MSIL, может быть выполнена в любой среде, где имеется реализация CLR. Именно таким образом отчасти достигается переносимость в среде .NET Framework. Псевдокод MSIL преобразуется в исполняемый код с помощью]1Т-компилятора.Сокращение JIT означаетточно в сроки отражает оперативный характер данного компилятора. Процесс преобразования кода происходит следующим образом. При выполнении программы среда CLR активизирует JIT-компилятор, который преобразует псевдокод MSIL в собственный код системы по требованию для каждой части программы. Таким образом, программа на C# фактически выполняется как собственный код, несмотря на то, что первоначально она скомпилирована в псевдокод MSIL. Это означает, что такая программа выполняется так же быстро, как и в том случае, когда она исходно скомпилирована в собственный код, но в то же время она приобретает все преимущества переносимости псевдокода MSIL. Помимо псевдокода MSIL, при компилировании программы на C# получаются такжеметаданные,которые служат для описания данных, используемых в программе, а также обеспечивают простое взаимодействие одного кода с другим. Метаданные содержатся в том же файле, что и псевдокод MSIL. Управляемый и неуправляемый код
Как правило, при написании программы на C# формируется так называемыйуправляемый код.Как пояснялось выше, такой код выполняется под управлением среды CLR, и поэтому на него накладываются определенные ограничения, хотя это и дает ряд преимуществ. Ограничения накладываются и удовлетворятся довольно просто: ком пи-лятор должен сформировать файл MSIL, предназначенный для выполнения в среде CLR, используя при этом библиотеку классов .NET, — и то и другое обеспечивается средствами С#. Ко многим преимуществам управляемого кода относятся, в частности, современные способы управления памятью, возможность программирования на разных языках, повышение безопасности, поддержка управления версиями и четкая организация взаимодействия программных компонентов. В отличие от управляемого кода, неуправляемый код не выполняется в среде CLR. Следовательно, до появления среды .NET Framework во всех программах для Windows применялся неуправляемый код. Впрочем, управляемый и неуправляемый коды могут взаимодействовать друг с другом, а значит, формирование управляемого кода в C# совсем не означает, что на его возможность* взаимодействия с уже существующими программами накладываются какие-то ограничения. Общеязыковая спецификация
Несмотря на все преимущества, которые среда CLR дает управляемому коду, для максимального удобства его использования вместе с программами, написанными на других языках, он должен подчинятсяобщеязыковой спецификации(Common Language Specification — CLS), которая определяет ряд общих свойств для разных .NET-совместимых языков. Соответствие CLS особенно важно при создании программных компонентов, предназначенных для применения в других языках. В CLS в качестве подмножества входитобщая система типов(Common Type System — CTS), в которой определяются правила, касающиеся типов данных. И разумеется, в C# поддерживается как CLS, так и CTS.
ГЛАВА 2 Краткий обзор элементов C# Наибольшие трудности в изучении языка программирования вызывает то обстоятельство, что ни один из его элементов не существует обособленно. Напротив, все элементы языка действуют совместно. Такая взаимосвязанность затрудняет рассмотрение одного аспекта C# безотносительно к другому. Поэтому для преодоления данного затруднения в этой главе дается краткий обзор нескольких средств языка С#, включая общую форму программы на С#, ряд основных управляющих и прочих операторов. Вместо того чтобы углубляться в детали, в этой главе основное внимание уделяется лишь самым общим принципам написания любой программы на С#. А большинство вопросов, затрагиваемых по ходу изложения материала этой главы, более подробно рассматриваются в остальных главах части I. Объектно-ориентированное программирование
Основным понятием C# является объектно-ориентированное программирование (ООП). Методика ООП неотделима от С#, и поэтому все программы на C# являются объектно-ориентированными хотя бы в самой малой степени. В связи с этим очень важно и полезно усвоить основополагающие принципы ООП, прежде чем приступать к написанию самой простой программы на С#. ООП представляет собой эффективный подход к программированию. Методики программирования претерпели существенные изменения с момента изобретения компьютера, постепенно приспосабливаясь, главным образом, к повышению сложности программ. Когда, например, появились первые ЭВМ, программирование заключалось в ручном переключении на разные двоичные машинные команды с переднего пульта управления ЭВМ. Такой подход был вполне оправданным, поскольку программы состояли всего из нескольких сотен команд. Дальнейшее усложнение программ привело к разработке языка ассемблера, который давал программистам возможность работать с более сложными программами, используя символическое представление отдельных машинных команд. Постоянное усложнение программ вызвало потребность в разработке и внедрении в практику программирования таких языков высокого уровня, как, например, FORTRAN и COBOL, которые предоставляли программистам больше средств для того, чтобы как-то справиться с постоянно растущей сложностью программ. Но как только возможности этих первых языков программирования были полностью исчерпаны, появились разработки языков структурного программирования, в том числе и С. На каждом этапе развития программирования появлялись методы и инструментальные средства для "обуздания" растущей сложности программ. И на каждом таком этапе новый подход вбирал в себя все самое лучшее из предыдущих, знаменуя собой прогресс в программировании. Это же можно сказать и об ООП. До ООП многие проекты достигали (а иногда и превышали) предел, за которым структурный подход к программированию оказывался уже неработоспособным. Поэтому для преодоления трудностей, связанных с усложнением программ, и возникла потребность в ООП. ООП вобрало в себя все самые лучшие идеи структурного программирования, объединив их с рядом новых понятий. В итоге появился новый и лучший способ организации программ. В самом общем виде программа может быть организована одним из двух способов: вокруг кода (т.е. того, что фактически происходит) или же вокруг данных (т.е. того, что подвергается воздействию). Программы, созданные только методами структурного программирования, как правило, организованы вокруг кода. Такой подход можно рассматривать "как код, воздействующий на данные". Совсем иначе работают объектно-ориентированные программы. Они организованы вокруг данных, исходя из главного принципа: "данные управляют доступом к коду". В объектно-ориентированном языке программирования определяются данные и код, которому разрешается воздействовать на эти данные. Следовательно, тип данных точно определяет операции, которые могут быть выполнены над данными. Для поддержки принципов ООП все объектно-ориентированные языки программирования, в том числе и С#, должны обладать тремя общими свойствами: инкапсуляцией, полиморфизмом и наследованием. Рассмотрим каждое из этих свойств в отдельности. Инкапсуляция
Инкапсуляция —это механизм программирования, объединяющий вместе код и данные, которыми он манипулирует, исключая как вмешательство извне, так и неправильное использование данных. В объектно-ориентированном языке данные и код могут быть объединены в совершенно автономныйчерный ящик.Внутри такого ящика находятся все необходимые данные и код. Когда код и данные связываются вместе подобным образом, создается объект. Иными словами,объект— это элемент, поддерживающий инкапсуляцию. В объекте код, данные или же и то и другое могут бытьзакрытымиили жеоткрытыми.Закрытые данные или код известны и доступны только остальной части объекта. Это означает, что закрытые данные или код недоступны части программы, находящейся за пределами объекта. Если же данные или код оказываются открытыми, то они доступны другим частям программы, хотя и определены внутри объекта. Как правило, открытые части объекта служат для организации управляемого интерфейса с закрытыми частями. Основной единицей инкапсуляции в C# являетсякласс, который определяет форму объекта. Он описывает данные, а также код, который будет ими оперировать. В C# описание класса служит для построения объектов, которые являются экземплярами класса. Следовательно, класс, по существу, представляет собой ряд схематических описаний способа построения объекта. Код и данные, составляющие вместе класс, называютчленами.Данные, определяемые классом, называютполями,илипеременными экземпляра.А код, оперирующий данными, содержится вфункциях-членах,самым типичным представителем которых являетсяметод.В C# метод служит в качестве аналога подпрограммы. (К числу других функций-членов относятся свойства, события и конструкторы.) Таким образом, методы класса содержат код, воздействующий на поля, определяемые этим классом. Полиморфизм
Полиморфизм,что по-гречески означает "множество форм", — это свойство, позволяющее одному интерфейсу получать доступ к общему классу действий. Простым примером полиморфизма может служить руль автомашины, который выполняет одни и те же функции своеобразного интерфейса независимо от вида применяемого механизма управления автомашиной. Это означает, что руль действует одинаково независимо от вида рулевого управления: прямого действия, с усилением или реечной передачей. Следовательно, при вращении руля влево автомашина всегда поворачивает влево, какой бы вид управления в ней ни применялся. Главное преимущество единообразного интерфейса заключается в том, что, зная, как обращаться с рулем, вы сумеете водить автомашину любого типа. Тот же самый принцип может быть применен и в программировании. Рассмотрим для примерастек,т.е. область памяти, функционирующую по принципу "последним пришел — первым обслужен". Допустим, что в программе требуются три разных типа стеков: один — для целых значений, другой — для значений с плавающей точкой, третий — для символьных значений. В данном примере алгоритм, реализующий все эти стеки, остается неизменным, несмотря на то, что в них сохраняются разнотипные данные. В языке, не являющемся объектно-ориентированным, для этой цели пришлось бы создать три разных набора стековых подпрограмм с разными именами. Но благодаря полиморфизму для реализации всех трех типов стеков в C# достаточно создать лишь один общий набор подпрограмм. Зная, как пользоваться одним стеком, вы сумеете воспользоваться и остальными. В более общем смысле понятие полиморфизма нередко выражается следующим образом: "один интерфейс — множество методов". Это означает, что для группы взаимосвязанных действий можно разработать общий интерфейс. Полиморфизм помогает упростить программу, позволяя использовать один и тот же интерфейс для описанияобщего класса действий.Выбрать конкретное действие (т.е. метод) в каждом отдельном случае — это задача компилятора. Программисту не нужно делать это самому. Ему достаточно запомнить и правильно использовать общий интерфейс. Наследование
Наследованиепредставляет собой процесс, в ходе которого один объект приобретает свойства другого объекта. Это очень важный процесс, поскольку он обеспечивает принцип иерархической классификации. Если вдуматься, то большая часть знаний поддается систематизации благодаря иерархической классификации по нисходящей. Например, сорт яблок "Джонатан" входит в общую классификацию сортовяблок,которые, в свою очередь, относятся к классуфруктов,а те — к еще более крупному классупищевых продуктов.Это означает, что класс пищевых продуктов обладает рядом свойств (съедобности, питательности и т.д.), которые по логике вещей распространяются и на его подкласс фруктов. Помимо этих свойств, класс фруктов обладает своими собственными свойствами (сочностью, сладостью и т.д.), которыми он отличается от других пищевых продуктов. У класса яблок имеются свои характерные особенности (растут на деревьях, не в тропиках и т.д.). Таким образом, сорт яблок "Джонатан" наследует свойства всех предшествующих классов, обладая в то же время свойствами, присущими только этому сорту яблок, например красной окраской кожицы с желтым бочком и характерным ароматом и вкусом. Если не пользоваться иерархиями, то для каждого объекта пришлось бы явно определять все его свойства. А если воспользоваться наследованием, то достаточно определить лишь те свойства, которые делают объект особенным в его классе. Он может также наследовать общие свойства своего родителя. Следовательно, благодаря механизму наследования один объект становится отдельным экземпляром более общего класса. Первая простая программа
А теперь самое время перейти к примеру конкретной программы на С#. Для начала скомпилируем и выполним короткую программу. /* Это простая программа на С#. Назовем ее Example.cs. */ using System; class Example { // Любая программа на C# начинается с вызова метода Main(). static void Main() { Console.WriteLine("Простая программа на С#."); } } Основной средой для разработки программ на C# служит Visual Studio корпорации Microsoft. Для компилирования примеров всех программ, приведенных для примера в этой книге, в том числе и тех, где используются новые средства C# 4.0, вам потребуется Visual Studio 2010 или же более поздняя версия, поддерживающая С#. Создавать, компилировать и выполнять программы на С#, используя Visual Studio, можно двумя способами: пользуясь, во-первых, интегрированной средой разработки Visual Studio, а во-вторых, — компилятором командной строки csc . ехе. Далее описываются оба способа. Применение компилятора командной строки csc. ехе
Для коммерческой разработки программ вам, скорее всего, придется пользоваться интегрированной средой Visual Studio, хотя для некоторых читателей более удобным может оказаться компилятор, работающий в режиме командной строки, особенно для компилирования и выполнения примеров программ, приведенных в этой книге. Объясняется это тем, что для работы над отдельной программой не нужно создавать целый проект. Для этого достаточно написать программу, а затем скомпилировать и выполнить ее, причем все это делается из командной строки. Таким образом, если вы умеете пользоваться окном Командная строка
(Command Prompt) и его интерфейсом в Windows, то компилятор командной строки окажется для вас более простым и оперативным инструментальным средством, чем интегрированная среда разработки. ПРЕДОСТЕРЕЖЕНИЕ
Если вы не знаете, как пользоваться окном Командная строка, то вам лучше работать в интегрированной среде разработки Visual Studio. Ведь пытаться усвоить одновременно команды интерфейса Командная строка и элементы языка C# не так-то просто, несмотря на то, что запомнить эти команды совсем нетрудно.
Для написания и выполнения программ на C# с помощью компилятора командной строки выполните следующую несложную процедуру. 1'. Введите исходный текст программы, используя текстовый редактор. 2. Скомпилируйте программу с помощью компилятора csc . ехе. 3. Выполните программу. Ввод исходного текста программы
Исходный текст примеров программ, приведенных в этой книге, доступен для загрузки по адресу www.mhprof es-sional.com.Но при желании вы можете сами ввести исходный текст этих программ вручную. Для этого воспользуйтесь избранным текстовым редактором, например Notepad. Но не забывайте, что вы должны создать файлы, содержащие простой, а не отформатированный текст, поскольку информация форматирования текста, сохраняемая в файле для обработки текста, может помешать нормальной работе компилятора С#. Введя исходный текст программы, присвойте ее файлу имяExample. cs. Компилирование программы
Для компилирования программы на C# запустите на выполнение компилятор csc. ехе, указав имя исходного файла в командной строке. C:\>csc Example.cs Компиляторcscсоздаст файлExample. ехе, содержащий версию MSIL данной программы. Несмотря на то что псевдокод MSIL не является исполняемым кодом, он содержится в исполняемом файле с расширением . ехе. Среда CLR автоматически вызывает JIT-компилятор при попытке выполнить файлExample. ехе. Следует, однако, иметь в виду, что если попытаться выполнить файлExample. ехе (или любой другой исполняемый файл, содержащий псевдокод MSIL) на том компьютере, где среда .NET Framework не установлена, то программа не будет выполнена, поскольку на этом компьютере отсутствует среда CLR. ПРИМЕЧАНИЕ
Прежде чем запускать на выполнение компилятор
csc. ехе, откройте окно Командная строка,
HacTpoeHHoenoAVisualStudio. Для этого проще всего выбрать команду Visual Studios Инструменты Visual Э^ю^Командная строка Visual Studio (Visual Studio^Visual Studio Tools^Visual Studio Command Prompt) из меню Пуск^Все программы (Start^AII Programs) на панели задач Windows. Кроме того, вы можете открыть ненастроенное окно Командная строка, а затем выполнить командный файл
vsvars32.bat, входящий в состав Visual Studio.
Выполнение программы
Для выполнения программы введите ее имя в командной строке следующим образом. С:\>Ехашр1
е В результате выполнения программы на экране появится такая строка. Простая программа на С#. Применение интегрированной среды разработки Visual Studio
Visual Studio представляет собой интегрированную среду разработки программ, созданную корпорацией Microsoft. Такая среда дает возможность править, компилировать, выполнять и отлаживать программы на С#, не покидая эту грамотно организованную среду. Visual Studio предоставляет не только все необходимые средства для работы с программами, но и помогает правильно организовать их. Она оказывается наиболее эффективной для работы над крупными проектами, хотя может быть с тем же успехом использована и для разработки небольших программ, например, тех, которые приведены в качестве примера в этой книге. Ниже приведена краткая процедура правки, компилирования и выполнения программы на C# в интегрированной среде разработки Visual Studio 2010. При этом предполагается, что интегрированная среда разработки входит в состав пакета Visual Studio 2010 Professional. В других версиях Visual Studio возможны незначительные отличия. 1. Создайте новый (пустой) проект С#, выбрав команду Файл■=>Создать
1^ Проект
(File ■=> New ^Project). Затем выберите элемент Windows
из списка Установленные шаблоны
(Installed Templates) и далее — шаблон Пустой проект
(Empty Project), как показано на рисунке. ПРИМЕЧАНИЕ
Имя и местоположение вашего проекта может отличаться от того, что показано здесь.
Щелкните на кнопке ОК, чтобы создать проект. После создания нового проекта среда Visual Studio будет выглядеть так, как показано на рисунке.
Если по какой-либо причине окно Обозреватель решений
(Solution Explorer) будет отсутствовать, откройте его с помощью команды ВидООбозреватель решений
(View^Solution Explorer). На данном этапе проект пуст, и вам нужно ввести в него файл с исходным текстом программы на С#.Дляэтого щелкните правой кнопкой мыши на имени проекта (в данном случае — Project 1) в окне Обозреватель решений,
а затем выберите команду Добавить
(Add) из контекстного меню. В итоге появится подменю, показанное на рисунке.
Выберите команду Создать элемент
(New Item), чтобы открыть диалоговое окно Добавление нового элемента
(Add New Item). Выберите сначала элемент Код (Code)
из списка Установленные шаблоны,
а затем шаблон Файл с текстом программы
(Code File) и измените имя файла на Example. cs,
как показано на рисунке.
5. Введите выбранный файл в проект, щелкнув на кнопке Добавить.
После этого экран будет выглядеть так, как показано на рисунке. 
6
. Введите исходный текст программы в окне с меткой Example. cs, после чего сохраните этот текст в файле. (Исходный текст примеров программ, приведенных в этой книге, можно свободно загрузить по адресу www.mhprofessional. com, чтобы не вводить его каждый раз вручную.) По завершении ввода исходного текста программы экран будет выглядеть так, как показано на рисунке.
7. Скомпилируйте программу, выбрав команду Построением Построить решение
(Build1=>Build Solution). 8
. Выполните программу, выбрав команду Отладка ^Запуск без отладки
(Debug^Start Without Debugging). В результате выполнения программы откроется окно, показанное на рисунке.
Как следует из приведенной выше процедуры, компилирование коротких программ в интегрированной среде разработки требует выполнения немалого числа шагов. Но для каждого примера программы из этой книги вам совсем не обязательно создавать новый проект. Вместо этого вы можете пользоваться одним и тем же проектом С#. С этой целью удалите текущий исходный файл и введите новый. Затем перекомпилируйте и выполните программу. Благодаря этому существенно упрощается весь процесс разработки коротких программ. Однако для разработки реальных приложений каждой программе потребуется отдельный проект. ПРИМЕЧАНИЕ
Приведенных выше инструкций достаточно для компилирования и выполнения примеров программ, представленных в этой книге, но если вы собираетесь пользоваться Visual Studio как основной средой для разработки программ, вам придется более подробно ознакомиться с ее возможностями и средствами. Это весьма эффективная среда разработки программ, помогающая поддерживать крупные проекты на поддающемся управлению организационном уровне. Данная интегрированная среда разработки позволяет также правильно организовать файлы и связанные с проектом ресурсы. Поэтому целесообразно потратить время и приложить усилия, чтобы приобрести необходимые навыки работы в среде Visual Studio.
Построчный анализ первого примера программы
Несмотря на то что пример программыExample. csдовольно краток, в нем демонстрируется ряд ключевых средств, типичных для всех программ на С#. Проанализируем более подробно каждую строку этой программы, начиная с ее имени. В отличие от ряда других языков программирования, и в особенности Java, где имя файла программы имеет большое значение, имя программы на C# может быть произвольным. Ранее вам было предложено присвоить программе из первого примера имяExa'mple . cs,чтобы успешно скомпилировать и выполнить ее, но в C# файл с исходным текстом этой программы можно было бы назвать как угодно. Например, его можно было назватьSample. cs, Test. csили дажеX. cs. В файлах с исходным текстом программ на C# условно принято расширение . cs, и это условие вы должны соблюдать. Кроме того, многие программисты называют файлы с исходным текстом своих программ по имени основного класса, определенного в программе. Именно поэтому в рассматриваемом здесь примере было выбрано имя файлаExample. cs.Но поскольку имена программ на C# могут быть произвольными, то они не указываются в большинстве примеров программ, приведенных в настоящей книге. Поэтому вы вольны сами выбирать для них имена. Итак, анализируемая программа начинается с таких строк. /* Это простая программа на С#. Назовем ее Example.cs. */ Эти строки образуюткомментарий. Как и в большинстве других языков программирования, в C# допускается вводить комментарии в файл с исходным текстом программы. Содержимое комментария игнорируется компилятором. Но, с другой стороны, в комментарии дается краткое описание или пояснение работы программы для всех, кто читает ее исходный текст. В данном случае в комментарии дается описание программы и напоминание о том, что ее исходный файл называетсяExample. cs.Разумеется, в комментариях к реальным приложениям обычно поясняется работа отдельных частей программы или же функции конкретных средств. В C# поддерживаются три стиля комментариев. Один из них приводится в самом начале программы и называетсямногострочным комментарием.Этот стиль комментария должен начинаться символами /* и оканчиваться символами */. Все, что находится между этими символами, игнорируется компилятором. Как следует из его названия, многострочный комментарий может состоять из нескольких строк. Рассмотрим следующую строку программы. using System; Эта строка означает, что в программе используется пространство именSystem.В C#пространство именопределяет область объявлений. Подробнее о пространстве имен речь пойдет далее в этой книге, а до тех пор поясним вкратце его назначение. Благодаря пространству имен одно множество имен отделяется от других. По существу, имена, объявляемые в одном пространстве имен, не вступают в конфликт с именами, объявляемыми в другом пространстве имен. В анализируемой программе используется пространство именSystem,которое зарезервировано для элементов, связанных с библиотекой классов среды .NET Framework, применяемой в С#. Ключевое словоusingпросто констатирует тот факт, что в программе используются имена в заданном пространстве имен. (Попутно обратим внимание на весьма любопытную возможность создавать собственные пространства имен, что особенно полезно для работы над крупными проектами.) Перейдем к следующей строке программы. class Example { В этой строке ключевое словоclassслужит для объявления вновь определяемого класса. Как упоминалось выше, класс является основной единицей инкапсуляции в С#, aExample— это имя класса. Определение класса начинается с открывающей фигурной скобки ({) и оканчивается закрывающей фигурной скобкой (}). Элементы, заключенные в эти фигурные скобки, являются членами класса. Не вдаваясь пока что в подробности, достаточно сказать, что в C# большая часть действий, выполняемых в программе, происходит именно в классе. Следующая строка программы содержитоднострочный комментарий. // Любая программа на C# начинается с вызова метода Main(). Это второй стиль комментариев, поддерживаемых в С#. Однострочный комментарий начинается и оканчивается символами //. Несмотря на различие стилей комментариев, программисты нередко пользуются многострочными комментариями для более длинных примечаний и однострочными комментариями для коротких, построчных примечаний к программе. (Третий стиль комментариев, поддерживаемых в С#, применяется при создании документации и описывается в приложении А.) Перейдем к анализу следующей строки программы. static void Main() { Эта строка начинается с методаMain (). Как упоминалось выше, в C# подпрограмма называется методом. И, как поясняется в предшествующем комментарии, именно с этой строки начинается выполнение программы. Выполнение всех приложений C# начинается с вызова методаMain (). Разбирать полностью значение каждого элемента данной строки пока что не имеет смысла, потому что для этого нужно знать ряд других средств С#. Но поскольку данная строка используется во многих примерах программ, приведенных в этой книге, то проанализируем ее вкратце. Данная строка начинается с ключевого слова static. Метод, определяемый ключевым словом static, может вызываться до создания объекта его класса. Необходимость в этом объясняется тем, что метод Main () вызывается при запуске программы. Ключевое слово void указывает на то, что метод Main () не возвращает значение. В дальнейшем вы узнаете, что методы могут также возвращать значения. Пустые круглые скобки после имени метода Main означают, что этому методу не передается никакой информации. Теоретически методу Main () можно передать информацию, но в данном примере этого не делается. И последним элементом анализируемой строки является символ {, обозначающий начало тела метода Main (). Весь код, составляющий тело метода, находится между открывающими и закрывающими фигурными скобками. Рассмотрим следующую строку программы. Обратите внимание на то, что она находится внутри методаMain (). Console.WriteLine("Простая программа на С#."); В этой строке осуществляется вывод на экран текстовой строки"Простая программа на C#. Сам вывод выполняется встроенным методомWriteLine (). В данном примере методWriteLine() выводит на экран строку, которая ему передается. Информация, передаваемая методу, называетсяаргументом.Помимо текстовых строк, методWriteLine() позволяет выводить на экран другие виды информации. Анализируемая строка начинается сConsole— имени предопределенного класса, поддерживающего ввод-вывод на консоль. Сочетание обозначенийConsoleиWriteLine() указывает компилятору на то, что методWriteLine() является членом классаConsole.Применение в C# объекта для определения вывода на консоль служит еще одним свидетельством объектно-ориентированного характера этого языка программирования. Обратите внимание на то, что оператор, содержащий вызов методаWriteLine (),оканчивается точкой с запятой, как, впрочем, и рассматривавшаяся ранее директиваusing System.Как правило, операторы в C# оканчиваются точкой с запятой. Исключением из этого правила служат блоки, которые начинаются символом { и оканчиваются символом }. Именно поэтому строки программы с этими символами не оканчиваются точкой с запятой. Блоки обеспечивают механизм группирования операторов и рассматриваются далее в этой главе. Первый символ } в анализируемой программе завершает методMain(), а второй — определение классаExample. И наконец, в C# различаются прописные и строчные буквы. Несоблюдение этого правила может привести к серьезным осложнениям. Так, если вы неумышленно наберетеmainвместоMainили жеwritelineвместоWriteLine,анализируемая программа окажется ошибочной. Более того, компилятор C# не предоставит возможность выполнить классы, которые не содержат методMain (), хотя искомпилируетих. Поэтому если вы неверно наберете имя методаMain,то получите от компилятора сообщение об ошибке, уведомляющее о том, что в исполняемом файлеExample. ехене определена точка входа. Обработка синтаксических ошибок
Если вы только начинаете изучать программирование, то вам следует научиться правильно истолковывать (и реагировать на) ошибки, которые могут появиться при попытке скомпилировать программу. Большинство ошибок компиляции возникает в результате опечаток при наборе исходного текста программы. Все программисты рано или поздно обнаруживают, что при наборе исходного текста программы очень легко сделать опечатку. Правда, если вы наберете что-нибудь неправильно, компилятор выдаст соответствующее сообщение осинтаксической ошибкепри попытке скомпилировать вашу программу. В таком сообщении обычно указывается номер строки исходного текста программы, где была обнаружена ошибка, а также кратко описывается характер ошибки. Несмотря на всю полезность сообщений о синтаксических ошибках, выдаваемых компилятором, они иногда вводят в заблуждение. Ведь компилятор C# пытается извлечь какой-то смысл из исходного текста, как бы он ни был набран. Именно по этой причине ошибка, о которой сообщает компилятор, не всегда отражает настоящую причину возникшего затруднения. Неумышленный пропуск открывающей фигурной скобки после методаMain() в рассмотренном выше примере программы приводит к появлению приведенной ниже последовательности сообщений об ошибках при компиляции данной программы компилятором командной строки с sc. (Аналогичные ошибки появляются при компиляции в интегрированной среде разработки Visual Studio.) EXl.CS (12,21): ошибка CS1002: ; ожидалось EXl.CS(13,22): ошибка CS1519: Недопустимая лексема '(' в объявлении члена класса, структуры или интерфейса EXl.CS(15,1): ошибка CS1022: Требуется определение типа или пространства имен либо признак конца файла Очевидно, что первое сообщение об ошибке нельзя считать верным, поскольку пропущена не точка с запятой, а фигурная скобка. Два других сообщения об ошибках вносят такую же путаницу. Из всего изложенного выше следует, что если программа содержит синтаксическую ошибку, то сообщения компилятора не следует понимать буквально, поскольку они могут ввести в заблуждение. Для выявления истинной причины ошибки может потребоваться критический пересмотр сообщения об ошибке. Кроме того, полезно проанализировать несколько строк кода, предшествующих той строке, в которой обнаружена сообщаемая ошибка. Иногда об ошибке сообщается лишь через несколько строк после того места, где она действительно произошла. Незначительное изменение программы
Несмотря на то что приведенная ниже строка указывается во всех примерах программ, рассматриваемых в этой книге, формально она не нужна. using System; Тем не менее она указывается ради удобства. Эта строка не нужна потому, что в C# можно всегдаполностью определитьимя с помощью пространства имен, к которому оно принадлежит. Например, строку Console.WriteLine("Простая программа на С#.");можно переписать следующим образом. System.Console.WriteLine("Простая программа на С#."); Таким образом, первый пример программы можно видоизменить так. // В эту версию не включена строка "using System;". class Example { // Любая программа на C# начинается с вызова метода Main(). static void Main() { // Здесь имя Console.WriteLine полностью определено. System.Console.WriteLine("Простая программа на С#."); } } Указывать пространство именSystemвсякий раз, когда используется член этого пространства, — довольно утомительное занятие, и поэтому большинство программистов на C# вводят директивуusing Systemв начале своих программ, как это сделано в примерах всех программ, приведенных в данной книге. Следует, однако, иметь в виду, что любое имя можно всегда определить, явно указав его пространство имен, если в этом есть необходимость. Вторая простая программа
В языке программирования, вероятно, нет более важной конструкции, чем переменная.Переменная —это именованная область памяти, для которой может быть уста-нрвлено значение. Она называется переменной потому, что ее значение может быть изменено по ходу выполнения программы. Иными словами, содержимое переменной подлежит изменению и не является постоянным. В приведенной ниже программе создаются две переменные —хиу. // Эта программа демонстрирует применение переменных. using System; * class Example2 { static void Main() { int x; // здесь объявляется переменная int у; // здесь объявляется еще одна переменная х = 100; // здесь переменной х присваивается значение 100 Console.WriteLine("х содержит " + х); у = х / 2; Console.Write("у содержит х / 2: "); Console.WriteLine(у); } } Выполнение этой программы дает следующий результат. х содержит 100 у содержит х / 2: 50 В этой программе вводится ряд новых понятий. Прежде всего, в операторе int х; // здесь объявляется переменная объявляется переменная целочисленного типа с именем х. В C# все переменные должны объявляться до их применения. Кроме того, нужно обязательно указать тип значения, которое будет храниться в переменной. Это так называемыйтиппеременной. В данном примере в переменной х хранится целочисленное значение, т.е. целое число. Для объявления в C# переменной целочисленного типа перед ее именем указывается ключевое слово int. Таким образом, в приведенном выше операторе объявляется переменная х типа int. В следующей строке объявляется вторая переменная с именем у. int у; // здесь объявляется еще одна переменная Как видите, эта переменная объявляется таким же образом, как и предыдущая, за исключением того, что ей присваивается другое имя. В целом, для объявления переменной служит следующий оператор: тип имя_переменной; гдетип —это конкретный тип объявляемой переменной, аимя_переменной —имя самой переменной. Помимо типа int, в C# поддерживается ряд других типов данных. В следующей строке программы переменной х присваивается значение 100. х = 100; // здесь переменной х присваивается значение 100 , В C# оператор присваивания обозначается одиночным знаком равенства (=). Данный оператор выполняет копирование значения, расположенного справа от знака равенства, в переменную, находящуюся слева от него. В следующей строке программы осуществляется вывод на экран текстовой строки11хсодержит11и значения переменной х. Console.WriteLine("х содержит " + х); В этом операторе знак + обозначает, что значение переменной х выводится вслед за предшествующей ему текстовой строкой. Если обобщить этот частный случай, то с помощью знака операции + можно организовать сцепление какого угодно числа элементов в одном операторе с вызовом методаWriteLine (). В следующей строке программы переменнойуприсваивается значение переменной х, деленное на 2
. у = х / 2; В этой строке значение переменной х делится на 2
, а полученный результат сохраняется в переменнойу.Таким образом, после выполнения данной строки в переменнойусодержится значение 50. При этом значение переменной х не меняется. Как и в большинстве других языков программирования, в C# поддерживаются все арифметические операции, в том числе и перечисленные ниже.
+
Сложение
-
Вычитание
*
Умножение
/
Деление
Рассмотрим две оставшиеся строки программы. Console.Write("у содержит х / 2: "); Console.WriteLine(у); В этих строках обнаруживаются еще две особенности. Во-первых, для вывода текстовой строки"у содержитх /2: 11на экран используется встроенный метод Write(). После этой текстовой строки новая строка не следует. Это означает, что последующий вывод будет осуществлен в той же самой строке. МетодWrite() подобен методуWriteLine(), за исключением того, что после каждого его вызова вывод не начинается с новой строки. И во-вторых, обратите внимание на то, что в вызове методаWriteLine() указывается только переменнаяу.Оба метода,Write()nWriteLine(), могут быть использованы для вывода значений любых встроенных в C# типов. Прежде чем двигаться дальше, следует упомянуть еще об одной особенности объявления переменных. Две или более переменных можно указать в одном операторе объявления. Нужно лишь разделить их запятой. Например, переменные х иумогут быть объявлены следующим образом. int х, у; // обе переменные объявляются в одном операторе ПРИМЕЧАНИЕ
В C# внедрено средство, называемое неявно типизированной переменной. Неявно типизированными являются такие переменные, тип которых автоматически определяется компилятором. Подробнее неявно типизированные переменные рассматриваются в главе 3.
Другие типы данных
В предыдущем примере программы использовались переменные типа int. Но в переменных типа int могут храниться только целые числа. Их нельзя использовать в операциях с числами, имеющими дробную часть. Например, переменная типа int может содержать значение 18, но не значение 18,3. Правда, int — далеко не единственный тип данных, определяемых в С#. Для операций с числами, имеющими дробную часть, в C# предусмотрены два типа данных с плавающей точкой: float и double. Они обозначают числовые значения с одинарной и двойнЬй точностью соответственно. Из этих двух типов чаще всего используется тип double. Для объявления переменной типа double служит оператор double result; где result — это имя переменной типа double. А поскольку переменная result имеет тип данных с плавающей точкой, то в ней могут храниться такие числовые значения, как, например, 122,23, 0,034 или -19,0. Для лучшего понимания.отличий между типами данных int и double рассмотрим такой пример программы. /* Эта программа демонстрирует отличия между типами данных int и double. */ using System; class Example3 { static void Main() { int ivar; // объявить целочисленную переменную double dvar; // объявить переменную с плавающей точкой ivar = 100; // присвоить переменной ivar значение 100 dvar = 100.0; // присвоить переменной dvar значение 100.0 Console.WriteLine("Исходное значение ivar: " + ivar); Console.WriteLine("Исходное значение dvar: " + dvar); Console.WriteLine(); // вывести пустую строку // Разделить значения обеих переменных на 3. ivar = ivar / 3; dvar = dvar / 3.0; - Console.WriteLine("Значение ivar после деления: " + ivar); Console.WriteLine("Значение dvar после деления: " + dvar); } } Ниже приведен результат выполнения приведенной выше программы. Исходное значение ivar: 100 Исходное значение dvar: 100 Значение ivar после деления: 33 Значение dvar после деления: 33.3333333333333 Как видите, при делении значения переменнойivarтипаintна3остается лишь целая часть результата — 33, а дробная его часть теряется. В то же время при делении значения переменнойdvarтипаdoubleна3дробная часть результата сохраняется. Как демонстрирует данный пример программы, в числовых значениях с плавающей точкой следует использовать обозначение самой десятичной точки. Например, значение 100 в C# считается целым, а значение 100,0 — с плавающей точкой. В данной программе обнаруживается еще одна особенность. Для вывода пустой строки достаточно вызвать методWriteLine() без аргументов. Типы данных с плавающей точкой зачастую используются в операциях с реальными числовыми величинами, где обычно требуется дробная часть числа. Так, приведенная ниже программа вычисляет площадь круга, используя значение 3,1416 числа "пи". // Вычислить площадь круга. using System; class Circle { static void Main() { double radius; double area; radius = 10.0; area = radius * radius * 3.1416; Console.WriteLine("Площадь равна " + area); } } Выполнение этой программы дает следующий результат. Площадь равна 314.16 Очевидно, что вычисление площади круга не дало бы удовлетворительного результата, если бы при этом не использовались данные с плавающей точкой. Два управляющих оператора
Выполнение программы внутри метода (т.е. в его теле) происходит последовательно от одного оператора к другому, т.е. по цепочке сверху вниз. Этот порядок выполнения программы можно изменить с помощью различных управляющих операторов, поддерживаемых в С#. Более подробно управляющие операторы будут рассмотрены в дальнейшем, а здесь они представлены вкратце, поскольку используются в последующих примерах программ. Условный оператор
С помощью условного оператораifв C# можно организовать выборочное выполнение части программы. Оператор if действует в C# практически так же, как и операторIFв любом другом языке программирования. В частности, с точки зрения синтаксиса он тождествен операторам i f в С, C++ и Java. Ниже приведена простейшая форма этого оператора. if (
условие) оператор;
Здесьусловиепредставляет собой булево, т.е. логическое, выражение, принимающее одно из двух значений: "истина" или "ложь". Если условие истинно, тооператорвыполняется. А если условие ложно, то выполнение программы происходит, минуяоператор.Ниже приведен пример применения условного оператора. if (10 < 11) Console . WriteLine (
1110 меньше 11
м);
В данном примере условное выражение принимает истинное значение, поскольку 10 меньше 11, и поэтому методWriteLine() выполняется. А теперь рассмотрим другой пример. if(10 < 9) Console.WriteLine ("не подлежит выводу");
В данном примере 10 не меньше 9. Следовательно, вызов методаWriteLine() не произойдет. В C# определен полный набор операторов отношения, которые можно использовать в условных выражениях. Ниже перечислены все эти операторы и их обозначения.
Операция
Значение
<
Меньше
<=
Меньше или равно
>
Больше
>=
Больше или равно
==
Равно
i =
Не равно
Далее следует пример еще одной программы, демонстрирующей применение условного оператора if. // Продемонстрировать применение условного оператора if. using System; class IfDemo { static void Main() { int a, b, c; a = 2; b = 3; if(a < b) Console.WriteLine("а меньше b"); // He подлежит выводу. if(a == b) Console.WriteLine("этого никто не увидит"); Console.WriteLine(); c=a-b; //с содержит -1 Console.WriteLine("с содержит -Iм); if(с >= 0) Console.WriteLine("значение с неотрицательно"); if(с < 0) Console.WriteLine("значение с отрицательно"); Console.WriteLine(); с = b - а; // теперь с содержит 1 Console.WriteLine("с содержит 1"); if(с >= 0) Console.WriteLine("значение с неотрицательно"); if(с < 0) Console.WriteLine ("значение с отрицательно "); } } Вот к какому результату приводит выполнение данной программы. а меньше b с содержит -1 значение с отрицательно с содержит 1 значение с неотрицательно Обратите внимание на еще одну особенность этой программы. В строке int а, Ь, с; три переменные,а,Ъи с, объявляютсясписком, разделяемым запятыми.Какупоминалось выше, если требуется объявить две или более переменные одного и того же типа, это можно сделать в одном операторе, разделив их имена запятыми. Оператор цикла
Дляповторного выполнения последовательности операций в программе можно организоватьцикл.Язык C# отличается большим разнообразием циклических конструкций. Здесь будет рассмотрен оператор цикла for. Как и у оператора if, у оператора f or в C# имеются аналоги в С, C++ и Java. Ниже приведена простейшая форма этого оператора. for (инициализация;условие; итерация) оператор; В самой общей форме в частиинициализацияданного оператора задается начальное значение переменной управления циклом. Частьусловиепредставляет собой булево выражение, проверяющее значение переменной управления циклом. Если результат проверки истинен, то цикл продолжается. Если же он ложен, то цикл завершается. В части итерация определяется порядок изменения переменной управления циклом на каждом шаге цикла, когда он повторяется. Ниже приведен пример краткой программы, демонстрирующей применение оператора цикла for. // Продемонстрировать применение оператора цикла- for. using System; class ForDemo { static void Main() { int count; for (count = 0; count < 5; count = count+1) Console.WriteLine("Это подсчет: " + count); Console.WriteLine("Готово!"); } } Вот как выглядит результат выполнения данной программы. Это подсчет: 0 Это подсчет: 1 Это подсчет: 2 Это подсчет: 3 Это подсчет: 4 Готово! В данном примереcountвыполняет роль переменной управления циклом. В инициализирующей части оператора циклаforзадается нулевое значение этой переменной. В начале каждого шага цикла, включая и первый, проверяется условиеcount< 5.Если эта проверка дает истинный результат, то выполняется оператор, содержащий методWriteLine (). Далее выполняется итерационная часть оператора циклаfor,где значение переменнойcountувеличивается на 1. Этот процесс повторяется до тех пор, пока значение переменнойcountне достигнет величины 5. В этот момент проверка упомянутого выше условия дает ложный результат, что приводит к завершению цикла. Выполнение программы продолжается с оператора, следующего после цикла. Любопытно, что в программах, профессионально написанных на С#, вы вряд ли увидите итерационную часть оператора цикла в том виде, в каком она представлена в приведенном выше примере программы, т.е. вы редко встретите следующую строку. count = count +1; • Дело в том, что в C# имеется специальный оператор инкремента, выполняющий приращение на 1 значение переменной, или так называемого операнда. Этот оператор обозначается двумя знаками + (++). Используя оператор инкремента, можно переписать приведенную выше строку следующим образом. count++; Таким образом, оператор циклаforиз приведенного выше примера программы обычно записывается в следующем виде. for (count = 0; count < 5; count++) Опробуйте этот более краткий способ записи итерационной части цикла. Вы сами можете убедиться, что данный цикл выполняется так же, как и прежде. В C# имеется также оператор декремента, обозначаемый двумя дефисами (—). Этот оператор уменьшает значение операнда на 1. Использование кодовых блоков
Еще одним важным элементом C# являетсякодовый блок,который представляет собой группу операторов. Для его организации достаточно расположить операторы между открывающей и закрывающей фигурными скобками. Как только кодовый блок будет создан, он станет логическим элементом, который можно использовать в любом месте программы, где применяется одиночный оператор. В частности, кодовый блок может служить адресатом операторовifиfor.Рассмотрим следующий операторif. if(w < h) { v = w * h; w = 0; } Если в данном примере кода значение переменной w меньше значения переменной h, то оба оператора выполняются в кодовом блоке. Они образуют внутри кодового блока единый логический элемент, причем один не может выполняться без другого. Таким образом, если требуется логически связать два (или более) оператора, то для этой цели следует создать кодовый блок. С помощью кодовых блоков можно более эффективно и ясно реализовать многие алгоритмы. / Ниже приведен пример программы, в которой кодовый блок служит для того, чтобы исключить деление на нуль. // Продемонстрировать применение кодового блока. using System; class BlockDemo { static void Main() { int i, j, d; i = 5; j = 10; // Адресатом этого оператора if служит кодовый блок, if(i != 0) { Console.WriteLine ("i не равно нулю"); d = j / i; Console.WriteLine("j / i равно " + d); } } } Вот к какому результату приводит выполнение данной программы. i не равно нулю j / i равно 2 В данном примере адресатом оператора if служит кодовый блок, а не единственный оператор. Если условие, управляющее операторомi f, оказывается истинным, то выполняются три оператора в кодовом блоке. Попробуйте задать нулевое значение переменнойi,чтобы посмотреть, что из этого получится. Рассмотрим еще один пример, где кодовый блок служит для вычисления суммы и произведения чисел от 1 до 10. // Вычислить сумму и произведение чисел от 1 до 10. using System; class ProdSum { static void Main() { int prod; int sum; int i; sum = 0; prod = 1; for (i=l; i <= 10; i++) { sum = sum + i; prod = prod * i; } Console.WriteLine("Сумма равна " + sum); Console.WriteLine("Произведение равно " + prod); } } Ниже приведен результат выполнения данной программы. Сумма равна 55 Произведение равно 362880 0 В данном примере внутри кодового блока организуется цикл для вычисления суммы и произведения. В отсутствие такого блока для достижения того же самого результата пришлось бы организовать два отдельных цикла. И последнее: кодовые блоки не снижают эффективность программ во время их выполнения. Иными словами, наличие символов { и }, обозначающих кодовый блок, никоим образом не замедляет выполнение программы. В действительности применение кодовых блоков, как правило, приводит к повышению быстродействия и эффективности программ, поскольку они упрощают программирование определенных алгоритмов.
Точка с запятой и оформление исходного текста программы
В C# точка с запятой обозначает конец оператора. Это означает, что каждый оператор в отдельности должен оканчиваться точкой с запятой.
Как вы уже знаете, кодовый блок представляет собой набор логически связанных операторов, заключенных в фигурные скобки. Блок не оканчивается точкой с запятой, поскольку он состоит из группы операторов. Вместо этого окончание кодового блока обозначается закрывающей фигурной скобкой.
В C# конец строки не означает конец оператора — о его окончании свидетельствует только точка с запятой. Именно поэтому оператор можно поместить в любой части строки. Например, на языке C# строки кода
X = у; у = у + 1;
Console.WriteLine(х + " " + у);означают то же самое, что и строка кода
х = у; у = у + 1; Console.WriteLine(х + " " + у);
Более того, составные элементы оператора можно располагать в отдельных строках. Например, следующий фрагмент кода считается в C# вполне допустимым.
Console.WriteLine("Это длинная строка вывода" + х + у + z +
"дополнительный вывод");
Такое разбиение длинных строк нередко применяется для того, чтобы сделать исходный текст программы более удобным для чтения. Оно помогает также исключить заворачивание слишком длинных строк.
Возможно, вы уже обратили внимание на то, что в предыдущих примерах программ некоторые операторы были набраны с отступом. В C# допускается свободная форма записи. Это означает, что взаимное расположение операторов в строке не имеет особого значения. Но с годами в программировании сложился общепринятый стиль оформления исходного текста программ с отступами, что существенно облегчает чтение этого текста. Именно этому стилю следуют примеры программ в данной книге, что рекомендуется делать и вам. В соответствии с этим стилем следует делать отступ (в виде нескольких пробелов) после каждой открывающей фигурной скобки и возвращаться назад после закрывающей фигурной скобки. А для некоторых операторов даже требуется дополнительный отступ, но об этом речь пойдет далее.
Ключевые слова C#
Основу любого языка программирования составляют его ключевые слова, поскольку они определяют средства, встроенные в этот язык. В C# определены два общих типа ключевых слов:зарезервированныеиконтекстные.Зарезервированные ключевые слова нельзя использовать в именах переменных, классов или методов. Их можно использовать только в качестве ключевых слов. Именно поэтому они и называютсязарезервированными.Их иногда еще называютзарезервированными словами, илизарезервированными идентификаторами.В настоящее время в версии 4.0 языка C# определено 77 зарезервированных ключ^евых слов (табл. 2.1).
Таблица 2.1. Ключевые слова, зарезервированные в языке C#
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
volatile
void
while
Кроме того, в версии C# 4.0 определены 18 контекстных ключевых слов, которые приобретают особое значение в определенном контексте. В таком контексте они выполняют роль ключевых слов, а вне его они могут использоваться в именах других элементов программы, например в именах переменных. Следовательно, контекстные ключевые слова*формально не являются зарезервированными. Но, как правило, их следует считать зарезервированными, избегая их применения в любыхдругихцелях. Ведь применение контекстного ключевого слова в качестве имени какого-нибудь другого элемента программы может привести к путанице, и поэтому считается многими программистами плохой практикой. Контекстные ключевые слова приведены в табл. 2.2.
Таблица 2.2. Контекстные ключевые слова в C#
add
dynamic
from
get
global
group
into
j oin
let
orderby
partial
remove
select
set
value
var
where
yield
Идентификаторы
В C# идентификатор представляет собой имя, присваиваемое методу, переменной или любому другому определяемому пользователем элементу программы. Идентификаторы могут состоять из одного или нескольких символов. Имена переменных могут начинаться с любой буквы алфавита или знака подчеркивания. Далее может следовать буква, цифра или знак подчеркивания. С помощью знака подчеркивания можно повысить удобочитаемость имени переменной, как, например,line_count.Но идентификаторы, содержащие два знака подчеркивания подряд, например,max_value,
зарезервированы для применения в компиляторе. Прописные и строчные буквы в C# различаются. Так, например myvar и MyVar — это разные имена переменных. Ниже приведены некоторые примеры допустимых идентификаторов.
Test
X
У2
MaxLoad
up
top
my var
sample23
Помните, что идентификатор не может начинаться с цифры. Например, 12х — недействительный идентификатор. Хорошая практика программирования требует выбирать идентификаторы, отражающие назначение или применение именуемых элементов.
Несмотря на то что зарезервированные ключевые слова нельзя использовать в качестве идентификаторов, в C# разрешается применять ключевое слово с предшествующим знаком @ в качестве допустимого идентификатора. Например,@for— действительный идентификатор. В этом случае в качестве идентификатора фактически служит ключевое словоfor,а знак @ просто игнорируется. Ниже приведен пример программы, демонстрирующей применение идентификатора со знаком @.
// Продемонстрировать применение идентификатора со знаком
using System;
class IdTest {
static void Main() { int @if; // применение ключевого слова if
//в качестве идентификатора
for(@if = 0; @if < 10; @if++)
Console . Writ-eLine ( "@if равно " + @if) ;
}
}
Приведенный ниже результат выполнения этой программы подтверждает, что @if правильно интерпретируется в качестве идентификатора.
@if равно 0 @if равно 1 @if равно 2 @if равно 3 @if равно 4 @if равно 5 @if равно 6 @if равно 7 @if равно 8 @if равно 9
Откровенно говоря, применять ключевые слова со знаком @ в качестве идентификаторов не рекомендуется, кроме особых случаев. Помимо того, знак @ может предшествовать любому идентификатору, но такая практика программирования считается плохой.
Библиотека классов среды .NET Framework
В примерах программ, представленных в этой главе, применялись два встроенных метода:WriteLine () иWrite (). Как упоминалось выше, эти методы являются членами классаConsole,относящегося к пространству именSystem,которое определяется в библиотеке классов для среды .NET Framework. Ранее в этой главе пояснялось, что среда C# опирается на библиотеку классов, предназначенную для среды .NET Framework, чтобы поддерживать операции ввода-вывода, обработку строк, работу в сети и графические пользовательские интерфейсы. Поэтому, вообще говоря, C# представляет собой определенное сочетание самого языка C# и стандартных классов .NET. Как будет показано далее, библиотека классов обеспечивает функциональные возможности, являющиеся неотъемлемой частью любой программы на С#. Для того чтобы научиться программировать на С#, нужно знать не только сам язык, но и уметь пользоваться стандартными классами. Различные элементы библиотеки классов для среды .NET Framework рассматриваются в части I этой книги, а в части II — сама библиотека по отдельным ее составляющим.
ГЛАВА 3 Типы данных, литералы и переменные
Вэтой главе рассматриваются три основополагающих элемента С#: типы данных, литералы и переменные.
В целом, типы данных, доступные в языке программирования, определяют те виды задач, для решения которых можно применять данный язык. Как и следовало ожидать, в C# предоставляется богатый набор встроенных типов данных, что делает этот язык пригодным для самого широкого применения. Любой из этих типов данных может служить для создания переменных и констант, которые в языке C# называютсялитералами.
О значении типов данных
Типы данных имеют особенное значение в С#, поскольку это строго типизированный язык. Это означает, что все операции подвергаются строгому контролю со стороны компилятора на соответствие типов, причем недопустимые операции не компилируются. Следовательно, строгий контроль типов позволяет исключить ошибки и повысить надежность программ.Дляобеспечения контроля типов все переменные, выражения и значения должны принадлежать к определенному типу. Такого понятия, как "бестиповая" переменная, в данном языке программирования вообще не существует. Более того, тип значения определяет те операции, которые разрешается выполнять над ним. Операция, разрешенная для одного типа данных, может оказаться не-. допустимой для другого.
ПРИМЕЧАНИЕ
В версии C# 4.0 внедрен новый тип данных, называемый dynamic и приводящий к отсрочке контроля типов до времени выполнения, вместо того чтобы производить подобный контроль во время компиляции. Поэтому тип dynamic является исключением из обычного правила контроля типов во время компиляции. Подробнее о типе dynamic речь пойдет в главе 17.
Типы значений в C#
В C# имеются две общие категории встроенных типов данных:типы значенийиссылочные типы.Они отличаются по содержимому переменной. Если переменная относится к типу значения, то она содержит само значение, например 3,1416 или 212. А если переменная относится к ссылочному типу, то она содержит ссылку на значение. Наиболее распространенным примером использования ссылочного типа является класс, но о классах и ссылочных типах речь пойдет далее в этой книге. А здесь рассматриваются типы значений.
В основу языка C# положены 13 типов значений, перечисленных в табл. 3.1. Все они называютсяпростыми типами, поскольку состоят из единственного значения. (Иными словами, они не состоят из двух или более значений.) Они составляют основу системы типов С#, предоставляя простейшие, низкоуровневые элементы данных, которыми можно оперировать в программе. Простые типы данных иногда еще называютпримитивными.
Таблица. 3.1. Типы значений в C#
Тип
Значение
bool
Логический, предоставляет два значения: “истина” или “ложь”
byte
8-разрядный целочисленный без знака
char
Символьный
decimal
Десятичный (для финансовых расчетов)
double
С плавающей точкой двойной точности
float
С плавающей точкой одинарной точности
int
Целочисленный
long
Длинный целочисленный
sbyte
8-разрядный целочисленный со знаком
short
Короткий целочисленный
uint
Целочисленный без знака
ulong
Длинный целочисленный без знака
ushort
Короткий целочисленный без знака
В C# строго определены пределы и характер действия каждого типа значения. Исходя из требований к переносимости программ, C# не допускает в этом отношении никаких компромиссов. Например, типintдолжен быть одинаковым во всех средах выполнения. Но в этом случае отпадает необходимость переписывать код для конкретной платформы. И хотя строгое определение размерности типов значений может стать причиной незначительного падения производительности в некоторых средах, эта мера необходима для достижения переносимости программ.
ПРИМЕЧАНИЕ
Помимо простыхтипов, в C# определены еще три категории типов значений: перечисления, структуры и обнуляемые типы. Все они рассматриваются далее в этой книге.
Целочисленные типы
В C# определены девять целочисленных типов:char, byte, sbyte, short, ushort, int, uint, longиulong.Но типcharприменяется, главным образом, для представления символов и поэтому рассматривается далее в этой главе. Остальные восемь целочисленных типов предназначены для числовых расчетов. Ниже представлены их диапазон представления чисел и разрядность в битах.
Тип
Разрядность в битах
Диапазон представления чисел
byte
8
0-255
sbyte
8
-128-127
short
16
-32 768-32 767
ushort
16
0-65 535
int
32
-2 147 483 648-2 147 483 647
uint
32
0-4 294 967 295
long
64
-9 223 372 036 854 775 808-9 223 372 036 854 775 807
ulong
64
0-18 446 744 073 709 551 615
Как следует из приведенной выше таблицы, в C# определены оба варианта различных целочисленных типов: со знаком и без знака. Целочисленные типы со знаком отличаются от аналогичных типов без знака способом интерпретации старшего разряда целого числа. Так, если в программе указано целочисленное значение со знаком, то компилятор C# сгенерирует код, в котором старший разряд целого числа используется в качествефлага знака.Число считается положительным, если флаг знака равен О, и отрицательным, если он равен 1. Отрицательные числа практически всегда представляются методом дополнения до двух, в соответствии с которым все двоичные разряды отрицательного числа сначала инвертируются, а затем к этому числу добавляется 1.
Целочисленные типы со знаком имеют большое значение для очень многих алгоритмов, но по абсолютной величине они наполовину меньше своих аналогов без знака. Вот как, например, выглядит число 32 767 типаshortв двоичном представлении.
0111111111111111
Если установить старший разряд этого числа равным 1, чтобы получить значение со знаком, то оно будет интерпретировано как -1, принимая во внимание формат дополнения до двух. Но если объявить его как значение типаushort,то после установки в 1 старшего разряда оно станет равным 65 535.
Вероятно, самым распространенным в программировании целочисленным типом является тип int. Переменные типа int нередко используются для управления циклами, индексирования массивов и математических расчетов общего назначения. Когда же требуется целочисленное значение с большим диапазоном представления чисел, чем у типа int, то для этой цели имеется целый ряд других целочисленных типов. Так, если значение нужно сохранить без знака, то для него можно выбрать тип uint, для больших значений со знаком — тип long, а для больших значений без знака — тип ulong. В качестве примера ниже приведена программа, вычисляющая расстояние от Земли до Солнца в дюймах. Для хранения столь большого значения в ней используется переменная типа long.
// Вычислить расстояние от Земли до Солнца в дюймах.
using System;
class Inches {
static void Main() { long inches; long miles;
miles = 93000000; // 93 000 000 миль до Солнца
// 5 280 футов в миле, 12 дюймов в футе, inches = miles * 5280 * 12;
Console.WriteLine("Расстояние до Солнца: " + inches + " дюймов.");
}
}
Вот как выглядит результат выполнения этой программы.
Расстояние до Солнца: 58 92480000000 дюймов.
Очевидно, что этот результат нельзя было бы сохранить в переменной типа int или uint.
Самыми мелкими целочисленными типами являются byte и sbyte. Тип byte представляет целые значения без знака в пределах от 0 до 255. Переменные типа byte особенно удобны для обработки исходных двоичных данных, например байтового потока, поступающего от некоторого устройства. А для представления мелких целых значений со знаком служит тип sbyte. Ниже приведен пример программы, в которой переменная типа byte используется для управления циклом, где суммируются числа от 1 до 100.
// Использовать тип byte.
using System;
class Use_byte {
static void Main() { byte x; int sum;
sum = 0;
for(x = 1; x <= 100; x++) sum = sum + x;
Console.WriteLine("Сумма чисел от 1 до 100 равна " + sum);
}
}
Результат выполнения этой программы выглядит следующим образом.
Сумма чисел от 1 до 100 равна 5050
В приведенном выше примере программы цикл выполняется только от 1 до 100, что не превышает диапазон представления чисел для типаbyte,и поэтому для управления этим циклом не требуется переменная более крупного типа.
Если же требуется целое значение, большее, чем значение типаbyteилиsbyte,но меньшее, чем значение типаintилиuint,то для него можно выбрать типshortилиushort.
Типы для представления чисел с плавающей точкой
Типы с плавающей точкой позволяют представлять числа с дробной частью. В C# имеются две разновидности типов данных с плавающей точкой:floatиdouble.Они представляют числовые значения с одинарной и двойной точностью соответственно. Так, разрядность типаfloatсоставляет 32 бита, что приближенно соответствует диапазону представления чисел от 5Е-45 до 3,4Е+38. А разрядность типаdoubleсоставляет 64 бита, что приближенно соответствует диапазону представления чисел от 5Е-324 до
1,7Е+308.
В программировании на C# чаще применяется типdouble,в частности, потому, что во многих математических функциях из библиотеки классов С#, которая одновременно является библиотекой классов для среды .NET Framework, используются числовые значения типаdouble.Например, методSqrt (), определенный в библиотеке классовSystem. Math,возвращает значение типаdouble,которое представляет собой квадратный корень из аргумента типаdouble,передаваемого данному методу. В приведенном ниже примере программы методSqrt() используется для вычисления радиуса окружности по площади круга.
// Определить радиус окружности по площади круга.
using System;
class FindRadius { static void Main() {
Double r;
Double area;
area = 10.0;
r = Math.Sqrt(area / 3.1416);
Результат выполнения этой программы выглядит следующим образом.
Радиус равен 1.78412203012729
В приведенном выше примере программы следует обратить внимание на вызов методаSqrt(). Как упоминалось выше, методSqrt() относится к классуMath,поэтому в его*вызове имяMathпредшествует имени самого метода. Аналогичным образом имя классаConsoleпредшествует имени методаWriteLine() в его вызове. При вызове некоторых, хотя и не всех, стандартных методов обычно указывается имя их класса, как показано в следующем примере.
В следующем примере программы демонстрируется применение нескольких тригонометрических функций, которые относятся к классуMathи входят в стандартную библиотеку классов С#. Они также оперируют данными типаdouble.В этом примере на экран выводятся значения синуса, косинуса и тангенса угла, измеряемого в пределах от 0,1 до 1,0 радиана.
// Продемонстрировать применение тригонометрических функций.
using System;
class Trigonometry { static void Main() {
Double theta; // угол в радианах „
for(theta = 0.1; theta <= 1.0;
theta = theta +0.1) {
Console.WriteLine("Синус угла " + theta +
" i равен " + Math.Sin(theta));
Console.WriteLine("Косинус угла " + theta +
" равен " + Math.Cos(theta));
Console.WriteLine("Тангенс угла " + theta +
" равен " + Math.Tan(theta));
Console.WriteLine ();
}
}
}
Ниже приведена лишь часть результата выполнения данной программы.
Синус угла 0.1 равен 0.0998334166468282 Косинус угла 0.1 равен 0.995004165278026 Тангенс угла 0.1 равен 0.100334672085451
Синус угла 0.2 равен 0.198 6693307 95061 Косинус угла 0.2 равен 0.980066577841242 Тангенс угла 0.2 равен 0.202710035508673
Синус угла 0.3 равен 0.2 9552020666134 Косинус угла 0.3 равен 0.955336489125606 Тангенс угла 0.3 равен 0.309336249609623
Для вычисления синуса, косинуса и тангенса угла в приведенном выше примере были использованы стандартные методыMath. Sin(), Math. Cos() иMath. Tan().Как и методMath.Sqrt(), эти тригонометрические методы вызываются с аргументом типаdoubleи возвращают результат того же типа. Вычисляемые углы должны быть указаны в радианах.
Десятичный тип данных
Вероятно, самым интересным среди всех числовых типов данных в C# является тип decimal, который предназначен для применения в финансовых расчетах. Этот тип имеет разрядность 128 бит для представления числовых значений в пределах от 1Е-28 до 7,9Е+28. Вам, вероятно, известно, что для обычных арифметических вычислений с плавающей точкой характерны ошибки округления десятичных значений. Эти ошибки исключаются при использовании типа decimal, который позволяет представить числа с точностью до 28 (а иногда и 29) десятичных разрядов. Благодаря тому что этот тип данных способен представлять десятичные значения без ошибок округления, он особенно удобен для расчетов, связанных с финансами.
Ниже приведен пример программы, в которой тип decimal используется в конкретном финансовом расчете. В этой программе цена со скидкой рассчитывается на основании исходной цены и скидки в процентах.
// Использовать тип decimal для расчета скидки.
using System;
class UseDecimal { static void Main() { decimal price; decimal discount; decimal discounted_price;
// Рассчитать цену со скидкой, price = 19.95m;
discount = 0.15m; // норма скидки составляет 15% discounted_price = price - ( price * discount);
Console.WriteLine("Цена со скидкой: $" + discounted_price);
}
}
Результат выполнения этой программы выглядит следующим образом.
Цена со скидкой: $16.9575
Обратите внимание на то, что значения констант типа decimal в приведенном выше примере программы указываются с суффиксом т. Дело в том, что без суффикса m эти значения интерпретировались бы как стандартные константы с плавающей точкой, которые несовместимы с типом данных decimal. Тем не менее переменной типа decimal можно присвоить целое значение без суффикса т, например 10. (Подробнее
о числовых константах речь пойдет далее в этой‘главе.)
Рассмотрим еще один пример применения типа decimal. В этом примере рассчитывается будущая стоимость капиталовложений с фиксированной нормой прибыли в течение ряда лет.
/*
Применить тип decimal для расчета будущей стоимости капиталовложений.
*/
using System;
class FutVal {
static void Main() { decimal amount; decimal rate_of_return; int years, i;
amount = 1000.0M; rate_of_return = 0.07M; years = 10;
Console.WriteLine("Первоначальные капиталовложения: $" + amount);
Console.WriteLine("Норма прибыли: " + rate_of_return);
Console.WriteLine("В течение " + years + " лет");
for(i =0; i < years; i++)
amount = amount + (amount * rate_of_return);
Console.WriteLine("Будущая стоимость равна $" + amount);
}
}
Вот как выглядит результат выполнения этой программы.
Первоначальные капиталовложения: $1000 Норма прибыли: 0.07 В течение 10 лет
Будущая стоимость равна $1967.151357289565322490000
Обратите внимание на то, что результат выполнения приведенной выше программы представлен с точностью ДО целого ряда десятичных разрядов, т.е. с явным избытком по сравнению с тем, что обычно требуется! Далее в этой главе будет показано, как подобный результат приводится к более "привлекательному" виду.
Символы
В C# символы представлены не 8-разрядным кодом, как во многих других языках программирования, например C++, а 16-разрядным кодом, который называетсяуникодом(Unicode). В уникоде набор символов представлен настолько широко, что он охватывает символы практически из всех естественных языков на свете. Если для многих естественных языков, в том числе английского, французского и немецкого, характерны относительно небольшие алфавиты, то в ряде других языков, например китайском, употребляются довольно обширные наборы символов, которые нельзя представить 8-разрядным кодом. Для преодоления этого ограничения в C# определен тип char, представляющий 16-разрядные значения без знака в пределах от 0 до 65 535. При этом стандартный набор символов в 8-разрядном коде ASCII является подмножеством уникода в пределах от 0 до 127. Следовательно, символы в коде ASCII по-прежнему остаются действительными в С#.
Для того чтобы присвоить значение символьной переменной, достаточно заключить это значение (т.е. символ) в одинарные кавычки. Так, в приведенном ниже фрагменте кода переменной ch присваивается символ X.
char ch; ch = 'X';
Значение типаcharможно вывести на экран с помощью методаWriteLine ().Например, в следующей строке кода на экран выводится значение переменнойch.
Console'.WriteLine ("Значение ch равно: " + ch) ;
Несмотря на то что тип char определен в C# как целочисленный, его не следует путать со всеми остальными целочисленными типами. Дело в том, что в C# отсутствует автоматическое преобразование символьных значений в целочисленные и обратно. Например, следующий фрагмент кода содержит ошибку.
char ch;
ch = 88; // ошибка, не выйдет
Ошибочность приведенного выше фрагмента кода объясняется тем, что 8 8 — это целое значение, которое не преобразуется автоматически в символьное. При попытке скомпилировать данный фрагмент кода будет выдано соответствующее сообщение об ошибке. Для того чтобы операция присваивания целого значения символьной переменной оказалась допустимой, необходимо осуществить приведение типа, о котором речь пойдет далее в этой главе.
Логический тип данных
Типboolпредставляет два логических значения: "истина" и "ложь". Эти логические значения обозначаются в C# зарезервированными словамиtrueиfalseсоответственно. Следовательно, переменная или выражение типаboolбудет принимать одно из этих логических значений. Кроме того, в C# не определено взаимное преобразование логических и целых значений. Например, 1 не преобразуется в значениеtrue,а 0 — в значениеfalse.
В приведенном ниже примере программы демонстрируется применение типаbool.
// Продемонстрировать применение типа bool.
using System;
class BoolDemo {
static void Main() { bool b; b = false;
Console.WriteLine("b равно " + b); b = true;
Console.WriteLine("b равно " + b);
// Логическое значение может управлять оператором if. if(b) Console.WriteLine("Выполняется.");
b = false;
if(b) Console.WriteLine("He выполняется.");
// Результатом выполнения оператора отношения // является логическое значение.
Console.WriteLine("10 > 9 равно " + (10 > 9));
}
• }
Эта программа дает следующий результат.
b равно False b равно True Выполняется.
10 > 9 равно True
В приведенной выше программе обнаруживаются три интересные особенности. Во-первых, при выводе логического значения тийаboolс помощью методаWriteLineOна экране появляется значение 'True" или "False". Во-вторых, самого значения переменной типаboolдостаточно для управления операторомif.Для этого не нужно, например, записывать операторifследующим образом.
if(b == true) . . .
И в-третьих, результатом выполнения оператора отношения является логическое значение. Именно поэтому в результате вычисления выражения 10 > 9 на экран выводится значение "True." Кроме того, выражение 10 > 9 следует заключить в скобки, поскольку оператор + имеет более высокий приоритет, чем оператор >.
Некоторые возможности вывода
До сих пор при выводе с помощью методаWriteLineOданные отображались в формате, используемом по умолчанию. Но в среде .NET Framework определен достаточно развитый механизм форматирования, позволяющий во всех деталях управлять выводом данных. Форматированный ввод-вывод подробнее рассматривается далее в этой книге, а до тех пор полезно ознакомиться с некоторыми возможностями форматирования. Они позволяют указать, в каком именно виде следует выводить значения с помощью методаWriteLine (). Благодаря этому выводимый результат выглядит более привлекательно. Следует, однако, иметь в виду, что механизм форматирования поддерживает намного больше возможностей, а не только те, которые рассматриваются в этом разделе.
При выводе списков данных в предыдущих примерах программ каждый элемент списка приходилось отделять знаком +, как в следующей строке.
Console.WriteLine("Вы заказали " + 2 +
" предмета по цене $" + 3 + " каждый.");
Конечно, такой способ вывода числовой информации удобен, но он не позволяет управлять внешним видом выводимой информации. Например, при выводе значения с плавающей точкой нельзя определить количество отображаемых десятичных разрядов. Рассмотрим оператор
Console.WriteLine("Деление 10/3 дает: " + 10.0/3.0);который выводит следующий результат.
Деление 10/3 дает: 3.33333333333333
В одних случаях такого вывода может оказаться достаточно, а в других — он просто недопустим. Например, в финансовых расчетах после десятичной точки принято указывать лишь два десятичных разряда.
Для управления форматированием числовых данных служит другая форма методаWriteLine (), позволяющая встраивать информацию форматирования, как показано ниже.
WriteLine("форматирующая строка", argO, argl, ... , argN);
В этой форме аргументы методаWriteLine() разделяются запятой, а не знаком +. Аформатирующая строкасостоит из двух элементов: обычных печатаемых символов, предназначенных для вывода в исходном виде, а также спецификаторов формата. Последние указываются в следующей общей форме:
{argnum, width: fmt}
гдеargnum— номер выводимого аргумента, начиная с нуля;width— минимальная ширина поля;fmt —формат. Параметрыwidth и fmtявляются необязательными.
Если во время выполнения в форматирующей строке встречается спецификатор формата, то вместо него подставляется и отображается соответствующий аргумент, обозначаемый параметромargnum.Таким образом, местоположение спецификатора формата в форматирующей строке определяет место отображения соответствующих данных. Параметрыwidth и fmt'указывать необязательно. Это означает, что в своей простейшей форме спецификатор формата обозначает конкретный отображаемый аргумент. Например, спецификатор { 0 } обозначает аргументагдО,спецификатор {1} — аргументarglи т.д.
Начнем с самого простого примера. При выполнение оператора
Console.WriteLine("В феврале {0} или {1} дней.", 28, 29);
получается следующий результат.
В феврале 28 или 2 9 дней
Как видите, значение 2 8 подставляется вместо спецификатора { 0 }, а значение 2 9 — вместо спецификатора {1}. Следовательно, спецификаторы формата обозначают место в строке, где отображаются соответствующие аргументы (в данном случае — значения 28 и 2 9). Кроме того, обратите внимание на то, что дополнительные значения разделяются запятой, а не знаком +.
Ниже приведен видоизмененный вариант предыдущего оператора, в котором указывается ширина полей.
Console.WriteLine("В феврале {0,10} или {1,5} дней.", 28, 29);
Выполнение этого оператора дает следующий результат.
В феврале 28 или 2 9 дней.
Как видите, неиспользуемые части полей заполнены пробелами. Напомним, чтоминимальнаяширина поля определяется параметромwidth.Если требуется, она может быть превышена при выводе результата.
Разумеется, аргументы, связанные с командой форматирования, не обязательно должны быть константами. Ниже приведен пример программы, которая выводит таблицу результатов возведения чисел в квадрат и куб. В ней команды форматирования используются для вывода соответствующих значений.
// Применить команды форматирования.
using System;
class DisplayOptions { static void Main() { int i;
Console .WriteLine ("Число^Квадрат^Куб") ;
for(i = 1; i < 10; i++)
Console.WriteLine("{0}\t{1}\t{2}", i, i*i, i*i*i);
}
}
Результат выполнения этой программы выглядит следующим образом.
Число Квадрат Куб f
1
1
1
2
4
8
3
9
27
4
16
64
5
25
125
6
36
216
7
49
343
8
64
512
9
81
729
В приведенных выше примерах сами выводимые значения не форматировались. Но ведь основное назначение спецификаторов формата — управлять внешним видом выводимых данных. Чаще всего форматированию подлежат следующие типы данных: с плавающей точкой и десятичный. Самый простой способ указать формат данных — описать шаблон, который будет использоваться в методеWriteLine().Для этого указывается образец требуемого формата с помощью символов #, обозначающих разряды чисел. Кроме того, можно указать десятичную точку и запятые, разделяющие цифры. Ниже приведен пример более подходящего вывода результата деления 10 на 3.
Console.WriteLine("Деление 10/3 дает: {0:#.##}", 10.0/3.0);
Выполнение этого оператора приводит к следующему результату.
Деление 10/3 дает: 3.33
В данном примере шаблон # . ## указывает методуWriteLine() отобразить два десятичных разряда в дробной части числа. Следует, однако, иметь в виду, что методWriteLine() может отобразить столько цифр слева от десятичной точки, сколько потребуется для правильной интерпретации выводимого значения.
Рассмотрим еще один пример. Оператор
Console.WriteLine("{0:###,###.##}", 123456.56) ;дает следующий результат.
123,456.56
Для вывода денежных сумм, например, рекомендуется использовать спецификатор форматаС.
decimal balance; balance = 12323.09m;
Console.WriteLine("Текущий баланс равен {0:C}", balance);
Результат выполнения этого фрагмента кода выводится в формате денежных сумм, указываемых в долларах США.
Текущий баланс равен $12,323.0 9
Форматом С можно также воспользоваться, чтобы представить в более подходящем виде результат выполнения рассматривавшейся ранее программы расчета цены со скидкой.
// Использовать спецификатор формата С для вывода // результата в местной валюте.
using System;
class UseDecimal { static void Main() { decimal price; decimal discount; decimal discounted_price;
// рассчитать цену со скидкой, price = 19.95m;
discount = 0.15m; // норма скидки составляет 15% discounted_price = price - ( price * discount);
Console.WriteLine("Цена со скидкой: {0:C}", discounted_price);
}
}
Вот как теперь выглядит результат выполнения этой программы.
Цена со скидкой: 16,96 грн.
Литералы
В C#литераламиназываются постоянные значения, представленные в удобной для восприятия форме. Например, число 100 является литералом. Сами литералы и их назначение настолько понятны, что они применялись во всех предыдущих примерах программ без всяких пояснений. Но теперь настало время дать им формальное объяснение.
В C# литералы могут быть любого простого типа. Представление каждого литерала зависит от конкретного типа. Как пояснялось ранее, символьные литералы заключаются в одинарные кавычки. Например, 1 а1 и 1 % 1 являются символьными литералами.
Целочисленные литералы указываются в виде чисел без дробной части. Например, 10 и -100 — это целочисленные литералы. Для обозначения литералов с плавающей точкой требуется указывать десятичную точку и дробную часть числа. Например,
11.123 — это литерал с плавающей точкой. Для вещественных чисел с плавающей точкой в C# допускается также использовать экспоненциальное представление.
У литералов должен быть также конкретный тип, поскольку C# является строго типизированным языком. В этой связи возникает естественный вопрос: к какому типу следует отнести числовой литерал, например 2, 123987 или 0 . 23? К счастью, для ответа на этот вопрос в C# установлен ряд простых для соблюдения правил.
Во-первых, у целочисленных литералов должен быть самый мелкий целочисленный тип, которым они могут быть представлены, начиная с типаint.Таким образом, у целочисленных литералов может быть один из следующих типов:int, uint, longилиulongв зависимости от значения литерала. И во-вторых, литералы с плавающей точкой относятся к типуdouble.
Если вас не устраивает используемый по умолчанию тип литерала, вы можете ярно указать другой его тип с помощью суффикса. Так, для указания типаlongк литералу присоединяется суффикс1илиL.Например,12— это литерал типаint, al2L— литерал типаlong.Для указания целочисленного типа без знака к литералу присоединяется суффиксиилиU.Следовательно,100— это литерал типаint, a 100U— литерал типаuint.А для указания длинного целочисленного типа без знака к литералу присоединяется суффиксulилиUL.Например,984375UL— это литерал типаulong.
Кроме того, для указания типаfloatк литералу присоединяется суффиксFилиf. Например,10 .19F— это литерал типаfloat.Можете даже указать типdouble,присоединив к литералу суффиксdилиD,хотя это излишне. Ведь, как упоминалось выше, по умолчанию литералы с плавающей точкой относятся к типуdouble.
И наконец, для указания типаdecimalк литералу присоединяется суффиксmилиМ.Например,9 . 95М— это десятичный литерал типаdecimal.
Несмотря на то что целочисленные литералы образуют по умолчанию значения типаint, uint, longилиulong,их можно присваивать переменным типаbyte, sbyte, shortилиushort,при условии, что присваиваемое значение может быть представлено целевым типом.
Шестнадцатеричные литералы
Вам, вероятно, известно, что в программировании иногда оказывается проще пользоваться системой счисления по основанию 16, чем по основанию 10. Система счисления по основанию 16 называетсяшестнадцатеричной.В ней используются числа от 0 до 9, а также буквы от А до F, которыми обозначаются десятичные числа 10,11,12,13, 14 и 15. Например, десятичному числу 16 соответствует шестнадцатеричное число 10. Вследствие того что шестнадцатеричные числа применяются в программировании довольно часто, в C# разрешается указывать целочисленные литералы в шестнадцатеричном формате. Шестнадцатеричные литералы должны начинаться с символов Ох, т.е. нуля и последующей латинской буквы "икс". Ниже приведены некоторые примеры шестнадцатеричных литералов.
count = OxFF; // 255 в десятичной системе incr = 0x1а; // 26 в десятичной системе
Управляющие последовательности символов
Большинство печатаемых символов достаточно заключить в одинарные кавычки, но набор в текстовом редакторе некоторых символов, например возврата каретки, вызывает особые трудности. Кроме того, ряд других символов, в том числе одинарные и двойные кавычки, имеют специальное назначение в С#, поэтому их нельзя использовать непосредственно. По этим причинам в C# предусмотрены специальныеуправляющие последовательности символов, иногда еще называемыеконстантами с обратной косой чертой(табл. 3.2). Такие последовательности применяются вместо тех символов, которых они представляют.'
Таблица 3.2. Управляющие последовательности символов
Управляющая последовательность
Описание
\a
Звуковой сигнал (звонок)
\b
Возврат на одну позицию
\f
Перевод страницы (переход на новую страницу)
\n
Новая строка (перевод строки)
\r
Возврат каретки
\t
Горизонтальная табуляция
\v
Вертикальная табуляция
\0
Пустой символ
V
Одинарная кавычка
\"
Двойная кавычка
w
Обратная косая черта
Например, в следующей строке кода переменной ch присваивается символ табуляции.
ch = 1\t1;
А в приведенном ниже примере кода переменной ch присваивается символ одинарной кавычки.
ch = 1 \ ' ';
Строковые литералы
В C# поддерживается еще один тип литералов —строковый.Строковый литерал представляет собой набор символов, заключенных в двойные кавычки. Например следующий фрагмент кода:
"это тест"
представляет собой текстовую строку. Образцы подобных строк не раз встречались в приведенных выше примерах программ.
Помимо обычных символов, строковый литерал может содержать одну или несколько управляющих последовательностей символов, о которых речь шла выше. Рассмотрим для примера программу, в которой используются управляющие последовательности \п и \t.
// Продемонстрировать применение управляющих // последовательностей символов в строковых литералах.
.using System;
class StrDemo {
static void Main() {
Console.WriteLine("Первая строка\пВторая строка\пТретья строка");
Console.WriteLine("OflHH\tflBa\tTpn");
Console.WriteLine("Четыре^Пять\Шесть" ) ;
// Вставить кавычки.
Console.WriteLine("\"3ачем?\", спросил он.");
}
}
Результат выполнения этой программы приведен ниже.
Первая строка Вторая строка Третья строка Один Два Три
Четыре Пять Шесть
"Зачем?", спросил он.
В приведенном выше примере программы обратите внимание на то, что для перехода на новую строку используется управляющая последовательность \п. Для вывода нескольких строк совсем не обязательно вызывать методWriteLine() несколько раз — достаточно вставить управляющую последовательность\пв тех местах удлиненной текстовой строки (или строкового литерала), где должен происходить переход на новую строку. Обратите также внимание на то, как в текстовой строке формируется знак кавычек.
Помимо описанной выше формы строкового литерала, можно также указатьбуквальный строковый литерал.Такой литерал начинается с символа @, после которого следует строка в кавычках. Содержимое строки в кавычках воспринимается без изменений и может быть расширено до двух и более строк. Это означает, что в буквальный строковый литерал можно включить символы новой строки, табуляции и прочие, не прибегая к управляющим последовательностям. Единственное исключение составляют двойные кавычки ("), для указания которых необходимо использовать две двойные кавычки подряд (" "). В приведенном ниже примере программы демонстрируется применение буквальных строковых литералов.
// Продемонстрировать применение буквальных строковых литералов, using System;
class Verbatim { ,
static void Main() {
Console.WriteLine(@"Это буквальный строковый литерал, занимающий несколько строк.
") ;
Console.WriteLine(@"А это вывод с табуляцией:
12 3 4
5 6 7 8
м) ;
Console.WriteLine(@"Отзыв программиста: ""Мне нравится С#.,г"");
Это буквальный строковый литерал, занимающий несколько строк.
А это вывод с-табуляцией:
1 ‘2 3 4
5 6 7 8
Отзыв программиста: "Мне нравится С#."
Следует особо подчеркнуть, что буквальные строковые литералы выводятся в том же виде, в каком они введены в исходном тексте программы.
Преимущество буквальных строковых литералов заключается в том, что они позволяют указать в программе выводимый результат именно так, как он должен выглядеть на экране. Но если выводится несколько строк, то переход на новую строку может нарушить порядок набора исходного текста программы с отступами. Именно по этой причине в примерах программ, приведенных в этой книге, применение буквальных строковых литералов ограничено. Тем не менее они приносят немало замечательных выгод во многих случаях, когда требуется форматирование выводимых результатов.
И последнее замечание: не путайте строки с символами. Символьный литерал, например 1X1, обозначает одиночную букву типа char. А строка, состоящая из одного символа, например "X", по-прежнему остается текстовой строкой.
Более подробное рассмотрение переменных
Переменные объявляются с помощью оператора следующей формы:
тип имя_переменной;
гдетип —это тип данных, хранящихся в переменной; аимя_переменной —это ее имя. Объявить можно переменную любого действительного типа, в том числе и описанных выше типов значений. Важно подчеркнуть, что возможности переменной определяются ее типом. Например, переменную типа bool нельзя использовать для хранения числовых значений с плавающей точкой. Кроме того, тип переменной нельзя изменять в течение срока ее существования. В частности, переменную типа int нельзя преобразовать в переменную типа char.
Все переменные в C# должны быть объявлены до их применения. Это нужно для того, чтобы уведомить компилятор о типе данных, хранящихся в переменной, прежде чем он попытается правильно скомпилировать любой оператор, в котором используется переменная. Это позволяет также осуществлять строгий контроль типов в С#.
В C# определено несколько различных видов переменных. Так, в предыдущих примерах программ использовались переменные, называемыелокальными, поскольку они объявляются внутри метода.
Инициализация переменной
Задать значение переменной можно, в частности, с помощью оператора присваивания, как было не раз продемонстрировано ранее. Кроме того, задать начальное значение переменной можно при ее объявлении. Для этого после имени переменной указывается знак равенства (=) и присваиваемое значение. Ниже приведена общая форма инициализации переменной:
тип имя_переменной = значение;
гдезна чение— это конкретное значение, задаваемое при создании переменной. Оно должно соответствовать указанному типу переменной.
Ниже приведены некоторые примеры инициализации переменных.
int count = 10; // задать начальное значение 10 переменной count.
char ch = 'X'; // инициализировать переменную ch буквенным значением X.
float f = 1.2F // переменная f инициализируется числовым значением 1,2.
Если две или более переменные одного и того же типа объявляются списком, разделяемым запятыми, то этим переменным можно задать, например, начальное значение.
int a, b=8, с=19, d; // инициализировать переменные b и с
В данном примере инициализируются только переменныеbи с.
Динамическая инициализация
В приведенных выше примерах в качестве инициализаторов переменных использовались только константы, но в C# допускается также динамическая инициализация переменных с помощью любого выражения, действительного на момент объявления переменной. Ниже приведен пример краткой программы для вычисления гипотенузы прямоугольного треугольника по длине его противоположных сторон.
// Продемонстрировать динамическую инициализацию.
using System;
class Dynlnit {
static void Main() {
// Длина сторонпрямоугольноготреугольника. double si = 4.0; double s2 = 5.0;
// Инициализировать переменную hypot динамически, double hypot = Math.Sqrt( (si * si) + (s2 * s2) );
Console.Write("Гипотенуза треугольника со сторонами " + si + " и " + s2 + " равна ");
Console.WriteLine("{0:#.###}.", hypot);
}
}
Результат выполнения этой программы выглядит следующим образом.
Гипотенуза треугольника со сторонами 4 и 5 равна 6.403
В данном примере объявляются три локальные переменные: si, s2 иhypot.Две из них (si и s2) инициализируются константами, А третья(hypot)динамически инициализируется вычисляемой длиной гипотенузы. Для такой инициализации используется выражение, указываемое в вызываемом методеMath.Sqrt(). Как пояснялось выше, для динамической инициализации пригодно любое выражение, действительное на момент объявления переменной. А поскольку вызов методаMath. Sqrt() (или любого другого библиотечного метода) является действительным на данный момент, то его можно использовать для инициализации переменной hypot. Следует особо подчеркнуть, что в выражении для инициализации можно использовать любой элемент, действительный на момент самой инициализации переменной, в том числе вызовы методов, другие переменные или литералы.
Неявно типизированные переменные
Как пояснялось выше, все переменные в C# должны быть объявлены. Как правило, при объявлении переменной сначала указывается тип, например int или bool, а затем имя переменной. Но начиная с версии C# 3.0, компилятору предоставляется возможность самому определить тип локальной переменной, исходя из значения, которым она инициализируется. Такая переменная называетсянеявно типизированной.
Неявно типизированная переменная объявляется с помощью ключевого слова var и должна быть непременно инициализирована. Для определения типа этой переменной компилятору служит тип ее инициализатора, т.е. значения, которым она инициализируется. Рассмотрим такой пример.
var е = 2.7183;
В данном примере переменная е инициализируется литералом с плавающей точкой, который по умолчанию имеет тип double, и поэтому она относится к типу double. Если бы переменная е была объявлена следующим образом:
var е = 2.7183F;
то она была бы отнесена к типу float.
В приведенном ниже примере программы демонстрируется применение неявно типизированных переменных. Он представляет собой вариант программы из предыдущего раздела, измененной таким образом, чтобы все переменные были типизированы неявно.
// Продемонстрировать применение неявно типизированных переменных, using System;
class ImplicitlyTypedVar { static void Main() {
// Эти переменные типизированы неявно. Они отнесены // к типу double, поскольку инициализирующие их // выражения сами относятся к типу double, var si = 4.0; var s2 = 5.0;
// Итак, переменная hypot типизирована неявно и // относится к типу double, поскольку результат,
// возвращаемый методом Sqrt(), имеет тип double, var hypot = Math.Sqrt( (si * si) + (s2 * s2) );
Console.Write("Гипотенуза треугольника со сторонами " + si + " by " + s2 + " равна ");
Console.WriteLine("{0:#.###}.", hypot);
// Следующий оператор не может быть скомпилирован,
// поскольку переменная si имеет тип double и // ей нельзя присвоить десятичное значение.
// si = 12.2М; // Ошибка!
}
}
Результат выполнения этой программы оказывается таким же, как и прежде. Важно подчеркнуть, что неявно типизированная переменная по-прежнему остается строго типизированной. Обратите внимание на следующую закомментированную строку из приведенной выше программы.
// si = 12.2М; // Ошибка!
Эта операция присваивания недействительна, поскольку переменная s1относится к типуdouble.Следовательно, ей нельзя присвоить десятичное значение. Единственное отличие неявно типизированной переменной от обычной, явно типизированной переменной, — в способе определения ее типа. Как только этот тип будет определен, он закрепляется за переменной до конца ее существования. Это, в частности, означает, что тип переменной s 1 не может быть изменен по ходу выполнения программы.
Неявно типизированные переменные внедрены в C# не для того, чтобы заменить собой обычные объявления переменных. Напротив, неявно типизированные переменные предназначены для особых случаев, и самый примечательный из них имеет отношение к языку интегрированных запросов (LINQ), подробно рассматриваемому в главе 19. Таким образом, большинство объявлений переменных должно и впредь оставаться явно типизированными, поскольку они облегчают чтение и понимание исходного текста программы.
И последнее замечание: одновременно можно объявить только одну неявно типизированную переменную. Поэтому объявление
var si =4.0, s2=5.0; // Ошибка!
является неверным и не может быть скомпилировано. Ведь в нем предпринимается попытка объявить обе переменные, si и s2, одновременно.
Область действия и время существования переменных
Все переменные, использовавшиеся в предыдущих примерах программ, объявлялись в самом начале метода Main (). Но в C# локальную переменную разрешается объявлять в любом кодовом блоке. Как пояснялось в главе 2, кодовый блок начинается открывающей фигурной скобкой и оканчивается закрывающей фигурной скобкой. Этот блок и определяетобласть действия.Следовательно, всякий раз, когда начинается блок, образуется новая область действия. Прежде всего область действия определяет видимость имен отдельных элементов, в том числе и переменных, в других частях программы без дополнительного уточнения. Она определяет также время существования локальных переменных.
В C# к числу наиболее важных относятся области действия, определяемые классом и методом. Рассмотрение области действия класса (и объявляемых в ней переменных) придется отложить до того момента, когда в этой книге будут описываться классы. А до тех пор будут рассматриваться только те области действия, которые определяются методом или же в самом методе.
Область действия, определяемая методом, начинается открывающей фигурной скобкой и оканчивается закрывающей фигурной скобкой. Но если у этого метода имеются параметры, то и они входят в область действия, определяемую данным методом.
Как правило, локальные переменные объявляются в области действия, невидимой для кода, находящегося вне этой области. Поэтому, объявляя переменную в определенной области действия, вы тем самым защищаете ее от доступа или видоизменения вне данной области. Разумеется, правила области действия служат основанием для инкапсуляции.
Области действия могут быть вложенными. Например, всякий раз, когда создается кодовый блок, одновременно образуется и новая, вложенная область действия. В этом случае внешняя область действия охватывает внутреннюю область. Это означает, что локальные переменные, объявленные во внешней области действия, будут видимы для кода во внутренней области действия. Но обратное не справедливо: локальные переменные, объявленные во внутренней области действия, не будут видимы вне этой области.
Для того чтобы стала более понятной сущность вложенных областей действия, рассмотрим следующий пример программы.
// Продемонстрировать область действия кодового блока, using System;
class ScopeDemo {
static void Main() {
int x; // Эта переменная доступна для всего кода внутри метода Main().
х = 10;
if (х == 10) { // начать новую область действия
int у = 20; // Эта переменная доступна только в данном кодовом блоке.
// Здесь доступны обе переменные, х и у.
Console.WriteLine("х и у: " + х + " " + у); х = у * 2;
}
// у = 100; // Ошибка! Переменна у здесь недоступна.
//А переменная х здесь по-прежнему доступна.
Console.WriteLine("х равно " + х) ;
}
}
Как поясняется в комментариях к приведенной выше программе, переменная х объявляется в начале области действия методаMain (), и поэтому она доступна для всего последующего кода в пределах этого метода. В блоке условного оператораi fобъявляется переменнаяу.А поскольку этот кодовый блок определяет свою собственную область действия, то переменнаяувидима только для кода в пределах данного блока. Именно поэтому строкаline у= 100;, находящаяся за пределами этого блока, закомментирована. Если удалить находящиеся перед ней символы комментария (//), то во время компиляции программы произойдет ошибка, поскольку переменнаяуневидима за пределами своего кодового блока. В то же время переменная х может использоваться в блоке условного оператораi f, поскольку коду из этого блока, находящемуся во вложенной области действия, доступны переменные, объявленные в охватывающей его внешней области действия.
Переменные могут быть объявлены в любом месте кодового блока, но они становятся действительными только после своего объявления. Так, если объявить переменную в начале метода, то она будет доступна для всего остального кода в пределах этого метода. А если объявить переменную в конце блока, то она окажется, по существу, бесполезной, поскольку не будет доступной ни одному коду.
Если в объявление переменной включается инициализатор, то такая переменная инициализируется повторно при каждом входе в тот блок, в котором она объявлена. Рассмотрим следующий пример программы.
// Продемонстрировать время существования переменной.
using System;
class VarlnitDemo { static void Main() { int x;
for(x = 0; x < 3; x++) {
int у = -1; // Переменная у инициализируется при каждом входе в блок. Console.WriteLine("у равно: " + у); // Здесь всегда выводится -1
у = 100;
Console.WriteLine("у теперь равно: " + у);
}
}
}
Ниже приведен результат выполнения этой программы.
У
равно:
-1
У
теперь
равно:
100
У
равно:
-1
У
теперь
равно:
100
У
равно:
-1
У
теперь
равно:
100
Как видите, переменная у повторно инициализируется одним и тем же значением -1 при каждом входе во внутренний цикл for. И несмотря на то, что после этого цикла ей присваивается значение 100, оно теряется при повторной ее инициализации.
В языке C# имеется еще одна особенность соблюдения правил области действия: несмотря на то, что блоки могут быть вложены, ни у одной из переменных из внутренней области действия не должно быть такое же имя, как и у переменной из внешней области действия. В приведенном ниже примере программы предпринимается попытка объявить две разные переменные с одним и тем же именем, и поэтому программа не может быть скомпилирована.
/*
В этой программе предпринимается попытка объявить во внутренней области действия переменную с таким же самым именем, как и у переменной, определенной во внешней области действия.
*** Эта программа не может быть скомпилирована. ***
*/
using System;
class NestVar {
static void Main() { int count;
for(count = 0; count < 10; count = count+1) {
Console.WriteLine("Это подсчет: " + count);
int count; // Недопустимо!!!
for(count = 0; count < 2; count++)
Console.WriteLine("В этой программе есть ошибка!");
}
}
}
Если у вас имеется некоторый опыт программирования на С или C++, то вам должно быть известно, что на присваивание имен переменным, объявляемым во внутренней области действия, в этих языках не существует никаких ограничений. Следовательно, в С и C++ объявление переменной count в кодовом блоке, входящем во внешний цикл for, как в приведенном выше примере, считается вполне допустимым. Но в С и C++ такое объявление одновременно означает сокрытие внешней переменной. Разработчики C# посчитали, что такого родасокрытие именможет легко привести к программным ошибкам, и поэтому решили запретить его.
Преобразование и приведение типов
В программировании нередко значения переменных одного типа присваиваются переменным другого типа. Например, в приведенном ниже фрагменте кода целое значение типа int присваивается переменной с плавающей точкой типа float.
int i; float f;
i = 10;
f = i; // присвоить целое значение переменной типа float
Если в одной операции присваивания смешиваются совместимые типы данных, то значение в правой части оператора присваивания автоматически преобразуется в тип, указанный в левой его части. Поэтому в приведенном выше фрагменте кода значение переменной i сначала преобразуется в тип float, а затем присваивается переменной f. Но вследствие строгого контроля типов далеко не все типы данных в C# оказываются полностью совместимыми, а следовательно, не все преобразования типов разрешены в неявном виде. Например, типы bool и int несовместимы. Правда, преобразование несовместимых типов все-таки может быть осуществлено путемприведения.Приведение типов, по существу, означает явное их преобразование. В этом разделе рассматривается как автоматическое преобразование, так и приведение типов.
Автоматическое преобразование типов
Когда данные одного типа присваиваются переменной другого типа,неявноепреобразование типов происходит автоматически при следующих условиях:
• оба типа совместимы;
• диапазон представления чисел целевого типа шире, чем у исходного типа.
Если оба эти условия удовлетворяются, то происходитрасширяющее преобразование.Например, типintдостаточно крупный, чтобы вмещать в себя все действительные значения типаbyte,а кроме того, оба типа,intиbyte,являются совместимыми целочисленными типами, и поэтому для них вполне возможно неявное преобразование.
Числовые типы, как целочисленные, так и с плавающей точкой, вполне совместимы друг с другом для выполнения расширяющих преобразований. Так, приведенная ниже программа составлена совершенно правильно, поскольку преобразование типаlongв типdoubleявляется расширяющим и выполняется автоматически.
// Продемонстрировать неявное преобразование типа long в тип double.
using System;
class LtoD {
static void Main() { long L; double D;
L = 100123285L;
D = L;
Console.WriteLine("L и D: " + L + " " + D);
}
}
Если типlongможет быть преобразован в типdoubleнеявно, то обратное преобразование типаdoubleв типlongнеявным образом невозможно, поскольку оно не является расширяющим. Следовательно, приведенный ниже вариант предыдущей программы составлен неправильно.
// *** Эта программа не может быть скомпилирована. ***
using System;
/
class LtoD {
static void Main() { long L; double D;
D = 100123285.0;
L = D; // Недопустимо!!!
Console.WriteLine("L и D:■ " + L + " " + D);
}
}
Помимо упомянутых выше ограничений, не допускается неявное взаимное преобразование типовdecimalиfloatилиdouble,а также числовых типов иcharилиbool.Кроме того, типыcharиboolнесовместимы друг с другом.
Приведение несовместимых типов
Несмотря на всю полезность неявных преобразований типов, они неспособны удовлетворить все потребности в программировании, поскольку допускают лишь расширяющие преобразования совместимых типов. А во всех остальных случаях приходится обращаться к приведению типов.Приведение— это команда компилятору преобразовать результат вычисления выражения в указанный тип. А для этого требуется явное преобразование типов. Ниже приведена общая форма приведения типов.
(целевой_тип) выражение
Здесьцелевой_типобозначает тот тип, в который желательно преобразовать указанноевыражение. Рассмотрим для примера следующее объявление переменных.
double х, у;
Если результат вычисления выражения х/у должен быть типа int, то следует записать следующее.
(int) (х / у)
Несмотря на то что переменные х и у относятся к типу double, результат вычисления выражения х/у преобразуется в тип int благодаря приведению. В данном примере выражение х/у следует непременно указывать в скобках, иначе приведение к типу int будет распространяться только на переменную х, а не на результат ее деления на переменную у. Приведение типов в данном случае требуется потому, что неявное преобразование типа double в тип int невозможно.
Если приведение типов приводит ксужающему преобразованию,то часть информации может быть потеряна. Например, в результате приведения типа long к типу int часть информации потеряется, если значение типа long окажется больше диапазона представления чисел для типа int, поскольку старшие разряды этого числового значения отбрасываются. Когда же значение с плавающей точкой приводится к целочисленному, то в результате усечения теряется дробная часть этого числового значения. Так, если присвоить значение 1,23 целочисленной переменной, то в результате в ней останется лишь целая часть исходного числа (1), а дробная его часть (0,23) будет потеряна.
В следующем примере программы демонстрируется ряд преобразований типов, требующих приведения. В этом примере показан также ряд ситуаций, в которых приведение типов становится причиной потери данных.
// Продемонстрировать приведение типов.
using System;
class CastDemo {
static void Main() { double x, y; byte b; int i; char ch; uint u; short s; long 1;
x = 10.0;
У = 3.0;
11Приведение типа double к типу int, дробная часть числа теряется, i = (int) (х / у) ;
Console.WriteLine("Целочисленный результат деления х / у: " + i) ; Console.WriteLine();
// Приведение типа int к типу byte без потери данных, i = 255; b = (byte) i;
Console.WriteLine("b после присваивания 255: " + b +
" -- без потери данных.");
// Приведение типа int к типу byte с потерей данных, i = 257; b = (byte) i;
Console.WriteLine("b после присваивания 257: " + b +
" — с потерей данных.");
Console.WriteLine();
// Приведение типа uint к типу short без потери данных, и = 32000; s = (short) u;
Console.WriteLine("s после присваивания 32000: " + s + " — без потери данных.");
// Приведение типа uint к типу short с потерей данных, и = 64000; s = (short) u;
Console.WriteLine("s после присваивания 64000: " + s + " — с потерей данных. ") ;
Console.WriteLine();
// Приведение типа long к типу uint без потери данных.
1 = 64000; u = (uint) 1;
Console.WriteLine("и после присваивания 64000: " + u +
" -- без потери данных.");
// Приведение типа long к типу uint с потерей данных.
1= -12;u = (uint) 1;
Console.WriteLine("и после присваивания -12: " + u +
" — с потерей данных.");
Console.WriteLine();
// Приведение типа int к типу char, b = 88; // код ASCII символа X ch = (char) b;
Console.WriteLine("ch после присваивания 88: " + ch);
}
}
Вот какой результат дает выполнение этой программы.
Целочисленный результат деления х / у: 3
b после присваивания 255: 255 -- без потери данных.
Ь после присваивания 257: 1 — с потерей данных.i
s после присваивания 32000: 32000 -- без потери данных,
s после присваивания 64000: -1536 -- с потерей данных.
и после-присваивания 64000: 64000 — без потери данных,
и после присваивания -12: 4294967284 -- с потерей данных.
ch после присваивания 88: X
Рассмотрим каждую операцию присваивания в представленном выше примере программы по отдельности. Вследствие приведения результата деления х/у к типу int отбрасывается дробная часть числа, а следовательно, теряется часть информации.
Когда переменной b присваивается значение 255, то информация не теряется, поскольку это значение входит в диапазон представления чисел для типа byte. Но когда переменной b присваивается значение 257, то часть информации теряется, поскольку это значение превышает диапазон представления чисел для типа byte. Приведение типов требуется в обоих случаях, поскольку неявное преобразование типа int в тип byte невозможно.
Когда переменной s типа short присваивается значение 32 000 переменной и типа uint, потери данных не происходит, поскольку это значение входит в диапазон представления чисел для типа short. Но в следующей операции присваивания переменная и имеет значение 64 000, которое оказывается вне диапазона представления чисел для типа short, и поэтому данные теряются. Приведение типов требуется в обоих случаях, поскольку неявное преобразование типа uint в тип short невозможно.
Далее переменной и присваивается значение 64 000 переменной 1 типа long. В этом случае данные не теряются, поскольку значение 64 000 оказывается вне диапазона представления чисел для типа uint. Но когда переменной и присваивается значение -12, данные теряются, поскольку отрицательные числа также оказываются вне диапазона представления чисел для типа uint. Приведение типов требуется в обоих случаях, так как неявное преобразование типа long в тип uint невозможно.
И наконец, когда переменной char присваивается значение типа byte, информация не теряется, но приведение типов все же требуется.
Преобразование типов в выражениях
Помимо операций прйсваивания, преобразование типов происходит и в самих выражениях. В выражении можно свободно смешивать два или более типа данных, при условии их совместимости друг с другом. Например, в одном выражении допускается применение типов short и long, поскольку оба типа являются числовыми. Когда в выражении смешиваются разные типы данных, они преобразуются в один и тот же тип по порядку следования операций.
Преобразования типов выполняются по принятым в C#правилам продвижения типов.Ниже приведен алгоритм, определяемый этими правилами для операций с двумя операндами.
ЕСЛИ один операнд имеет тип decimal, ТО и второй операнд продвигается к типу decimal (но если второй операнд имеет тип float или double, результат будет ошибочным).
ЕСЛИ один операнд имеет тип double, ТО и второй операнд продвигается к типу double.
ЕСЛИ один операнд имеет тип float, ТО и второй операнд продвигается к типу float.
ЕСЛИ один операнд имеет тип ulong, ТО и второй операнд продвигается к типу ulong (но если второй операнд имеет тип sbyte, short, int или long, результат будет ошибочным).
ЕСЛИ один операнд имеет тип long, ТО и второй операнд продвигается к типу long.
ЕСЛИ один операнд имеет тип uint, а второй — тип sbyte, short или int, ТО оба операнда продвигаются к типу long.
ЕСЛИ один операнд имеет тип uint, ТО и второй операнд продвигается к типу uint.
ИНАЧЕ оба операнда продвигаются к типу int.
Относительно правил продвижения типов необходимо сделать ряд важных замечаний. Во-первых, не все типы могут смешиваться в выражении. В частности, неявное преобразование типа float или double в тип decimal невозможно, как, впрочем, и смешение типа ulong с любым целочисленным типом со знаком. Для смешения этих типов требуется явное их приведение.
Во-вторых, особого внимания требует последнее из приведенных выше правил. Оно гласит: если ни одно из предыдущих правил не применяется, то все операнды продвигаются к типу int. Следовательно, все значения типа char, sbyte, byte, ushort и short продвигаются к типу int в целях вычисления выражения. Такое продвижение типов называетсяцелочисленным.Это также означает, что результат выполнения всех арифметических операций будет иметь тип не ниже int.
Следует иметь в виду, что правила продвижения типов применяются только к значениям, которыми оперируют при вычислении выражения. Так, если значение переменной типа byte продвигается к типу int внутри выражения, то вне выражения эта переменная по-прежнему относится к типу byte. Продвижение типов затрагивает только вычисление выражения.
Но продвижение типов может иногда привести к неожиданным результатам. Если, например, в арифметической операции используются два значения типа byte, то происходит следующее. Сначала операнды типа byte продвигаются к типу int. А затем выполняется операция, дающая результат типа int. Следовательно, результат выполнения операции, в которой участвуют два значения типа byte, будет иметь тип int. Но ведь это не тот результат, который можно было бы с очевидностью предположить. Рассмотрим следующий пример программы.
// Пример неожиданного результата продвижения типов!
using System;
class PromDemo {
static void Main() { byte b;
b = 10;
b = (byte) (b * Ь); // Необходимо приведение типов!!
Console.WriteLine("b: "+ b);
}
}
Как ни странно, но когда результат вычисления выражения b*b присваивается обратно переменной Ь, то возникает потребность в приведении к типу byte! Объясняется это тем, что в выражении b*b значение переменной b продвигается к типу int и поэтому не может быть присвоено переменной типа byte без приведения типов. Имейте это обстоятельство в виду, если получите неожиданное сообщение об ошибке несовместимости типов в выражениях, которые, на первый взгляд, кажутся совершенно правильными.
Аналогичная ситуация возникает при выполнении операций с символьными операндами. Например, в следующем фрагменте кода требуется обратное приведение к типу char, поскольку операнды chi и ch2 в выражении продвигаются к типу int.
char chi = 'a', ch2 = 'b1;
chi = (char) (chi•+ ch2);
Без приведения типов результат сложения операндов chi и ch2 будет иметь тип int, и поэтому его нельзя присвоить переменной типа char.
Продвижение типов происходит и при выполнении унарных операций, например с унарным минусом. Операнды унарных операций более мелкого типа, чем int (byte, sbyte, short и ushort), т.е. с более узким диапазоном представления чисел, продвигаются к типу int. То же самое происходит и с операндом типа char. Кроме того, если выполняется унарная операция отрицания значения типа uint, то результат продвигается к типу long.
Приведение типов в выражениях
Приведение типов можно применять и к отдельным частям крупного выражения. Это позволяет точнее управлять преобразованиями типов при вычислении выражения. Рассмотрим следующий пример программы, в которой выводятся квадратные корни чисел от 1 до 10 и отдельно целые и дробные части каждого числового результата.Дляэтого в данной программе применяется приведение типов, благодаря которому результат, возвращаемый методомMath.Sqrt (), преобразуется в типint.
// Пример приведения типов в выражениях.
using System;
class CastExpr {
static void Main() { double n;
Console.WriteLine ();
}
}
}
Вот как выглядит результат выполнения этой программы.
Квадратный корень из
1
равен 1
Целая часть числа: 1
Дробная часть числа:
0
Квадратный корень из
2
равен 1.4142135623731
Целая часть числа: 1
Дробная часть числа:
0
.414213562373095
Квадратный корень из
3
равен 1.73205080756888
Целая часть числа: 1
Дробная часть числа:
0
.732050807568877
Квадратный корень из
4
равен 2
Целая часть числа: 2
Дробная часть числа:
0
Квадратный корень из
5
равен 2.23606797749979
Целая часть числа: 2
Дробная часть числа:
0
.23606797749979
Квадратный корень из
6
равен 2.44948974278318
Целая ч^сть числа: 2
Дробная часть числа:
0
.449489742783178
Квадратный корень из
7
равен 2.64575131106459
Целая часть числа: 2
Дробная часть числа:
0
.645751311064591
Квадратный корень из
8
равен 2.82842712474619
Целая часть числа: 2
Дробная часть числа:
0
.82842712474619
Квадратный корень из
9
равен 3
Целая часть числа: 3
Др'обная часть числа:
0
Квадратный корень из
10 равен 3.16227766016838
Целая часть числа: 3
Дробная часть числа:
0
.16227766016838
Как видите, приведение результата, возвращаемого методомMath.Sqrt (), к типуintпозволяет получить целую часть числа. Так, в выражении
Math.Sqrt(n) - (int) Math.Sqrt(n)
приведение к типуintдает целую часть числа, которая затем вычитается из всего числа, а в итоге получается дробная его часть. Следовательно, результат вычисления данного выражения имеет типdouble.Но к типуintприводится только значение, возвращаемое вторым методомMath. Sqrt ().
ГЛАВА 4 Операторы
Вязыке C# предусмотрен обширный ряд операторов, предоставляющих программирующему возможность полного контроля над построением и вычислением выражений. Большинство операторов в C# относится к следующим категориям:арифметические, поразрядные,логическиеи операторыотношения.Все перечисленные категории операторов рассматриваются в этой главе. Кроме того, в C# предусмотрен ряд других операторов для_ особых случаев, включая индексирование массивов, доступ к членам класса и обработку лямбда-выражений. Эти специальные операторы рассматриваются далее в книге вместе с теми средствами, в которых они применяются.
Арифметические операторы
Арифметические операторы, представленные в языке С#, приведены ниже.
Оператор
Действие
+
Сложение
-
Вычитание, унарный минус
*
Умножение
/
Деление
о.
Деление по модулю
—
Декремент
++
Инкремент
Операторы +, * и / действуют так, как предполагает их обозначение. Их можно
применять к любому встроенному числовому типу данных.
Действие арифметических операторов не требует особых пояснений, за исключением следующих особых случаев. Прежде всего, не следует забывать, что когда оператор / применяется к целому числу, то любой остаток от деления отбрасывается; например, результат целочисленного деления 10/3 будет равен 3. Остаток от этого деления можно получить с помощью оператора деления по модулю (%), который иначе называетсяоператором вычисления остатка.Он дает остаток от целочисленного деления. Например, 10 % 3 равно 1. В C# оператор % можно применять как к целочисленным типам данных, так и к типам с плавающей точкой. Поэтому 10.0 % 3.0 также равно 1. В этом отношении C# отличается от языков С и C++, где операции деления по модулю разрешаются только для целочисленных типов данных. В приведенном ниже примере программы демонстрируется применение оператора деления по модулю.
// Продемонстрировать применение оператора %.
using System;
class ModDemo {
static void Main() { int iresult, irem; double dresult, drem;
iresult = 10 / 3; irem = 10 % 3;
dresult = 10.0 / 3.0; drem = 10.0 % 3.0;
Console.WriteLine("Результат и остаток от деления 10/3: " + iresult + " " + irem);
Console.WriteLine("Результат и остаток от деления 10.0 / 3.0: " + dresult + " " + drem);
}
}
Результат выполнения этой программы приведен ниже.
Результат и остаток от деления 10/3: 3 1
Результат и остаток от деления 10.0 / 3.0: 3.33333333333333 1
Как видите, обе операции, % целочисленного типа и с плавающей точкой, дают один и тот же остаток, равный 1.
Операторы инкремента и декремента
Операторы инкремента (++) и декремента (—) были представлены в главе 2. Как станет ясно в дальнейшем, они обладают рядом особых и довольно интересных свойств. Но сначала выясним основное назначение этих операторов.
Оператор инкремента увеличивает свой операнд на 1, а оператор декремента уменьшает операнд на 1. Следовательно, оператор
х+ + ;
равнозначен операторух = х + 1;а оператор
х—;
равносилен оператору
х = х - 1;
Следует, однако, иметь в виду, что в инкрементной или декрементной форме значение переменной х вычисляется только один, а не два раза. В некоторых случаях это позволяет повысить эффективность выполнения программы.
Оба оператора инкремента и декремента можно указывать до операнда (в префиксной форме) или же после операнда (в постфиксной форме). Например, оператор
х = х + 1;
может быть записан в следующем виде:
++х; // префиксная форма
или же в таком виде:
х++; // постфиксная форма
В приведенном выше примере форма инкремента (префиксная или постфиксная) особого значения не имеет. Но если оператор инкремента или декремента используется в длинном выражении, то отличие в форме его записи уже имеет значение. Когда оператор инкремента или декрементапредшествуетсвоему операнду, то результатом операции становится значение операндапослеинкремента или декремента. А когда оператор инкремента или декремента следуетпослесвоего операнда, то результатом операции становится значение операндадоинкремента или декремента. Рассмотрим следующий фрагмент кода.
х = 10; у = ++х;
В данном случае значение переменной у будет установлено равным 11, поскольку значение переменной х сначала увеличивается на 1, а затем присваивается переменной у. Но во фрагменте кода
X = 10;
у = х++;
значение переменной у будет установлено равным 10, так как в этом случае значение переменной х сначала присваивается переменной у, а затем увеличивается на 1. В обоих случаях значение переменной х оказывается равным 11. Отличие состоит лишь том, когда именно это значение станет равным 11: до или после его присваивания переменной у.
Возможность управлять моментом инкремента или декремента дает немало преимуществ при программировании. Обратимся к следующему примеру программы, в которой формируется последовательный ряд чисел.
// Продемонстрировать отличие между префиксной // и постфиксной формами оператора инкремента (++).
using System;
class PrePostDemo { static void Main() { int* x, y;
У
int i;
x = 1;
У = 0;
Console.WriteLine("Ряд чисел, полученных " +
"с помощью оператора у = у + х++;"),
for(i = 0; i < 10; i++) {
у = у + х++; // постфиксная форма оператора ++
Console.WriteLine(у + " ");
}
Console.WriteLine();
х = 1; у = 0;
Console.WriteLine("Ряд чисел, полученных " +
"с помощью оператора у = у + ++х;")<
for(i = 0; i < 10; i++) {
у = у + ++х; // префиксная форма оператора ++
Console.WriteLine(у + " ");
}
Console.WriteLine();
}
}
Выполнение этой программы дает следующий результат.
Ряд чисел, полученных с помощью оператора у = у + х++
1
3
б
10
15
14
21
28
36
45
55
Ряд чисел, полученных с помощью оператора у = у + ++х;
2
5
9
14
20
27
35
44
54
65 .
Как подтверждает приведенный выше результат, в операторе
у = у + х++;
первоначальное значение переменной х складывается с самим собрй, а полученный результат присваивается переменной у. После этого значение переменной х увеличивается на 1. Но в операторе
у = у + ++х;
значение переменной х сначала увеличивается на 1, затем складывается с первоначальным значением этой же переменной, а полученный результат присваивается переменной у. Как следует из приведенного выше результата, простая замена префиксной формы записи оператора ++х постфиксной формой х++ приводит к существенному изменению последовательного ряда получаемых чисел.
И еще одно замечание по поводу приведенного выше примера: не пугайтесь выражений, подобных следующему:
у + ++Х
Такое расположение рядом двух операторов может показаться не совсем привычным, но компилятор воспримет их в правильной последовательности. Нужно лишь запомнить, что в данном выражении значение переменной у складывается с увеличенным на 1 значением переменной х.
Операторы отношения и логические операторы
В обозначенияхоператор отношенияилогический оператортерминотношенияозначает взаимосвязь, которая может существовать между двумя значениями, а терминлогический —взаимосвязь между логическими значениями "истина7' и "ложь". И поскольку операторы отношения дают истинные или ложные результаты, то они нередко применяются вместе с логическими операторами. Именно по этой причине они и рассматриваются совместно в данном разделе.
Ниже перечислены операторы отношения.
Оператор
Значение
==
Равно
I =
Не равно
>
Больше
<
Меньше
>=
Больше или равно
<=
Меньше или равно
К числу логических относятся операторы, приведенные ниже.
Оператор
Значение
&
И
1
ИЛИ
/ч
Исключающее ИЛИ
&&
Укороченное И
11
Укороченное ИЛИ
1
НЕ
Результатом выполнения оператора отношения или логического оператора является логическое значение типа bool.
В целом, объекты можно сравнивать на равенство или неравенство, используя операторы отношения == и ! =. А операторы сравнения <, >, <= или >= могут применяться только к тем типам данных, которые поддерживают отношение порядка. Следовательно, операторы отношения можно применять ко всем числовым типам данных. Но значения типа bool могут сравниваться только на равенство или неравенство, поскольку истинные (true) и ложные (false) значения не упорядочиваются. Например, сравне-' ние true > false в C# не имеет смысла.
Операнды логических операторов должны относиться к типу bool, а результат выполнения логической операции также относится к типу bool. Логические операторы &, |, л и ! поддерживают основные логические операции И, ИЛИ, исключающее ИЛИ и НЕ в соответствии с приведенной ниже таблицей истинности.
p
q
p & q
p 1 q
p A q
!p
false
false
false
false
false
true
true
false
false
true
true
false
false
true
false
true
true
true
true
true
true
true
false
false
Как следует из приведенной выше таблицы, результатом выполнения логической операции исключающее ИЛИ будет истинное значение (true), если один и только один ее операнд имеет значение true.
Ниже приведен пример программы, демонстрирующий применение нескольких операторов отношения и логических операторов.
// Продемонстрировать применение операторов // отношения и логических операторов.
using System;
class RelLogOps {
static void Main() { int i, j; bool bl, b2;
i = 10;j = 11;
if(i < j) Console.WriteLine("i < j"); if(i <= j) Console.WriteLine("i <= j"); if (i != j) Console.WriteLine("i != j");
if(i == j) Console.WriteLine("Нельзя выполнить"); if(i >= j) Console.WriteLine("Нельзя выполнить"); if(i > j) Console.WriteLine("Нельзя выполнить");
Ы = true7 Ь2 = false;
if(Ы & b2) Console.WriteLine("Нельзя выполнить"); if(!(bl & b2)) Console.WriteLine("!(Ы & Ь2) — true"); if(Ы | b2) Console.WriteLine("bl I b2 - true"); if(Ы A b2) Console.WriteLine("bl A b2 — true");
}
}
Выполнение этой программы дает следующий результат.
i < j i <= j i != j
!(bl & b2) — true bl | b2 — true bl A b2 - true
Логические операторы в C# выполняют наиболее распространенные логические операции. Тем не менее существует ряд операций, выполняемых по правилам формальной логики. Эти логические операции могут быть построены с помощью логических операторов, поддерживаемых в С#. Следовательно, в С# предусмотрен такой набор логических операторов, которого достаточно для построения практически любой логической операции, в том числе импликации.Импликация —это двоичная операция, результатом которой является ложное значение только в том случае, если левый ее операнд имеет истинное значение, а правый — ложное. (Операция импликации отражает следующий принцип: истина не может подразумевать ложь.) Ниже приведена таблица истинности для операции импликации.
p
q
Результат импликации p и q
true
true
true
true
false
false
false
false
true
false
true
true
Операция импликации может быть построена на основе комбинации логических операторов ! и |, как в приведенной ниже строке кода.
•р I q
В следующем примере программы демонстрируется подобная реализация операции импликации.
// Построение операции импликации в С#.
using System;
class Implication { static void Main() { bool p=false, q=false;
int i, j;
for(i=0;i < 2; i++) {
for(j = 0; j < 2; j++) {
if (i==0) p = true;
if (i==l) p = false;
if (j==0) q = true;
if(j==l) q = false;
Console.WriteLine("p равно " + p + ", q равно " + q);
if ( !p I q)
Console.WriteLine("Результат импликации " + p +
" и " + q + " равен " + true);
Console.WriteLine ();
}
}
}
}
Результат выполнения этой программы выглядит так.
р равно True, q равно True
Результат импликации True и True равен True р равно True, q равно False р равно False, q равно False
Результат импликации False и True равен True р равно False, q равно False
Результат импликации False и False равен True
Укороченные логические операторы
В C# предусмотрены также специальные,укороченные,варианты логических операторов И и ИЛИ, предназначенные для получения более эффективного кода. Поясним это на следующих примерах логических операций. Если первый операнд логической операции И имеет ложное значение (false), то ее результат будет иметь ложное значение независимо от значения второго операнда. Если же первый операнд логической операции ИЛИ имеет истинное значение (true), то ее результат будет иметь истинное значение независимо от значения второго операнда. Благодаря тому что значение второго операнда в этих операциях вычислять не нужно, экономится время и повышается эффективность кода.
Укороченная логическая операция И выполняется с помощью оператора &&, а укороченная логическая операция ИЛИ — с помощью оператора | |. Этим укороченным логическим операторам соответствуют обычные логические операторы & и |. Единственное отличие укороченного логического оператора от обычного заключается в том, что второй его операнд вычисляется только по мере необходимости. -
В приведенном ниже примере программы демонстрируется применение укороченного логического оператора И. В этой программе с помощью операции деления по модулю определяется следующее: делится ли значение переменной d на значение переменной п нацело. Если остаток от деления n/d равен нулю, то п делится на d нацело.
Но поскольку данная операция подразумевает деление, то для проверки условия деления на нуль служит укороченный логический оператор И.
// Продемонстрировать применение укороченных логических операторов.
using System;'
class SCops {
static void Main() { int n, d;
n = 10; d = 2;
if(d != 0 && (n % d) == 0)
Console.WriteLine(n + " делится нацело на " + d);
d = 0; // задать нулевое значение переменной d
// d равно нулю, поэтому второй операнд не вычисляется if(d != 0 && (n % d) == 0)
Console.WriteLine(n + " делится нацело на " + d);
// Если теперь попытаться сделать то же самое без укороченного // логического оператора, то возникнет ошибка из-за деления на нуль, if(d != 0 & (n % d) == 0)
Console.WriteLine(n + " делится нацело на " + d);
}
}
Для исключения ошибки из-за деления на нуль в операторе i f сначала проверяется условие: равно ли нулю значение переменной d. Если оно равно нулю, то на этом выполнение укороченного логического оператора И завершается, а последующая операция деления по модулю не выполняется. Так, при первой проверке значение переменной d оказывается равным 2, поэтому выполняется операция деления по модулю. А при второй проверке это значение оказывается равным нулю, следовательно, операция деления по модулю пропускается, чтобы исключить деление на нуль. И наконец, выполняется обычный логический оператор И, когда вычисляются оба операнда. Если при этом происходит деление на нуль, то возникает ошибка при выполнении.
Укороченные логические операторы иногда оказываются более эффективными, чем их обычные аналоги. Так зачем же нужны обычные логические операторы И и ИЛИ? Дело в том, что в некоторых случаях требуется вычислять оба операнда логической операции И либо ИЛИ из-за возникающих побочных эффектов. Рассмотрим следующий пример программы.
// Продемонстрировать значение побочных эффектов.
using System;
class SideEffects { static void Main() { int i;
bool someCondition = false;
i = 0;
11Значение переменной i инкрементируется,
11несмотря на то, что оператор if не выполняется, if(someCondition & (++i < 100))
Console.WriteLine("Не выводится");
Console.WriteLine("Оператор if выполняется: " + i); // выводится 1
// В данном случае значение переменной i не инкрементируется,
// поскольку инкремент в укороченном логическом операторе опускается, if(someCondition && ( + + i < 100))
Console.WriteLine("Не выводится");
Console.WriteLine("Оператор if выполняется: " + i); // по-прежнему 1 !!
}
}
Прежде всего обратим внимание на то, что переменнаяsomeConditionтипаboolинициализируется значениемfalse.Далее проанализируем каждый операторif.Как следует из комментариев к данной программе, в первом оператореi fпеременнаяiинкрементируется, несмотря на то что значение переменнойsomeConditionравноfalse.Когда применяется логический оператор &, как это имеет место в первом оператореi f, выражение в правой части этого оператора вычисляется независимо от значения выражения в его левой части. А во втором оператореi fприменяется укороченный логический оператор. В этом случае значение переменнойiне инкрементируется, поскольку левый операнд (переменнаяsomeCondition)имеет значениеfalse,следовательно, выражение в правой части данного оператора пропускается. Из этого следует вывод: если в коде предполагается вычисление правого операнда логической операции И либо ИЛИ, то необходимо пользоваться неукороченными формами логических операций, доступных в С#.
И последнее замечание: укороченный оператор И называется такжеусловным логическим оператором И,а укороченный оператор ИЛИ —условным логическим оператором ИЛИ.
Оператор присваивания
Оператор присваиванияобозначается одиночным знаком равенства (=). В C# оператор присваивания действует таким же образом, как и в других языках программирования. Ниже приведена его общая форма.
имя_переменной = выражение
Здесьимя_переменнойдолжно быть совместимо с типомвыражения.
У оператора присваивания имеется одна интересная особенность, о которой вам будет полезно знать: он позволяет создавать цепочку операций присваивания. Рассмотрим, например, следующий фрагмент кода.
int х, у, z;
х = у = z = 100; // присвоить значение 100 переменным х, у и z
В приведенном выше фрагменте кода одно и то же значение 100 задается для переменных х,уи z с помощью единственного оператора присваивания. Это значение присваивается сначала переменной z, затем переменнойуи, наконец, переменной х. Такой способ присваивания "по цепочке" удобен для задания общего значения целой группе переменных.
Составные операторы присваивания
В C# предусмотрены специальные составные операторы присваивания, упрощающие программирование некоторых операций присваивания. Обратимся сначала к простому примеру. Приведенный ниже оператор присваивания
X = X + 10;
можно переписать, используя следующий составной оператор присваивания.
X += 10;
Пара операторов += указывает компилятору на то, что переменной х должно быть присвоено ее первоначальное значение, увеличенное на 10.
Рассмотрим еще один пример. Оператор
х = х - 100;
и оператор
X -= 100;
выполняют одни и те же действия. Оба оператора присваивают переменной х ее первоначальное значение, уменьшенное на 100.
Для многих двоичных операций, т.е. операций, требующих наличия двух операндов, существуют отдельные составные операторы присваивания. Общая форма всех этих операторов имеет следующий вид:
имя_переменной ор = выражение
гдеор— арифметический или логический оператор, применяемый вместе с оператором присваивания.
Ниже перечислены составные операторы присваивания для арифметических и логических операций.
+=
-=
* —
/=
%=
&=
1 =
л _
Составные операторы присваивания записываются более кратко, чем их несоставные эквиваленты. Поэтому их иногда еще называютукороченными операторами присваивания.
У составных операторов присваивания имеются два главных преимущества. Во-первых, они более компактны, чем их "несокращенные" эквиваленты. И во-вторых, они дают более эффективный исполняемый код, поскольку левый операнд этих операторов вычисляется только один раз. Именно по этим причинам составные операторы присваивания чаще всего применяются в программах, профессионально написанных на С#.
Поразрядные операторы
В C# предусмотрен рядпоразрядныхоператоров, расширяющих круг задач, для решения которых можно применять С#. Поразрядные операторы воздействуют на отдельные двоичные разряды (биты) своих операндов. Они определены только для целочисленных операндов, поэтому их нельзя применять к данным типа bool, float или double. 1
Эти операторы называютсяпоразрядными, поскольку они служат для проверки, установки или сдвига двоичных разрядов, составляющих целое значение. Среди прочего поразрядные операторы применяются для решения самых разных задач программирования на уровне системы, включая, например, анализ информации состояния устройства. Все доступные в C# поразрядные операторы приведены в табл. 4.1.
Таблица 4.1. Поразрядные операторы
Оператор
Значение
&
Поразрядное И
1
Поразрядное ИДИ
Поразрядное исключающее ИДИ
>>
Сдвиг вправо
<<
Сдвйг влево
Дополнение до 1 (унарный оператор НЕ)
Поразрядные операторы И, ИЛИ, исключающее ИЛИ и НЕ
Поразрядные операторы И, ИЛИ, исключающее ИЛИ и НЕ обозначаются следую
щим образом: &, |, л и
~. Они выполняют те же функции, что и их логические аналоги,
рассмотренные выше.
Но в отличие от логических операторов, поразрядные операто-
ры действуют на уровне отдельных двоичных разрядов. Ниже приведены результаты
поразрядных операций с двоичными единицами и нулями.
р q
р & q plq pAq ~р
0 0
0 0 0 1
1 0
0 110
0 1
0 1 11*
1 1
1 .1 0 0
С точки зрения наиболее распространенного применения поразрядную операцию И можно рассматривать как способ подавления отдельных двоичных разрядов. Это означает, что если какой-нибудь бит в любом из операндов равен 0, то соответствующий бит результата будет сброшен в 0. Например:
1101 ООН 10101010
& _
10000010
В приведенном ниже примере программы демонстрируется применение поразрядного оператора & для преобразования нечетных чисел в четные. Для этой цели достаточно сбросить младший разряд числа. Например, число 9 имеет следующий двоичный вид: 0000 1001. Если сбросить младший разряд этого числа, то оно станет числом 8, а в двоичной форме — 0000 1000.
// Применить поразрядный оператор И, чтобы сделать число четным.
using System;
class MakeEven {
static void Main() { ushort num; ushort i;”
for(i =1; i <= 10; i++) {
num = i;
Console.WriteLine("num: "■ + num); num = (ushort) (num & OxFFFE);
Console.WriteLine("num после сброса младшего разряда: "
+ num + "\n");
}
}
}
Результат выполнения этой программы приведен ниже.
num: 1
num после сброса младшего разряда: О num: 2
num после сброса младшего разряда: 2 num: 3
num после сброса младшего разряда: 2 num: 4
num после сброса младшего разряда: 4 num: 5
num после сброса младшего разряда: 4 num: 6
num после сброса младшего разряда: 6 num: 7
num после сброса младшего разряда: 6 num: 8
num после сброса младшего разряда: 8 num: 9
num после сброса младшего разряда: 8 num: 10
num после сброса младшего разряда: 10
Шестнадцатеричное значениеOxFFFE,используемое в поразрядном операторе И, имеет следующую двоичную форму: 1111 1111 1111 1110. Таким образом, поразрядная операция И оставляет без изменения все двоичные разряды в числовом значении переменнойnum,кроме младшего разряда, который сбрасывается в нуль. В итоге четные числа не претерпевают никаких изменений, а нечетные уменьшаются на 1 и становятся четными.
Поразрядным оператором И удобно также пользоваться для определения установленного или сброшенного состояния отдельного двоичного разряда. В следующем примере программы определяется, является ли число нечетным.
// Применить поразрядный оператор И, чтобы определить,
// является ли число нечетным.
using System;
class IsOdd {
static void Main() { ushort num;
num = 10;
if((num & 1) == 1)
Console.WriteLine("He выводится.") ;
num = 11;
if((num & 1) == 1)
Console.WriteLine(num + " — нечетное число.");
}
}
Вот как выглядит результат выполнения этой программы.
11 — нечетное число.
В обоих операторах if из приведенной выше программы выполняется поразрядная операция И над числовыми значениями переменной num и 1. Если младший двоичный разряд числового значения переменной num установлен, т.е. содержит двоичную 1, то результат поразрядной операции num & 1 оказывается равным 1. В противном случае он равен нулю. Поэтому оператор ifможет быть выполнен успешно лишь в том случае, если проверяемое число оказывается нечетным.
Возможностью проверять состояние отдельных двоичных разрядов с помощью поразрядного оператора & можно воспользоваться для написания программы, в которой отдельные двоичные разряды проверяемого значения типа byte приводятся в двоичной форме. Ниже показан один из способов написания такой программы.
// Показать биты, составляющие байт.
using System;
class ShowBits {
static void Main() { int t; byte val;
val = 123;
for(t=128; t > 0; t = t/2) {
if((val & t) != 0) Console.Write("1 "); if((val & t) == 0) Console.Write("0 ");
}
}
}
Выполнение этой программы дает следующий результат.
01111011
В цикле for из приведенной выше программы каждый бит значения переменной val проверяется с помощью поразрядного оператора И, чтобы выяснить, установлен ли этот бит или сброшен. Если он установлен, то выводится цифра 1, а если сброшен, то выводится цифра 0.
Поразрядный оператор ИЛИ может быть использован для установки отдельных двоичных разрядов. Если в 1 установлен какой-нибудь бит в любом из операндов этого оператора, то в 1 будет установлен и соответствующий бит в другом операнде. Например:
1101 ООН
* 10101010
11111011
Используя поразрядный оператор ИЛИ, можно без особого труда превратить упоминавшийся выше пример программы, преобразующей нечетные числа в четные, в приведенный ниже обратный пример, где четные числа преобразуются в нечетные.
// Применить поразрядный оператор ИЛИ, чтобы сделать число нечетным.
using System;
class MakeOdd {
static void Main() { ushort num; ushort i;
for(i = 1; i <= 10; i++) {
num = i;
Console.WriteLine("num: " + num); num = (ushort) (num | 1);
Console.WriteLine("num после установки младшего разряда: " + num + "\n");
}
}
}
Результат выполнения этой программы выглядит следующим образом.
num: 1
num после установки младшего разряда: 1
num: 2
num
после
установки
младшего
разряда:
3
num:
num
: 3
после
установки
младшего
разряда:
3
num:
num
: 4
после
установки
младшего
разряда:
5
num:
num
: 5
после
установки
младшего
разряда:
5
num:
num
: 6
после
установки
младшего
разряда:
7
num:
num
: 7
после
установки
младшего
разряда:
7
num: num
: 8
после
установки
младшего
разряда:
9
num: num
: 9
после
установку
младшего
разряда:
9
num: num
: 10 после
установки
младшего
разряда:
11
В приведенной выше программе выполняется поразрядная операция ИЛИ над каждым числовым значением переменной num и 1, поскольку 1 дает двоичное значение, в котором установлен младший разряд. В результате поразрядной операции ИЛИ над 1 и любым другим значением младший разряд последнего устанавливается, тогда как все остальные разряды остаются без изменения. Таким образом, результирующее числовое значение получается нечетным, если исходное значение было четным.
Поразрядный оператор исключающее ИЛИ устанавливает двоичный разряд операнда в том и только в том случае, если двоичные разряды сравниваемых операндов оказываются разными, как в приведенном ниже примере.
01111111 10111001
А
11000110
У поразрядного оператора исключающее ИЛИ имеется одно интересное свойство, которое оказывается полезным в самых разных ситуациях. Так, если выполнить сначала поразрядную операцию исключающее ИЛИ одного значения X с другим значениемY,а затем такую же операцию над результатом предыдущей операции и значениемY,то вновь получится первоначальное значение X. Это означает, что в приведенном ниже фрагменте кода
R1 = X л Y;
R2 = R1 л Y;
значение переменнойR2оказывается в итоге таким же, как и значение переменнойX.Следовательно, в результате двух последовательно выполняемых поразрядных операций исключающее ИЛИ, в которых используется одно и то же значение, получается первоначальное значение. Этим свойством данной операции можно воспользоваться для написания простой программы шифрования, в которой некоторое целое значение служит в качестве ключа для кодирования и декодирования сообщения с помощью операции исключающее ИЛИ над символами этого сообщения. В первый раз операция исключающее ИЛИ выполняется для кодирования открытого текста в зашифрованный, а второй раз — для декодирования зашифрованного текста в открытый. Разумеется, такое шифрование не представляет никакой практической ценности, поскольку оно может быть легко разгадано. Тем не менее оно служит интересным примером для демонстрации результатов применения поразрядных операторов исключающее ИЛИ, как в приведенной ниже программе.
// Продемонстрировать применение поразрядного оператора исключающее ИЛИ. using System;
class Encode {
static void Main() { char chi = 'H'; char ch2 = 1i 1 ; char ch3 = 1!1; int key = 88;
Console.WriteLine("Исходное сообщение: " + chi + ch2 + ch3) ;
// Зашифровать сообщение, chi = (char) (chi л key);
ch2 = (char) (ch2 л key) ;
ch3 = (char) (ch3 л key);
Console.WriteLine("Зашифрованное сообщение: " + chi + ch2 + ch3);
// Расшифровать сообщение.
chi = (char) (chi л key); 1
ch2 = (char) (ch2 л key);
ch3 = (char) (ch3 л key);
Console.WriteLine("Расшифрованное сообщение: " + chi + ch2 + ch3);
}
}
Ниже приведен результат выполнения этой программы.
Исходное сообщение: Hi!
Зашифрованное сообщение: Qly Расшифрованное сообщение: Hi!
Как видите, в результате выполнения двух последовательностей поразрядных операций исключающее ИЛИ получается расшифрованное сообщение. (Еще раз напомним, что такое шифрование не имеет никакой практической ценности, поскольку оно, в сущности, ненадежно.)
Поразрядный унарный оператор НЕ (или оператор дополнения до 1) изменяет на обратное состояние всех двоичных разрядов операнда. Так, если некоторое целое значение А имеет комбинацию двоичных разрядов 1001 0110, то в результате поразрядной операции ~А получается значение с комбинацией двоичных разрядов 0110 1001.
В следующем примере программы демонстрируется применение поразрядного оператора НЕ с выводом некоторого числа и его дополнения до 1 в двоичном коде.
// Продемонстрировать применение поразрядного унарного оператора НЕ.
using System;
class NotDemo {
static void Main() { sbyte b = -34;
}
Console.WriteLine ();
// обратить все биты b = (sbyte) ~b;
}
}
}
Результат выполнения этой программы приведен ниже.
11011110
00100001
Операторы сдвига
В C# имеется возможность сдвигать двоичные разряды, составляющие целое значение, влево или вправо на заданную величину. Для этой цели в C# определены два приведенных ниже оператора сдвига двоичных разрядов.
«
Сдвиг влево
>>
Сдвиг вправо
Ниже приведена общая форма для этих операторов:
значение « число_битов значение » число_битов
гдечисло_битов— это число двоичных разрядов, на которое сдвигается указанноезна чение.
При сдвиге влево все двоичные разряды в указываемом значении сдвигаются на одну позицию влево, а младший разряд сбрасывается в нуль. При сдвиге вправо все двоичные разряды в указываемом значении сдвигаются на одну позицию вправо. Если вправо сдвигается целое значение безвнака, то старший разряд сбрасывается в нуль. А если вправо сдвигается целое значение со знаком, то разряд знака сохраняется. Напомним, что для представления отрицательных чисел старший разряд целого числа устанавливается в 1. Так, если сдвигаемое значение является отрицательным, то при каждом сдвиге вправо старший разряд числа устанавливается в 1. А если сдвигаемое значение является положительным, то при каждом сдвиге вправо старший разряд числа сбрасывается в нуль.
При сдвиге влево и вправо крайние двоичные разряды теряются. Восстановить потерянные при сдвиге двоичные разряды нельзя, поскольку сдвиг в данномслучаене является циклическим.
Ниже приведен пример программы, наглядно демонстрирующий действие сдвига влево и вправо. В данном примере сначала задается первоначальное целое значение, равное 1. Это означает, что младший разряд этого значения установлен. Затем это целое значение сдвигается восемь раз подряд влево. После каждого сдвига выводятся восемь младших двоичных разрядов данного значения. Далее процесс повторяется, но на этот раз 1 устанавливается на позиции восьмого разряда, а по существу, задается целое значение 128, которое затем сдвигается восемь раз подряд вправо.
// Продемонстрировать применение операторов сдвига.
using System;
class ShiftDemo {
static void Main() { int val = 1;
for(int i = 0; i < 8; i++) {
for(int t=128; t > 0; t = t/2) {
if((val & t) != 0) Console.Write("1 "); if((val & t) == 0) Console.Write("0 ");
}
Console.WriteLine();
val = val <<1; // сдвиг влево
}
Console.WriteLine() ; val = 128;
for(int i = 0; i < 8; i++) {
for(int t=128; t > 0; t = t/2) {
if((val & t) != 0) Console.Write("1 "); if((val & t) == 0) Console.Write("0 ");
}
Console.WriteLine();
val = val >>1; // сдвиг вправо
}
}
}
Результат выполнения этой программы выглядит следующим образом.
00000001
00000010
00000100
00001000
00010000
00100000
01000000
10000000
10000000
01000000
00100000
00010000
00001000
00000100
00000010
00000001
Двоичные разряды соответствуют форме представления чисел в степени 2, и поэтому операторы сдвига могут быть использованы для умножения или деления целых значений на 2. Так, при сдвиге вправо целое значение удваивается, а при сдвиге влево — уменьшается наполовину. Разумеется, все это справедливо лишь в том случае, если крайние разряды не теряются при сдвиге в ту или иную сторону. Ниже приведен соответствующий пример.
// Применить операторы сдвига для умножения и деления на 2.
using System;
class MultDiv {
static void Main() { int n;
n = 10;
Console.WriteLine("Значение переменной n: " + n) ;
// Умножить на 2.
n = n << l^-
Console.WriteLine ();
// Установить переменную n в исходное состояние, n = 10;
Console.WriteLine("Значение переменной n: " + n);
// Умножить на 2 тридцать раз. n = п << 30; // данные теряются
Console.WriteLine("Значение переменной п после " +
"сдвига на 30 позиций влево: " + п);
}
}
Ниже приведен результат выполнения этой программы.
Значение переменной п после сдвига на 30 позиций влево: -2147483648
Обратите внимание на последнюю строку приведенного выше результата. Когда целое значение 10 сдвигается влево тридцать раз подряд, информация теряется, поскольку двоичные разряды сдвигаются за пределы представления чисел для типа int. В данном случае получается совершенно ''непригодное7' значение, которое оказывается к тому же отрицательным, поскольку в результате сдвига в старшем разряде, используемом в качестве знакового, оказывается 1, а следовательно, данное числовое значение должно интерпретироваться как отрицательное. Этот пример наглядно показывает, что применять операторы сдвига для умножения или деления на 2 следует очень аккуратно. (Подробнее о типах данных со знаком и без знака см. в главе 3.)
Поразрядные составные операторы присваивания
Все двоичные поразрядные операторы могут быть использованы в составных операциях присваивания. Например, в двух приведенных ниже операторах переменной х присваивается результат выполнения операции исключающее ИЛИ над первоначальным значением переменной х и числовым значением 127.
х = х л 127; х л= 127;
Оператор ?
Оператор ? относится к числу самых примечательных в С#. Он представляет собой условный оператор и часто используется вместо определенных видов конструкций if-then-else. Оператор ? иногда еще называюттернарным,поскольку для него требуются три операнда. Ниже приведена общая форма этого оператора.
Выражение 1?Выражение2:Выражение3;
ЗдесьВыражение 1должно относиться к типу bool, аВыражение2иВыражение3—к одному и тому же типу. Обратите внимание на применение двоеточия и его местоположение в операторе ?.
Значение выражения ? определяется следующим образом. Сначала вычисляетсяВыражение!.Если оно истинно, то вычисляетсяВыражение2,а полученный результат определяет значение всего выражения ? в целом. Если жеВыражение1оказывается ложным, то вычисляетсяВыражение3,и его значение становится общим для всего выражения ?. Рассмотрим следующий пример, в котором переменной absval присваивается значение переменной val.
absval = val < 0 ? -val : val; // получить абсолютное значение переменной val
В данном примере переменной absval присваивается значение переменной val, если оно больше или равно нулю. Если же значение переменной val отрицательно, то переменной absval присваивается результат отрицания этого значения, что в итоге дает положительное значение.
Ниже приведен еще один пример применения оператора ?. В данной программе одно число делится на другое, но при этом исключается деление на нуль.
// Исключить деление на нуль, используя оператор?.
using System;
class NoZeroDiv {
static void Main() { int result;
for(int i = -5; i < 6; i++) {
result = i != 0 ? 100 / i : 0; if (i ! = 0)
Console.WriteLine("100 / " + i + " равно " + result);
}
}
}
Выполнение этой программы дает следующий результат.
100 / -5 равно -20 100 / -4 равно -25 100 / -3 равно -33 100 / -2 равно -50 100 / -1 равно -100 100 / 1 равно 100 100 / 2 равно 50 100 / 3 равно 33 100 / 4 равно 25 100 / 5 равно 20
Обратите особое внимание на следующую строку из приведенной выше программы.
result = i != 0 ? 100 / i : 0;
В этой строке переменной result присваивается результат деления числа 100 на значение переменной i. Но это деление осуществляется лишь в том случае, если значение переменной i не равно нулю. Когда же оно равно нулю, переменной result присваивается значение, обнуляющее результат.
Присваивать переменной результат выполнения оператора ? совсем не обязательно. Например, значение, которое дает оператор ?, можно использовать в качестве аргумента при вызове метода. А если все выражения в операторе ? относятся к типу bool, то такой оператор может заменить собой условное выражение в цикле или операторе
if. В приведенном ниже примере программы выводятся результаты деления числа 100 только на четные, ненулевые значения.
// Разделить только на четные, ненулевые значения.
using System;
class NoZeroDiv2 { static void Main() {
for(int i = -5; i < 6; i++)
if(i != 0 ? (i%2 == 0) : false)
Console.WriteLine("100 / " + i + " равно " + 100 / i);
}
}
Обратите внимание на оператор if в приведенной выше программе. Если значение переменной i равно нулю, то оператор i f дает ложный результат. А если значение переменной i не равно нулю, то оператор if дает истинный результат, когда значение переменной i оказывается четным, и ложный результат, если оно нечетное. Благодаря этому допускается деление только на четные и ненулевые значения. Несмотря на то что данный пример служит лишь для целей демонстрации, подобные конструкции иногда оказываются весьма полезными.
Использование пробелов и круглых скобок
В выражении на C# допускается наличие символов табуляции и пробелов, благодаря которым оно становится более удобным для чтения. Например, оба приведенных ниже выражения, по существу, одинаковы, но второе читается легче.
х=10/у*(127+х) ; х = 10 / у * (127 + х) ;
I
Скобки могут служить для группирования подвыражений, по существу, повышая порядок предшествования заключенных в них операций, как в алгебре. Применение лишних или дополнительных скобок не приводит к ошибкам и не замедляет вычис^ ление выражения. Поэтому скобки рекомендуется использовать, чтобы сделать более ясным и понятным порядок вычисления как для самого автора программы, так и для тех, кто будет разбираться в ней впоследствии. Например, какое из двух приведенных ниже выражение легче читается?
х = у/3-34*temp+127; х = (у/3) - (34*temp) + 127;
Предшествование операторов
В табл. 4.2 приведен порядок предшествования всех операторов в С#: от самого высокого до самого низкого. В таблицу включен ряд операторов, рассматриваемых далее в этой книге.
Таблица 4.2. Предшествование операторов в C#
Наивысший
порядок
О
[]
.
++
--
checked
new sizeof typeof unchecked
(постфиксный)
(постфиксный)
j
(приведение
+ (унарный)
- (унарный)
++
--
типов)
(префиксный) префиксный)
★
/
о
о
+
-
«
»
<
>
1 =
<=
>=
is
&
А
1
&&
1 I
1 1 ? ?
?:
=
ор=
=>
Наинизший
порядок
ГЛАВА 5 Управляющие операторы
Вэтой главе речь пойдет об операторах, управляющих ходом выполнения программы на С#. Управляющие операторы разделяются на три категории: операторывыбора, к числу которых относятся операторыifиswitch,итерационныеоператоры, в том числе операторы циклаfor, while, do-whileиforeach,а также операторыперехода:break, continue, goto, returnиthrow.За исключением оператораthrow,который является неотъемлемой частью встроенного в C# механизма обработки исключительных ситуаций, рассматриваемого в главе 13, все остальные управляющие операторы представлены в этой главе.
Оператор if
Оператор i f уже был представлен в главе 2, а здесь он рассматривается более подробно. Ниже приведена полная форма этого оператора:
if (условие) оператор;elseоператор;
гдеусловие— это некоторое условное выражение, аоператор— адресат операторов if иelse.Операторelseне является обязательным. Адресатом обоих операторов, if иelse,могут также служить блоки операторов. Ниже приведена общая форма оператора i f, в котором используются блоки операторов.
if (условие)
{
последовательность операторов
else
{
последовательность операторов
}
Если условное выражение оказывается истинным, то выполняется адресат оператора if. В противном случае выполняется адресат оператора else, если таковой существует. Но одновременно не может выполняться и то и другое. Условное выражение, управляющее оператором if, должно давать результат типа bool.
Ниже приведен пример простой программы, в которой операторы if и else используются для того, чтобы сообщить, является ли число положительным или отрицательным.
// Определить, является ли числовое значение положительным или отрицательным.
using System;
class PosNeg {
static void Main() { int i;
for(i=-5; i <= 5; i++) {
Console.Write("Проверка " + i + ": ");
if(i < 0) Console.WriteLine("отрицательное число"); else Console.WriteLine("положительное число");
}
}
}
Результат выполнения этой программы выглядит следующим образом.
Проверка
-5
отрицательное
число
Проверка
-4
отрицательное
число
Проверка
-3
отрицательное
число
Проверка
-2
отрицательное
число
Проверка
-1
отрицательное
число
Проверка
0
положительное
число
Проверка
1
положительное
число
Проверка
2
положительное
число
Проверка
3
положительное
число
Проверка
4
положительное
число
Проверка
5
положительное
число
Если в данном примере значение переменнойiоказывается меньше нуля, то выполнятся адресат оператораif. В противном случае выполняется адресат оператора else, одновременно они не выполняются.
Вложенные операторы if
Вложеннымназывается такой операторif,который является адресатом другого оператора if или же оператора else. Вложенные операторы if очень часто применяются в программировании. Что же касается их применения в С#, то не следует забывать, что любой оператор else всегда связан с ближайшим оператором if, т.е. с тем
оператором if, который находится в том же самом блоке, где и оператор else, но не с другим оператором else. Рассмотрим следующий пример.
if (i == Ю) {
if (j < 20) -a = b; if(k > 100) с = d;
else a = с; // этот оператор else связан с оператором if(k > 100)
}
else a = d; // этот оператор else связан с оператором if(i == 10)
Как следует из комментариев к приведенному выше фрагменту кода, последний оператор else не связан с оператором if (j < 20), поскольку они не находятся в одном и том же блоке, несмотря на то, что этот оператор является для него ближайшим оператором if без вспомогательного оператора else. Напротив, последний оператор else связан с оператором if (i == 10). А внутренний оператор else связан с оператором i f (k > 100), поскольку этот последний является для него ближайшим оператором i f в том же самом блоке.
В приведенном ниже примере программы демонстрируется применение вложенного оператора if. В представленной ранее программе определения положительных и отрицательных чисел о нуле сообщалось как о положительном числе. Но, как правило, нуль считается числом, не имеющим знака. Поэтому в следующей версии данной программы о нуле сообщается как о числе, которое не является ни положительным, ни отрицательным.
// Определить, является ли числовое значение // положительным, отрицательным или нулевым.
using System;
class PosNegZero { static void Main() { int i;
for(i=-5; i <= 5; i++) {
Console.Write("Проверка " + i + ": "); if(i < 0) Console.WriteLine("отрицательное число"); else if(i == 0) Console.WriteLine("число без знака"); else Console.WriteLine("положительное число");
}
}
}
Ниже приведен результат выполнения этой программы.
Проверка -5: отрицательное число Проверка -4: отрицательное число Проверка -3: отрицательное число Проверка -2: отрицательное число Проверка -1: отрицательное число Проверка 0: число без знака Проверка 1: положительное число Проверка 2: положительное число Проверка 3: положительное число Проверка 4: положительное число Проверка 5: положительное число
Конструкция if-else-if
В программировании часто применяетсямногоступенчатая конструкцияif-else-if, состоящая из вложенных операторов if. Ниже приведена ее общая форма.
if(условие) оператор; else if(условие) оператор; else if (условие) оператор;
else
оператор;
Условные выражения в такой конструкции вычисляются сверху вниз. Как только обнаружится истинное условие, выполняется связанный с ним оператор, а все остальные операторы в многоступенчатой конструкции опускаются.
Если ни одно из условий не является истинным, то выполняется последний операторelse,который зачастую служит в качестве условия, устанавливаемого по умолчанию. Когда же последний операторelseотсутствует, а все остальные проверки по условию дают ложный результат, то никаких действий вообще не выполняется.
В приведенном ниже примере программы демонстрируется применение многоступенчатой конструкцииif-else-if.В этой программе обнаруживается наименьший множитель заданного целого значения, состоящий из одной цифры.
// Определить наименьший множитель заданного // целого значения, состоящий из одной цифры.
using System;
class Ladder {
static void Main(), { int num;
for(num = 2; num < 12; num++) { if((num %2) ==0)
Console.WriteLine("Наименьший множитель числа " + num + " равен 2.") else if((num % 3) == 0)
Console.WriteLine("Наименьший множитель числа " + num + " равен 3.") else if((num % 5) == 0)
Console.WriteLine("Наименьший множитель числа " + num + " равен 5.") else if((num % 7) == 0)
Console.WriteLine("Наименьший множитель числа " + num + " равен 7.") else
Console.WriteLine(num + " не делится на 2, 3, 5 или 7.");
}
}
}
Вот к какому результату приводит выполнение этой программы.
Наименьший множитель числа 2 равен 2 Наименьший множитель числа 3 равен 3
Наименьший множитель числа 10 равен 2
11 не делится на 2, 3, 5 или 7.
Как видите, последний оператор else выполняется лишь в том случае, если не удается выполнить ни один из предыдущих операторов.
Оператор switch
Вторым оператором выбора в C# является оператор switch, который обеспечивает многонаправленное ветвление программы. Следовательно, этот оператор позволяет сделать выбор среди нескольких альтернативных вариантов дальнейшего выполнения программы. Несмотря на то что многонаправленная проверка может быть организована с помощью последовательного ряда вложенных операторов if, во многих случаях более эффективным оказывается применение оператора switch. Этот оператор действует следующим образом. Значение выражения последовательно сравнивается с константами выбора из заданного списка. Как только будет обнаружено совпадение с одним из условий выбора, выполняется последовательность операторов, связанных с этим условием. Ниже приведена общая форма оператора switch.
switch(выражение) {caseконстанта1:
последовательность операторовbreak; caseконстанта2:
последовательность операторовbreak; case константаЗ:
последовательность операторовbreak;
default:
последовательность операторовbreak;
}
Заданноевыражениев операторе switch должно быть целочисленного типа (char, byte, short или int), перечислимого или же строкового. (О перечислениях и символьных строках типа string речь пойдет далее в этой книге.) А выражения других типов, например с плавающей точкой, в операторе switch не допускаются. Зачастую выражение, управляющее оператором switch, просто сводится к одной переменной. Кроме того, константы выбора должны иметь тип, совместимый с типом выражения. В одном операторе switch не допускается наличие двух одинаковых по значению констант выбора.
Последовательность операторов из ветвиdefaultвыполняется в том случае, если ни одна из констант выбора не совпадает с заданным выражением. Ветвьdefaultне является обязательной. Если же она отсутствует и выражение не совпадает ни с одним из условий выбора, то никаких действий вообще не выполняется. Если же происходит совпадение с одним из условий выбора, то выполняются операторы, связанные с этим условием, вплоть до оператораbreak.
Ниже приведен пример программы, в котором демонстрируется применение оператораswitch.
// Продемонстрировать применение оператора switch.
using System;
class SwitchDemo { static void Main() { int i;
for(i=0; i<10; i++) switch(i) {
case 0:
Console.WriteLine("i равно нулю"); break; case 1:
Console.WriteLine("i равно единице"); break; case 2:
Console.WriteLine("i равно двум"); break; case 3:
Console.WriteLine("i равно трем"); break; case 4:
Console.WriteLine ("i равно четырем"); break;
default: (
Console.WriteLine("i равно или больше пяти"); break;
}
}
}
Результат выполнения этой программы выглядит следующим образом.
i
равно
нулю.
i
равно
единице.
i
равно
двум.
i
равно
трем.
i
равно
четырем.
i
равно
или больше
пяти
i
равно
или больше
пяти
i
равно
или больше
пяти
i
равно
или больше
пяти
i
равно
или больше
пяти
Как видите, на каждом шаге цикла выполняются операторы, связанные с совпадающей константой выбора, в обход всех остальных операторов. Когда же значение
переменнойiстановится равным или больше пяти, то оно не совпадает ни с одной из констант выбора, а следовательно, выполняются операторы из ветвиdefault.
В приведенном выше примере операторомswitchуправляла переменнаяiтипаint.Как пояснялось ранее, для управления операторомswitchможет быть использовано выражений любого целочисленного типа, включая иchar.Ниже приведен пример применения выражения и констант выбора типаcharв оператореswitch.
// Использовать элементы типа char для управления оператором switch.
using System;
class SwitchDemo2 { static void Main() { char ch;
for(ch='A'; ch<= ' E'; ch++) switch (ch) { case 'A1:
Console.WriteLine("ch содержит A"); break; case 'В':
Console.WriteLine("ch содержит В"); break; case 'С':
Console.WriteLine("ch содержит С"); break; case ' D' :
Console.WriteLine("ch содержит D"); break; case 'E':
Console.WriteLine("ch содержит E"); break;
}
}
}
Вот какой результат дает выполнение этой программы.
ch содержит А ch содержит В ch содержит С ch содержит D ch содержит Е
Обратите в данном примере внимание на отсутствие ветвиdefaultв оператореswitch.Напомним, что ветвьdefaultне является обязательной. Когда она не нужна, ее можно просто опустить.
Переход последовательности операторов, связанных с одной ветвьюcase,в следующую ветвьcaseсчитается ошибкой, поскольку в C# должно непременно соблюдаться правило недопущения "провалов" в передаче управления ходом выполнения программы. Именно поэтому последовательность операторов в каждой ветвиcaseоператораswitchоканчивается операторомbreak.(Избежать подобных "провалов", можно также с помощью оператора безусловного переходаgoto,рассматриваемого далее в этой главе, но для данной цели чаще применяется оператррbreak.)Когда
в последовательности операторов отдельной ветви case встречается оператор break, происходит выход не только из этой ветви, но из всего оператора switch, а выполнение программы возобновляется со следующего оператора, находящегося за пределами оператора switch. Последовательность операторов в ветви default также должна быть лишена ''провалов'7, поэтому она завершается, как правило, оператором break.
Правило недопущения "провалов" относится к тем особенностям языка С#, которыми он отличается от С, C++ и Java. В этих языках программирования одна ветвь case может переходить (т.е. "проваливаться") в другую. Данное правило установлено в C# для ветвей case по двум причинам. Во-первых, оно дает компилятору возможность свободно изменять порядок следования последовательностей операторов из ветвей case для целей оптимизации. Такая реорганизация была бы невозможной, если бы одна ветвь case могла переходить в другую. И во-вторых, требование завершать каждую ветвь case явным образом исключает непроизвольные ошибки программирования, допускающие переход одной ветви case в другую.
Несмотря на то что правило недопущения "провалов" не допускает переход одной ветви case в другую, в двух или более ветвях case все же разрешается ссылаться с помощью меток на одну и ту же кодовую последовательность, как показано в следующем примере программы.
// Пример "проваливания" пустых ветвей case.
using System;
class EmptyCasesCanFall { static void Main() { int i;
for(i=l; i < 5; i++) switch(i) {
case 1: case 2:
case 3: Console.WriteLine("i равно 1, 2 или 3м); break;
case 4: Console.WriteLine("i равно 4"); break;
}
}
}
Ниже приведен результат выполнения этой программы.
Если значение переменнойiв данном примере равно 1, 2 или 3, то выполняется первый оператор, содержащий вызов методаWriteLine(). Такое расположение нескольких меток ветвейcaseподряд не нарушает правило недопущения "провалов"; поскольку во всех этих ветвях используется одна и та же последовательность операторов.
Расположение нескольких меток ветвейcaseподряд зачастую применяется в том случае, если у нескольких ветвей имеется общий код. Благодаря этому исключается излишнее дублирование кодовых последовательностей.
Вложенные операторы switch
Один операторswitchможет быть частью последовательности операторов другого, внешнего оператораswitch.И такой операторswitchназываетсявложенным.Константы выбора внутреннего и внешнего операторовswitchмогут содержать общие значения, не вызывая никаких конфликтов. Например, следующий фрагмент кода является вполне допустимым.
switch(chi) {
case 'A': Console.WriteLine("Эта ветвь А — ^асть " +
"внешнего оператора switch.");
switch(ch2) {
case 'A':
Console.WriteLine("Эта ветвь A — часть " +
"внутреннего оператора switch");
break; case 'В1: // ...
} // конец внутреннего оператора switch break; case 'В': // ...
Оператор цикла for
Оператор for уже был представлен в главе 2, а здесь он рассматривается более подробно. Вас должны приятно удивить эффективность и гибкость этого оператора. Прежде всего, обратимся к самым основным и традиционным формам оператора for.
Ниже приведена общая форма оператора for для повторного выполнения единственного оператора.
for{инициализация; условие; итерация)оператор;
А вот как выглядит его форма для повторного выполнения кодового блока:
for(инициализация; условие;итерация)
{
последовательность операторов;
}
гдеинициализация;как правило, представлена оператором присваивания, задающим первоначальное значение переменной, которая выполняет роль счетчика и управляет циклом;условие— это логическое выражение, определяющее необходимость повторения цикла; а итерация — выражение, определяющее величину, на которую должно изменяться значение переменной, управляющей циклом, при каждом повторе цикла. Обратите внимание на то, что эти три основные части оператора цикла for должны быть разделены точкой с запятой. Выполнение цикла for будет продолжаться до тех пор, пока проверка условия дает истинный результат. Как только эта проверка даст ложный результат, цикл завершится, а выполнение программы будет продолжено с оператора, следующего после цикла for.
Цикл for может продолжаться как в положительном, так и в отрицательном направлении, изменяя значение переменной управления циклом на любую величину. В приведенном ниже примере программы выводятся числа; постепенно уменьшающиеся от 100 до -100 на величину 5.
// Выполнение цикла for в отрицательном направлении.
using System;
class DecrFor {
static void Main() { int x;
for(x = 100; x > -100; x -= 5)
Console.WriteLine(x);
}
}
В отношении циклов for следует особо подчеркнуть, что условное выражение всегда проверяется в самом начале цикла. Это означает, что код в цикле может вообще не выполняться, если проверяемое условие с самого начала оказывается ложным. Рассмотрим следующий пример.
for(count=10; count < 5; count++)
x += count; // этот оператор не будет выполняться
Данный цикл вообще не будет выполняться, поскольку первоначальное значение переменной count, которая им управляет, сразу же оказывается больше 5. Это означает, что условное выражение count < 5 оказывается ложным с самого начала, т.е. еще до выполнения первого шага цикла.
Оператор цикла for — наиболее полезный для повторного выполнения операций известное число раз. В следующем примере программы используются два цикла for для выявления простых чисел в пределах от 2 до 20. Если число оказывается непростым, то выводится наибольший его множитель.
// Выяснить, является ли число простым. Если оно // непростое, вывести наибольший его множитель.
using System;
class FindPrimes { static void Main() { int num; int i; int factor; bool isprime;
for(num = 2; num < 20; num++) { isprime = true; factor = 0;
// Выяснить, делится ли значение переменной num нацело. for(i=2; i <= num/2; i++) {
if((num % i) == 0) {
// Значение переменной num делится нацело.
// Следовательно, это непростое число, isprime = false; factor = i;
}
if(isprime)
Console.WriteLine(num + " — простое число."); else
-Console.WriteLine("Наибольший множитель числа " + num + " равен " + factor);
}
}
}
Ниже приведен результат выполнения этой программы.
2 — простое число
3 — простое число
Наибольший множитель
числа
4
равен 2
5 — простое число Наибольший множитель
числа
6
равен 3
7 — простое число Наибольший множитель
числа
8
равен 4
Наибольший множитель
числа
9
равен 3
Наибольший множитель
числа
10
равен 5
11 — простое число Наибольший множитель
числа
12
равен 6
13 — простое число Наибольший множитель
числа
14
равен 7
Наибольший множитель
числа
15
равен 5
Наибольший множитель
числа
16
равен 8
17 — простое число Наибольший множитель
числа
18
равен 9
19 — простое число
Некоторые разновидности оператора цикла for
Оператор цикла for относится к самым универсальным операторам языка С#, поскольку он допускает самые разные варианты своего применения. Некоторые разновидности оператора цикла for рассматриваются ниже.
Применение нескольких переменных управления циклом
В операторе цикла for разрешается использовать две или более переменных для управления циклом. В этом случае операторы инициализации и инкремента каждой переменной разделяются запятой. Рассмотрим следующий пример программы.
// Использовать запятые в операторе цикла for.
using System;
class Comma {
static void Main() { int i, j;
for(i=0, j = 10; i < j; i++, j —)
Console.WriteLine("i и j: " + i + " " + j) ;
Выполнение этой программы дает следующий результат.
i
и
j :
0
10
i
и
j :
1
9
i
и
j :
2
8
i
и
j :
3
7
i
и
j :
4
6
В данном примере запятыми разделяются два оператора инициализации и еще два итерационных выражения. Когда цикл начинается, инициализируются обе переменные, i и j. Всякий раз, когда цикл повторяется, переменная i инкрементируется, а переменная j декрементируется. Применение нескольких переменных управления циклом нередко оказывается удобным, упрощая некоторые алгоритмы. Теоретически в операторе циклаforможет присутствовать любое количество операторов инициализации и итерации, но на практике цикл получается слишком громоздким, если применяется более двух подобных операторов.
Ниже приведен практический пример применения нескольких переменных управления циклом в оператореfor.В этом примере программы используются две переменные управления одним цикломforдля выявления наибольшего и наименьшего множителя целого числа (в данном случае — 100). Обратите особое внимание на условие окончания цикла. Оно опирается на обе переменные управления циклом.
// Использовать запятые в операторе цикла for для // выявления наименьшего и наибольшего множителя числа.
using System; >
class Comma {
static void Main() { int i, j;
int smallest, largest; int num;
num = 100;
smallest = largest = 1;
for(i=2, j=num/2; (i <= num/2) & (j >= 2); i++, j—) {
if((smallest == 1) & ((num % i) == 0)) smallest = i;
if ( (largest == 1) & ((num % j) == 0)) largest = j;
}
Console.WriteLine("Наибольший множитель: " + largest);
Console.WriteLine("Наименьший множитель: " + smallest);
}
}
Ниже приведен результат выполнения этой программы.
Наибольший множитель: 50 Наименьший множитель: 2
Благодаря применению двух переменных управления циклом удается выявить наименьший и наибольший множители числа в одном циклеfor.В частности, управляющая переменнаяiслужит для выявления наименьшего множителя. Первоначально ее значение устанавливается равным 2 и затем инкрементируется до тех пор, пока не превысит половину значения переменнойnum. Ауправляющая переменнаяjслужит для выявления наибольшего множителя. Ее значение первоначально устанавливается равным половине значения переменнойnumи затем декрементируется до тех пор, пока не станет меньше 2. Цикл продолжает выполняться до тех пор, пока обе переменные,iиj,не достигнут своих конечных значений. По завершении цикла оба множителя оказываются выявленными.
Условное выражение
Условным выражением, управляющим цикломfor,может быть любое действительное выражение, дающее результат типаbool.В него не обязательно должна входить переменная управления циклом. В следующем примере программы управление цикломforосуществляется с помощью значения переменнойdone.
// Условием выполнения цикла может служить любое выражение типа bool.
using System;
class forDemo {
static void Main() { int i, j;
bool done = false;
for(i=0, j=100; !done; i++, j—) {
if(i*i >= j) done = true;
Console.WriteLine("i, j: " + i + " " + j);
}
}
}
Ниже приведен результат выполнения этой программы.
i, j
0 100
i/ j
1 99
i, j
2 98
i, j
3 97
if j
4 96
1a j
5 95
1a j
6 94
1a j
7 93
j
8 92
1a j
9 91
ir j
10 90
В данном примере цикл for повторяется до тех пор, пока значение переменной done типа не окажется истинным (true). Истинное значение переменной done устанавливается в цикле, когда квадрат значения переменнойiоказывается больше или равным значению переменной j.
Отсутствующие части цикла
Ряд интересных разновидностей цикла for получается в том случае, если оставить пустыми отдельные части определения цикла. В C# допускается оставлять пустыми любые или же все части инициализации, условия и итерации в операторе цикла for. В качестве примера рассмотрим такую программу.
// Отдельные части цикла for могут оставаться пустыми.
using System;
class Empty {
static void Main() { int i;
for (i = 0; i < 10; ) {
Console.WriteLine("Проход №" + i);
i++; // инкрементировать переменную управления циклом
}
}
}
В данном примере итерационное выражение в определении цикла for оказывается пустым, т.е. оно вообще отсутствует. Вместо этого переменная i, управляющая циклом, инкрементируется в теле самого цикла. Это означает, что всякий раз, когда цикл повторяется, значение переменной i проверяется на равенство числу 10, но никаких других действий при этом не происходит. А поскольку переменная i инкрементируется в теле цикла, то сам цикл выполняется обычным образом, выводя приведенный ниже результат.
Проход №0 Проход №1 Проход №2 Проход №3 Проход №4 Проход №5 Проход №6 Проход №7 Проход №8 Проход №9
В следующем примере программы из определения цикла for исключена инициализирующая часть.
// Исключить еще одну часть из определения цикла for.
using System;
class Empty2 {
static void Main() { int i;
i = 0; // исключить инициализацию из определения цикла for(; i < 10; ) {
Console.WriteLine("Проход №" + i);
i++; // инкрементировать переменную управления циклом
}
}
}
В данном примере переменная i инициализируется перед началом цикла, а не в самом циклеfor.Как правило, переменная управления циклом инициализируется в циклеfor.Выведение инициализирующей части за пределы цикла обычно делается лишь в том случае, если первоначальное значение данной переменной получается в результате сложного процесса, который нецелесообразно вводить в операторе циклаfor.
Бесконечный цикл
Еслиоставить пустым выражение условия в операторе цикла for, то получитсябесконечный цикл,т.е. такой цикл, который никогда не заканчивается. В качестве примера в следующем фрагменте кода показано, каким образом в C# обычно создается бесконечный цикл.
for(;;) // цикл, намеренно сделанный бесконечным{
//. . .
}
Этот цикл будет выполняться бесконечно. Несмотря на то что бесконечные циклы требуются для решения некоторых задач программирования, например при разработке командных процессоров операционных систем, большинство так называемых "бесконечных" циклов на самом деле представляет собой циклы со специальными требованиями к завершению. (Подробнее об этом — в разделе "Применение оператораbreakдля выхода из цикла" далее в этой главе.)
Циклы без тела
В C# допускается оставлять пустым тело циклаforили любого другого цикла, посколькупустой операторс точки зрения синтаксиса этого языка считается действительным. Циклы без тела нередко оказываются полезными. Например, в следующей программе цикл без тела служит для получения суммы чисел от 1 до 5.
// Тело цикла может быть пустым, using system;
class Empty3 {
static void Main() { int i;
int sum = 0;
• // получить сумму чисел от 1 до 5 for(i = 1; i <= 5; sum += i++);
Console.WriteLine("Сумма равна " + sum);
}
}
Выполнение этой программы дает следующий результат.
Сумма равна 15
Обратите внимание на то, что процесс суммирования выполняется полностью в операторе циклаfor,и для этого тело цикла не требуется. В этом цикле особое внимание обращает на себя итерационное выражение.
sum += i++
Подобные операторы не должны вас смущать. Они часто встречаются в программах, профессионально написанных на С#, и становятся вполне понятными, если разобрать их по частям. Дословно приведенный выше оператор означает следующее: сложить со значением переменнойsumрезультат суммирования значений переменныхsumиi,а затем инкрементировать значение переменнойi.Следовательно, данный оператор равнозначен следующей последовательности операторов.
sum = sum + i;
i + + ;
Объявление управляющих переменных в цикле for
Нередко переменная, управляющая циклом for, требуется только для выполнения самого цикла и нигде больше не используется. В таком случае управляющую переменную можно объявить в инициализирующей части оператора циклаfor.Например, в приведенной ниже программе вычисляется сумма и факториал чисел от 1 до 5, а переменнаяi,управляющая цикломfor,объявляется в этом цикле. // Объявить переменную управления циклом в самом цикле for. using System; class ForVar { static void Main() { int sum = 0; int fact = 1; // вычислить факториал чисел от 1 до 5 for(int i = 1; i <= 5; i++) { sum += i; // Переменная i действует в цикле, fact *= i; } // А здесь переменная i недоступна. Console.WriteLine("Сумма равна " + sum); Console.WriteLine("Факториал равен " + fact); } } Объявляя переменную в циклеfor,не следует забывать о том, что область действия этой переменной ограничивается пределами оператора циклаfor.Это означает, что за пределами цикла действие данной переменной прекращается. Так, в приведенном выше примере переменнаяiоказывается недоступной за пределами циклаfor.Для того чтобы использовать переменную управления циклом в каком-нибудь другом месте программы, ее нельзя объявлять в циклеfor. Прежде чем переходить к чтению следующего материала, поэкспериментируйте с собственными разновидностями оператора циклаfor.В ходе эксперимента вы непременно обнаружите замечательные свойства этого оператора цикла. Оператор цикла while
Еще одним оператором цикла в C# является оператор while. Ниже приведена общая форма этого оператора: while (условие) оператор; гдеоператор— это единственный оператор или же блок операторов, аусловиеозначает конкретное условие управления циклом и может быть любым логическим выражением. В этом циклеоператорвыполняется до тех пор, покаусловиеистинно. Как только условие становится ложным, управление программой передается строке кода, следующей непосредственно после цикла. Ниже приведен простой пример программы, в которой цикл while используется для вычисления порядка величины целого числа. // Вычислить порядок величины целого числа, using System; class WhileDemo { static void Main() { int num; int mag; num = 435679; mag = 0; Console.WriteLine("Число: " + num); while(num > 0) { mag++; num = num / 10; }; Console.WriteLine("Порядок величины: " + mag); } } Выполнение этой программы дает следующий результат. Число: 435679 Порядок величины: 6 Приведенный выше цикл while действует следующим образом. Сначала проверяется значение переменной num. Если оно больше нуля, то переменная mag, выполняющая роль счетчика порядка величины, инкрементируется, а значение переменной num делится на 10. Цикл повторяется до тех пор, пока значение переменной num остается больше нуля. Как только оно окажется равным нулю, цикл завершается, а в переменной mag остается порядок величины первоначального числового значения. Как и в цикле for, в цикле while проверяется условное выражение, указываемое в самом начале цикла. Это означает, что код в теле цикла может вообще не выполняться, а также избавляет от необходимости выполнять отдельную проверку перед самим циклом. Данное свойство цикла while демонстрируется в следующем примере программы, где вычисляются целые степени числа 2 от 0 до 9. // Вычислить целые степени числа 2. using System; class Power { static void Main() { int e; int result; for (int i=0; i < 10; i++) { result = 1; e = i; while (e > 0) { result *= 2; e—;
} Console.WriteLine ("2 в степени " + i + " равно " + result); } } } Результат выполнения этой программы приведен ниже. Обратите внимание на то, что цикл while выполняется только в том случае, если значение переменной е больше нуля. А когда оно равно нулю, как это имеет место на первом шаге цикла for, цикл while пропускается. Оператор цикла do-while
Третьим оператором цикла в C# является оператор do-while. В отличие от операторов цикла for и while, в которых условие проверялось в самом начале цикла, в операторе do-while условие выполнения цикла проверяется в самом его конце. Это означает, что цикл do-while всегда выполняется хотя бы один раз. Ниже приведена общая форма оператора цикла do-while. do { операторы; } while (условие) ; При наличии лишь одного оператора фигурные скобки в данной форме записи необязательны. Тем не менее они зачастую используются для того, чтобы сделать конструкцию do-while более удобочитаемой и не путать ее с конструкцией цикла while. Цикл do-while выполняется до тех пор, пока условное выражение истинно. В приведенном ниже примере программы цикл do-while используется для представления отдельных цифр целого числа в обратном порядке. // Отобразить цифры целого числа в обратном порядке. using System; class DoWhileDemo { static void Main() { int num; int nextdigit; num = 198; Console.WriteLine("Число: " + num); Console.Write("Число в обратном порядке: "); do { nextdigit = num % 10; Console.Write(nextdigit); num = num / 10; } while(num > 0); Console.WriteLine() ; } } Выполнение этой программы дает следующий результат. Число: 198 Число в обратном порядке: 8 91 Приведенный выше цикл действует следующим образом. На каждом его шаге крайняя слева цифра получается в результате расчета остатка от деления целого числа (значения переменной num) на 10. Полученная в итоге цифра отображается. Далее значение переменной num делится на 10. А поскольку это целочисленное деление, то в его результате крайняя слева цифра отбрасывается. Этот процесс повторяется до тех пор, пока значение переменной num не достигнет нуля. Оператор цикла foreach
Оператор цикла f oreach служит для циклического обращения к элементамколлекции, которая представляет собой группу объектов. В C# определено несколько видов коллекций, к числу которых относится массив. Подробнее о цикле f oreach речь пойдет в главе 7, где рассматриваются массивы. Применение оператора break для выхода из цикла
С помощью оператора break можно специально организовать немедленный выход из цикла в обход любого кода, оставшегося в теле цикла, а также минуя проверку условия цикла. Когда в теле цикла встречается оператор break, цикл завершается, а выполнение программы возобновляется с оператора, следующего после этого цикла. Рассмотрим простой пример программы. // Применить оператор break для выхода из цикла. using System; class BreakDemo { static void Main() { // Использовать оператор break для выхода из этого цикла, for(int i=—10; i <= 10; i++) { if(i > 0) break; // завершить цикл, как только значение // переменной i станет положительным Console .Write (i + " ") ;. } Console .-WriteLine ("Готово ! ") ; } } Выполнение этой программы дает следующий результат. -10 -9 -8 -7 -6 -5 -4 -3 -2 -1 0 Готово! Как видите, цикл for организован для выполнения в пределах от -10 до 10, но, несмотря на это, оператор break прерывает его раньше, когда значение переменной i становится положительным. Оператор break можно применять в любом цикле, предусмотренном в С#. В качестве примера ниже приведена версия предыдущей программы, переделанная с целью использовать цикл do-while. // Применить оператор break для выхода из цикла do-while. using System; * class BreakDemo2 { static void Main() { int i; i = -10; do { if (i > 0) break; Console.Write(i + " ") ; i++ ; } while (i <= 10); Console.WriteLine("Готово!"); } } А теперь рассмотрим более практический пример применения оператора break. В приведенной ниже программе выявляется наименьший множитель числа. // Выявить наименьший множитель числа. using System; class FindSmallestFactor { static void Main() { int factor = 1; int num =-1000; for (int i=2; i <= num/i;' i++) { if((num%i) == 0) { factor = inbreak; // прервать цикл, как только будет // выявлен наименьший множитель числа } } Console.WriteLine("Наименьший множитель равен " + factor); } } Результат выполнения этой программы выглядит следующим образом. Наименьший множитель равен 2 Операторbreakпрерывает выполнение циклаfor,как только будет выявлен наименьший множитель числа. Благодаря такому применению оператораbreakисключается опробование любых других значений после выявления наименьшего множителя числа, а следовательно, и неэффективное выполнение кода. Если операторbreakприменяется в целом ряде вложенных циклов, то он прерывает выполнение только самого внутреннего цикла. В качестве примера рассмотрим следующую программу. // Применить оператор break во вложенных циклах, using System; class BreakNested { static void Main() { for(int i=0; i<3; i++) { Console.WriteLine("Подсчет во внешнем цикле: " + i); Console.Write(" Подсчет во внутреннем цикле: "); int t = 0; while(t < 100) { if(t == 10) break; // прервать цикл, если t равно 10 Console.Write(t + " "); t++; } Console.WriteLine (); } Console.WriteLine("Циклы завершены."); } } Выполнение этой программы дает следующий результат. Подсчет во внешнем цикле: 0 Подсчет во внутреннем цикле: 0123456789 Подсчет во внешнем цикле: 1 Подсчет во внутреннем цикле: 0 1 2 3 4 5 б 7 8 9 Подсчет во внешнем цикле: 2 Подсчет во внутреннем цикле: 0123456789 Циклы завершены Как видите, операторbreakиз внутреннего цикла вызывает прерывание только этого цикла, а на выполнение внешнего цикла он не оказывает никакого влияния. В отношении оператораbreakнеобходимо также иметь в виду следующее. Во-первых, в теле цикле может присутствовать несколько операторовbreak,но применять их следует очень аккуратно, поскольку чрезмерное количество операторовbreakобычно приводит к нарушению нормальной структуры кода. И во-вторых, операторbreak,выполняющий выход из оператораswitch,оказывает воздействие только на этот оператор, но не на объемлющие его циклы. Применение оператора continue
С помощью оператораcontinueможно организовать преждевременное завершение шага итерации цикла в обход обычной структуры управления циклом. Операторcontinueосуществляет принудительный переход к следующему шагу цикла, пропуская любой код, оставшийся невыполненным. Таким образом, операторcontinueслужит своего рода дополнением оператораbreak.В приведенном ниже примере программы операторcontinueиспользуется в качестве вспомогательного средства для вывода четных чисел в пределах от 0 до 100. // Применить оператор continue, using System; class ContDemo { static void Main() { // вывести четные числа от 0 до 100. for (int i = 0; i <= 100; i++) { if((i%2) != 0) continue; // перейти к следующему шагу итерации Console.WriteLine(i); } } } В данном примере выводятся только четные числа, поскольку при обнаружении нечетного числа шаг итерации цикла завершается преждевременно в обход вызова методаWriteLine(). В циклахwhileиdo-whileоператорcontinueвызывает передачу управления непосредственно условному выражению, после чего продолжается процесс выполнения цикла. А в циклеforсначала вычисляется итерационное выражение, затем условное выражение, после чего цикл продолжается. Операторcontinueредко находит удачное применение, в частности, потому, что в C# предоставляется богатый набор операторов цикла, удовлетворяющих большую часть прикладных потребностей. Но в тех особых случаях, когда требуется преждевременное прерывание шага итерации цикла, операторcontinueпредоставляет структурированный способ осуществления такого прерывания. Оператор return
Операторreturnорганизует возврат из метода. Его можно также использовать для возврата значения. Более подробно он рассматривается в главе 6. Оператор goto
Имеющийся в C# операторgotoпредставляет собой оператор безусловного перехода. Когда в программе встречается операторgoto,ее выполнение переходит непосредственно к тому месту, на которое указывает этот оператор. Он уже давно "вышел из употребления" в программировании, поскольку способствует созданию "макаронного" кода. Тем не менее операторgotoвсе еще находит применение — иногда даже эффективное. В этой книге не делается никаких далеко идущих выводов относительно правомочности использования оператораgotoдля управления программой. Следует, однако, подчеркнуть, что этому оператору трудно найти полезное применение, и поэтому он не особенно нужен для полноты языка программирования. Хотя в некоторых случаях он оказывается удобным и дает определенные преимущества, если используется благоразумно. В силу этих причин операторgotoупоминается только в данном разделе книги. Главный недостаток оператораgotoс точки зрения программирования заключается в том, что он вносит в программу беспорядок и делает ее практически неудобочитаемой. Но иногда применение оператораgotoможет, скорее, прояснить, чем запутать ход выполнения программы. Для выполнения оператораgotoтребуетсяметка —действительный в C# идентификатор с двоеточием. Метка должна находиться в том же методе, где и операторgoto,а также в пределах той же самой области действия. В приведенном ниже примере программы цикл суммирования чисел от 1 до 100 организован с помощью оператораgotoи соответствующей метки. х = 1; loopl: х++; if(х < 100) goto loopl; Кроме того, операторgotoможет быть использован для безусловного перехода к ветвиcaseилиdefaultв оператореswitch.Формально ветвиcaseилиdefaultвыполняют в оператореswitchроль меток. Поэтому они могут служить адресатами оператораgoto.Тем не менее операторgotoдолжен выполняться в пределах оператораswitch.Это означает, что его нельзя использовать как внешнее средство для безусловного перехода в операторswitch.В приведенном ниже примере программы демонстрируется применение оператораgotoв оператореswitch. // Применить оператор goto в операторе switch. using System; class SwitchGoto { static void Main() { for(int i=l; i < 5; i++) { switch(i) { ' case 1: Console.WriteLine("В ветви case 1"); goto case 3; case 2: Console.WriteLine("В ветви case 2"); goto case 1; case 3: Console.WriteLine("В ветви case 3"); goto default; default: Console.WriteLine("В ветви default"); break; } Console.WriteLine(); } // goto case 1; // Ошибка! Безусловный переход к оператору switch недопустим. } } Вот к какому результату приводит выполнение этой программы. В ветви case 1 В ветви case 3 В ветви default В ветви case 2 В ветви case 1 В ветви case 3 В ветви default В ветви case 3 В ветви default В ветви default Обратите внимание на то, как операторgotoиспользуется в оператореswitchдля перехода к другим его ветвямcaseили к ветвиdefault.Обратите также внимание на то, что ветвиcaseне оканчиваются операторомbreak.Благодаря тому что операторgotoпрепятствует последовательному переходу от одной ветвиcaseк другой, упоминавшееся ранее правило недопущения "провалов" не нарушается, а следовательно, необходимость в применении оператораbreakв данном случае отпадает. Но как пояснялось выше, операторgotoнельзя использовать как внешнее средство для безусловного перехода к операторуswitch.Так, если удалить символы комментария в начале следующей строки: // goto case 1; // Ошибка! Безусловный переход к оператору switch недопустим. приведенная выше программа не будет скомпилирована. Откровенно говоря, применение оператораgotoв оператореswitch,в общем, не рекомендуется как стиль программирования, хотя в ряде особых случаев это может принести^определенную пользу. Ниже приведен один из полезных примеров применения оператораgotoдля выхода из глубоко вложенной части программы. // Продемонстрировать практическое применение оператора goto. using System; class Use_goto { static void Main() { int i=0, j=0, k=0; for(i=0; i < 10; i++) { for(j=0; j < 10; j++ ) { for(k=0; k < 10; k++) { Console.WriteLine ("i, j, k: " + i + " " + j + " " + k) ; if(k == 3) goto stop; } } } stop: Console.WriteLine("Остановлено! i, j, k: " + i + ", " + j + " " + k) ; } } Выполнение этой программы дает следующий результат. i, j, k: ООО Остановлено! i, j, k: 0, 0 3 Если бы не оператор goto, то в приведенной выше программе пришлось бы прибегнуть к трем операторам if и break, чтобы выйти из глубоко вложенной части этой программы. В данном случае оператор goto действительно упрощает код. И хотя приведенный выше пример служит лишь для демонстрации применения оператора goto, вполне возможны ситуации, в которых этот оператор может на самом деле оказаться полезным. И последнее замечание: как следует из приведенного выше примера, из кодового блока можно выйти непосредственно, но войти в него так же непосредственно нельзя. ГЛАВА 6 Введение в классы, объекты и методы Эта глава служит введением в классы. Класс составляет основу языка С#, поскольку он определяет характер объекта. Кроме того, класс служит основанием для объектно-ориентированного программирования (ООП). В пределах класса определяются данные и код. А поскольку классы и объекты относятся к основополагающим элементам С#, то для их рассмотрения требуется не одна глава книги. В данной главе рассмотрение классов и объектов начинается с их главных особенностей. Основные положения о классах
Классы использовались в примерах программ с самого начала этой книги. Разумеется, это были лишь самые простые классы, что не позволяло выгодно воспользоваться большинством их возможностей. На самом же деле классы намного более эффективны, чем это следует из приведенных ранее примеров их ограниченного применения. Начнем рассмотрение классов с основных положений. Класспредставляет собой шаблон, по которому определяется форма объекта. В нем указываются данные и код, который будет оперировать этими данными. В C# используется спецификация класса для построенияобъектов, которые являютсяэкземплярамикласса. Следовательно, класс, по существу, представляет собой ряд схематических описаний способа построения объекта. При этом очень важно подчеркнуть, что класс является логической абстракцией. Физическое представление класса появится в оперативной памяти лишь после того, как будет создан объект этого ( класса. Общая форма определения класса
При определении класса объявляются данные, которые он содержит, а также код, оперирующий этими данными. Если самые простые классы могут содержать только код или только данные, то большинство настоящих классов содержит и то и другое. Вообще говоря, данные содержатся вчленах данных, определяемых классом, а код — вфункциях-членах.Следует сразу же подчеркнуть, что в C# предусмотрено несколько разновидностей членов данных и функций-членов. Например, к членам данных, называемым такжеполями,относятся переменные экземпляра и статические переменные, а к функциям-членам — методы, конструкторы, деструкторы, индексаторы, события, операторы и свойства. Ограничимся пока что рассмотрением самых основных компонентов класса: переменных экземпляра и методов. А далее в этой главе будут представлены конструкторы и деструкторы. Об остальных разновидностях членов класса речь пойдет в последующих главах. Класс создается с помощью ключевого словаclass.Ниже приведена общая форма определения простого класса, содержащая только переменные экземпляра и методы. classимя_класса { // Объявление переменных экземпляра. доступ тип переменная1; доступ тип переменная2; //... доступ тип переменнаяЫ; // Объявление методов. доступ возращаемый_тип метод1 (параметры) { 11тело метода } доступ возращаемый_тип метод2 (параметры){ // тело метода } //. . .
доступ возращаемый_тип методы(параметры){ // тело метода } } Обратите внимание на то, что перед каждым объявлением переменной и метода указываетсядоступ.Это спецификатор доступа, напримерpublic,определяющий порядок доступа к данному члену класса. Как упоминалось в главе 2, члены класса могут быть как закрытыми(private)в пределах класса, так открытыми(public),т.е. более доступными. Спецификатор доступа определяеттипразрешенного доступа. Указывать спецификатор доступа не обязательно, но если он отсутствует, то объявляемый член считается закрытым в пределах класса. Члены с закрытым доступом могут использоваться только другими членами их класса. В примерах программ, приведенных в этой главе, все члены, за исключением методаMain (), обозначаются как открытые(public).Это означает, что их можно использовать во всех остальных фрагментах кода — даже в тех, что определены за пределами класса. Мы еще вернемся к обсуждению спецификаторов доступа в главе 8. ПРИМЕЧАНИЕ
Помимо спецификатора доступа, в объявлении члена класса могут также присутствовать один или несколько модификаторов. О модификаторах речь пойдет далее в этой главе.
Несмотря на отсутствие соответствующего правила в синтаксисе С#, правильно сконструированный класс должен определять одну и только одну логическую сущность. Например, класс, в котором хранятся Ф.И.О. и номера телефонов, обычно не содержит сведения о фондовом рынке, среднем уровне осадков, циклах солнечных пятен или другую информацию, не связанную с перечисляемыми фамилиями. Таким образом, в правильно сконструированном классе должна быть сгруппирована логически связанная информация. Если же в один и тот же класс помещается логически несвязанная информация, то структурированность кода быстро нарушается. Классы, использовавшиеся в приведенных ранее примерах программ, содержали только один метод:Main (). Но в представленной выше общей форме определения класса методMain() не указывается. Этот метод требуется указывать в классе лишь в том случае, если программа начинается с данного класса. Определение класса
Для тогочтобы продемонстрировать классы на конкретных примерах, разработаем постепенно класс, инкапсулирующий информацию о зданиях, в том числе о домах, складских помещениях, учреждениях и т.д. В этом классе (назовем егоBuilding)будут храниться три элемента информации о зданиях: количество этажей, общая площадь и количество жильцов. Ниже приведен первый вариант классаBuilding.В нем определены три переменные экземпляра:Floors, AreaиOccupants.Как видите, в классеBuildingвообще отсутствуют методы. Это означает, что в настоящий момент этот класс состоит только из данных. (Впоследствии в него будут также введены методы.) class Building { public int Floors; // количество этажей public int Area; // общая площадь здания public int Occupants; // количество жильцов } Переменные экземпляра, определенные в классеBuilding,демонстрируют общий порядок объявления переменных экземпляра. Ниже приведена общая форма для объявления переменных экземпляра: доступ тип имя_переменной; гдедоступобозначает вид доступа;тип— конкретный тип переменной, аимя_пере-менной— имя, присваиваемое переменной. Следовательно, за исключением спецификатора доступа, переменная экземпляра объявляется таким же образом, как и локальная переменная. Все переменные объявлены в классеBuildingс предваряющим их модификатором доступаpublic.Как пояснялось выше, благодаря этому они становятся доступными за пределами классаBuilding. Определениеclassобозначает создание нового типа данных. В данном случае новый тип данных называетсяBuilding.С помощью этого имени могут быть объявлены объекты типаBuilding.Не следует, однако, забывать, что объявлениеclassлишь описывает тип, но не создает конкретный объект. Следовательно, в приведенном выше фрагменте кода объекты типаBuildingне создаются. Для того чтобы создать конкретный объект типаBuilding,придется воспользоваться следующим оператором. Building house = new Building(); // создать объект типа Building После выполнения этого оператора объектhouseстанет экземпляром классаBuilding,т.е. обретет "физическую" реальность. Не обращайте пока что внимание на отдельные составляющие данного оператора. Всякий раз, когда получается экземпляр класса, создается также объект, содержащий собственную копию каждой переменной экземпляра, определенной в данном классе. Таким образом, каждый объект типаBuildingбудет содержать свои копии переменных экземпляраFloors, AreaиOccupants.Для доступа к этим переменным служит оператор доступа к члену класса, который принято называтьоператором-точкой.Оператор-точка связывает имя объекта с именем члена класса. Ниже приведена общая форма оператора-точки. объект.член В этой формеобъектуказывается слева, ачлен —справа. Например, присваивание значения 2 переменнойFloorsобъектаhouseосуществляется с помощью следующего оператора. house.Floors = 2; В целом, оператор-точка служит для доступа к переменным экземпляра и методам. Ниже приведен полноценный пример программы, в которой используется класс Building. // Программа, в которой используется класс Building. using System; class Building { public int Floors; // количество этажей public int Area; // общая площадь здания public int Occupants; // количество жильцов } // В этом классе объявляется объект типа Building, class BuildingDemo { static void Main() { Building house = new Building(); // создать объект типа Building int areaPP; // площадь на одного человека // Присвоить значения полям в объекте house, house.Occupants = 4; house.Area = 2500; house.Floors = 2; // Вычислить площадь на одного человека. areaPP = house.Area / house.Occupants; Console.WriteLine("Дом имеет:\п " + house.Floors + " этажа\п " + house.Occupants + " жильца\п " + house.Area + " кв. футов общей площади, из них\п " + агеаРР + " приходится на одного человека"); } } Эта программа состоит из двух классов:BuildingиBuildingDemo.В классеBuildingDemoсначала создается экземплярhouseклассаBuildingс помощью методаMain(), а затем в коде методаMain() осуществляется доступ к переменным экземпляраhouseдля присваивания им значений и последующего использования этих значений. Следует особо подчеркнуть, чтоBuildingиBuildingDemo— это два совершенно отдельных класса. Единственная взаимосвязь между ними состоит в том, что в одном из них создается экземпляр другого. Но, несмотря на то, что это раздельные классы, у кода из классаBuildingDemoимеется доступ к членам классаBuilding,поскольку они объявлены как открытые(public).Если бы при их объявлении не был указан спецификатор доступаpublic,то доступ к ним ограничивался бы пределамиBuilding,а следовательно, их нельзя было бы использовать в классеBuildingDemo. Допустим, что исходный текст приведенной выше программы сохранен в файлеUseBuilding.cs.В результате ее компиляции создается файлUseBuilding.exe.При этом оба класса,BuildingиBuildingDemo,автоматически включаются в состав исполняемого файла. При выполнении данной программы выводится следующий результат. Дом имеет: 2 этажа 4 жильца 2500 кв. футов общей площади, из них 625 приходится на одного человека Но классамBuildingиBuildingDemoсовсем не обязательно находиться в одном и том же исходном файле. Каждый из них можно поместить в отдельный файл, напримерBuilding. csиBuildingDemo. cs,а компилятору C# достаточно сообщить, что оба файла должны быть скомпилированы вместе. Так, если разделить рассматриваемую здесь программу на два таких файла, для ее компилирования можно воспользоваться следующей командной строкой. csc Building.cs BuildingDemo.es Если вы пользуетесь интегрированной средой разработки Visual Studio, то вам нужно ввести оба упомянутых выше файла в свой проект и затем скомпоновать их. Прежде чем двигаться дальше, рассмотрим следующий основополагающий принцип: у каждого объекта имеются свои копии переменных экземпляра, определенных в его классе. Следовательно, содержимое переменных в одном объекте может отличаться от их содержимого в другом объекте. Между обоими объектами не существует никакой связи, за исключением того факта, что они являются объектами одного и того же типа. Так, если имеются два объекта типаBuilding,то у каждого из них своя копия переменныхFloors, AreaиOccupants,а их содержимое в обоих объектах может отличаться. Этот факт демонстрируется в следующей программе. // В этой программе создаются два объекта типа Building. using System; class Building { public int Floors; // количество этажей public int Area; // общая площадь здания public int Occupants; // количество жильцов } // В этом классе объявляются два объекта типа Building, class BuildingDemo { static void Main() { Building house = new Building(); Building office = new BuildingO; int areaPP; // площадь на одного человека // Присвоить значения полям в объекте house, house.Occupants = 4; house.Area = 2500; house.Floors = 2; // Присвоить значения полям в объекте office, office.Occupants = 25; office.Area = 4200; office.Floors = 3; // Вычислить площадь на одного человека в жилом доме. areaPP = house.Area / house.Occupants; Console.WriteLine("Дом имеет:\n " + house.Floors + " этажа\п " + house.Occupants + " жильца\п " + house.Area + " кв. футов общей площади, из них\п " + areaPP + " приходится на одного человека"); // Вычислить площадь на одного человека в учреждении. areaPP = office.Area / office.Occupants; Console.WriteLine("Учреждение имеет:\n " + office.Floors + " этажа\п " + office.Occupants + " работников\п " + office.Area + " кв. футов общей площади, из них\п " + areaPP + " приходится на одного человека"); } } Ниже приведен результат выполнения этой программы. Дом имеет: 2 этажа 4 жильца 2500 кв. футов общей площади, из них 625 приходится на одного человека Учреждение имеет: 3 этажа 25 работников 4200 кв. фу^ов общей площади, из них 168 приходится на одного человека Как видите, данные из объектаhouseполностью отделены от данных, содержащихся в объектеoffice.Эта ситуация наглядно показана на рис. 6.1. 
Рис. 6.1. Переменные экземпляра одного объекта полностью отделены от переменных экземпляра другого объекта
Создание объектов
В предыдущих примерах программ для объявления объекта типаBuildingиспользовалась следующая строка кода. Building house = new Building(); Эта строка объявления выполняет три функции. Во-первых, объявляется переменнаяhouse,относящаяся к типу классаBuilding.Сама эта переменная не является объектом, а лишь переменной, которая можетссылатьсяна объект. Во-вторых, создается конкретная, физическая, копия объекта. Это делается с помощью оператораnew.И наконец, переменнойhouseприсваивается ссылка на данный объект. Таким образом, после выполнения анализируемой строки объявленная переменнаяhouseссылается на объект типаBuilding. Операторnewдинамически (т.е. во время выполнения) распределяет память для объекта и возвращает ссылку на него, которая затем сохраняется в переменной. Следовательно, в C# для объектов всех классов должна быть динамически распределена память. Как и следовало ожидать, объявление переменнойhouseможно отделить от создания объекта, на который она ссылается, следующим образом. Building house; // объявить ссылку на объект house = new Building(); // распределить память для объекта типа Building В первой строке объявляется переменнаяhouseв виде ссылки на объект типаBuilding.Следовательно,house— это переменная, которая может ссылаться на объект, хотя сама она не является объектом. А во второй строке создается новый объект типаBuilding,и ссылка на него присваивается переменнойhouse.В итоге переменнаяhouseоказывается связанной с данным объектом. То обстоятельство, что объекты классов доступны по ссылке, объясняет, почему классы называютсяссылочными типами.Главное отличие типов значений от ссылочных типов заключается в том, что именно содержит переменная каждого из этих типов. Так, переменная типа значения содержит конкретное значение. Например, во фрагменте кода int х; х = 10; переменная х содержит значение 10, поскольку она относится к типу int, который является типом значения. Но в строке Building house = new Building(); переменнаяhouseсодержит не сам объект, а лишь ссылку на него. Переменные ссылочного типа и присваивание
В операции присваивания переменные ссылочного типа действуют иначе, чем переменные типа значения, например типаint.Когда одна переменная типа значения присваивается другой, ситуация оказывается довольно простой. Переменная, находящаяся в левой части оператора присваивания, получает копию значения переменной, находящейся в правой части этого оператора. Когда же одна переменная ссылки на объект присваивается другой, то ситуация несколько усложняется, поскольку такое присваивание приводит к тому, что переменная, находящаяся в левой части оператора присваивания, ссылается на тот же самый объект, на который ссылается переменная, находящаяся в правой части этого оператора. Сам же объект не копируется. В силу этого отличия присваивание переменных ссылочного типа может привести к несколько неожиданным результатам. В качестве примера рассмотрим следующий фрагмент кода. Building housel = new Building(); Building house2 = housel; На первый взгляд, переменныеhouselиhouse2ссылаются на совершенно разные объекты, но на самом деле это не так. Переменныеhouselиhouse2, напротив, ссылаются на один и тот же объект. Когда переменнаяhouselприсваивается переменойhouse2, то в конечном итоге переменнаяhouse2просто ссылается на тот же самый объект, что и переменнаяhousel.Следовательно, этим объектом можно оперировать с помощью переменнойhouselилиhouse2. Например, после очередного присваивания housel.Area = 2600;оба методаWriteLine() Console.WriteLine(housel.Area); Console.WriteLine(house2.Area); выводят одно и то же значение: 2600. Несмотря на то что обе переменные,houselиhouse2, ссылаются на один и тот же объект, они никак иначе не связаны друг с другом. Например, в результате следующей последовательности операций присваивания просто изменяется объект, на который ссылается переменнаяhouse2. Building housel = new Building(); Building house2 = housel; Building house3 = new Building(); house2 = house3; // теперь обе переменные, house2 и house3, // ссылаются на один и тот же объект. После выполнения этой последовательности операций присваивания переменнаяhouse2ссылается на тот же самый объект, что и переменнаяhouse3.А ссылка на объект в переменнойhouselне меняется. Методы
Как пояснялось выше, переменные экземпляра и методы являются двумя основными составляющими классов. До сих пор классBuilding,рассматриваемый здесь в качестве примера, содержал только данные, но не методы. Хотя классы, содержащие только данные, вполне допустимы, у большинства классов должны быть также методы.Методыпредставляют собой подпрограммы, которые манипулируют данными, определенными в классе, а во многих случаях они предоставляют доступ к этим данным. Как правило, другие части программы взаимодействуют с классом посредством его методов. Метод состоит из одного или нескольких операторов. В грамотно написанном коде C# каждый метод выполняет только одну функцию. У каждого метода имеется свое имя, по которому он вызывается. В общем, методу в качестве имени можно присвоить любой действительный идентификатор. Следует, однако, иметь в виду, что идентификаторMain() зарезервирован для метода, с которого начинается выполнение программы. Кроме того, в качестве имен методов нельзя использовать ключевые слова С#. В этой книге методы именуются в соответствии с условными обозначениями, принятыми в литературе по С#. В частности, после имени метода следуют круглые скобки. Так, если методу присвоено имяGet Val,то в тексте книги он упоминается в следующем виде:Get Val (). Такая форма записи помогает отличать имена методов от имен переменных при чтении книги. Ниже приведена общая форма определения метода: доступ возращаемый_тип имя(список_параметров){ // тело метода } гдедоступ —это модификатор доступа, определяющий те части программы, из которых может вызываться метод. Как пояснялось выше, указывать модификатор доступа необязательно. Но если он отсутствует, то метод оказывается закрытым(private)в пределах того класса, в котором он объявляется. Мы будем пока что объявлять методы открытыми(public),чтобы вызывать их из любой другой части кода в программе. Затемвозращаемый_типобозначает тип данных, возвращаемых методом. Этот тип должен быть действительным, в том числе и типом создаваемого класса. Если метод не возвращает значение, то в качестве возвращаемого для него следует указать типvoid.Далееимяобозначает конкретное имя, присваиваемое методу. В качестве имени метода может служить любой допустимый идентификатор, не приводящий к конфликтам в текущей области объявлений. И наконец,список_параметров —это последовательность пар, состоящих из типа и идентификатора и разделенных запятыми. Параметры представляют собой переменные, получающие значениеаргументов,передаваемых методу при его вызове. Если у метода отсутствуют параметры, то список параметров оказывается пустым.
Добавление метода в класс Building
Как пояснялось выше, методы класса, как правило, манипулируют данными класса и предоставляют доступ к ним. С учетом этого напомним, что в приведенных выше примерах в методеMain() вычислялась площадь на одного человека путем деления общей площади здания на количество жильцов. И хотя такой способ формально считается правильным, на самом деле он оказывается далеко не самым лучшим для организации подобного вычисления. Площадь на одного человека лучше всего вычислять в самом классеBuilding,просто потому, что так легче понять сам характер вычисления. Ведь площадь на одного человека зависит от значений в поляхAreaиOccupants,инкапсулированных в классеBuilding.Следовательно, данное вычисление может быть вполне произведено в самом классеBuilding.Кроме того, вводя вычисление площади на одного человека в классBuilding,мы тем самым избавляем все программы, пользующиеся классомBuilding,от необходимости выполнять это вычисление самостоятельно. Благодаря этому исключается ненужное дублирование кода. И наконец, добавление в классBuildingметода, вычисляющего площадь на одного человека, способствует улучшению его объектно-ориентированной структуры, поскольку величины, непосредственно связанные со зданием, инкапсулируются в классеBuilding.
Для того чтобы добавить метод в классBuilding,достаточно указать его в области объявлений в данном классе. В качестве примера ниже приведен переработанный вариант классаBuilding,содержащий методAreaPerPerson (), который выводит площадь, рассчитанную на одного человека в конкретном здании.
// Добавить метод в класс Building.
using System;
class Building {
public int Floors; // количество этажей
public int Area; // общая площадь здания
public int Occupants; // количество жильцов
// Вывести площадь на одного человека,
public void AreaPerPerson() {
Console.WriteLine(" " + Area / Occupants +
" приходится на одного человека");
}
}
// Использовать метод AreaPerPerson(). class BuildingDemo { static void Main() {
Building house = new Building();
Building office = new Building();
// Присвоить значения полям в объекте house, house.Occupants = 4; house.Area = 2500; house.Floors = 2;
// Присвоить значения полям в объекте office, office.Occupants = 25;
office.Area = 4200; office.Floors = 3;
Console.WriteLine("Дом имеет:\n " +
house.Floors + " этажа\п " + house.Occupants + " жильца\п " + house.Area +
^ "кв. футов общей площади, из них");
house.AreaPerPerson() ;
Console.WriteLine ();
Console.WriteLine("Учреждение имеет:\n " +
office.Floors + " этажа\п " +
office.Occupants + " работников\п " +
office.Area +
" кв. футов общей площади, из них"); office.AreaPerPerson() ;
}
}
Эта программа дает такой же результат, как и прежде.
Дом имеет:
2 этажа
4 жильца
2500 кв. футов общей площади, из них 625 приходится на одного человека
Учреждение имеет:
3 этажа
25 работников
4200 кв. футов общей площади, из них 168 приходится на одного человека
Рассмотрим основные элементы этой программы, начиная с методаAreaPerPerson (). Первая его строка выглядит следующим образом.
public void AreaPerPerson () {
В этой строке объявляется метод, именуемыйAreaPerPersonи не имеющий параметров. Для него указывается типpublic,а это означает, что его можно вызывать из любой другой части программы. МетодAreaPerPerson() возвращает пустое значение типаvoid,т.е. он практически ничего не возвращает вызывающей части программы. Анализируемая строка завершается фигурной скобкой, открывающей тело данного метода. Тело методаAreaPerPerson() состоит всего лишь из одного оператора.
Console.WriteLine(" " + Area / Occupants +
" приходится на одного человека");
Этот оператор осуществляет вывод величины площади на одного человека, которая получается путем деления общей площади здания (переменнойArea)на количество жильцов (переменнуюOccupants).А поскольку у каждого объекта типаBuildingимеется своя копия переменныхAreaиOccupants,то при вызове методаAreaPerPerson() в вычислении используются копии этих переменных, принадлежащие вызывающему объекту.
МетодAreaPerPerson() завершается закрывающейся фигурной скобкой. Когда встречается эта скобка, управление передается обратно вызывающей части программы.
Далее проанализируем внимательно следующую строку кода из методаMain ().
house.AreaPerPerson() ;
В этой строке вызывается методAreaPerPerson() для объектаhouse.Это означает, что методAreaPerPerson() вызывается относительно объекта, на который ссылается переменнаяhouse,и для этой цели служит оператор-точка. Когда методAreaPerPerson() вызывается, ему передается управление программой. А по его завершении управление передается обратно вызывающей части программы, выполнение которой возобновляется со строки кода, следующей после вызова данного метода.
В данном случае в результате вызоваhouse .AreaPerPerson() выводится площадь на одного человека в здании, определенном в объектеhouse.Аналогично, в результате вызоваoffice .AreaPerPerson() выводится площадь на одного человека в здании, определенном в объектеoffice.Таким образом, при каждом вызове методаAreaPerPerson() выводится площадь на одного человека для указанного объекта.
В методеAreaPerPerson() особого внимания заслуживает следующее обстоятельство: обращение к переменным экземпляраAreaиOccupantsосуществляется непосредственно, т.е. без помощи оператора-точки. Если в методе используется переменная экземпляра, определенная в его классе, то делается это непосредственно, без указания явной ссылки на объект и без помощи оператора-точки. Понять это нетрудно, если хорошенько подумать. Ведь метод всегда вызывается относительно некоторого объекта его класса. Как только вызов произойдет, объект становится известным. Поэтому объект не нужно указывать в методе еще раз. В данном случае это означает, что переменные экземпляраAreaиOccupantsв методеAreaPerPerson() неявно ссылаются на копии этих же переменных в том объекте, который вызывает методAreaPerPerson ().
ПРИМЕЧАНИЕ
~ Попутно следует заметить, что значение переменной Occupants в методе AreaPerPerson () не должно быть равно нулю (это касается всех примеров, приведенных в данной главе). Если бы значение переменной Occupants оказалось равным нулю, то произошла бы ошибка из-за деления на нуль. В главе 13, где рассматриваются исключительные ситуации, будет показано, каким образом в C# отслеживаются и обрабатываются ошибки, которые могут возникнуть во время выполнения программы.
Возврат из метода
В целом, возврат из метода может произойти при двух условиях. Во-первых, когда встречается фигурная скобка, закрывающая тело метода, как показывает пример методаAreaPerPerson() из приведенной выше программы. И во-вторых, когда выполняется операторreturn.Имеются две формы оператораreturn:одна — для методов типаvoid,т.е. тех методов, которые не возвращают значения, а другая — для методов, возвращающих конкретные значения. Первая форма рассматривается в этом разделе, а в следующем разделе будет пояснено, каким образом значения возвращаются из методов.
Для немедленного завершения метода типаvoidдостаточно воспользоваться следующей формой оператораreturn, return;
Когда выполняется этот оператор, управление возвращается вызывающей части программы, а оставшийся в методе код пропускается. В качестве примера рассмотрим следующий метод.
public void MyMethO { int i;
for(i=0; i<10; i++) {
if(i == 5) return; // прервать на шаге 5 Console.WriteLine();
}
}
В данном примере выполняется лишь 5 полноценных шагов цикла for, поскольку при значении 5 переменной i происходит возврат из метода.
В методе допускается наличие нескольких операторовreturn,особенно если имеются два или более вариантов возврата из него. Например:
public void MyMethO {
П...
if(done) return;
П...
if (error) return;
}
В данном примере возврат из метода происходит в двух случаях: если метод завершает свою работу или происходит ошибка. Но пользоваться таким приемом программирования следует очень аккуратно. Ведь из-за слишком большого числа точек возврата из метода может нарушиться структура кода.
Итак, напомним еще раз: возврат из метода типа void может произойти при двух условиях: по достижении закрывающей фигурной скобки или при выполнении оператораreturn.
Возврат значения
Методы с возвратом типа void нередко применяются в программировании, тем не менее, большинство методов возвращает конкретное значение. В действительности способность возвращать значение является одним из самых полезных свойств метода. Возврат значения уже демонстрировался в главе 3 на примере методаMath. Sqrt (),использовавшегося для получения квадратного корня.
Возвращаемые значения используются в программировании с самыми разными целями. В одних случаях, как в примере методаMath.Sqrt (), возвращаемое значение содержит результат некоторого вычисления, в других — оно может просто указывать на успешное или неудачное завершение метода, а в третьих — содержать код состояния. Но независимо от преследуемой цели использование возвращаемых значений является неотъемлемой частью программирования на С#.
Для возврата значения из метода в вызывающую часть программы служит следующая форма оператораreturn:
returnзначение;
гдезначение —это конкретное возвращаемое значение.
Используя возвращаемое значение, можно усовершенствовать рассматривавшийся ранее методAreaPerPerson (). Вместо того чтобы выводить величину площади на одного человека, лучше возвратить ее из этого метода. Среди прочих преимуществ такого подхода следует особо отметить возможность использовать возвращаемое значение для выполнения других вычислений. Приведенный ниже пример представляет собой улучшенный вариант рассматривавшейся ранее программы с усовершенствованным методомAreaPerPerson (), возвращающим величину площади на одного человека вместо того, чтобы выводить ее.
// Возвратить значение из метода AreaPerPerson().
using System;
class Building {
public int Floors; // количество этажей
public int Are-a; // общая площадь здания
public int Occupants; // количество жильцов
// Возвратить величину площади на одного человека, public int AreaPerPerson() {
return Area / Occupants;
}
}
// Использовать значение, возвращаемое методом AreaPerPerson(). class BuildingDemo { static void Main() {
Building house = new Building();
Building office = new Building();
int areaPP; // площадь на одного человека
// Присвоить значения полям в объекте house, house.Occupants = 4; house.Area = 2500; house.Floors = 2;
// Присвоить значения полям в объекте office, office.Occupants = 25; office.Area = 4200; office.Floors= 3;
// Получить площадь на одного человека в жилом доме. areaPP = house.AreaPerPerson();
Console.WriteLine("Дом имеет:\n " +
house.Floors + " этажа\п " + house.Occupants + " жильца\п " + house.Area +
" кв. футов общей площади, из них\п " + areaPP + " приходится на одного человека");
Console.WriteLine ();
// Получить площадь на одного человека в учреждении. areaPP = office.AreaPerPerson();
Console'. WriteLine ("Учреждение имеет :\n " +
office.Floors + " этажа\п " +
office.Occupants + " работников\п " +
office.Area +
" кв. футов общей площади, из них\п " + areaPP + " приходится на одного человека");
}
}
Эта программа дает такой же результат, как и прежде.
'В данной программе обратите внимание на следующее: когда методAreaPerPerson() вызывается, он указывается в правой части оператора присваивания. А в левой части этого оператора указывается переменная, которой передается значение, возвращаемое методомAreaPerPerson (). Следовательно, после выполнения оператора
areaPP = house.AreaPerPerson ();
в переменнойareaPPсохраняется величина площади на одного человека в жилом доме (объектhouse).
Обратите также внимание на то, что теперь методAreaPerPerson() имеет возвращаемый типint.Это означает, что он будет возвращать целое значение вызывающей части программы. Тип, возвращаемый методом, имеет очень большое значение, поскольку тип данных, возвращаемых методом, должен быть совместим с возвращаемым типом, указанным в методе. Так, если метод должен возвращать данные типаdouble,то в нем следует непременно указать возвращаемый типdouble.
Несмотря на то что приведенная выше программа верна, она, тем не менее, написана не совсем эффективно. В частности, в ней можно вполне обойтись без переменнойareaPP,указав вызов методаAreaPerPerson() непосредственно в операторе, содержащем вызов методаWriteLine (), как показано ниже.
Console.WriteLine("Дом имеет:\п " +
house.Floors + " этажа\п " + house.Occupants + " жильца\п " + house.Area +
" кв. футов общей площади, из них\п " + house.AreaPerPerson() +
" приходится на одного человека");
В данном случае при выполнении оператора, содержащего вызов методаWriteLine(), автоматически вызывается методhouse.AreaPerPerson(), а возвращаемое им значение передается методуWriteLine().Кроме того, вызов методаAreaPerPerson() можно использовать всякий раз, когда требуется получить величину площади на одного человека для конкретного объекта типаBuilding.Например, в приведенном ниже операторе сравниваются величины площади на одного человека для двух зданий.
if(Ы.AreaPerPerson() > Ь2.AreaPerPerson())
Console.WriteLine("В здании Ы больше места для каждого человека");
Использование параметров
При вызове метода ему можно передать одно или несколько значений. Значение, передаваемое методу, называетсяаргументом.А переменная, получающая аргумент, называетсяформальным параметром, или простопараметром.Параметры объявляются в скобках после имени метода. Синтаксис объявления параметров такой же, как и у переменных. А областью действия параметров является тело метода. За исключением особых случаев передачи аргументов методу, параметры действуют так же, как и любые другие переменные.
Ниже приведен пример программы, в котором демонстрируется применение параметра. В классеChkNumиспользуется методIs Prime (), который возвращает значениеtrue,если ему передается значение, являющееся простым числом. В противном случае он возвращает значениеfalse.Следовательно, возвращаемым для методаIsPrimeOявляется типbool.
// Простой пример применения параметра.
using System;
class ChkNum {
// Возвратить значение true, если значение // параметра х окажется простым числом, public bool IsPrime(int х) { if (х <= 1) return false;.
for (int i=2; i <= x/i; i++) if ( (x %i) == 0) return false;
return true;
}
}
class ParmDemo {
static void Main() {
ChkNum ob = new ChkNum();
for (int i=2; i < 10; i++)
if(ob.IsPrime(i)) Console.WriteLine(i + " простое число."); else Console.WriteLine(i + " непростое число.");
}
}
Вот какой результат дает выполнение этой программы.
2 простое число.
3 простое число.
4 непростое число.
5 простое число.
6 непростое число.
7 простое число.
8 непростое число.
9 непростое число.
В данной программе методIsPrime() вызывается восемь раз, и каждый раз ему передается другое значение. Проанализируем этот процесс более подробно. Прежде всего обратите внимание на то, как вызывается методIs Prime (). Его аргумент указывается в скобках. Когда методIs Prime ()вызывается в первый раз, ему передается значение 2. Следовательно, когда методIs Prime ()начинает выполняться, его параметр х принимает значение 2. При втором вызове этого метода его параметр х принимает значение 3, при третьем вызове — значение 4 и т.д. Таким образом, значение, передаваемое методуIs Prime() в качестве аргумента при его вызове, представляет собой значение, которое принимает его параметр х.
У метода может быть не только один, но и несколько параметров. Каждый его параметр объявляется, отделяясь от другого запятой. В качестве примера ниже приведен классChkNum,который расширен дополнительным методомLeastComFactor (),возвращающим наименьший общий множитель двух его аргументов. Иными словами, этот метод возвращает наименьшее число, на которое оба его аргумента делятся нацело.
// Добавить метод, принимающий два аргумента.
using System;
class ChkNum {
// Возвратить значение true, если значение // параметра х окажется простым числом, public bool IsPrime(int х) { if(х <= 1) return false;
for (int i=2; i <= x/i; i++) if((x %i) == 0) return false;
return true;
}
// Возвратить наименьший общий множитель, public int LeastComFactor(int a, int b) { int max;
if(IsPrime(a) || IsPrime(b)) return 1;
max = a < b ? a : b;
for (int i=2; i <= max/2; i++)
if(((a%i) == 0) && ( (b%i) == 0)) return i; return 1;
}
}
class ParmDemo {
static void Main() {
ChkNum ob = new ChkNum(); int a, b;
for (int i=2; i < 10; i++)
if(ob.IsPrime(i)) Console.WriteLine(i + ” простое число."); else Console.WriteLine(i + " непростое число.");
а = 7; b = 8;
Console.WriteLine("Наименьший общий множитель чисел " + а + " и " + b + " равен " + ob.LeastComFactor(а, Ь));
а = 100;
Ь = 8;
Console.WriteLine("Наименьший общий множитель чисел " + а + " и " + b + " равен " + ob.LeastComFactor(а, Ь));
а = 100;
Ь = 75;
Console.WriteLine("Наименьший общий множитель чисел " + а + " и " + b + " равен " + ob.LeastComFactor(а, Ь));
}
}
Обратите внимание на следующее: когда вызывается методLeastComFactor(), его аргументы также разделяются запятыми. Ниже приведен результат выполнения данной программы.
2 простое число.
3 простое число.
4 непростое число.
5 простое число.
6 непростое число.
7 простое число.
8 непростое число.
9 непростое число.
Наименьший
общий
множитель
чисел
7 и
8
равен 1
Наименьший
общий
множитель
чисел
100
и
8 равен 2
Наименьший
общий
множитель
чисел
100
и
75 равен 5
Если в методе используется несколько параметров, то для каждого из них указывается свой тип, отличающийся от других. Например, приведенный ниже код является вполне допустимым.
int MyMeth(int a, double b, float с) {
П...
Добавление параметризированного метода в класс Building
С помощью параметризированного метода можно дополнить классBuildingновым средством, позволяющим вычислять максимальное количество жильцов в здании, исходя из определенной величины минимальной площади на одного человека. Этим новым средством является приведенный ниже методMaxOccupant ().
// Возвратить максимальное количество человек, занимающих здание,
// исходя из заданной минимальной площади на одного человека, public int MaxOccupant(int minArea) { return Area / minArea;
Когда вызывается методMaxOccupant (), его параметрminAreaпринимает величину необходимой минимальной площади на одного человека. На эту величину делится общая площадь здания при выполнении данного метода, после чего он возвращает результат.
Ниже приведен весь классBuilding,включая и методMaxOccupant ().
/*
Добавить параметризированный метод, вычисляющий максимальное количество человек, которые могут занимать здание, исходя из заданной минимальной площади на одного человека.
*/
using System;
class Building {
public int Floors; // количество этажей
public int Area; // общая площадь здания
public int Occupants; // количество жильцов
// Возвратить площадь на одного человека,
public int AreaPerPerson() {
return Area / Occupants;
}
// Возвратить максимальное количество человек, занимающих здание,
// исходя из заданной минимальной площади на одного человека, public int MaxOccupant(int minArea) { return Area / minArea;
}
}
// Использовать метод MaxOccupant(). class BuildingDemo { static void Main() {
Building house = new Building();
Building office = new Building();
// Присвоить значения полям в объекте house, house.Occupants = 4; house.Area = 2500; house.Floors = 2;
// Присвоить значения полям в объекте office, office.Occupants = 25; office.Area = 4200; office.Floors = 3;
Console.WriteLine("Максимальное количество человек в доме, \п" + "если на каждого должно приходиться " +
300 + " кв. футов: " + house.MaxOccupant(300));
"в учреждении, \п" +
"если на каждого должно приходиться " +
300 + " кв. футов: " + office.MaxOccupant(300));
}
}
Выполнение этой программы дает следующий результат.
Максимальное количество человек в доме,
если на каждого должно приходиться 300 кв. футов: 8
Максимальное количество человек в учреждении,
если на каждого должно приходиться 300 кв. футов: 14
Исключение недоступного кода
При создании методов следует исключить ситуацию, при которой часть кода не может быть выполнена ни при каких обстоятельствах. Такой код называетсянедоступными считается в C# неправильным. Если создать метод, содержащий недоступный код, компилятор выдаст предупреждающее сообщение соответствующего содержания. Рассмотрим следующий пример кода.
public void MyMeth() { char a, b;
// . . .
if(a==b) {
Console.WriteLine("равно") ; return;
} else {
Console.WriteLine("не равно") ; return;
}
Console.WriteLine ("это недоступный код");
}
В данном примере возврат из методаMyMeth() всегда происходит до выполнения последнего оператора, содержащего вызов методаWriteLine().Если попытаться скомпилировать этот код, то будет выдано предупреждающее сообщение. Вообще‘говоря, недоступный код считается ошибкой программирования, и поэтому предупреждения о таком коде следует воспринимать всерьез.
Конструкторы
В приведенных выше примерах программ переменные экземпляра каждого объекта типаBuildingприходилось инициализировать вручную, используя, в частности, следующую последовательность операторов.
house.Occupants = 4; house.Area = 2500; house.Floors = 2;
Такой прием обычно не применяется в профессионально написанном коде С#. Кроме того, он чреват ошибками (вы можете просто забыть инициализировать одно из полей). Впрочем, существует лучший способ решить подобную задачу: воспользоваться конструктором^
Конструкторинициализирует объект при его создании. У конструктора такое же имя, как и у его класса, а с точки зрения синтаксиса он подобен методу. Но у конструкторов нет возвращаемого типа, указываемого явно. Ниже приведена общая форма конструктора.
доступ имя_класса{список_параметров){
// тело конструктора
}
^ Как правило, конструктор используется для задания первоначальных значений переменных экземпляра, определенных в классе, или же для выполнения любых других установочных процедур, которые требуются для создания полностью сформированного объекта. Кроме того,доступобычно представляет собой модификатор доступа типа public, поскольку конструкторы зачастую вызываются в классе. Асписок_па-раметровможет быть как пустым, так и состоящим из одного или более указываемых параметров.
У всех классов имеются конструкторы, независимо от того, определите вы их или нет, поскольку в C# автоматически предоставляется конструктор, используемый по умолчанию и инициализирующий все переменные экземпляра их значениями по умолчанию. Для большинства типов данных значением по умолчанию является нулевое, для типа bool — значение false, а для ссылочных типов — пустое значение. Но как только вы определите свой собственный конструктор, то конструктор по умолчанию больше не используется.
Ниже приведен простой пример применения конструктора.
// Простой конструктор.
using System;
class MyClass { public int x;
public MyClass() {
x = 10;
}
}
class ConsDemo {
static void Main() {
MyClass tl = new MyClass();
MyClass t2 = new MyClass();
Console.WriteLine(tl,x + " " + t2.x);
}
}
В данном примере конструктор класса MyClass имеет следующий вид.
public MyClassO {
X =10;
}
Обратите внимание на то, что этот конструктор обозначается как public. Дело в том, что он должен вызываться из кода, определенного за пределами его класса. В этом конструкторе переменной экземпляра класса MyClass присваивается значение 10. Он вызывается в операторе new при создании объекта. Например, в следующей строке:
MyClass tl = new MyClassO;
конструктор MyClass () вызывается для объекта tl, присваивая переменной его экземпляра tl. х значение 10. То же самое происходит и для объекта t2. После конструирования переменная t2 . х будет содержать то же самое значение 10. Таким образом, выполнение приведенного выше кода приведет к следующему результату.
10 10
Параметризированные конструкторы
В предыдущем примере использовался конструктор без параметров. В некоторых случаях этого оказывается достаточно, но зачастую конструктор, должен принимать один или несколько параметров. В конструктор параметры вводятся таким же образом, как и в метод. Для этого достаточно объявить их в скобках после имени конструктора. Ниже приведен пример применения параметризированного конструктора MyClass.
// Параметризированный конструктор.
using System;
class MyClass { public int x;
public MyClass(int i) { x = i;
}
}
class ParmConsDemo { static void Main() {
MyClass tl = new MyClass(10);
MyClass t2 = new MyClass(88);
Console.WriteLine(tl.x + " " + t2.x);
}
}
При выполнении этого кода получается следующий результат.
10 88
В данном варианте конструктора MyClass () определен параметр i, с помощью которого инициализируется переменная экземпляра х. Поэтому при выполнении следующей строки кода:
MyClass tl = new MyClass(10);
параметру i передается значение, которое затем присваивается переменной х.
Добавление конструктора в класс Building
КлассBuildingможно усовершенствовать, добавив в него конструктор, автоматически инициализирующий поляFloors, AreaиOccupantsпри создании объекта. Обратите особое внимание на то, как создаются объекты классаBuilding.
// Добавить конструктор в класс Building.
using System;
class Building {
public int Floors; // количество этажей
public int Area; // общая площадь здания
public int Occupants; // количество жильцов
// Параметризированный конструктор для класса Building, public Building(int f, int a, int o) {
Floors = f;
Area = a;
Occupants = o;
} \
// Возвратить площадь на одного человека, public int AreaPerPerson() {
return Area / Occupants;
}
// Возвратить максимальное количество человек, занимающих здание,
// исходя из заданной минимальной площади на одного человека. ^ public int MaxOccupant(int minArea) { return Area / minArea;
}
}
// Использовать параметризированный конструктор класса Building, class BuildingDemo { static void Main() {
Building house = new Building(2, 2500, 4);
Building office = new Building(3, 4200, 25);
Console.WriteLine("Максимальное количество человек в доме, \п" +
"если на каждого должно приходиться " +
300 ,+ " кв. футов: " + house.MaxOccupant(300));
Console.WriteLine("Максимальное количество человек " +
"в учреждении, \п" +
"если на каждого должно приходиться " +
300 + " кв. футов: " + office.MaxOccupant(300) );
}
}
Результат выполнения этой программы оказывается таким же, как и в предыдущей ее версии.
Оба объекта,houseиoffice,были инициализированы конструкторомBuilding ()при их создании в соответствии с параметрами, указанными в этом конструкторе. Например, в строке
Building house = new Building(2, 2500, 4);
конструкторуBuilding () передаются значения 2, 2500 и 4 при создании нового объекта. Следовательно, в копиях переменных экземпляраFloors, AreaиOccupantsобъектаhouseбудут храниться значения 2, 2500 и 4 соответственно.
Еще раз об операторе new
Теперь, когда вы ближе ознакомились с классами и их конструкторами, вернемся к оператору new, чтобы рассмотреть его более подробно. В отношении классов общая форма оператора new такова:
new имя_класса(список_аргументов)
гдеимя_классаобозначает имя класса, реализуемого в виде экземпляра его объекта. Аимя_классас последующими скобками обозначает конструктор этого класса. Если в классе не определен его собственный конструктор, то в операторе new будет использован конструктор, предоставляемый в C# по умолчанию. Следовательно, оператор new может быть использован для создания объекта, относящегося к классу любого типа.
Оперативная память не бесконечна, и поэтому вполне возможно, что оператору new не удастся распределить память для объекта из-за нехватки имеющейся оперативной памяти. В этом случае возникает исключительная ситуация во время выполнения (подробнее об обработке исключительных ситуаций речь пойдет в главе 13). В примерах программ, приведенных в этой книге, ситуация, связанная с исчерпанием оперативной памяти, не учитывается, но при написании реальных программ такую возможность, вероятно, придется принимать во внимание.
Применение оператора new вместе с типами значений
В связи с изложенным выше возникает резонный вопрос: почему оператор new нецелесообразно применять к переменным таких типов значений, как int или float? В C# переменная типа значения содержит свое собственное значение. Память для хранения этого значения выделяется автоматически во время прогона программы. Следовательно, распределять память явным образом с помощью оператора new нет никакой необходимости. С другой стороны, в переменной ссылочного типа хранится ссылка на объект, и поэтому память для хранения этого объекта должна распределяться динамически во время выполнения программы.
Благодаря тому что основные типы данных, например int или char, не преобразуются в ссылочные типы, существенно повышается производительность программы. Ведь при использовании ссылочного типа существует уровень косвенности, повышающий издержки на доступ к каждому объекту. Такой уровень косвенности исключается при использовании типа значения.
Но ради интереса следует все же отметить, что оператор new разрешается использовать вместе с типами значений, как показывает следующий пример.
int i = new int ();
При этомдлятипа int вызывается конструктор, инициализирующий по умолчанию переменную i нулевым значением. В качестве примера рассмотрим такую программу.
// Использовать оператор new вместе с типом значения.
using System;
class newValue {
static void Main() {
int i = new int(); // инициализировать переменную i нулевым значением
Console.WriteLine("Значение переменной i равно: " + i);
}
}
Выполнение этой программы дает следующий результат.
Значение переменной i равно: О
Как показывает результат выполнения данной программы, переменная i инициализируется нулевым значением. Напомним, что если не применить оператор new, то переменная i окажется неинициализированной. Это может привести к ошибке при попытке воспользоваться ею в операторе, содержащем вызов метода WriteLine (), если предварительно не задать ее значение явным образом.
В общем, обращение к оператору new для любого типа значения приводит к вызову конструктора, используемого по умолчанию для данного типа. Но в этом случае память динамически не распределяется. Откровенно говоря, в программировании обычно не принято пользоваться оператором new вместе с типами значений.
“Сборка мусора” и применение деструкторов
Как было показано выше, при использовании оператора new свободная память для создаваемых объектов динамически распределяется из доступной буферной области оперативной памяти. Разумеется, оперативная память не бесконечна, и поэтому свободно доступная память рано или поздно исчерпывается. Это может привести к неудачному выполнению оператора new из-за нехватки свободной памяти для создания требуемого объекта. Именно по этой причине одной из главных функций любой схемы динамического распределения памяти является освобождение свободной памяти от неиспользуемых объектов, чтобы сделать ее доступной для последующего перераспределения. Во многих языках программирования освобождение распределенной ранее памяти осуществляется вручную. Например, в C++ для этой цели служит оператор delete. Но в C# применяется другой, более надежный подход:"сборка мусора".
Система "сборки мусора" в C# освобождает память от лишних объектов автоматически, действуя незаметно и без всякого вмешательства со стороны программиста. "Сборка мусора" происходит следующим образом. Если ссылки на объект отсутствуют, то такой объект считается ненужным, и занимаемая им память в итоге освобождается и накапливается. Эта утилизированная память может быть затем распределена для других объектов.
"Сборка мусора" происходит лишь время от времени по ходу выполнения программы. Она не состоится только потому, что существует один или более объектов, которые больше не используются. Следовательно, нельзя заранее знать или предположить, когда именно произойдет "сборка мусора".
Деструкторы
В языке C# имеется возможность определить метод, который будет вызываться непосредственно перед окончательным уничтожением объекта системой "сборки мусора". Такой метод называетсядеструктороми может использоваться в ряде особых случаев, чтобы гарантировать четкое окончание срока действия объекта. Например, деструктор может быть использован для гарантированного освобождения системного ресурса, задействованного освобождаемым объектом. Следует, однако, сразу же подчеркнуть, что деструкторы — весьма специфические средства, применяемые только в редких, особых случаях. И, как правило, они не нужны. Но здесь они рассматриваются вкратце ради полноты представления о возможностях языка С#.
Ниже приведена общая форма деструктора:
~имя_ кла сса() {
// код деструктора
}
гдеимя_классаозначает имя конкретного класса. Следовательно, деструктор объявляется аналогично конструктору, за исключением того, что перед его именем указывается знак "тильда" (~). Обратите внимание на то, что у деструктора отсутствуют возвращаемый тип и передаваемые ему аргументы.
Для того чтобы добавить деструктор в класс, достаточно включить его в класс в качестве члена. Он вызывается всякий раз, когда предполагается утилизировать объект его класса. В деструкторе можно указать те действия, которые следует выполнить перед тем, как уничтожать объект.
Следует, однако, иметь в виду, что деструктор вызывается непосредственно перед "сборкой мусора". Он не вызывается, например, в тот момент, когда переменная, содержащая ссылку на объект, оказывается за пределами области действия этого объекта. (В этом отношении деструкторы в C# отличаются от деструкторов в C++, где они вызываются в тот момент, когда объект оказывается за пределами области своего действия.) Это означает, что заранее нельзя знать, когда именно следует вызывать деструктор. Кроме того, программа может завершиться до того, как произойдет "сборка мусора", а следовательно, деструктор может быть вообще не вызван.
Ниже приведен пример программы, демонстрирующий применение деструктора. В этой программе создается и уничтожается большое число объектов. В какой-то момент по ходу данного процесса активизируется "сборка мусора" и вызываются деструкторы для уничтожения ненужных объектов.
// Продемонстрировать применение деструктора.
using System;
class Destruct { public int x;
public Destruct(int i) {
х = i ;
}
// Вызывается при утилизации объекта.
-Destruct() -{
Console.WriteLine("Уничтожить " + х);
}
// Создает объект и тут же уничтожает его. public void Generator(int i) {
Destruct о = new Destruct (i);
}
}
class DestructDemo { static void Main() { int count;
Destruct ob = new Destruct(0);
/* А теперь создать большое число объектов.
В какой-то момент произойдет "сборка мусора".
Примечание: для того чтобы активизировать "сборку мусора", возможно, придется увеличить число создаваемых объектов. */
for(count=l; count < 100000; count++) ob.Generator(count);
Console.WriteLine("Готово!");
}
}
Эта программа работает следующим образом. Конструктор инициализирует переменную х известным значением. В данном примере переменная х служит в качестве идентификатора объекта. А деструктор выводит значение переменной х, когда объект утилизируется. Особый интерес вызывает методGenerator (), который создает и тут же уничтожает объект типаDestruct.Сначала в классеDestructDemoсоздается исходный объектobтипаDestruct,а затем осуществляется поочередное создание и уничтожение 100 тыс. объектов. В разные моменты этого процесса происходит "сборка мусора". Насколько часто она происходит — зависит от нескольких факторов, в том числе от первоначального объема свободной памяти, типа используемой операционной системы и т.д. Тем не менее в какой-то момент начинают появляться сообщения, формируемые деструктором. Если же они не появятся до окончания программы, т.е. до того момента, когда будет выдано сообщение "Готово!", попробуйте увеличить число создаваемых объектов, повысив предельное количество подсчитываемых шагов в циклеfor.
И еще одно важное замечание: методWriteLine() вызывается в деструкторе-Destruct() исключительно ради наглядности данного примера его использования. Как правило, деструктор должен воздействовать только на переменные экземпляра, определенные в его классе.
В силу того что порядок вызова деструкторов не определен точно, их не следует применять для выполнения действий, которые должны происходить в определенный момент выполнения программы. В то же время имеется возможность запрашивать "сборку мусора", как будет показано в части II этой книги при рассмотрении библиотеки классов С#. Тем не менее инициализация "сборки мусора" вручную в большинстве случаев не рекомендуется, поскольку это может привести к снижению эффективности программы. Кроме того, у системы "сборки мусора" имеются свои особенности — даже если запросить "сборку мусора" явным образом, все равно нельзя заранее знать, когда именно будет утилизирован конкретный объект.
Ключевое слово this
Прежде чем завершать этуглаву^необходимо представить ключевое слово this. Когда метод вызывается, ему автоматически передается ссылка на вызывающий объект, т.е. тот объект, для которого вызывается данный метод. Эта ссылка обозначается ключевым словомthis.Следовательно, ключевое словоthisобозначает именно тот объект, по ссылке на который действует вызываемый метод.Длятого чтобы стало яснее назначение ключевого словаthis,рассмотрим сначала пример программы, в которой создается классRect,инкапсулирующий ширину и высоту прямоугольника и включающий в себя методArea (), возвращающий площадь прямоугольника.
using System;
class Rect {
public int Width; public int Height;
public Rect(int w, int h) {
Width = w;
Height = h;
}
public int Area() {
return Width * Height;
}
}
class UseRect {
static void Main() {
Rect rl = new Rect(4, 5);
Rect r2 = new Rect(7, 9);
Console.WriteLine("Площадь прямоугольника rl: " + rl.AreaO);
Console.WriteLine("Площадь прямоугольника r2: " + r2.Area());
}
}
Как вам должно уже быть известно, другие члены класса могут быть доступны непосредственно без дополнительного уточнения имени объекта или класса. Поэтому оператор
return Width * Height;
в методеArea() означает, что копии переменныхWidthиHeight,связанные с вызывающим объектом, будут перемножены, а метод возвратит их произведение. Но тот же самый оператор можно написать следующим образом.
return this.Width * this.Height;
В этом операторе ключевое словоthisобозначает объект, для которого вызван методArea (). Следовательно, в выраженииthis .Widthделается ссылка на копию переменнойWidthданного объекта, а в выраженииthis.Height— ссылка на копию переменнойHeightэтого же объекта. Так, если бы методArea() был вызван для объекта х, то ключевое словоthisв приведенном выше операторе обозначало бы ссылку на объект х. Написание оператора без ключевого словаthisпредставляет собой не более чем сокращенную форму записи.
Ключевое словоthisможно также использовать в конструкторе. В этом случае оно обозначает объект, который конструируется. Например, следующие операторы в методеRect()
Width = w;
Height = h;
можно было бы написать таким образом.
this.Width = w; this.Height = h;
Разумеется, такой способ записи не дает в данном случае никаких преимуществ. Ради примера ниже приведен весь классRect,написанный с использованием ссылкиthis.
using System;
class Rect {
public int Width; public int Height;
public Rect(int w, int h) { this.Width = w; this.Height = h;
}
public int Area() {
return this.Width * this.Height;
}
}
class UseRect {
static void Main() {
Rect rl = new Rect(4, 5);
Rect r2 = new Rect(7, 9);
Console.WriteLine("Площадь прямоугольника rl: " + rl.AreaO);
Console.WriteLine("Площадь прямоугольника r2: " + r2.Area());
}
}
В действительности ключевое слово this не используется приведенным выше способом в программировании на С#, поскольку это практически ничего не дает, да и стандартная форма записи намного проще и понятнее. Тем не менее ключевому слову this можно найти не одно полезное применение. Например, в синтаксисе C# допускается называть параметр или локальную переменную тем же именем, что и у переменной экземпляра. В этом случае имя локальной переменнойскрываетпеременную экземпляра. Для доступа к скрытой переменной экземпляра и служит ключевое слово this. Например, приведенный ниже код является правильным с точки зрения синтаксиса C# способом написания конструктора Rect ().
public Rect(int Width, int Height) { this.Width = Width; this.Height = Height;
}
В этом варианте написания конструктора Rect () имена параметров совпадают с именами переменных экземпляра, а следовательно, скрывают их. Но для "обнаружения" скрытых переменных служит ключевое слово this.
ГЛАВА 7 Массивы и строки Вэтой главе речь вновь пойдет о типах данных в С#. В ней рассматриваются массивы и тип string, а также оператор цикла fore a ch. Массивы
Массивпредставляет собой совокупность переменных одного типа с общим для обращения к ним именем. В C# массивы могут быть как одномерными, так и многомерными, хотя чаще всего применяются одномерные массивы. Массивы служат самым разным целям, поскольку они предоставляют удобные средства для объединения связанных вместе переменных. Например, в массиве можно хранить максимальные суточные температуры, зарегистрированные в течение месяца, перечень биржевых курсов или же названия книг по программированию из домашней библиотеки. Главное преимущество массива — в организации данных таким образом, чтобы ими было проще манипулировать. Так, если имеется массив, содержащий дивиденды, выплачиваемые по определенной группе акций, то, организовав циклическое обращение к элементам этого массива, можно без особого труда рассчитать средний доход от этих акций. Кроме того, массивы позволяют организовать данные таким образом, чтобы легко отсортировать их. Массивами в C# можно пользоваться практически так же, как и в других языках программирования. Тем не менее у них имеется одна особенность: они реализованы в виде объектов. Именно поэтому их рассмотрение было отложено до тех пор, пока в этой книге не были представлены объекты. Реализация массивов в виде объектов дает ряд существенных преимуществ, и далеко не самым последним среди них является возможность утилизировать неиспользуемые массивы средствам "сборки мусора". Одномерные массивы
Одномерный массивпредставляет собой список связанных переменных. Такие списки часто применяются в программировании. Например, в одномерном массиве можно хранить учетные номера активных пользователей сети или текущие средние уровни достижений бейсбольной команды. Для foro чтобы воспользоваться массивом в программе, требуется двухэтапная процедура, поскольку в C# массивы реализованы в виде объектов. Во-первых, необходимо объявить переменную, которая может обращаться к массиву. И во-вторых, нужно создать экземпляр массива, используя оператор new. Так, для объявления одномерного массива обычно применяется следующая общая форма: тип[] имя_мас сив а =newтип[размер]; гдетипобъявляет конкретный тип элемента массива. Тип элемента определяет тип данных каждого элемента, составляющего массив. Обратите внимание на квадратные скобки, которые сопровождаюттип.Они указывают на то, что объявляется одномерный массив. Аразмеропределяет число элементов массива. ПРИМЕЧАНИЕ
Если у вас имеется некоторый опыт программирования на С иди C++, обратите особое внимание на то, как объявляются массивы в С#. В частности, квадратные скобки следуют после названия типа, а не имени массива.
Обратимся к конкретному примеру. В приведенной ниже строке кода создается массив типа int, который составляется из десяти элементов и связывается с переменной ссылки на массив, именуемой sample. int[] sample = new int[10]; В переменной sample хранится ссылка на область памяти, выделяемой для массива оператором new. Эта область памяти должна быть достаточно большой, чтобы в ней могли храниться десять элементов массива типа int. Как и при создании экземпляра класса, приведенное выше объявление массива можно разделить на два отдельных оператора. Например: int[] sample; sample = new int[10]; В данном случае переменная sample не ссылается на какой-то определенный физический объект, когда она создается в первом операторе. И лишь после выполнения второго оператора эта переменная ссылается на массив. Доступ к отдельному элементу массива осуществляется по индексу:Индексобозначает положение элемента в массиве. В языке C# индекс первого элемента всех массивов оказывается нулевым. В частности, массив sample состоит из 10 элементов с индексами от 0 до 9. Для индексирования массива достаточно указать номер требуемого элемента в квадратных скобках. Так, первый элемент массиваsampleобозначается какsample [ 0 ], а последний его элемент — какsample [9 ]. Ниже приведен пример программы, в которой заполняются все 10 элементов массиваsample. // Продемонстрировать одномерный массив. using System; class ArrayDemo { static void Main() { int[] sample = new int[10]; 4 int i; for(i =0; i < 10; i = i+1) sample[i] = i; for(i = 0; i < 10; i ■= i + 1) Console.WriteLine("sample[" + i + "]: " + sample[i]); } } При выполнении этой программы получается следующий результат. sample[0]: 0 sample[1]: 1 sample[2]: 2 sample[3]: 3 r sample[4]: 4 sample[5]: 5 sample[6]: 6 sample[7]: 7 sample[8]: 8 sample[9]: 9 Схематически массив sample можно представить таким образом.
0
1
2
3
4
5
6
7
8
9
о
CN
CO
i/ч
40
r-
00
Os
£
£
JJ
QJ
£
£
£
£
£
'cl
Q,
"cl
'E
TL
g.
В
В
В
В
й
В
в
В
В
03
03
03
03
03
03
cd
03
C/3
C/3
C/3
C/3
ХЛ
C/3
C/3
C/3
C/3
СЛ
Массивы часто применяются в программировании потому, что они дают возможность легко обращаться с большим числом взаимосвязанных переменных. Например, в приведенной ниже программе выявляется среднее арифметическое ряда значений, хранящихся в массиве nums, который циклически опрашивается с помощью оператора цикла for. // Вычислить среднее арифметическое ряда значений. using System; class Average { static void Main() { int[] nums = new int[10]; int avg = 0; nums[0] = 99; nums[1] = 10; nums[2] = 100; nums[3] = 18; nums[4] = 78; nums[5] = 23; nums[6] = 63; nums[7] = 9; nums[8] = 87; nums[9] = 4 9; for (int i=0; i < 10; i++) avg = avg + nums[i]; avg = avg / 10; Console.WriteLine("Среднее: " + avg); } } Результат выполнения этой программы выглядит следующим образом. Среднее: 53 Инициализация массива
В приведенной выше программе первоначальные значения были заданы для элементов массива nums вручную в десяти отдельных операторах присваивания. Конечно, такая инициализация массива совершенно правильна, но то же самое можно сделать намного проще. Ведь массивы могут инициализироваться, когда они создаются. Ниже приведена общая форма инициализации одномерного массива: тип[] имя_массива={vail, val2, val3,...,valN); гдеvail -valN обозначают первоначальные значения, которые присваиваются по очереди, слева направо и по порядку индексирования. Для хранения инициализаторов массива в C# автоматически распределяется достаточный объем памяти. А необходимость пользоваться оператором new явным образом отпадает сама собой. В качестве примера ниже приведен улучшенный вариант программы, вычисляющей среднее арифметическое. // Вычислить среднее арифметическое ряда значений. using System; class Average { static void Main() { int[] nums = { 99, 10, 100, 18, 78, 23, 63, 9, 87, 49 }; int avg = 0; for(int i=0; i < 10; i++) avg = avg + nums[i]; avg = avg /10; Console.WriteLine("Среднее: " + avg); } } Любопытно, что при инициализации массива можно также воспользоваться оператором new, хотя особой надобности в этом нет. Например, приведенный ниже фрагмент кода считается верным, но избыточным для инициализации массива nums в упомянутой выше программе. int[] nums = new int[] { 99, 10, 100, 18, 78, 23, 63, 9, 87, 49 }; Несмотря на свою избыточность, форма инициализации массива с оператором new оказывается полезной в том случае, если новый массив присваивается уже существую-щейгпеременной ссылки на массив. Например: int[] nums; nums = new int[] { 99, 10, 100, 18, 78, 23, 63, 9, 87, 49 }; В данном случае переменная nums объявляется в первом операторе и инициализируется во втором. И последнее замечание: при инициализации массива его размер можно указывать явным образом, но этот размер должен совпадать с числом инициализаторов. В качестве примера ниже приведен еще один способ инициализации массива nums. int[] nums = new int[10] { 99, 10, 100, 18, 78, 23, 63, 9, 87, 49 }; В этом объявлении размер массива nums задается равным 10 явно. Соблюдение границ массива
Границы массива в C# строго соблюдаются. Если границы массива не достигаются или же превышаются, то возникает ошибка при выполнении. Для того чтобы убедиться в этом, попробуйте выполнить приведенную ниже программу, в которой намеренно превышаются границы массива. // Продемонстрировать превышение границ массива. using System; class ArrayErr { static void Main() { int[] sample = new int[10]; int i; // Воссоздать превышение границ массива. for(i =0; i < 100; i = i+1) sample[i] = i; Как только значение переменнойiдостигает 10, возникнет исключительная ситуация типаIndexOutOfRangeException,связанная с выходом за пределы индексирования массива, и программа преждевременно завершится. (Подробнее об исключительных ситуациях и их обработке речь пойдет в главе 13.) Многомерные массивы
В программировании чаще всего применяются одномерные массивы, хотя и многомерные не так уж и редки.Многомернымназывается такой массив, который отличается двумя или более измерениями, причем доступ к каждому элементу такого массива осуществляется с помощью определенной комбинации двух или более индексов. Двумерные массивы
Простейшей формой многомерного массива является двумерный массив. Местоположение любого элемента в двумерном массиве обозначается двумя индексами. Такой массив можно представить в виде таблицы, на строки которой указывает один индекс, а на столбцы — другой. В следующей строке кода объявляется двумерный массивintegerразмерами 10x20. int[,] table = new int[10, 20]; Обратите особое внимание на объявление этого массива. Как видите, оба его размера разделяются запятой. В первой части этого объявления синтаксическое обозначение [, ] означает, что создается переменная ссылки на двумерный массив. Если же память распределяется для массива с помощью оператора new, то используется следующее синтаксическое обозначение. int[10, 20] В данном объявлении создается массив размерами 10x20, но и в этом случае его размеры разделяются запятой. Для доступа к элементу двумерного массива следует указать оба индекса, разделив их запятой. Например, в следующей строке кода элементу массиваtableс координатами местоположения (3,5) присваивается значение 10. table[3, 5] = 10; Ниже приведен более наглядный пример в виде небольшой программы, в которой двумерный массив сначала заполняется числами от 1 до 12, а затем выводится его содержимое. // Продемонстрировать двумерный массив. using System; class TwoD { static void Main() { int t, i; int[,] table = new int[3, 4]; for(t=0; t < 3; ++t) { for(i=0; i < 4; ++i) { tablejt,i] = (t*4)+i+l; • Console.Write(table[t, i] + " "); } Console.WriteLine(); } } } В данном примере элемент массива table [ 0
, 0 ] будет иметь значение 1, элемент массива table [0
,1
] — значение 2
, элемент массива table [0
,2
] — значение 3 и т.д. А значение элемента массива table [2,3] окажется равным 12. На рис. 7.1 показано схематически расположение элементов этого массива и их значений. 
Рис. 7.1. Схематическое представление массива table, созданного в программе TwoD
СОВЕТ
Если вам приходилось раньше программировать на С, C++ или Java, то будьте особенно внимательны, объявляя или организуя доступ к многомерным массивам в С#. В этих языках программирования размеры массива и индексы указываются в отдельных квадратных скобках, тогда как в C# они разделяются запятой.
Массивы трех и более измерений
В C# допускаются массивы трех и более измерений. Ниже приведена общая форма объявления многомерного массива. тип[,. . ., ]имя_массива= newтип[размер1, размер2,. . .размеры]; Например, в приведенном ниже объявлении создается трехмерный целочисленный массив размерами 4><10хЗ. int[,,] multidim = new int[4, 10, 3]; А в следующем операторе элементу массива multidim с координатами местоположения (2,4,1) присваивается значение 100. multidim[2, 4, 1] = 100; Ниже приведен пример программы, в которой сначала организуется трехмерный массив, содержащий матрицу значений 3х3><3, а затем значения элементов этого массива суммируются по одной из диагоналей матрицы. // Суммировать значения по одной из диагоналей матрицы 3x3x3. using System; class ThreeDMatrix { static void Main() { int[,,] m = new int[3, 3, 3]; int sum = 0; int n = 1; for(int x=0; x < 3; x++) for (int y=0; у < 3; y++) for(int z=0; z < 3; z++) m[x, y, z] = n++; sum = m[0, 0, 0] +m[l, 1, 1] +m[2, 2, 2]; Console.WriteLine("Сумма значений по первой диагонали: " + sum); } } Вот какой результат дает выполнение этой программы. Сумма значений по первой диагонали: 42 Инициализация многомерных массивов
Дляинициализации многомерного массива достаточно заключить в фигурные скобки список инициализаторов каждого его размера. Ниже в качестве примера приведена общая форма инициализации двумерного массива: тип[,] имя_мас сив а= { {val, val, valf...,val}, {val, val, val, . . ., val}, {val, val, val,. . .,val} }; гдеvalобозначает инициализирующее значение, а каждый внутренний блок — отдельный ряд. Первое значение в каждом ряду сохраняется на первой позиции в массиве, второе значение — на второй позиции и т.д. Обратите внимание на то, что блоки инициализаторов разделяются запятыми, а после завершающей эти блоки закрывающей фигурной скобки ставится точка с запятой. В качестве примера ниже приведена программа, в которой двумерный массив sqrs инициализируется числами от 1
до 10
и квадратами этих чисел. // Инициализировать двумерный массив, using System; class Squares { static void Main() { int[,] sqrs = { { 1
, 1
}, { 2, 4 Ь {'3, 9 }, { 4, 16 }, { 5, 25 }, { 6, 36 }, { 7, 49 }, { 8, 64 }, { 9, 81 }, {
10,
100 }
}; int i, j; for(i=0; i < 10; i++) { for(j =0; j < 2; j++) Console.Write(sqrs[i,j] + " "); Console.WriteLine(); } } } При выполнении этой программы получается следующий результат. Ступенчатые массивы
В приведенных выше примерах применения двумерного массива, по существу, создавался так называемыйпрямоугольный массив.Двумерный массив можно представить в виде таблицы, в которой длина каждой строки остается неизменной по всему массиву. Но в C# можно также создавать специальный тип двумерного массива, называемыйступенчатым массивом.Ступенчатый массив представляет собоймассив массивов,в котором длина каждого массива может быть разной. Следовательно, ступенчатый массив может быть использован для составления таблицы из строк разной длины. Ступенчатые массивы объявляются с помощью ряда квадратных скобок, в которых указывается их размерность. Например, для объявления двумерного ступенчатого массива служит следующая общая форма: тип[][]имя_ма с сив а= newтип [размер][]; гдеразмеробозначает число строк в массиве. Память для самих строк распределяется индивидуально, и поэтому длина строк может быть разной. Например, в приведенном ниже фрагменте кода объявляется ступенчатый массив j agged. Память сначала распределяется для его первого измерения автоматически, а затем для второго измерения вручную. int[][] jagged = new int[3][]; jagged[0] = new int [4]; jagged[1] = new int[3]; jagged[2] = new int[5]; После выполнения этого фрагмента кода массив j agged выглядит так, как показано ниже.
Теперь нетрудно понять, почему такие массивы называются ступенчатыми! После создания ступенчатого массива доступ к его элементам осуществляется по индексу, указываемому в отдельных квадратных скобках. Например, в следующей строке кода элементу массива j agged, находящемуся на позиции с координатами (2
,1
), присваивается значение 10
. j agged[2] [1] = 10; Обратите внимание на синтаксические отличия в доступе к элементу ступенчатого и прямоугольного массива. В приведенном ниже примере программы демонстрируется создание двумерного ступенчатого массива. // Продемонстрировать применение ступенчатых массивов. using System; class Jagged { static void Main() { int[][] jagged = new int[3][]; jagged[0] = new int[4]; jagged[1] = new int[3]; jagged[2] = new int[5]; int ъ; // Сохранить значения в первом массиве. for(i=0; i < 4; i++) jagged[0][i]=i; // Сохранить значения во втором массиве. for(i=0; i < 3; i++) jagged[1][i] = i; // Сохранить значения в третьем массиве. for(i=0; i < 5; i++) jagged[2][i] = i; // Вывести значения из первого массива. for(i=0; i < 4; i++) Console.Write(jagged[0] [i] + " ") ; Console.WriteLine(); // Вывести значения из второго массива, for (i=0; i < 3; i++) Console.Write(jagged[1][i] + " "); Console.WriteLine() ; // Вывести значения из третьего массива. for(i=0; i < 5; i++) Console.Write(jagged[2] [i] + " ") ; Console.WriteLine() ; } } Выполнение этой программы приводит к следующему результату. 0 12 3 0
12 0 12 3 4 Ступенчатые массивы находят полезное применение не во всех, а лишь в некоторых случаях. Так, если требуется очень длинный двумерный массив, который заполняется не полностью, т.е. такой массив, в котором используются не все, а лишь отдельные его элементы, то для этой цели идеально подходит ступенчатый массив. И последнее замечание: ступенчатые массивы представляют собой массивы массивов, и поэтому они не обязательно должны состоять из одномерных массивов. Например, в приведенной ниже строке кода создается массив двумерных массивов. int[] [,] j agged = new int [ 3] [,]; В следующей строке кода элементу массива j agged [ 0 ] присваивается ссылка на массив размерами 4><2. jagged[0] = new int [4, 2]; А в приведенной ниже строке кода элементу массива j agged [ 0 ] [1,0] присваивается значение переменной i. jagged[0][1,0] = i; Присваивание ссылок на массивы
Присваивание значения одной переменной ссылки на массив другой переменной, по существу, означает, что обе переменные ссылаются на один и тот же массив, и в этом отношении массивы ничем не отличаются от любых других объектов. Такое присваивание не приводит ни к созданию копии массива, ни к копированию содержимого одного массива в другой. В качестве примера рассмотрим следующую программу. // Присваивание ссылок на массивы. using System; class AssignARef { static void Main() { int i; int[] numsl = new int [10]; int[] nums2 = new int [10]; for(i=0; i < 10; i++) numsl[i] = i; for(i=0; i < 10; i++) nums2[i] = -i; Console.Write("Содержимое массива numsl: "); for(i=0; i < 10; i++) Console.Write(numsl[i] + " "); Console.WriteLine() ; Console.Write("Содержимое массива nums2: "); for(i=0; i < 10; i++) Console.Write(nums2[i] + " ") ; Console.WriteLine() ; nums2 = numsl; // теперь nums2 ссылается на numsl Console.Write("Содержимое массива nums2\n" + "после присваивания: "); for(i=0; i < 10; i++) Console.Write(nums2[i] + " ") ; Console.WriteLine() ; // Далее оперировать массивом numsl посредством // переменной ссылки на массив nums2. nums2[3] = 99; Console.Write("Содержимое массива numsl после изменения\п" + "посредством переменной nums2: "); for (i=0; i < 10; i++) Console.Write(numsl[i] + " ") ; Console.WriteLine() ; } } Выполнение этой программы приводит к следующему результату. Содержимое массива numsl: 0123456789 Содержимое массива nums2: 0 -1 -2 -3 -4 -5 -6 -7 -8 -9 Содержимое массива nums2 после присваивания: 0123456789 Содержимое массива numsl после изменения посредством переменной nums2: 012 99 456789 Как видите, после присваивания переменнойnums 2значения переменнойnumslобе переменные ссылки на массив ссылаются на один и тот же объект. Применение свойства Length
Реализация в C# массивов в виде объектов дает целый ряд преимуществ. Одно из них заключается в том, что с каждым массивом связано свойствоLength,содержащее число элементов, из которых может состоять массив. Следовательно, у каждого массива имеется специальное свойство, позволяющее определить его длину. Ниже приведен пример программы, в которой демонстрируется это свойство. // Использовать свойство Length массива. using System; class LengthDemo { static void Main() { int[] nums = new int[10]; Console.WriteLine("Длина массива nums равна " + nums.Length); // Использовать свойство Length для инициализации массива nums. for(int i=0; i < nums.Length; i++) nums[i] = i * i; // А теперь воспользоваться свойством Length // для вывода содержимого массива nums. Console.Write("Содержимое массива nums: "); for(int i=0; i < nums.Length; i++) Console.Write(nums[i] + " "); Console.WriteLine(); } } При выполнении этой программы получается следующий результат. Длина массива nums равна 10 Содержимое массива nums: 0 1 4 9 16 25 36 49 64 81 Обратите внимание на то, как в классеLengthDemoсвойствоnums . Lengthиспользуется в циклахforдля управления числом повторяющихся шагов цикла. У каждого массива имеется своя длина, поэтому вместо отслеживания размера массива вручную можно использовать информацию о его длине. Следует, однако, иметь в виду, что значение свойстваLengthникак не отражает число элементов, которые в нем используются на самом деле. СвойствоLengthсодержит лишь число элементов, из которых может состоять массив. Когда запрашивается длина многомерного массива, то возвращается общее число элементов, из которых может состоять массив, как в приведенном ниже примере кода. // Использовать свойство Length трехмерного массива. using System; class LengthDemo3D { static void Main() { int[,,] nums = new int[10, 5, 6]; Console.WriteLine("Длина массива nums равна " + nums.Length); } } При выполнении этого кода получается следующий результат. Длина массива nums равна 300 Как подтверждает приведенный выше результат, свойствоLengthсодержит число элементов, из которых может состоять массив (в данном случае — 300 (10><5х6) элементов). Тем не менее свойствоLengthнельзя использовать для определения длины массива в отдельном его измерении. Благодаря наличию у массивов свойстваLengthоперации с массивами во многих алгоритмах становятся более простыми, а значит, и более надежными. В качестве примера свойствоLengthиспользуется в приведенной ниже программе с целью поменять местами содержимое элементов массива, скопировав их в обратном порядке в другой массив. // Поменять местами содержимое элементов массива. using System; class RevCopy { static void Main() { int i,j; int[] numsl = new int[10]; int[] nums2 = new int[10]; for(i=0; i < numsl.Length; i++) numsl[i] = i; Console.Write("Исходное содержимое массива: "); for(i=0; i < nums2.Length; i++) Console.Write(numsl[i] + " "); Console.WriteLine(); // Скопировать элементы массива numsl в массив nums2 в обратном порядке, if(nums2.Length >= numsl.Length) // проверить, достаточно ли // длины массива nums2 for(i=0, j=numsl.Length-1; i < numsl.Length; i++, j--) nums2[j] = numsl[i]; Console.Write("Содержимое массива в обратном порядке: "); for(i=0; i < nums2.Length; i++) Console.Write(nums2[i] + " "); Выполнение этой программы дает следующий результат. Исходное содержимое массива: 0123456789 Содержимое массива в обратном порядке: 9876543210 В данном примере свойствоLengthпомогает выполнить две важные функции. Во-первых, оно позволяет убедиться в том, что длины целевого массива достаточно для хранения содержимого исходного массива. И во-вторых, оно предоставляет условие для завершения циклаfor,в котором выполняется копирование исходного массива в обратном порядке. Конечно, в этом простом примере размеры массивов нетрудно выяснить и без свойстваLength,но аналогичный подход может быть применен в целом ряде других, более сложных ситуаций. Применение свойства Length при обращении со ступенчатыми массивами
Особый случай представляет применение свойстваLengthпри обращении со ступенчатыми массивами. В этом случае с помощью данного свойства можно получить длину каждого массива, составляющего ступенчатый массив. В качестве примера рассмотрим следующую программу, в которой имитируется работа центрального процессора (ЦП) в сети, состоящей из четырех узлов. // Продемонстрировать применение свойства Length // при обращении со ступенчатыми массивами. using System; class Jagged { static void Main() { int[][] network_nodes = new int[4][]; network_nodes[0] = new int[3]; network_nodes[1] = new int[7]; network_nodes[2] = new int[2]; network_nodes[3] = new int[5]; int i, j; // Сфабриковать данные об использовании ЦП. for(i=0; i < network_nodes.Length; i++) for(j=0; j < network_nodes[i].Length; j++) network_nodes[i][j] = i * j + 70; Console.WriteLine("Общее количество узлов сети: " + network_nodes.Length + "\n"); for(i=0; i < network_nodes.Length; i++) { for(j=0; j < network_nodes[i].Length; j++) { Console.Write("Использование в узле сети " + i + " ЦП " + j + ": "); Console.Write(network_nodes[i][j] + "% "); Console.WriteLine (); } При выполнении этой программы получается следующий результат.
Общее количество узлов
сети:
: 4
Использование
в
узле
0
ЦП
0
70%
Использование
в
узле
0
ЦП
1
70%
Использование
в
узле
0
ЦП
2
70%
Использование
в
узле
1
ЦП
0
70%
Использование
в
узле
1
ЦП
1
71%
Использование
в
узле
1
ЦП
2
72%
Использование
в
узле
1
ЦП
3
73%
Использование
в
узле
1
ЦП
4
74%
Использование
в
узле
1
ЦП
5
75%
Использование
в
узле
1
ЦП
6
76%
Использование
в
узле
2
ЦП
0:
: 70%
Использование
в
узле
2
ЦП
1:
: 72%
Использование
в
узле
3
ЦП
0:
: 70%
Использование
в
узле
3
ЦП
1:
: 73%
Использование
в
узле
3
ЦП
2:
: 76%
Использование
в
узле
3
ЦП
3:
: 79%
Использование
в
узле
3
ЦП
4:
: 82%
Обратите особое внимание на то, как свойствоLengthиспользуется в ступенчатом массивеnetwork_nodes.Напомним, что двумерный ступенчатый массив представляет собой массив массивов. Следовательно, когда используется выражение network_nodes.Length то в нем определяется число массивов, хранящихся в массивеnetwork_nodes(в данном случае — четыре массива). А для получения длины любого отдельного массива, составляющего ступенчатый массив, служит следующее выражение. n.etwork_nodes [0] .Length В данном случае это длина первого массива. Неявно типизированные массивы
Как пояснялось в главе 3, в версии C# 3.0 появилась возможность объявлять неявно типизированные переменные с помощью ключевого словаvar.Это переменные, тип которых определяется компилятором, исходя из типа инициализирующего выражения. Следовательно, все неявно типизированные переменные должны быть непременно инициализированы. Используя тот же самый механизм, можно создать и неявно типизированный массив. Как правило, неявно типизированные массивы предназначены для применения в определенного рода вызовах, включающих в себя элементы языка LINQ, о котором речь пойдет в главе 19. А в большинстве остальных случаев используется "обычное" объявление массивов. Неявно типизированйые массивы рассматриваются здесь лишь ради полноты представления о возможностях языка С#. Неявно типизированный массив объявляется с помощью ключевого словаvar,нобезпоследующих квадратных скобок [ ]. Кроме того, неявно типизированный маесив должен быть непременно инициализирован, поскольку по типу инициализаторов определяется тип элементов данного массива. Все инициализаторы должны быть одного и того же согласованного типа. Ниже приведен пример объявления неявно типизированного массива. var vals = new[] { 1, 2, 3, 4, 5 }; В данном примере создается массив типа int, состоящий из пяти элементов. Ссылка на этот массив присваивается переменной vals. Следовательно, тип этой переменной соответствует типу int массива, состоящего из пяти элементов. Обратите внимание на то, что в левой части приведенного выше выражения отсутствуют квадратные скобки [ ]. А в правой части этого выражения, где происходит инициализация массива, квадратные скобки присутствуют. В данном контексте они обязательны. Рассмотрим еще один пример, в котором создается двумерный массив типа double. var vals = new[,] { {1.1, 2.2}, {3.3, 4.4}, { 5.5, 6.6} }; В данном случае получается массив vals размерами 2x3. Объявлять можно также неявно типизированные ступенчатые массивы. В качестве примера рассмотрим следующую программу. // Продемонстрировать неявно типизированный ступенчатый массив. using System; class Jagged { static void Main() { var jagged = new[] { new [ ] { 1, 2, 3, 4 }, new[] { 9, 8, 7 }, new[] { 11, 12, 13, 14, 15 } }; for(int j =0; j < jagged.Length; j++) { for(int i-0; i < jagged[j].Length; i++) Console.Write(jagged[j] [i] + " ") ; Console.WriteLine(); }
} } Выполнение этой программы дает следующий результат. 12 3 4 9 8 7 11 12 13 14 15 Обратите особое внимание на объявление массива j agged. var jagged = new[] { new [ ] { 1, 2, 3, 4 }, new[] { 9, 8, 7 }, new [ ] { 11, 12, 13, 14, 15 } }; Как видите, оператор new [ ] используется в этом объявлении двояким образом. Во-первых, этот оператор создает массив массивов. И во-вторых, он создает каждый массив в отдельности, исходя из количества инициализаторов и их типа. Как и следовало ожидать, все инициализаторы отдельных массивов должны быть одного и того же типа. Таким образом, к объявлению любого неявно типизированного ступенчатого массива применяется тот же самый общий подход, что и к объявлению обычных ступенчатых массивов. Как упоминалось выше, неявно типизированные массивы чаще всего применяются в LINQ-ориентированных запросах. А в остальных случаях следует использовать явно типизированные массивы. Оператор цикла f oreach
Как упоминалось в главе 5, в языке C# определен оператор цикла f oreach, но его рассмотрение было отложено до более подходящего момента. Теперь этот момент настал. Оператор f oreach служит для циклического обращения к элементамколлекции, представляющей собой группу объектов. В C# определено несколько видов коллекций, каждая из которых является массивом. Ниже приведена общая форма оператора цикла foreach. foreach(тип имя_переменной_циклаinколлекция) оператор; Здесьтип имя_переменной_циклаобозначает тип и имя переменной управления циклом, которая получает значение следующего элемента коллекции на каждом шаге выполнения цикла foreach. Аколлекцияобозначает циклически опрашиваемую коллекцию, которая здесь и далее представляет собой массив. Следовательно,типпеременной цикла должен соответствовать типу элемента массива. Кроме того,типможет обозначаться ключевым словом var. В этом случае компилятор определяет тип переменной цикла, исходя из типа элемента массива. Это может оказаться полезным для работы с определенного рода запросами, как будет показано далее в данной книге. Но, как правило, тип указывается явным образом. Оператор цикла foreach действует следующим образом. Когда цикл начинается, первый элемент массива выбирается и присваивается переменной цикла. На каждом последующем шаге итерации выбирается следующий элемент массива, который сохраняется в переменной цикла. Цикл завершается, когда все элементы массива окажутся выбранными. Следовательно, оператор foreach циклически опрашивает массив по отдельным его элементам от начала и до конца. Следует, однако, иметь в виду, что переменная цикла в операторе foreach служит только для чтения. Это означает, что, присваивая этой переменной новое значение, нельзя изменить содержимое массива. Ниже приведен простой пример применения оператора цикла foreach. В этом примере сначала создается целочисленный массив и задается ряд его первоначальных значений, а затем эти значения выводятся, а по ходу дела вычисляется их сумма. // Использовать оператор цикла foreach. using System; class ForeachDemo { static void Main() { int sum = 0; int[] nums = new int [10]; // Задать первоначальные значения элементов массива nums. for(int i = 0; i < 10; i++) nums[i] = i; // Использовать цикл foreach для вывода значений // элементов массива и подсчета их суммы, foreach(int х in nums) { Console.WriteLine("Значение элемента равно: " + х); sum += х; } Console.WriteLine("Сумма равна: " + sum); } } Выполнение приведенного выше кода дает следующий результат.
Значение
элемента
равно:
0
Значение
элемента
равно:
1
Значение
элемента
равно:
2
Значение
элемента
равно:
3
Значение
элемента
равно:
4
Значение
элемента
равно:
5
Значение
элемента
равно:
6
Значение
элемента
равно:
7
Значение
элемента
равно:
8
Значение
элемента
равно:
9
Сумма равна: 45
Как видите, оператор foreach циклически опрашивает массив по порядку индексирования от самого первого до самого последнего его элемента. Несмотря на то что цикл foreach повторяется до тех пор, пока не будут опрошены все элементы массива, его можно завершить преждевременно, воспользовавшись оператором break. Ниже приведен пример программы, в которой суммируются только пять первых элементов массива nums. // Использовать оператор break для преждевременного завершения цикла foreach. using System; class ForeachDemo { static void Main() { int sum = 0; int[] nums = new int [10]; // Задать первоначальные значения элементов массива nums. for(int i = 0; i < 10; i++) nums[i] = i; // Использовать цикл foreach для вывода значений // элементов массива и подсчета их суммы. foreach(int x in nums) {
Console.WriteLine("Значение элемента равно: " + x); sum += x;
if(x == 4) break; // прервать цикл, как только индекс массива достигнет 4
} Console.WriteLine("Сумма первых 5 элементов: " + sum);
} } Вот какой результат дает выполнение этой программы. Значение элемента равно: О Значение элемента равно: 1 Значение элемента равно: 2 Значение элемента равно: 3 Значение элемента равно: 4 Сумма первых 5 элементов: 10
Совершенно очевидно, что цикл foreach завершается после выбора и вывода значения пятого элемента массива. Оператор цикла foreach можно также использовать для циклического обращения к элементам многомерного массива. В этом случае элементы многомерного массива возвращаются по порядку следования строк от первой до последней, как демонстрирует приведенный ниже пример программы. // Использовать оператор цикла foreach для обращения к двумерному массиву.
using System;
class ForeachDemo2 { static void Main() { int sum = 0;
int[,] nums = new int'[3,5];
// Задать первоначальные значения элементов массива nums. for (int i = 0; i < 3; i++) for (int j=0; j < 5; j++) nums[i,j] = (i+l)*(j+l);
// Использовать цикл foreach для вывода значений // элементов массива и подсчета их суммы, foreach(int х in nums) {
Console.WriteLine("Значение элемента равно: " + х); sum += х;
} Console.WriteLine("Сумма равна: " + sum);
} } Выполнение этой программы дает следующий результат. Значение элемента равно: 1 Значение элемента равно: 2 Значение элемента равно: 3 Значение элемента равно: 4
Значение
элемента
равно:
5
Значение
элемента
равно:
2
Значение
элемента
равно:
4
Значение
элемента
равно:
6
Значение
элемента
равно:
8
Значение
элемента
равно:
10
Значение
элемента
равно:
3
Значение
элемента
равно:
6
Значение
элемента
равно:
9
Значение
элемента
равно:
12
Значение
элемента
равно:
15
Сумма равна: 90
Оператор foreach допускает циклическое обращение к массиву только в определенном порядке: от начала и до конца массива, поэтому его применение кажется, на первый взгляд, ограниченным. Но на самом деле это не так. В большом числе алгоритмов, самым распространенным из которых является алгоритм поиска, требуется именно такой механизм. В качестве примера ниже приведена программа, в которой цикл foreach используется для поиска в массиве определенного значения. Как только это значение будет найдено, цикл прервется. // Поиск в массиве с помощью оператора цикла foreach.
using System;
class Search {
static void Main() {
int[] nums = new int [10]; int val;
bool found = false;
// Задать первоначальные значения элементов массива nums. for (int i = 0; i < 10; i++) nums[i] = i;
val = 5;
// Использовать цикл foreach для поиска заданного // значения в массиве nums. foreach(int х in nums) { if(x == val) {
found = true; break;
} } if(found)
Console.WriteLine("Значение найдено!");
} } При выполнении этой программы получается следующий результат. Значение найдено!
Оператор циклаforeachотлично подходит для такого применения, поскольку при поиске в массиве приходится анализировать каждый его элемент. К другим примерам применения оператора циклаforeachотносится вычисление среднего, поиск минимального или максимального значения среди ряда заданных значений, обнаружение дубликатов и т.д. Как будет показано далее в этой книге, оператор циклаforeachоказывается особенно полезным для работы с разными типами коллекций.
Строки
С точки зрения регулярного программирования строковый тип данныхstringотносится к числу самых важных в С#. Этот тип определяет и поддерживает символьные строки. В целом ряде других языков программирования строка представляет собой массив символов. А в C# строки являются объектами. Следовательно, типstringотносится к числу ссылочных. И хотяstringявляется встроенным в C# типом данных,егорассмотрение пришлось отложить до тех пор, пока не были представлены классы и объекты.
На самом деле класс типаstringуже не раз применялся в примерах программ, начиная с главы 2, но это обстоятельство выясняется только теперь, когда очередь дошла до строк. При создании строкового литерала в действительности формируется строковый объект. Например, в следующей строке кода:
Console.WriteLine("В C# строки являются объектами.");
текстовая строка"В C# строки являются объектами. " автоматически преобразуется в строковый объект средствами С#. Следовательно, применение класса типаstringпроисходило в предыдущих примерах программ неявным образом. А в этом разделе будет показано, как обращаться со строками явным образом.
Построение строк
Самый простой способ построить символьную строку — воспользоваться строковым литералом. Например, в следующей строке кода переменной ссылки на строкуstrприсваивается ссылка на строковый литерал.
string str = "Строки в C# весьма эффективны.";
В данном случае переменная str инициализируется последовательностью символов"Строки в C# весьма эффективны.11.
Объект типаstringможно также создать из массива типаchar.Например:
char[] charray = {'t', ' е', 's', ' t'};
string str = new string(charray);
Как только объект типаstringбудет создан, его можно использовать везде, где только требуется строка текста, заключенного в кавычки. Как показано в приведенном ниже примере программы, объект типаstringможет служить в качестве аргумента при вызове методаWriteLine ().
// Создать и вывести символьную строку.
using System;
class StringDemo { static void Main() {
char[] charray ={'Э', 'т1, 'o', 1 ', 'с', ' t', 1 p', 'o', 'к', 'a',
1 •1} ;
string strl = new string(charray);
string str2 = "Еще одна строка.";
Console.WriteLine(strl);
Console.WriteLine(str2);
}
}
Результат выполнения этой программы приведен ниже.
Это строка.
Еще одна строка.
Обращение со строками
Класс типаstringсодержит ряд методов для обращения со строками. Некоторые из этих методов перечислены в табл. 7.1. Обратите внимание на то, что некоторые методы принимают параметр типаStringComparison.Это перечислимый тип, определяющий различные значения, которые определяют порядок сравнения символьных строк. (О перечислениях речь пойдет в главе 12, но для применения типаStringComparisonк символьным строкам знать о перечислениях необязательно.) Нетрудно догадаться, что символьные строки можно сравнивать разными способами. Например, их можно сравнивать на основании двоичных значений символов, из которых они состоят. Такое сравнение называетсяпорядковым.Строки можно также сравнивать с учетом различных особенностей культурной среды, например, в лексикографическом порядке. Это так называемое сравненениес учетом культурной qpedbi.(Учитывать культурную среду особенно важно в локализуемых приложениях.) Кроме того, строки можно сравниватьс учетомилибез учетарегистра. Несмотря на то что существуют перегружаемые варианты методовCompare (), Equals (), IndexOf ()иLast IndexOf (), обеспечивающие используемый по умолчанию подход к сравнению символьных строк, в настоящее время считается более приемлемым явно указывать способ требуемого сравнения, чтобы избежать неоднозначности, а также упростить локализацию приложений. Именно поэтому здесь рассматривают разные способы сравнения символьных строк.
Как правило и за рядом исключений, для сравнения символьных строк с учетом культурной среды (т.е. языковых и региональных стандартов) применяется способStringComparison . CurrentCulture.Если же требуется сравнить строки только на основании значений их символов, то лучше воспользоваться способомStringComparison . Ordinal,а для сравнения строк без учета регистра — одним из двух способов:StringComparison . CurrentCulturelgnoreCaseилиStringComparison . OrdinallgnoreCase.Кроме того, можно указать сравнение строк без учета культурной среды (подробнее об этом — в главе 22).
Обратите внимание на то, что методCompare() объявляется в табл. 7.1 какstatic.Подробнее о модификатореstaticречь пойдет в главе 8, а до тех пор вкратце поясним, что он обозначает следующее: методCompare() вызывается по имени своего класса, а не по его экземпляру. Следовательно, для вызова методаCompare() служит следующая общая форма:
результат =string.Compare(strl,str2, способ);
гдеспособобозначает конкретный подход к сравнению символьных строк.
ПРИМЕЧАНИЕ
Дополнительные сведения о способах сравнения и поиска символьных строк, включая и особое значение выбора подходящего способа, приведены в главе 22, где подробно рассматривается обработка строк.
Обратите также внимание на методыToUpper () иToLower (), преобразующие содержимое строки в символы верхнего и нижнего регистра соответственно. Их формы, представленные в табл. 7.1, содержат параметрCulture Inf о,относящийся к классу, в котором описываются атрибуты культурной среды, применяемые для сравнения. В примерах, приведенных в этой книге, используются текущие настройки культурной среды (т.е. текущие языковые и региональные стандарты). Эти настройки указываются при передаче методу аргументаCulturelnf о . CurrentCulture.КлассCulturelnfоотносится к пространству именSystem. Globalization.Любопытно, имеются варианты рассматриваемых здесь методов, в которых текущая культурная среда используется по умолчанию, но во избежание неоднозначности в примерах из этой книги аргументCulturelnf о . CurrentCultureуказывается явно.
Объекты типаstringсодержат также свойствоLength,где хранится длина строки.
Таблица 7.1. Некоторые общеупотребительные методы обращения со строками
Описание
Метод
static int Compare(stringstrA,stringstrB,StringComparisoncomparisonType)
bool Equals(stringvalue,StringComparisoncomparisonType)
int IndexOf(charvalue)
int IndexOf(stringvalue,StringComparisoncomparisonType)
Возвращает отрицательное значение, если строка strA меньше строки strB; положительное значение, если строка strA больше строки strB; и нуль, если сравниваемые строки равны. Способ сравнения определяется аргументом comparisonType Возвращает логическое значение true, если вызывающая строка имеет такое же значение, как и у аргумента value. Способ сравнения определяется аргументом comparisonType Осуществляет поиск в вызывающей строке первого вхождения символа, определяемого аргументом value. Применяется порядковый способ поиска. Возвращает индекс первого совпадения с искомым символом или -1, если он не обнаружен .Осуществляет поиск в вызывающей строке первого вхождения подстроки, определяемой аргументом value. Возвращает индекс первого совпадения с искомой подстрокой или -1, если она не обнаружена. Способ поиска определяется аргументом comparisonType int LastlndexOf(char value)
int LastlndexOf(stringvalue,StringComparisoncomparisonType)
string ToLower(Culturelnfo. CurrentCultureculture)
string ToUpper(Culturelnfo. CurrentCultureculture)
Осуществляет поиск в вызывающей строке последнего вхождения символа, определяемого аргументом value. Применяется порядковый способ поиска. Возвращает индекс последнего совпадения с искомым символом или -1, если он не обнаружен Осуществляет поиск в вызывающей строке последнего вхождения подстроки, определяемой аргументом value. Возвращает индекс последнего совпадения с искомой подстрокой или -1, если она не обнаружена. Способ пойска определяется аргументом comparisonType
Возвращает вариант вызывающей строки в нижнем регистре. Способ преобразования определяется аргументом culture
Возвращает вариант вызывающей строки в верхнем регистре. Способ преобразования определяется ар-гументом culture
Отдельный символ выбирается из строки с помощью индекса, как в приведенном ниже фрагменте кода.
string str = "тест";
Console.WriteLine(str [0] ) ;
В этом фрагменте кода выводится символ "т", который является первым в строке"тест".Как и в массивах, индексирование строк начинается с нуля. Следует, однако, иметь в виду, что с помощью индекса нельзя присвоить новое значение символу в строке. Индекс может служить только для выборки символа из строки.
Для проверки двух строк на равенство служит оператор ==. Как правило, если оператор == применяется к ссылкам на объект, то он определяет, являются ли они ссылками на один и тот же объект. Совсем иначе обстоит дело с объектами типаstring.Когда оператор == применяется к ссылкам на две строки, он сравнивает содержимое этих строк. Это же относится и к оператору ! =. В обоих случаях выполняется порядковое сравнение. Для проверки двух строк на равенство с учетом культурной среды служит методEquals(), где непременно нужно указать способ сравнения в виде аргументаStringComparison . CurrentCulture.Следует также иметь в виду, что методCompare() служит для сравнения строк с целью определить отношение порядка, например для сортировки. Если же требуется проверить символьные строки на равенство, то для этой цели лучше воспользоваться методомEquals() или строковыми операторами.
В приведенном ниже примере программы демонстрируется несколько операций со строками.
// Некоторые операции со строками.
using System;
using System.Globalization;
class StrOps {
static void Main() {
string strl = "Программировать в .NET лучше всего на С#.";
string str2 = "Программировать в .NET лучше всего на С#.";
string str3 = "Строки в C# весьма эффективны.";
string strUp, strLow;
int result, idx;
Console.WriteLine("strl: " + strl);
Console.WriteLine("Длина строки strl: " + strl.Length);
// Создать варианты строки strl, набранные // прописными и строчными буквами.
strLow = strl.ToLower(Cirlturelnfo.CurrentCulture) ; strUp = strl.ToUpper (Culturelnfo.CurrentCulture);
Console.WriteLine("Вариант строки strl, " +
"набранный строчными буквами:\n " + strLow); Console.WriteLine("Вариант строки strl, " +
"набранный прописными буквами:\n " + strUp);
Console.WriteLine();
// Вывести строку strl посимвольно.
Console.WriteLine("Вывод строки strl посимвольно.") ; for (int i=0; i < strl.Length; i++)
Console.Write(strl[i]);
Console.WriteLine("\n");
// Сравнить строки способом порядкового сравнения, if (strl == str2)
Console.WriteLine("strl == str2"); else
Console.WriteLine("strl != str2"); if (strl == str3)
Console.WriteLine("strl == str3"); else
Console.WriteLine("strl != str3");
// Сравнить строки с учетом культурной среды.
result = string.Compare(str3, strl, StringComparison.CurrentCulture) ; if(result == 0)
Console.WriteLine("Строки strl и str3 равны"); else if (result < 0)
Console.WriteLine("Строка strl-меньше строки str3"); else
Console.WriteLine("Строка strl больше строки str3");
Console.WriteLine();
// Присвоить новую строку переменной str2. str2 = "Один Два Три Один";
// Поиск подстроки.
idx = str2.IndexOf("Один", StringComparison.Ordinal);
Console.WriteLine("Индекс первого вхождения подстроки <Один>: " + idx)
idx = str2.LastlndexOf("Один", StringComparison.Ordinal);
Console.WriteLine("Индекс последнего вхождения подстроки <0дин>: " + idx) ;
}
}
При выполнении этой программы получается следующий результат.
strl: Программировать в .NET лучше всего на С#.
Длина строки strl: 41
Вариант строки strl, набранный строчными буквами: программировать в .net лучше всего на с#.
Вариант строки strl, набранный прописными буквами: программировать в .net лучше всего на с#.
Вывод строки strl посимвольно.
Программировать в .NET лучше всего на С#.
strl == str2 strl != str3
Строка strl больше строки str3
Индекс первого вхождения подстроки <0дин>: О
Индекс последнего вхождения подстроки <0дин>: 13
Прежде чем читать дальше, обратите внимание на то, что методCompare() вызы
вается следующим образом.
result = string.Compare(strl, str3, StringComparison.CurrentCulture);
Как пояснялось ранее, методCompare() объявляется какstatic,и поэтому он вызывается по имени, а не по экземпляру своего класса.
С помощью оператора + можно сцепить (т.е. объединить вместе) две строки. Например, в следующем фрагменте кода:
string strl = "Один";
string str2 = "Два";
string str3 = "Три";
string str4 = strl + str2 + str3;
переменнаяstr4инициализируется строкой"ОдинДваТри".
И еще одно замечание: ключевое словоstringявляетсяпсевдонимомклассаSystem. String,определенного в библиотеке классов для среды .NET Framework, т.е. оно устанавливает прямое соответствие с этим классом. Следовательно, поля и методы, определяемые типомstring,относятся непосредственно к классуSystem. String,в который входят и многие другие компоненты. Подробнее о классеSystem. Stringречь пойдет в части II этой книги.
Массивы строк
Аналогично данным любого другого типа, строки могут быть организованы в массивы. Ниже приведен соответствующий пример.
// Продемонстрировать массивы строк.
using System;
class StringArrays { static void Main() {
string[] str = { "Это", "очень", "простой", "тест." };
Console.WriteLine("Исходный массив: "); for (int i=0; i < str.Length; i++)
Console.Write(str[i] + " ");
Console.WriteLine("\n");
// Изменить строку.
str[l] = "тоже";
str[3] = "до предела тест!";
Console.WriteLine("Видоизмененный массив: "); for (int i=0; i < str.Length; i++)
Console.Write(str[i] + " ");
}
}
Вот какой результат дает выполнение приведенного выше кода.
Исходный массив:
Это очень простой тест.
Видоизмененный массив:
Это тоже простой до предела тест!
Рассмотрим более интересный пример. В приведенной ниже программе целое число выводится словами. Например, число 19 выводится словами "один девять".
// Вывести отдельные цифры целого числа словами, using System;
class ConvertDigitsToWords { static void Main() { int num; int nextdigit; int numdigits; int[] n = new int[20];
string[] digits = { "нуль", "один", "два",
"три", "четыре", "пять",
"шесть", "семь", "восемь",
"девять" };
num =1908;
Console.WriteLine("Число: " + num);
Console.Write("Число словами: ");
nextdigit = 0; numdigits = 0;
// Получить отдельные цифры и сохранить их в массиве п.
// Эти цифры сохраняются в обратном порядке, do {
nextdigit = num % 10; n[numdigits] = nextdigit; numdigits++; num = num /10;
} while(num > 0); numdigits--;
// Вывести полученные слова.
for( ; numdigits >= 0; numdigits--)
Console.Write(digits[n[numdigits]] + " ");
Console.WriteLine() ;
}
}
Выполнение этой программы приводит к следующему результату.
Число: 1908
Число словами: один девять нуль восемь
В данной программе использован массив строкdigitsдля хранения словесных обозначений цифр от 0 до 9. По ходу выполнения программы целое число преобразуется в слова. Для этого сначала получаются отдельные цифры числа, а затем они сохраняются в обратном порядке следования в массивептипаint.После этого выполняется циклический опрос массивапв обратном порядке. При этом каждое целое значение из массивапслужит в качестве индекса, указывающего на слова, соответствующие полученным цифрам числа и выводимые как строки.
Постоянство строк
Как ни странно, содержимое объекта типаstringне подлежит изменению. Это означает, что однажды созданную последовательность символов изменить нельзя. Но данное ограничение способствует более эффективной реализации символьных строк. Поэтому этот, на первый взгляд, очевидный недостаток на самом деле превращается в преимущество. Так, если требуется строка в качестве разновидности уже имеющейся строки, то для этой цели следует создать новую строку, содержащую все необходимые изменения. А поскольку неиспользуемые строковые объекты автоматически собираются в "мусор", то о дальнейшей судьбе ненужных строк можно даже не беспокоиться.
Следует, однако, подчеркнуть, что переменные ссылки на строки (т.е. объекты типаstring)подлежат изменению, а следовательно, они могут ссылаться на другой объект. Но содержимое самого объекта типаstringне меняется после его создания.
Для того чтобы стало понятнее, почему неизменяемые строки не являются помехой, воспользуемся еще одним методом обращения со строками:Substring (). Этот метод возвращает новую строку, содержащую часть вызывающей строки. В итоге создается новый строковый объект, содержащий выбранную подстроку, тогда как исходная строка не меняется, а следовательно, соблюдается принцип постоянства строк. Ниже приведена рассматриваемая здесь форма методаSubstring():
string Substring(intиндекс_начала,intдлина)
гдеиндекс_началаобозначает начальный индекс исходной строки, адлина —длину выбираемой подстроки.
Ниже приведена программа, в которой принцип постоянства строк демонстрируется на примере использования методаSubstring().
// Применить метод Substring().
using System;
class SubStr {
static void Main() {
string orgstr = "В C# упрощается обращение со строками.";
// сформировать подстроку
string substr = orgstr.Substring(5, 20);
Console.WriteLine("orgstr: " + orgstr);
Console.WriteLine("substr: " + substr);
}
}
Вот к какому результату приводит выполнение этой программы.
orgstr: В C# упрощается обращение со строками, substr: упрощается обращение
Как видите, исходная строка из переменнойorgstrне меняется, а выбранная из нее подстрока содержится в переменнойsubstr.
И последнее замечание: несмотря на то, что постоянство строк обычно не является ни ограничением, ни помехой для программирования на С#, иногда оказывается полезно иметь возможность видоизменять строки. Для этой цели в С# имеется классStringBuilder,который определен в пространстве именSystem.Text.Этот класс позволяет создавать строковые объекты, которые можно изменять. Но, как правило, в программировании на C# исгкмьзуется типstring,а не классStringBuilder.
Применение строк в операторах switch
Объекты типаstringмогут использоваться для управления операторомswitch.Это единственный нецелочисленный тип данных, который допускается применять в оператореswitch.Благодаря такому применению строк в некоторых сложных ситуациях удается найти более простой выход из положения, чем может показаться на первый взгляд. Например, в приведенной ниже программе выводятся отдельные цифры, соответствующие словам "один", "два" и "три".
// Продемонстрировать управление оператором switch посредством строк.
using System;
class StringSwitch { static void Main() {
string[] strs = { "один", "два", "три", "два", "один" };
foreach(string s in strs) { switch (s) {
case "один":
Console.Write (1);
break; case "два":
Console.Write (2); break; case "три":
Console.Write (3); break;
}
}
Console.WriteLine ();
}
}
При выполнении этой программы получается следующий результат.
12321
ГЛАВА 8 Подробнее о методах и классах
Вданной главе возобновляется рассмотрение классов и методов. Оно начинается с пояснения механизма управления доступом к членам класса. А затем обсуждаются такие вопросы, как передача и возврат объектов, перегрузка методов, различные формы метода Main (), рекурсия и применение ключевого слова static.
Управление доступом к членам класса
Поддержка свойства инкапсуляции в классе дает два главных преимущества. Во-первых, класс связывает данные с кодом. Это преимущество использовалось в предыдущих примерах программ, начиная с главы 6. И во-вторых, класс предоставляет средства для управления доступом к его членам. Именно эта, вторая преимущественная особенность и будет рассмотрена ниже.
В языке С#, по существу, имеются два типа членов класса: открытые и закрытые, хотя в действительности дело обстоит немного сложнее. Доступ к открытому члену свободно осуществляется из кода, определенного за пределами класса. Именно этот тип члена класса использовался в рассматривавшихся до сих пор примерах программ. А закрытый член класса доступен только методам, определенным в самом классе. С помощью закрытых членов и организуется управление доступом.
Ограничение доступа к членам класса является основополагающим этапом объектно-ориентированного программирования, поскольку позволяет исключить неверное использование объекта. Разрешая доступ к закрытым
данным только с помощью строго определенного ряда методов, можно предупредить присваивание неверных значений этим данным, выполняя, например, проверку диапазона представления чисел. Для закрытого члена класса нельзя задать значение непосредственно в коде за пределами класса. Но в то же время можно полностью управлять тем, как и когда данные используются в объекте. Следовательно, правильно реализованный класс образует некий "черный ящик7', которым можно пользоваться, но внутренний механизм его действия закрыт для вмешательства извне.
Модификаторы доступа
Управление доступом в языке C# организуется с помощью четырехмодификаторов доступа:public, private, protectedиinternal.В этой главе основное внимание уделяется модификаторам доступаpublicиprivate.Модификаторprotectedприменяется только в тех случаях, которые связаны с наследованием, и поэтому речь о нем пойдет в главе 11. А модификаторinternalслужит в основном длясборки,которая в широком смысле означает в C# разворачиваемую программу или библиотеку, и поэтому данный модификатор подробнее рассматривается в главе 16.
Когда член класса обозначается спецификаторомpublic,он становится доступным из любого другого кода в программе, включая и методы, определенные в других классах. Когда же член класса обозначается спецификаторомprivate,он может быть доступен только другим членам этого класса. Следовательно, методы из других классов не имеют доступа к закрытому члену(private)данного класса. Как пояснялось в главе
6, если ни один из спецификаторов доступа не указан, член класса считается закрытым для своего класса по умолчанию. Поэтому при создании закрытых членов класса спецификатор private указывать для них необязательно.
Спецификатор доступа указывается перед остальной частью описания типа отдельного члена. Это означает, что именно с него должен начинаться оператор объявления члена класса. Ниже приведены соответствующие примеры.
public string errMsg; private double bal;
private bool isError(byte status) { // ...
Длятого чтобы стали более понятными отличия между модификаторамиpublicиprivate,рассмотрим следующий пример программы.
// Отличия между видами доступа public и private к членам класса.
using System;
class MyClass {
private int alpha; // закрытый доступ, указываемый явно
int beta; // закрытый доступ по. умолчанию
public int gamma; // открытый доступ
// Методы, которым доступны члены alpha и beta данного класса.
// Член класса может иметь доступ к закрытому члену этого же класса.
public void SetAlpha(int а) { alpha = а;
public int GetAlphaO { return alpha;
}
public void-SetBeta(int a) { beta = a;
}
public int GetBeta() { return beta;
}
}
class AccessDemo { static void Main() {
MyClass ob = new MyClassO;
// Доступ к членам alpha и beta данного класса // разрешен только посредством его методов, ob.SetAlpha(-99) ; ob.SetBeta(19) ;
Console.WriteLine("ob.alpha равно " + ob.GetAlpha());
Console.WriteLine("ob.beta равно " + ob.GetBeta());
// Следующие виды доступа к членам alpha и beta // данного класса не разрешаются.
// ob.alpha =10; // Ошибка! alpha - закрытый член!
// ob.beta =9; // Ошибка! beta - закрытый член!
// Член gamma данного класса доступен непосредственно,
// поскольку он является открытым, ob.gamma = 99;
}
}
Как видите, в классеMyClassчленalphaуказан явно какprivate,членbetaстановитсяprivateпо умолчанию, а членgammaуказан какpublic.Таким образом, членыalphaиbetaнедоступны непосредственно из кода за пределами данного класса, поскольку они являются закрытыми. В частности, ими нельзя пользоваться непосредственно в классеAccessDemo.Они доступны только с помощью таких открытых(public)методов, какSetAlpha () иGetAlpha (). Так, если удалить символы комментария в начале следующей строки кода:
// ob.alpha =10; // Ошибка! alpha - закрытый член!
то приведенная выше программа не будет скомпилирована из-за нарушения правил доступа. Но несмотря на то, что членalphaнедоступен непосредственно за пределами классаMyClass,свободный доступ к нему организуется с помощью методов, определенных в классеMyClass,как наглядно показывают методыSetAlpha() иGetAlpha (). Это же относится и к членуbeta.
Из всего сказанного выше можно сделать следующий важный вывод: закрытый член может свободно использоваться другими членами этого же класса, но недоступен для кода за пределами своего класса.
Организация закрытого и открытого доступа
Правильная организация закрытого и открытого доступа — залог успеха в объектно-ориентированном программировании. И хотя для этого не существует твердо установленных правил, ниже перечислен ряд общих принципов, которые могут служить в качестве руководства к действию.
• Члены, используемые только в классе, должны быть закрытыми.
• Данные экземпляра, не выходящие за определенные пределы значений, должны быть закрытыми, а при организации доступа к ним с помощью открытых методов следует выполнять проверку диапазона представления чисел.
• Если изменение члена приводит к последствиям, распространяющимся за пределы области действия самого члена, т.е. оказывает влияние на другие аспекты объекта, то этот член должен быть закрытым, а доступ к нему — контролируемым.
• Члены, способные нанести вред объекту, если они используются неправильно, должны быть закрытыми. Доступ к этим членам следует организовать с помощью открытых методов, исключающих неправильное их использование.
• Методы, получающие и устанавливающие значения закрытых данных, должны быть открытыми.
• Переменные экземпляра допускается делать открытыми лишь в том случае, если нет никаких оснований для того, чтобы они были закрытыми.
Разумеется, существует немало ситуаций, на которые приведенные выше принципы не распространяются, а в особых случаях один или несколько этих принципов могут вообще нарушаться. Но в целом, следуя этим правилам, вы сможете создавать объекты, устойчивые к попыткам неправильного их использования.
Практический пример организации управления доступом
Длячтобы стали понятнее особенности внутреннего механизма управления доступом, обратимся к конкретному примеру. Одним из самых характерных примеров объектно-ориентированного программирования служит класс, реализующийстек —структуру данных, воплощающую магазинный список, действующий по принципу "первым пришел — последним обслужен". Свое название он получил по аналогии со стопкой тарелок, стоящих на столе. Первая тарелка в стопке является в то же время последней использовавшейся тарелкой.
Стек служит классическим примером объектно-ориентированного программирования потому, что он сочетает в себе средства хранения информации с методами доступа к ней. Для реализации такого сочетания отлично подходит класс, в котором члены, обеспечивающие хранение информации в стеке, должны быть закрытыми, а методы доступа к ним — открытыми. Благодаря инкапсуляции базовых средств хранения информации соблюдается определенный порядок доступа к отдельным элементам стека из кода, в котором он используется.
Для стека определены две основные операции:поместитьданные в стек иизвлечьих оттуда. Первая операция помещает значение на вершину стека, а вторая — извлекает значение из вершины стека. Следовательно, операция извлечения является безвозвратной: как только значение извлекается из стека, оно удаляется и уже недоступно в стеке.
В рассматриваемом здесь примере создается классStack,реализующий функции стека. В качестве базовых средств для хранения данных в стеке служит закрытый массив. А операции размещения и извлечения данных из стека доступны с помощью открытых методов классаStack.Таким образом, открытые методы действуют по упомянутому выше’принципу "последним пришел — первым обслужен". Как следует из приведенного ниже кода, в классеStackсохраняются символы, но тот же самый механизм может быть использован и для хранения данных любого другого типа.
// Класс для хранения символов в стеке.
using System;
class Stack {
// Эти члены класса являются закрытыми, char[] stck; // массив, содержащий стек int tos; // индекс вершины стека
// Построить пустой класс Stack для реализации стека заданного размера, public Stack(int size) {
stck = new char[size]; // распределить память для стека tos = 0;
}
// Поместить символы в стек, public void Push(char ch) { if(tos==stck.Length) {
Console.WriteLine(" - Стек заполнен."); return;
}
stck[tos] = ch; tos++;
}
// Извлечь символ из стека, public char Pop() {
if(tos==0) {
Console.WriteLine(" - Стек пуст."); return (char) 0;
}
tos — ;
return stck[tos];
}
// Возвратить значение true, если стек заполнен, public bool IsFullO { return tos==stck.Length;
}
// Возвратить значение true, если стек пуст, public bool IsEmptyO { return tos==0;
// Возвратить общую емкость стека, public int Capacity() {
return stck.Length;
}
// Возвратить количество объектов, находящихся в данный момент в стеке, public int GetNum() { return tos;
}
}
Рассмотрим класс Stack более подробно. В начале этого класса объявляются две следующие переменные экземпляра.
// Эти члены класса являются закрытыми, char[] stck; // массив, содержащий стек int tos; // индекс вершины стека
Массив stck предоставляет базовые средства для хранения данных в стеке (в данном случае — символов). Обратите внимание на то, что память для этого массива не распределяется. Это делается в конструкторе класса Stack. А член tos данного класса содержит индекс вершины стека.
Оба члена, tosnstck, являются закрытыми, и благодаря этому соблюдается принцип "последним пришел — первым обслужен". Если же разрешить открытый доступ к члену stck, то элементы стека окажутся доступными не по порядку. Кроме того, член tos содержит индекс вершины стека, где находится первый обслуживаемый в стеке элемент, и поэтому манипулирование членом tos в коде, находящемся за пределами класса Stack, следует исключить, чтобы не допустить разрушение самого стека. Но в то же время члены stckntos доступны пользователю класса Stack косвенным образом с помощью различных отрытых методов, описываемых ниже.
Рассмотрим далее конструктор класса Stack.
// Построить пустой класс Stack для реализации стека заданного размера, public Stack(int size) {
stck = new char[size]; // распределить память для стека tos = 0;
}
Этому конструктору передается требуемый размер стека. Он распределяет память для базового массива и устанавливает значение переменной tos в нуль. Следовательно, нулевое значение переменной tos указывает на то, что стек пуст.
Открытый метод Push () помещает конкретный элемент в стек, как показано ниже.
// Поместить символы в стек, public void Push(char ch) { if (tos==stck.Length) {
Console.WriteLine(" - Стек заполнен."); return;
}
stck[tos] = ch; tos++;
Элемент, помещаемый в стек, передается данному методу в качестве параметраch.Перед тем как поместить элемент в стек, выполняется проверка на наличие свободного места в базовом массиве, а именно: не превышает ли значение переменнойtosдлину массиваstck.Если свободное место в массивеstckесть, то элемент сохраняется в нем по индексу, хранящемуся в переменнойtos,после чего значение этой переменной инкрементируется. Таким образом, в переменнойtosвсегда хранится индекс следующего свободного элемента массиваstck.
Для извлечения элемента из стека вызывается открытый метод Pop (), приведенный ниже.
// Извлечь символ из стека, public char Рор() { if(tos==0) {
Console.WriteLine (" - Стек пуст."); return (char) 0;
}
tos — ;
return stck[tos];
}
Вэтом методе сначала проверяется значение переменнойtos.Если оно равно нулю, значит, стек пуст.Впротивном случае значение переменнойtosдекрементируется, и затем из стека возвращается элемент по указанному индексу.
Несмотря на то что для реализации стека достаточно методов Push () и Pop (), полезными могут оказаться и другие методы. Поэтому в классе Stack определены еще четыре метода: IsFull (), IsEmpty (), Capacity () и GetNum (). Эти методы предоставляют всю необходимую информацию о состоянии стека и приведены ниже.
return tos;
}
Метод IsFull () возвращает логическое значение true, если стек заполнен, а иначе — логическое значение false. Метод IsEmpty () возвращает логическое значение true, если стек пуст, а иначе — логическое значение false. Для получения общей емкости стека (т.е. общего числа элементов, которые могут в нем храниться) достаточно вызвать методCapacity(), а для получения количества элементов, хранящихся в настоящий момент в стеке, — методGet Num (). Польза этих методов состоит в том, что для получения информации, которую они предоставляют, требуется доступ к закрытой переменнойtos.Кроме того, они служат наглядными примерами организации безопасного доступа к закрытым членам класса с помощью открытых методов.
Конкретное применение классаStackдля реализации стека демонстрируется в приведенной ниже программе.
// Продемонстрировать применение класса Stack, using System;
// Класс для хранения символов в стеке, class Stack {
// Эти члены класса являются закрытыми, char[] stck; // массив, содержащий стек int tos; // индекс вершины стека
// Построить пустой класс Stack для реализации стека заданного размера, public Stack(int size) {
stck = new char[size]; // распределить память для стека tos = 0;
}
// Поместить символы в стек, public void Push(char ch) { if(tos==stck.Length) {
Console.WriteLine (" - Стек заполнен."); return;
}
stck[tos] = ch; tos++;
}
// Извлечь символ из стека, public char Pop() {
if(tos==0) {
Console.WriteLine(" - Стек пуст."); return (char) 0;
}
tos — ;
return stck[tos];
}
// Возвратить значение true, если стек заполнен, public bool IsFullO { return tos==stck.Length;
}
// Возвратить значение true, если- стек пуст, public bool IsEmptyO {
return tos==0;
}
// Возвратить общую емкость стека, public int Capacity() {
return stck.Length;
}
// Возвратить количество объектов, находящихся в данный момент в стеке, public int GetNum() { return tos;
}
}
class StackDemo {
static void Main() {
Stack stkl = new Stack (10);
Stack stk2 = new Stack(lO);
Stack stk3 = new Stack(10);
char ch; int i;
// Поместить ряд символов в стек stkl.
Console.WriteLine("Поместить символы А-J в стек stkl."); for(i=0; !stkl.IsFull(); i++) stkl.Push((char) ('A1 + i));
if(stkl.IsFull()) Console.WriteLine("Стек stkl заполнен.");
// Вывести содержимое стека stkl.
Console.Write("Содержимое стека stkl: "); while( !stkl.IsEmpty() ) {
ch = stkl.Pop();
Console.Write(ch);
}
Console.WriteLine();
if(stkl.IsEmpty()) Console.WriteLine("Стек stkl пуст.\п");
// Поместить дополнительные символы в стек stkl.
Console.WriteLine("Вновь поместить символы А-J в стек stkl."); for(i=0; !stkl.IsFull(); i++) stkl.Push((char) ('A' + i));
// А теперь извлечь элементы из стека stkl и поместить их в стек stk2. // В итоге элементы сохраняются в стеке stk2 в обратном порядке. Console.WriteLine("А теперь извлечь символы из стека stkl\n" +
"и поместить их в стек stk2."); while( !stkl.IsEmpty() ) {
ch = stkl.Pop(); stk2.Push(ch);
Console.Write("Содержимое стека stk2: "); while( !stk2.IsEmpty() ) {
ch = stk2.Pop();
Console.Write(ch);
}
Console.WriteLine("\n");
// Поместить 5 символов в стек.
Console.WriteLine("Поместить 5 символов в стек stk3."); for(i=0; i < 5; i++)
stk3.Push((char) ('A1 + i)) ;
Console.WriteLine("Емкость стека stk3: " + stk3.Capacity()); Console.WriteLine("Количество объектов в стеке stk3: " + stk3.GetNum());
}
}
При выполнении этой программы получается следующий результат.
Поместить символы А-J в стек stkl.
Стек stkl заполнен.
Содержимое стека stkl: JIHGFEDCBA Стек stkl пуст.
Вновь поместить символы А-J в стек stkl.
А теперь извлечь символы из стека stkl и поместить их в стек stk2.
Содержимое стека stk2: ABCDEFGHIJ
Поместить 5 символов в стек stk3.
Емкость стека stk3: 10 Количество объектов в стеке stk3: 5
Передача объектов методам по ссылке
В приведенных до сих пор примерах программ при указании параметров, передаваемых методам, использовались типы значений, например int или double. Но в методах можно также использовать параметры ссылочного типа, что не только правильно, но и весьма распространено в ООП. Подобным образом объекты могут передаваться методам по ссылке. В качестве примера рассмотрим следующую программу.
// Пример передачи объектов методам по ссылке.
using System;
class MyClass { int alpha, beta;
public MyClass(int i, int j) { alpha = i; beta = j;
// Возвратить значение true, если параметр ob // имеет те же значения, что и вызывающий объект, public bool SameAs(MyClass ob) {
if ((ob.alpha == alpha) & (ob.beta == beta)) return true; else return false;
}
// Сделать копию объекта ob. public void Copy(MyClass ob) { alpha = ob.alpha; beta = ob.beta;
}
public void Show() {
Console.WriteLine("alpha: {0}, beta: {1}", alpha, beta);
}
}
class PassOb {
static void Main() {
MyClass obi = new MyClass(4, 5);
MyClass ob2 = new MyClass (6, 7);
Console.Write("obi: "); obi.Show ();
Console.Write("ob2: "); ob2.Show();
if(obi.SameAs(ob2))
Console.WriteLine("obi и ob2 имеют одинаковые значения."); else
Console.WriteLine("obi и ob2 имеют разные значения."); Console.WriteLine() ;
// А теперь сделать объект obi копией объекта ob2. obi.Copy(ob2);
Console.Write("obi после копирования: "); obi.Show();
if(obi.SameAs(ob2) )
Console.WriteLine("obi и ob2 имеют одинаковые значения."); else
Console.WriteLine("obi и ob2 имеют разные значения.");
}
}
Выполнение этой программы дает следующий результат.
obi: alpha: 4, beta: 5 ob2: alpha: 6, beta: 7
оЫ и ob2 имеют разные значения.
оЫ после копирования: alpha: 6, beta: 7 obi и оЬ2 имеют одинаковые значения.
Каждый из методовSame As() иСору() в приведенной выше программе получает ссылку на объект типаMyClassв качестве аргумента. МетодSame As() сравнивает значения переменных экземпляраalphaиbetaв вызывающем объекте со значениями аналогичных переменных в объекте, передаваемом посредством параметраob.Данный метод возвращает логическое значениеtrueтолько в том случае, если оба объекта имеют одинаковые значения этих переменных экземпляра. А методСору() присваивает значения переменныхalphaиbetaиз объекта, передаваемого по ссылке посредством параметраob,переменнымalphaиbetaиз вызывающего объекта. Как показывает данный пример, с точки зрения синтаксиса объекты передаются методам по ссылке таким же образом, как и значения обычных типов.
Способы передачи аргументов методу
Как показывает приведенный выше пример, передача объекта методу по ссылке делается достаточно просто. Но в этом примере показаны не все нюансы данного процесса. В некоторых случаях последствия передачи объекта по ссылке будут отличаться от тех результатов, к которым приводит передача значения обычного типа. Для выяснения причин этих отличий рассмотрим два способа передачи аргументов методу.
Первым способом являетсявызов по значению.В этом случае значение аргумента копируется в формальный параметр метода. Следовательно, изменения, вносимые в параметр метода, не оказывают никакого влияния на аргумент, используемый для вызова. А вторым способом передачи аргумента являетсявызов по ссылке.В данном случае параметру метода передается ссылка на аргумент, а не значение аргумента. В методе этассылкаиспользуется для доступа к конкретному аргументу, указываемому при вызове. Это означает, что изменения, вносимые в параметр, будут оказывать влияние на аргумент, используемый для вызова метода.
По умолчанию в C# используется вызов по значению, а это означает, что копия аргумента создается и затем передается принимающему параметру. Следовательно, при передаче значения обычного типа, напримерintилиdouble,все, что происходит с параметром, принимающим аргумент, не оказывает никакого влияния за пределами метода. В качестве примера рассмотрим следующую программу.
// Передача аргументов обычных типов по значению, using System;
class Test {
/* Этот метод не оказывает никакого влияния на аргументы, используемые для его вызова. */ public void NoChange(int i, int j) {
i = i + j; j = -j;
}
}
class CallByValue {
static void Main() {
Test ob = new Test();
int a = 15, b = 20;
Console.WriteLine("а и b до вызова: " + a + " " + b) ;
ob.NoChange(a, b);
Console.WriteLine("а и b после вызова: " + a + " " + b) ;
}
}
Вот какой результат дает выполнение этой программы.
а и b до вызова: 15 2 0 а и b после вызова: 15 2 0
Как видите, операции, выполняемые в методеNoChange (), не оказывают никакого влияния на значения аргументов а и Ь, используемых для вызова данного метода. Это опять же объясняется тем, что параметрам i и j переданы копии значений аргументов а и Ь, а сами аргументы а и b совершенно не зависят от параметров i и j. В частности, присваивание параметруiнового значения не будет оказывать никакого влияния на аргумент а.
Дело несколько усложняется при передаче методу ссылки на объект. В этом случае сама ссылка по-прежнему передается по значению. Следовательно, создается копия ссылки, а изменения, вносимые в параметр, не оказывают никакого влияния на аргумент. (Так, если организовать ссылку параметра на новый объект, то это изменение не повлечет за собой никаких последствий для объекта, на который ссылается аргумент.) Но главное отличие вызова по ссылке заключается в том, что изменения, происходящие с объектом, на который ссылается параметр,окажут влияниена тот объект, на который ссылается аргумент. Попытаемся выяснить причины подобного влияния.
Напомним, что при создании переменной типа класса формируется только ссылка на объект. Поэтому при передаче этой ссылки методу принимающий ее параметр будет ссылаться на тот же самый объект, на который ссылается аргумент. Это означает, что и аргумент, и параметр ссылаются на один и тот же объект и что объекты, по существу, передаются методам по ссылке. Таким образом, объект в методебудетоказывать влияние на объект, используемый в качестве аргумента. Для примера рассмотрим следующую программу.
// Передача объектов по ссылке.
using System;
class Test {
public int a, b;
public Test(int i, int j) { a = i; b = j;
/* Передать объект. Теперь переменные ob.a и ob.b из объекта, используемого в вызове метода, будут изменены. */ public void Change(Test ob) { ob.a = ob.a + ob.b; ob.b = -ob.b;
}
}
class CallByRef {
static void Main() {
Test ob = new Test(15, 20);
Console.WriteLine("ob.а и ob.b до вызова: " + ob.a + " " + ob.b);
ob.Change(ob);
Console.WriteLine("ob.а и ob.b после вызова: " + ob.a + " " + ob.b);
}
}
Выполнение этой программы дает следующий результат.
ob.a и ob.b до вызова: 15 20 ob.a и ob.b после вызова: 35 -20
Как видите, действия в методеChange() оказали в данном случае влияние на объект, использовавшийся в качестве аргумента.
Итак, подведем краткий итог. Когда объект передается методу по ссылке, сама ссылка передается по значению, а следовательно, создается копия этой ссылки. Но эта копия будет по-прежнему ссылаться на тот же самый объект, что и соответствующий аргумент. Это означает, что объекты передаются методам неявным образом по ссылке.
Использование модификаторов параметров ref и out
Как пояснялось выше, аргументы простых типов, напримерintилиchar,передаются методу по значению. Это означает, что изменения, вносимые в параметр, принимающий значение, не будут оказывать никакого влияния на аргумент, используемый для вызова. Но такое поведение можно изменить, используя ключевые словаrefиoutдля передачи значений обычных типов по ссылке. Это позволяет изменить в самом методе аргумент, указываемый при его вызове.
Прежде чем переходить к особенностям использования ключевых словrefиout,полезно уяснить причины, по которым значение простого типа иногда требуется передавать по ссылке. В общем, для этого существуют две причины: разрешить методу изменить содержимое его аргументов или же возвратить несколько значений. Рассмотрим каждую из этих причин более подробно.
Нередко требуется, чтобы метод оперировал теми аргументами, которые ему передаются. Характерным тому примером служит методSwap(), осуществляющий перестановку значений своих аргументов. Но поскольку аргументы простых типов передаются по значению, то, используя выбираемый в C# по умолчанию механизм вызова по значению для передачи аргумента параметру, невозможно написать метод, меняющий местами значения двух его аргументов, например типаint.Это затруднение разрешает модификаторref.
Как вам должно быть уже известно, значение возвращается из метода вызывающей части программы с помощью оператораreturn.Но метод может одновременно возвратить лишьоднозначение. А что, если из метода требуется возвратить два или более фрагментов информации, например, целую и дробную части числового значения с плавающей точкой? Такой метод можно написать, используя модификаторout.
Использование модификатора параметра ref
Модификатор параметраrefпринудительно организует вызов по ссылке, а не по значению. Этот модификатор указывается как при объявлении, так и при вызове метода. Для начала рассмотрим простой пример. В приведенной ниже программе создается методSqr (), возвращающий вместо своего аргумента квадрат его целочисленного значения. Обратите особое внимание на применение и местоположение модификатораref.
// Использовать модификатор ref для передачи значения обычного типа по ссылке.
using System;
class RefTest {
// Этот метод изменяет свой аргумент. Обратите // внимание на применение модификатора ref. public void Sqr(ref int i) {
i = i * i;
}
}
class RefDemo {
static void Main() {
RefTest ob = new RefTest();
int a = 10;
Console.WriteLine("а до вызрва: " + a); ob.Sqr(ref a); // обратите внимание на применение модификатора ref Console.WriteLine("а после вызова: " + а);
}
}
Как видите, модификаторrefуказывается перед объявлением параметра в самом методе и перед аргументом при вызове метода. Ниже приведен результат выполнения данной программы, который подтверждает, что значение аргументаадействительно было изменено с помощью методаSqr ().
а до вызова: 10 а после вызова: 100
Теперь, используя модификаторref,можно написать метод, переставляющий местами значения двух своих аргументов простого типа. В качестве примера ниже приведена программа, в которой метод Swap () выполняет перестановку значений двух своих целочисленных аргументов, когда он вызывается.
// Поменять местами два значения.
using System;
class ValueSwap {
// Этот метод меняет местами свои аргументы, public void Swap(ref int a, ref int b) { int t; t = a; a = b; b = t;
}
}
class ValueSwapDemo { static void Main() {
ValueSwap ob = new ValueSwap();
int x=10, у = 20;
Console.WriteLine("x и у до вызова: " + х + " " + у); ob.Swap (ref х, ref у);
Console.WriteLine("х и у после вызова: " + х + " " + у);
}
}
Вот к какому результату приводит выполнение этой программы.
х и у до вызова: 10 20 х и у после вызова: 20 10
В отношении модификатораrefнеобходимо иметь в виду следующее. Аргументу, передаваемому по ссылке с помощью этого модификатора, должно быть присвоено значениедовызова метода. Дело в том, что в методе, получающем такой аргумент в качестве параметра, предполагается, что параметр ссылается на действительное значение. Следовательно, при использовании модификатораrefв методе нельзя задать первоначальное значение аргумента.
Использование модификатора параметра out
Иногда ссылочный параметр требуется использовать для получения значения из метода, а не для передачи ему значения. Допустим, что имеется метод, выполняющий некоторую функцию, например, открытие сетевого сокета и возврат кода успешного или неудачного завершения данной операции в качестве ссылочного параметра. В этом случае методу не передается никакой информации, но в то же время он должен возвратить определенную информацию. Главная трудность при этом состоит в том, что параметр типаrefдолжен быть инициализирован определенным значением до вызова метода. Следовательно, чтобы воспользоваться параметром типаref,придется задать для аргумента фиктивное значение и тем самым преодолеть данное ограничение. Правда, в C# имеется более подходящий вариант выхода из подобного затруднения — воспользоваться модификатором параметраout.
Модификатор параметраoutподобен модификаторуref,за одним исключением: он служит только для передачи значения за пределы метода. Поэтому переменной, используемой в качестве параметраout,не нужно (да и бесполезно) присваивать какое-то значение. Более того, в методе параметрoutсчитаетсянеинициализированным, т.е. предполагается, что у него отсутствует первоначальное значение. Это означает, что значение должно быть присвоено данному параметру в методедоего завершения. Следовательно, после вызова метода параметрoutбудет содержать некоторое значение.
Ниже приведен пример применения модификатора параметраout.В этом примере программы для разделения числа с плавающей точкой на целую и дробную части используется методGetParts() из классаDecompose.Обратите внимание на то, как возвращается каждая часть исходного числа.
// Использовать модификатор параметра out.
using System;
class Decompose {
/* Разделить числовое значение с плавающей точкой на целую и дробную части. */ public int GetParts(double n, out double frac) { int whole;
whole = (int) n;
frac = n - whole; // передать дробную часть числа через параметр frac return whole; // возвратить целую часть числа
}
}
class UseOut {
static void Main() {
Decompose ob = new Decompose(); int i; double f;
i = ob.GetParts(10.125, out f) ;
Console.WriteLine("Целая часть числа равна " + i);
Console.WriteLine("Дробная часть числа равна " + f);
}
}
Выполнение этой программы дает следующий результат.
Целая часть числа равна 10 Дробная часть числа равна 0.125
МетодGet Parts() возвращает два фрагмента информации. Во-первых, целую часть исходного числового значения переменной п обычным образом с помощью оператораreturn.И во-вторых, дробную часть этого значения посредством параметраf гастипаout.Как показывает данный пример, используя модификатор параметраout,можно организовать возврат двух значений из одного и того же метода.
Разумеется, никаких ограничений на применение параметровoutв одном методе не существует. С их помощью из метода можно возвратить сколько угодно фрагментов информации. Рассмотрим пример применения двух параметровout.В этом примере программы методHasComFactor() выполняет две функции. Во-первых, он определяет общий множитель (кроме 1) для двух целых чисел, возвращая логическое значениеtrue,если у них имеется общий множитель, а иначе — логическое значениеfalse.И во-вторых,.он возвращает посредством параметров типаoutнаименьший и наибольший общий множитель двух чисел, если таковые обнаруживаются.
// Использовать два параметра типа out. using System; class Num {
/* Определить, имеется ли у числовых значений переменных х и v общий множитель. Если имеется, то * возвратить наименьший и наибольший множители посредством параметров типа out. */ public bool HasComFactor(int x, int y,
out int least, out int greatest) {
int i;
int max = x < у ? x : y; bool first = true;
least = 1; greatest = 1;
// Найти наименьший и наибольший общий множитель. for(i=2; i <= max/2 + 1; i++) {
if( ((y%i)==0) & ((x%i)==0) ) {
if (first) { least = i; first = false;
}
greatest = i;
}
}
if(least != 1) return true; else return false;
}
}
<ш
class DemoOut {
static void Main() {
Num ob = new Num(); int lcf, gcf;
if(ob.HasComFactor(231, 105, out lcf, out gcf)) {
Console.WriteLine("Наименьший общий множитель " +
"чисел 231 и 105 равен " + lcf) ;
Console.WriteLine("Наибольший общий множитель " +
"чисел 231 и 105 равен " + gcf);
}
else
Console.WriteLine("Общий множитель у чисел 35 и 49 отсутствует.");
if(ob.HasComFactor(35, 51, out lcf, out gcf)) {
Console.WriteLine("Наименьший общий множитель " +
"чисел 35 и 51 равен " + lcf);
Console.WriteLine("Наибольший общий множитель " +
"чисел 35 и 51 равен " + gcf);
}
else
Console.WriteLine("Общий множитель у чисел 35 и 51 отсутствует.");
}
}
Обратите внимание на то, что значения присваиваются переменнымlcfиgcfв методеMain () до вызова методаHasComFactor (). Если бы параметры методаHasComFactor() были типаref,а неout,это привело бы к ошибке. Данный метод возвращает логическое значениеtrueилиfalse,в зависимости от того, имеется ли общий множитель у двух целых чисел. Если он имеется, то посредством параметров типаoutвозвращаются наименьший и наибольший общий множитель этих чисел. Ниже приведен результат выполнения данной программы.
Наименьший общий множитель чисел 231 и 105 равен 3 Наибольший общий множитель чисел 231 и 105 равен 21 Общий множитель у чисел 35 и 51 отсутствует.
Использование модификаторов ref и out для ссылок на объекты
Применение модификаторовrefиoutне ограничивается только передачей значений обычных типов. С их помощью можно также передавать ссылки на объекты. Если модификаторrefилиoutуказывает на ссылку, то сама ссылка передается по ссылке. Это позволяет изменить в методе объект, на который указывает ссылка. Рассмотрим в качестве примера следующую программу, в которой ссылочные параметры типаrefслужат для смены объектов, на которые указывают ссылки.
// Поменять местами две ссылки.
using System;
class RefSwap { int a, b;
public RefSwap(int i, int j) { a = i; b = j;
public void Show() {
Console.WriteLine("a: {0}, b: {1}", a, b);
}
// Этот метод изменяет свои аргументы.
public void Swap(ref RefSwap obi, ref RefSwap ob2) {
RefSwap t;
t = obi; obi = ob2; ob2 = t;
}
}
class RefSwapDemo { static void Main() {
RefSwap x = new RefSwap(1, 2);
RefSwap у = new RefSwap(3, 4);
Console.Write("x до вызова: ") ; x.Show ();
Console.Write("у до вызова: "); у.Show();
Console.WriteLine() ;
// Смена объектов, на которые ссылаются аргументы х и у. х.Swap (ref х, ref у);
Console.Write("х после вызова: "); х.Show();
Console.Write("у после вызова: ") ; у.Show ();
}
}
При выполнении этой программы получается следующий результат.
х до вызова: а: 1, Ь: 2 у до вызова: а: 3, Ь: 4
х после вызова: а: 3, Ь: 4 у после вызова: а: 1, Ь: 2
В данном примере в методеSwap() выполняется смена объектов, на которые ссылаются два его аргумента. До вызова методаSwap() аргумент х ссылается на объект, содержащий значения 1 и 2, тогда как аргументуссылается на объект, содержащий значения 3 и 4. А после вызова методаSwap() аргумент х ссылается на объект, содержащий значения 3 и 4, тогда как аргументуссылается на объект, содержащий значения 1 и 2. Если бы не параметры типаref,то перестановка в методеSwap() не имела бы никаких последствий за пределами этого метода. Для того чтобы убедиться в этом, исключите параметры типаrefиз методаSwap ().
Использование переменного числа аргументов
При создании метода обычно заранее известно число аргументов, которые будут переданы ему, но так бывает не всегда. Иногда возникает потребность создать метод, которому можно было бы передать произвольное число аргументов. Допустим, что требуется метод, обнаруживающий наименьшее среди ряда значений. Такому методу можно было бы передать не менее двух, трех, четырех или еще больше значений. Но в любом случае метод доАжен возвратить наименьшее из этих значений. Такой метод нельзя создать, используя обычные параметры. Вместо этого придется воспользоваться специальным типом параметра, обозначающим произвольное число параметров. И это делается с помощью создаваемого параметра типаparams.
Для объявления массива параметров, способного принимать от нуля до нескольких аргументов, служит модификаторparams.Число элементов массива параметров будет равно числу аргументов, передаваемых методу. А для получения аргументов в программе организуется доступ к данному массиву.
Ниже приведен пример программы, в которой модификаторparamsиспользуется для создания методаMinVal (), возвращающего наименьшее среди ряда заданных значений.
// Продемонстрировать применение модификатора params.*
using System;
class Min {
public int MinVal(params int[] nums) { int m;
if(nums.Length ==0) {
Console.WriteLine("Ошибка: нет аргументов."); return 0;
}
m = nums[0];
for(int i=l; i < nums.Length; i++) » if(nums[i] < m) m = nums[i];
return m;
}
}
class ParamsDemo { static void Main() {
Min ob = new Min(); int min;
int a = 10, b = 20;
// Вызвать метод с двумя значениями, min = ob.MinVal(a, b);
Console.WriteLine("Наименьшее значение равно " + min);
// Вызвать метод с тремя значениями, min = ob.MinVal(a, b, -1);
Console.WriteLine("Наименьшее значение равно " + min);
//Вызвать метод с пятью значениями, min = ob.MinVal(18, 23, 3, 14, 25);
Console.WriteLine("Наименьшее значение равно " + min);
// Вызвать метод с массивом целых значений, int[] args = { 45, 67, 34, 9, 112, 8 }; min = ob.MinVal(args);
Console.WriteLine("Наименьшее значение равно " + min);
}
}
При выполнении этой программы получается следующий результат.
Наименьшее значение равно 10 Наименьшее значение равно -1 Наименьшее значение равно 3 Наименьшее значение равно 8
Всякий раз, когда вызывается методMinVal(), ему передаются аргументы в массивеnums.Длина этого массива равна числу передаваемых аргументов. Поэтому с помощью методаMinVal() можно обнаружить наименьшее среди любого числа значений.
Обратите внимание на последний вызов методаMinVal(). Вместо отдельных значений в данном случае передается массив, содержащий ряд значений. И такая передача аргументов вполне допустима. Когда создается параметр типаparams,он воспринимает список аргументов переменной длины или же массив, содержащий аргументы.
Несмотря на то что параметру типаparamsможет быть передано любое число аргументов, все они должны иметь тип массива, указываемый этим параметром. Например, вызов методаMinVal()
min = ob.MinVal(1, 2.2); // Неверно!
считается недопустимым, поскольку нельзя автоматически преобразовать типdouble(значение 2.2) в типint,указанный для массиваnumsв методеMinVal ().
Пользоваться модификаторомparamsследует осторожно, соблюдая граничные условия, так как параметр типаparamsможет принимать любое число аргументов — даженулевое! Например, вызов методаMinVal() в приведенном ниже фрагменте кода считается правильным с точки зрения синтаксиса С#.
min = ob.MinVal(); // нет аргументов min = ob.MinVal(3); // 1 аргумент
Именно поэтому в методеMinVal() организована проверка на наличие в массивеnumsхотя бы одного элемента перед тем, как пытаться получить доступ к этому элементу. Если бы такой проверки не было, то при вызове методаMinVal() без аргументов возникла бы исключительная ситуация во время выполнения. (Подробнее об исключительных ситуациях речь пойдет в главе 13.) Больше того, код методаMinVal() написан таким образом, чтобы его можно было вызывать с одним аргументом. В этом случае возвращается этот единственный аргумент.
У метода могут быть как обычные параметры, так и параметр переменной длины. В качестве примера ниже приведена программа, в которой методShowArgs()
принимает один параметр типа string, а также целочисленный массив в качестве параметра типа params.
// Использовать обычный параметр вместе с параметром // переменной длины типа params.
using System;
class MyClass {
public void ShowArgs(string msg, params int[] nums) {
Console.Write(msg + ": ");
foreach(int i in nums)
Console.Write (i + " ") ;
Console.WriteLine ();
}
}
class ParamsDemo2 { static void Main() {
MyClass ob = new MyClass ();
ob.ShowArgs("Это ряд целых чисел",
1, 2, 3, 4, 5);
ob.ShowArgs("А это еще два целых числа ",
17, 20);
}
}
Вот какой результат дает выполнение этой программы.
Это ряд целых чисел: 1,2,3, 4, 5 А это еще два целых числа: 17, 20
В тех случаях, когда у метода имеются обычные параметры, а также параметр переменной длины типа params, он должен быть указан последним в списке параметров данного метода. Но в любом случае параметр типа params должен быть единственным.
Возврат объектов из методов
Метод может возвратить данные любого типа, в том числе и тип класса. Ниже в качестве примера приведен вариант класса Rect, содержащий метод Enlarge (), в котором строится прямоугольник с теми же сторонами, что и у вызывающего объекта прямоугольника, но пропорционально увеличенными на указанный коэффициент.
// Возвратить объект из метода.
using System;
class Rect { int width; int height;
public Rect(int w, int h) { width = w; height = h;
}
public int Area() {
return width * height;
}
public void Show() {
Console.WriteLine(width + " " + height);
}
/* Метод возвращает прямоугольник со сторонами, пропорционально увеличенными на указанный коэффициент по сравнению с вызывающим объектом прямоугольника. */ public Rect Enlarge(int factor) {
return new Rect(width * factor, height * factor);
}
}
class RetObj {
static void Main() {
Rect rl = new Rect(4, 5);
Console.Write("Размеры прямоугольника rl: "); rl.Show ();
Console.WriteLine("Площадь прямоугольника rl: " + rl.AreaO);
Console.WriteLine();
// Создать прямоугольник в два раза больший прямоугольника rl.
Rect r2 = rl.Enlarge(2);
Console.Write("Размеры прямоугольника г2: "); r2.Show();
Console.WriteLine("Площадь прямоугольника г2: " + г2.Агеа());
}
}
Выполнение этой программы дает следующий результат.
Размеры прямоугольника rl: 4 5 Площадь прямоугольника rl: 20
Размеры прямоугольника г2: 8 10 Площадь прямоугольника г2: 80
Когда метод возвращает объект, последний продолжает существовать до тех пор, пока не останется ссылок на него. После этого он подлежит сборке как "мусор". Следовательно, объект не уничтожается только потому, что завершается создавший его метод.
Одним из практических примеров применения возвращаемых данных типа объектов служитфабрика класса, которая представляет собой метод, предназначенный для построения объектов его же класса. В ряде случаев предоставлять пользователям класса доступ к его конструктору нежелательно из соображений безопасности или же потому, что построение объекта зависит от некоторых внешних факторов. В подобных случаях для построения объектов используется фабрика класса. Обратимся к простому примеру.
// Использовать фабрику класса.
using System;
class MyClass {
int a, b; // закрытые члены класса
// Создать фабрику для класса MyClass. public MyClass Factory(int i, int j) {
MyClass t = new MyClass ();
t.a = i; t.b = j;
return t; // возвратить объект
}
public void Show() {
Console.WriteLine("а и b: " + a + " " + b);
}
}
class MakeObjects { static void Main() {
MyClass ob = new MyClass (); int i, j;
// Сформировать объекты, используя фабрику класса. for(i=0, j =10; i < 10; i++, j —) {
MyClass anotherOb = ob.Factory(i, j); // создать объект anotherOb.Show();
}
Console.WriteLine () ;
}
}
Вот к какому результату приводит выполнение этого кода.
а и Ь: 0 10 а и Ь: 19 а и Ь: 2 8 а и b: 3 7 а и Ь: 4 6 а и Ь: 5 5 а и Ь: 6 4
а и b: 7 3 а и b: 8 2 а и b: 9 1
Рассмотрим данный пример более подробно. В этом примере конструктор для класса МуС lass не определяется, и поэтому доступен только конструктор, вызываемый по умолчанию. Это означает, что значения переменныхаиbнельзя задать с помощью конструктора. Но в фабрике классаFactory() можно создать объекты, в которых задаются значения переменныхаи Ь. Более того, переменныеаи b являются закрытыми, и поэтому их значения могут быть заданы только с помощью фабрики классаFactory ().
В методеMain() получается экземпляр объекта классаМуС lass,а его фабричный метод используется в циклеforдля создания десяти других объектов. Ниже приведена строка кода, в которой создаются эти объекты.
MyClass anotherOb = ob.Factory(i, j); // создать объект
На каждом шаге итерации цикла создается переменная ссылки на объектanotherOb,которой присваивается ссылка на объект, формируемый фабрикой класса. По завершении каждого шага итерации цикла переменнаяanotherObвыходит за пределы области своего действия, а объект, на который она ссылается, утилизируется.
Возврат массива из метода
В C# массивы реализованы в виде объектов, а это означает, что метод может также возвратить массив. (В этом отношении C# отличается от C++, где не допускается возврат массивов из методов.) В качестве примера ниже приведена программа, в которой методFindFactors() возвращает массив, содержащий множители переданного ему аргумента.
// Возвратить массив из метода, using System; class Factor {
*
/* Метод возвращает массив facts, содержащий множители аргумента num.
При возврате из метода параметр numfactors типа out будет содержать количество обнаруженных множителей. */ public int[] FindFactors(int num, out int numfactors) {
int[] facts = new int[80]; // размер массива 80 выбран произвольно int i, j;
// Найти множители и поместить их в массив facts. for(i=2, j=0; i < num/2 + 1; i++) if( (num%i)==0 ) {
facts[j] = i; j++;
}
numfactors = j ; return facts;
class FindFactors { static void Main() {
Factor f = new Factor(); int numfactors; inti] factors;
factors = f.FindFactors(1000, out numfactors);
Console.WriteLine("Множители числа 1000: "); for(int i=0; i < numfactors; i++)
Console.Write(factors[i] + " ") ;
Console.WriteLine() ;
}
}
При выполнении этой программы получается следующий результат.
Множители числа 1000:
2 4 5 8 10 20 25 40 50 100 125 200 250 500
В классеFactorметодFindFactors() объявляется следующим образом.
public int[] FindFactors(int num, out int numfactors) {
Обратите внимание на то, как указывается возвращаемый массив типаint.Этот синтаксис можно обобщить. Всякий раз, когда метод возвращает массив, он указывается аналогичным образом, но с учетом его типа и размерности. Например, в следующей строке кода объявляется методsomeMeth (), возвращающий двумерный массив типаdouble.
public doublet,] someMeth() { // ...
Перегрузка методов
В C# допускается совместное использование одного и того же имени двумя или более методами одного и того же класса, при условии, что их параметры объявляются по-разному. В этом случае говорят, что методыперегружаются, а сам процесс называетсяперегрузкой методов.Перегрузка методов относится к одному из способов реализации полиморфизма в С#.
В общем, для перегрузки метода достаточно объявить разные его варианты, а об остальном позаботится компилятор. Но при этом необходимо соблюсти следующее важное условие: тип или число параметров у каждого метода должны быть разными. Совершенно недостаточно, чтобы два метода отличались только типами возвращаемых значений. Они должны также отличаться типами или числом своих параметров. (Во всяком случае, типы возвращаемых значений дают недостаточно сведений компилятору С#, чтобы решить, какой именно метод следует использовать.) Разумеется, перегружаемые методы могут отличаться и типами возвращаемых значений. Когда вызывается перегружаемый метод, то выполняется тот его вариант, параметры которого соответствуют (по типу и числу) передаваемым аргументам.
Ниже приведен простой пример, демонстрирующий перегрузку методов.
// Продемонстрировать перегрузку методов.
«
using System;
class Overload {
public void OvlDemo() {
Console.WriteLine("Без параметров");
}
// Перегрузка метода OvlDemo с одним целочисленным параметром, public void OvlDemo(int a) {
Console.WriteLine("Один параметр: " + a);
}
// Перегрузка метода OvlDemo с двумя целочисленными параметрами, public int OvlDemo(int a, int b) {
Console.WriteLine("Два параметра: " + a + " " + b); return a + b;
}
// Перегрузка метода OvlDemo с двумя параметрами типа double, public double OvlDemo(double a, double b) {
Console.WriteLine("Два параметра типа double: " + a + " "+ b) ;
return a + b;
}
class OverloadDemo {• static void Main() {
Overload ob = new Overload(); int resl; double resD;
// Вызвать все варианты метода OvlDemo(). ob.OvlDemo();
Console.WriteLine();
ob.OvlDemo(2);
Console.WriteLine ();
resl = ob.OvlDemo(4, 6);
Console.WriteLine("Результат вызова метода ob.OvlDemo(4, 6): " + Console.WriteLine ();
resl
" +
resD = ob.OvlDemo(1.1, 2.32);
Console.WriteLine("Результат вызова метода ob.OvlDemo(1.1, 2.32): resD);
}
}
Вот к какому результату приводит выполнение приведенного выше кода.
Без параметров Один параметр: 2 Два параметра: "4 6
Результат вызова метода ob.OvlDemo(4, 6): 10 Два параметра типа double: 1.1 2.32
Результат вызова метода ob.OvlDemo(1.1, 2.32): 3.42
Как видите, методOvlDemo() перегружается четыре раза. Первый его вариант не получает параметров, второй получает один целочисленный параметр, третий — два целочисленных параметра, а четвертый — два параметра типаdouble.Обратите также внимание на то, что два первых варианта методаOvlDemo() возвращают значение типаvoid,а по существу, не возвращают никакого значения, а два других — возвращают конкретное значение. И это совершенно допустимо, но, как пояснялось выше, тип возвращаемого значения не играет никакой роли для перегрузки метода. Следовательно, попытка использовать два разных (по типу возвращаемого значения) варианта методаOvlDemo() в приведенном ниже фрагменте кода приведет к ошибке.
// Одно объявление метода OvlDemo(int) вполне допустимо, public void OvlDemo(int a) {
Console.WriteLine("Один параметр: " + a);
}
/* Ошибка! Два объявления метода OvlDemo(int) не допускаются, хотя они и возвращают разнотипные значения. */ public int OvlDemo(int a) {
Console.WriteLine("Один параметр: " + a); return a * a;
}
Как следует из комментариев к приведенному выше коду, отличий в типах значений, возвращаемых обоими вариантами методаOvlDemo (), оказывается недостаточно для перегрузки данного метода.
И как пояснялось в главе 3, в C# предусмотрен ряд неявных (т.е. автоматических) преобразований типов. Эти преобразования распространяются также на параметры перегружаемых методов. В качестве примера рассмотрим следующую программу.
// Неявные преобразования типов могут повлиять на // решение перегружать метод.
using System;
class Overload2 {
public void MyMeth(int x) {
Console.WriteLine("В методе MyMeth(int): " + x);
}
public void MyMeth(double x) {
Console.WriteLine("В методе MyMeth(double): " + x);
class TypeConv {
static void Main() {
0verload2 ob = new 0verload2();
int i = 10; double d = 10.1;
byte b = 99; short s = 10; float f = 11.5F;
ob.MyMeth(i); // вызвать метод ob.MyMeth(int)
ob.MyMeth(d); // вызвать метод ob.MyMeth(double)
ob.MyMeth(b); // вызвать метод ob.MyMeth(int) — с преобразованием типа
ob.MyMeth(s); // вызвать метод ob.MyMeth(int) — с преобразованием типа
ob.MyMeth(f); // вызвать метод ob.MyMeth(double) — с преобразованием типа
}
}
При выполнении этой программы получается следующий результат.
В методе MyMeth(int): 10 В методе MyMeth(double): 10.1 В методе MyMeth(int): 99 В методе MyMeth(int): 10 В методе MyMeth(double): 11.5
В данном примере определены только два варианта методаMyMeth(): с параметром типаintи с параметром типаdouble.Тем не менее методуMyMeth() можно передать значение типаbyte, shortилиfloat.Так, если этому методу передается значение типаbyteилиshort,то компилятор C# автоматически преобразует это значение в типintи в итоге вызывается вариантMyMeth (int)данного метода. А если ему передается значение типаfloat,то оно преобразуется в типdoubleи в результате вызывается вариантMyMeth (double)данного метода.
Следует, однако, иметь в виду, что неявные преобразования типов выполняются лишь в том случае, если отсутствует точное соответствие типов параметра и аргумента. В качестве примера ниже приведена чуть измененная версия предыдущей программы, в которую добавлен вариант методаMyMeth (), где указывается параметр типаbyte.
// Добавить метод MyMeth(byte).
using System;
class Overload2 {
public void MyMeth(byte x) {
Console.WriteLine("В методе MyMeth(byte): " + x);
}
public void MyMeth(int x) {
Console.WriteLine("В методе MyMeth(int): " + x) ;
}
public void MyMeth(double x) {
Console.WriteLine("Вметоде MyMeth(double): " +x); '
}
}
class TypeConv {
static void Main() {
0verload2 ob = new 0verload2();
int i = 10; double d = 10.1;
byte b = 99; short s = 10; float f = 11.5F;
ob.MyMeth(i); // вызвать метод ob.MyMeth(int)
ob.MyMeth(d); // вызвать метод ob.MyMeth(double)
ob.MyMeth(b); // вызвать метод ob.MyMeth(byte) —
// на этот раз без преобразования типа
ob.MyMeth(s); // вызвать метод ob.MyMeth(int) — с преобразованием типа ob.MyMeth(f); // вызвать метод ob.MyMeth(double) — с преобразованием типа
}
}
Выполнение этой программы приводит к следующему результату.
В методе MyMeth(int): 10 В методе MyMeth(double): 10.1 В методе MyMeth(byte): 99 В методе MyMeth(int): 10 В методе MyMeth(double): 11.5
В этой программе присутствует вариант методаMyMeth(), принимающий аргумент типаbyte,поэтому при вызове данного метода с аргументом типаbyteвыбирается его вариантMyMeth (byte) без автоматического преобразования в типint.
Оба модификатора параметров,refиout,также учитываются, когда принимается решение о перегрузке метода. В качестве примера ниже приведен фрагмент кода, в котором определяются два совершенно разных метода.
public void MyMeth(int x) {
Console.WriteLine("В методе MyMeth(int): " + x);
}
public void MyMeth(ref int x) {
Console.WriteLine("В методе MyMeth(ref int): " + x);
}
Следовательно, при обращении
ob.MyMeth(i)
вызывается методMyMeth (int x), но при обращенииob.MyMeth(ref i)
вызывается методMyMe th(ref intx).
Несмотря на то что модификаторы параметровrefиoutучитываются, когда принимается решение о перегрузке метода, отличие между ними не столь существенно. Например, два следующих варианта методаMyMeth() оказываются недействительными.
I
// Неверно!
public void MyMeth(out int x) {//...
public void MyMeth(ref int x) { // . . . 1
В данном случае компилятор не в состоянии различить два варианта одного и того же методаMyMeth() только на основании того, что в одном из них используется параметрout,а в другом — параметрref.
Перегрузка методов поддерживает свойство полиморфизма, поскольку именно таким способом в C# реализуется главный принцип полиморфизма: один интерфейс — множество методов. Для того чтобы стало понятнее, как это делается, обратимся к конкретному примеру. В языках программирования, не поддерживающих перегрузку методов, каждому методу должно быть присвоено уникальное имя. Но в программи-« ровании зачастую возникает потребность реализовать по сути один и тот же метод для обработки разных типов данных. Допустим, что требуется функция, определяющая абсолютное значение. В языках, не поддерживающих перегрузку методов, обычно приходится создавать три или более вариантов такой функции с несколько отличающимися, но все же разными именами. Например, в С функцияabs() возвращает абсолютное значение целого числа, функцияlabs() — абсолютное значение длинного целого числа, а функцияf abs () — абсолютное значение числа с плавающей точкой обычной (одинарной) точности.
В С перегрузка не поддерживается, и поэтому у каждой функции должно быть свое, особое имя, несмотря на то, что все упомянутые выше функции, по существу, делают одно и то же — определяют абсолютное значение. Но это принципиально усложняет положение, поскольку приходится помнить имена всех трех функций, хотя они реализованы по одному и тому же основному принципу. Подобные затруднения в C# не возникают, поскольку каждому методу, определяющему абсолютное значение, может быть присвоено одно и то же имя. И действительно, в состав библиотеки классов для среды .NET Framework входит методAbs (), который перегружается в классеSystem.Mathдля обработки данных разных числовых типов. Компилятор C# сам определяет, какой именно вариант методаAbs() следует вызывать, исходя из типа передаваемого аргумента.
Главная ценность перегрузки заключается в том, что она обеспечивает доступ к связанным вместе методам по общему имени. Следовательно, имяAbsобозначает общее выполняемое действие, а компилятор сам выбирает конкретный вариант метода по обстоятельствам. Благодаря полиморфизму несколько имен сводятся к одному. Несмотря на всю простоту рассматриваемого здесь примера, продемонстрированный в нем принцип полиморфизма можно расширить, чтобы выяснить, каким образом перегрузка помогает справляться с намного более сложными ситуациями в программировании.
Когда метод перегружается, каждый его вариант может выполнять какое угодно действие. Для установления взаимосвязи между перегружаемыми методами не существует какого-то одного правила, но с точки зрения правильного стиля программирования перегрузка методов подразумевает подобную взаимосвязь. Следовательно, использовать одно и то же имя для несвязанных друг с другом методов не следует, хотя это и возможно. Например, имяSqrможно было бы выбрать для методов, возвращающих квадрат и квадратный корень числа с плавающей точкой. Но ведь это
принципиально разные операции. Такое применение перегрузки методов противоречит ее первоначальному назначению. На практике перегружать следует только тесно связанные операции.
В C# определено понятиесигнатуры, обозначающее имя метода и список его параметров; Применительно к перегрузке это понятие означает, что в одном классе не должно существовать двух методов с одной и той же сигнатурой. Следует подчеркнуть, что в сигнатуру не входит тип возвращаемого значения, поскольку он не учитывается, когда компилятор C# принимает решение о перегрузке метода. В сигнатуру не входит также модификатор params.
Перегрузка конструкторов
Как и методы, конструкторы также могут перегружаться. Это дает возможность конструировать объекты самыми разными способами. В качестве примера рассмотрим следующую программу.
// Продемонстрировать перегрузку конструктора.
using System;
class MyClass { public int x;
public MyClass() {
Console.WriteLine("В конструкторе MyClass()."); x = 0;
}
public MyClass(int i) {
Console.WriteLine("В конструкторе MyClass(int)."); x = i ;
}
public MyClass(double d) {
Console.WriteLine("В конструкторе MyClass(double)."); x = (int) d;
}
public MyClass(int i, int j) {
Console.WriteLine("В конструкторе MyClass(int, int)."); x = i * j;
}
}
class OverloadConsDemo { static void MainO- {
MyClass tl = new MyClass ();
MyClass t2 = new MyClass(88);
MyClass t3 = new MyClass(17.23);
MyClass t4 = new MyClass(2, 4);
Console.WriteLine("tl.x: " + tl.x);
Console.WriteLine("t2.х: " + t2.x);
Console.WriteLine("t3.x: " + t3.x);
Console.WriteLine("t4.x: " + t4.x);
}
}
При выполнении этой программы получается следующий результат.
В конструкторе MyClass().
В конструкторе MyClass (int) .
В конструкторе MyClass(double).
В конструкторе MyClass (int, int). tl.x: О t2.x: 88 t3.x: 17 t4.x: 8
В данном примере конструктор MyClass () перегружается четыре раза, всякий раз конструируя объект по-разному. Подходящий конструктор вызывается каждый раз, исходя из аргументов, указываемых при выполнении оператора new. Перегрузка конструктора класса предоставляет пользователю этого класса дополнительные преимущества в конструировании объектов.
Одна из самых распространенных причин для перегрузки конструкторов заключается в необходимости предоставить возможность одним объектам инициализировать другие. В качестве примера ниже приведен усовершенствованный вариант разработанного ранее класса Stack, позволяющий конструировать один стек из другого.
// Класс для хранения символов в стеке.
using System;
class Stack {
// Эти члены класса являются закрытыми, char[] stck; // массив, содержащий стек int tos; // индекс вершины стека
// Сконструировать пустой объект класса Stack по заданному размеру стека, public Stack(int size) {
stck = new char[size]; // распределить память для стека tos = 0;
}
// Сконструировать объект класса Stack из существующего стека, public Stack(Stack ob) {
// Распределить память для стека, stck = new char[ob.stck.Length];
// Скопировать элементы в новый стек, for (int i=0; i < ob.tos; i++) stck[i] = ob.stck[i];
// Установить переменную tos для нового стека, tos = ob.tos;
// Поместить символы в стек, public void Push(char ch) { if(tos==stck.Length) {
Console.WriteLine(" - Стек заполнен."); return; -
}
stck[tos] = ch; tos++;
}
// Извлечь символ из стека, public char Pop () {
if(tos==0) {
Console.WriteLine (" - Стек пуст."); return (char) 0;
}
tos—;
return stck[tos];
}
// Возвратить значение true, если стек заполнен, public bool IsFullO { return tos==stck.Length;
}
// Возвратить значение true, если стек пуст, public bool IsEmptyO { return tos==0;
}
// Возвратить общую емкость стека, public int Capacity() {
return stck.Length;
}
// Возвратить количество объектов, находящихся в настоящий момент в стеке, public int GetNum() { return tos;
}
}
// Продемонстрировать применение класса Stack.
class StackDemo {
static void Main() {
Stack stkl = new Stack(10); char ch; int i;
// Поместить ряд символов в стек stkl.
Console.WriteLine("Поместить символы А-J в стек stkl."); for(i=0; !stkl.IsFull(); i++)
stkl.Push((char) ('A' + i));
// Создать копию стека stckl.
Stack stk2 = new Stack(stkl);
// Вывести содержимое стека stkl.
Console.Write("Содержимое стека stkl: "); while ( !stkl.IsEmpty() ) {
ch = stkl.Pop ();
Console.Write(ch);
}
Console.WriteLine ();
Console.Write("Содержимое стека stk2: "); while ( !stk2.IsEmpty() ) {
ch = stk2.Pop ();
Console.Write(ch);
}
Console.WriteLine ("\n");
}
}
Результат выполнения этой программы приведен ниже.
Поместить символы А-J в стек stkl.
Содержимое стека stkl: JIHGFEDCBA Содержимое стека stk2: JIHGFEDCBA
В классеStackDemoсначала конструируется первый стек(stkl),заполняемый символами. Затем этот стек используется, для конструирования второго стека(stk2).Это приводит к выполнению следующего конструктора классаStack.
// Сконструировать объект класса Stack из существующего стека, public Stack(Stack ob) {
// Распределить память для стека, stck = new char[ob.stck.Length];
// Скопировать элементы в новый стек, for (int i=0; i < ob.tos; i++) stck[i] = ob.stck[i];
// Установить переменйую tos для нового стека, tos = ob.tos;
}
В этом конструкторе сначала распределяется достаточный объем памяти для массива, чтобы хранить в нем элементы стека, передаваемого в качестве аргументаob.Затем содержимое массива, образующего стекob,копируется в новый массив, после чего соответственно устанавливается переменнаяtos,содержащая индекс вершины стека. По завершении работы конструктора новый и исходный стеки существуют как отдельные, хотя и одинаковые объекты.
Вызов перегружаемого конструктора с помощью ключевого слова this
Когда приходится работать с перегружаемыми конструкторами, то иногда очень полезно предоставить возможность одному конструктору вызывать другой. В C# это дается с помощью ключевого слова this. Ниже приведена общая форма такого вызова.
имя_конструктора{список_параметров1) :this (список_параметров2) {
II... Тело конструктора, которое может быть пустым.
}
В исходном конструкторе сначала выполняется перегружаемый конструктор, список параметров которого соответствует критериюсписок_параметров2, азатем все остальные операторы, если таковые имеются в исходном конструкторе. Ниже приведен соответствующий пример.
// Продемонстрировать вызов конструктора с помощью ключевого слова this.
using System;
class XYCoord { public int x, y;
public XYCoord() : this(0, 0) {
Console.WriteLine("В конструкторе XYCoord()");
}
public XYCoord(XYCoord obj) : this(obj.x, obj.y) {
Console.WriteLine("В конструкторе XYCoord(obj)");
}
public XYCoord(int i, int j) {
Console.WriteLine("В конструкторе XYCoord(int, int)"); x = i;
У = j;
}
}
class OverloadConsDemo { static void Main() {
XYCoord tl = new XYCoord();
XYCoord t2 = new XYCoord(8, 9);
XYCoord t3 = new XYCoord(t2);
Console.WriteLine("tl.x,
tl.y:
" +
tl.x
+ \
" +
tl.y);
Console.WriteLine("t2.x,
t2. у:
" +
t2.x
+",
" +
t2.y);
Console.WriteLine("t3.x,
t3.y:
" +
t3. x
+ ",
" +
t3.y);
}
}
Выполнение этого кода приводит к следующему результату.
В конструкторе XYCoord(int, int)
В конструкторе XYCoord()
В конструкторе XYCoord(int, int)
В конструкторе XYCoord(int, int)
В конструкторе XYCoord(obj)
tl.x, tl.y: 0, 0 t2 . x, t2 . у: 8, 9 t3.x, t3.у: 8, 9
Код в приведенном выше примере работает следующим образом. Единственным конструктором, фактически инициализирующим поляхиув классеXYCoord,является конструкторXYCoord(int, int).А два других конструктора просто вызывают этот конструктор с помощью ключевого словаthis.Например, когда создается объект
11, то вызывается его конструкторXYCoord(), что приводит к вызовуthis(0, 0),
который в данном случае преобразуется в вызов конструктораXYCoord (0,0). То же самое происходит и при создании объектаt2.
Вызывать перегружаемый конструктор с помощью ключевого словаthisполезно, в частности, потому, что он позволяет исключить ненужное дублирование кода. В приведенном выше примере нет никакой необходимости дублировать во всех трех конструкторах одну и ту же последовательность инициализации, и благодаря применению ключевого словаthisтакое дублирование исключается. Другое преимущество организации подобного вызова перезагружаемого конструктора заключается в возможности создавать конструкторы с задаваемыми "по умолчанию" аргументами, когда эти аргументы не указаны явно. Ниже приведен пример создания еще одного конструктораXYCoord.
public XYCoord(int х) : this(х, х) { }
По умолчанию в этом конструкторе для координатыуавтоматически устанавливается то же значение, что и для координатыу.Конечно, пользоваться такими конструкциями с задаваемыми "по умолчанию" аргументами следует благоразумно и осторожно, чтобы не ввести в заблуждение пользователей классов.
Инициализаторы объектов
Инициализаторы объектовпредоставляют еще один способ создания объекта и инициализации его полей и свойств. (Подробнее о свойствах речь пойдет в главе 10.) Если используются инициализаторы объектов, то вместо обычного вызова конструктора класса указываются имена полей или свойств, инициализируемых первоначально задаваемым значением. Следовательно, синтаксис инициализатора объекта предоставляет альтернативу явному вызову конструктора класса. Синтаксис инициализатора объекта используется главным образом при создании анонимных типов в LINQ-выражениях. (Подробнее об анонимных типах и LINQ-выражениях — в главе 19.) Но поскольку инициализаторы объектов можно, а иногда и должно использовать в именованном классе, то ниже представлены основные положения об инициализации объектов.
Обратимся сначала к простому примеру.
// Простой пример, демонстрирующий применение инициализаторов объектов.
using System;
class MyClass { public int Count; public string Str;
class ObjlnitDemo { static void Main() {
// Сконструировать объект типа MyClass, используя инициализаторы объектов.
MyClass obj = new MyClass { Count = 100, Str = "Тестирование" };
Console.WriteLine(obj.Count + " " + obj.Str);
}
}
Выполнение этого кода дает следующий результат.
100 Тестирование
Как показывает результат выполнения приведенного выше кода, переменная экземпляраobj .Countинициализирована значением100,а переменная экземпляраobj . Str— символьной строкой "Тестирование". Но обратите внимание на то, что в классеMyClassотсутствуют явно определяемые конструкторы и не используется обычный синтаксис конструкторов. Вместо этого объектobjклассаMyClassсоздается с помощью следующей строки кода.
MyClass obj = new MyClass { Count = 100, Str = "Тестирование" };
В этой строке кода имена полей указываются явно вместе с их первоначальными значениями. Это приводит к тому, что сначала конструируется экземпляр объекта типаMyClass(с помощью неявно вызываемого по умолчанию конструктора), а затем задаются первоначальные значения переменныхCountиStrданного экземпляра.
Следует особо подчеркнуть, что порядок указания инициализаторов особого значения не имеет. Например, объектobjможно было бы инициализировать и так, как показано ниже.
MyClass obj = new MyClass { Str = "Тестирование", Count = 100 };
В этой строке кода инициализация переменной экземпляраStrпредшествует инициализации переменной экземпляраCount,а в приведенном выше коде все происходило наоборот. Но в любом случае результат получается одинаковым.
Ниже приведена общая форма синтаксиса инициализации объектов:
newимя_класса {имя=выражение, имя=выражение,. . . }
гдеимяобозначает имя поля или свойства, т.е. доступного члена класса, на который указываетимя_класса.Авыражениеобозначает инициализирующее выражение, тип которого, конечно, должен соответствовать типу поля или свойства.
Инициализаторы объектов обычно не используются в именованных классах, как, например, в представленном выше классеMyClass,хотя это вполне допустимо. Вообще, при обращении с именованными классами используется синтаксис вызова обычного конструктора. И, как упоминалось выше, инициализаторы объектов применяются в основном в анонимных типах, формируемых в LINQ-выражениях.
Необязательные аргументы
В версии C# 4.0 внедрено новое средство, повышающее удобство указания аргументов при вызове метода. Это средство называетсянеобязательными аргументамии позволяет определить используемое по умолчанию значение для параметра метода.
Данное значение будет использоваться по умолчанию в том случае, если для параметра не указан соответствующий аргумент при вызове метода. Следовательно, указывать аргумент для такого параметра не обязательно. Необязательные аргументы позволяют упростить вызов методов, где к некоторым параметрам применяются аргументы, выбираемые по умолчанию. Их можно также использовать в качестве "сокращенной7' формы перегрузки методов.
Применение необязательного аргумента разрешается при созданиинеобязательного параметра.Для этого достаточно указать используемое по умолчанию значение параметра с помощью синтаксиса, аналогичного инициализации переменной. Используемое по умолчанию значение должно быть константным выражением. В качестве примера рассмотрим следующее определение метода.
static void OptArgMeth(int alpha, int beta=10, int gamma = 20) {
В этой строке кода объявляются два необязательных параметра:betaиgamma,причем параметруbetaпо умолчанию присваивается значение 10, а параметруgamma —значение 20. Эти значения используются по умолчанию, если для данных параметров не указываются аргументы при вызове метода. Следует также иметь в виду, что параметрalphaне является необязательным. Напротив, это обычный параметр, для которого всегда нужно указывать аргумент.
Принимая во внимание приведенное выше объявление методаOptArgMeth (),последний можно вызвать следующими способами.
// Передать все аргументы явным образом.
OptArgMeth(1, 2, 3);
// Сделать аргумент gamma необязательным.
OptArgMeth(1, 2);
// Сделать оба аргумента beta и gamma необязательными.
OptArgMeth(1);
При первом вызове параметруalphaпередается значение 1, параметруbeta —значение 2, а параметруgamma— значение 3. Таким образом, все три аргумента задаются явным образом, а значения, устанавливаемые по умолчанию, не используются. При втором вызове параметруalphaпередается значение 1, а параметруbeta— значение 2, но параметруgammaприсваивается устанавливаемое по умолчанию значение 20. И наконец, при третьем вызове упомянутого выше метода параметруalphaпередается значение 1, а параметрамbetaиgammaприсваиваются устанавливаемые по умолчанию значения. Следует, однако, иметь в виду, что параметрbetaне получит устанавливаемое по умолчанию значение, если то же самое не произойдет с параметромgamma.Если первый аргумент устанавливается по умолчанию, то и все остальные аргументы должны быть установлены по умолчанию.
Весь описанный выше процесс демонстрируется в приведенном ниже примере программы.
// Продемонстрировать необязательные аргументы.
using System;
class OptionArgDemo {
static void OptArgMeth(int alpha, int beta=10, int gamma = 20) {
Console.WriteLine ("Это аргументы alpha, beta и gamma: " + alpha + " " + beta + " " + gamma);
}
static void Main() {
// Передать все аргументы явным образом.
OptArgMeth(1, 2, 3); i
IIСделать аргумент gamma необязательным.
OptArgMeth(1, 2);
// Сделать оба аргумента beta и gamma необязательными.
OptArgMeth(1);
}
}
Результат выполнения данной программы лишь подтверждает применение используемых по умолчанию аргументов.
Это аргументы alpha, beta и gamma: 12 3
Это аргументы alpha, beta и gamma: 1 2 20
Это аргументы alpha, beta и gamma: 1 10 20
Как следует из приведенного выше результата, если аргумент не указан, то исполь
зуется его значение, устанавливаемое по умолчанию.
Следует иметь в виду, что все необязательные аргументы должны непременно указыватьсясправаот обязательных. Например, следующее объявление оказывается недействительным.
int Sample(string name = "пользователь", int userid) { // Ошибка!
Для исправления ошибки в этом объявлении необходимо указать аргументuseridдо аргументаname.Раз уж вы начали объявлять необязательные аргументы, то указывать после них обязательные аргументы нельзя. Например, следующее объявление также оказывается неверным.
int Sample(int accountld, string name =• "пользователь", int userid) { //.Ошибка!
Аргументnameобъявляется как необязательный, и поэтому аргументuseridследует указать до аргументаname(или же сделать его также необязательным).
Помимо методов, необязательные аргументы можно применять в конструкторах, индексаторах и делегатах. (Об индексаторах и делегатах речь пойдет далее в этой книге.)
Преимущество необязательных аргументов заключается, в частности, в том, что они упрощают программирующему обращение со сложными вызовами методов и конструкторов. Ведь нередко в методе приходится задавать больше параметров, чем обычно требуется. И в подобных случаях некоторые из этих параметров могут быть сделаны необязательными благодаря аккуратному применению необязательных аргументов. Это означает, что передавать нужно лишь те аргументы, которые важны в данном конкретном случае, а не все аргументы, которые в противном случае должны быть обязательными. Такой подход позволяет рационализировать метод и упростить программирующему обращение с ним.
Необязательные аргументы и перегрузка методов
В некоторых случаях необязательные аргументы могут стать альтернативой перегрузке методов. Для того чтобы стало понятнее, почему это возможно, обратимся еще раз к примеру методаOptArgMeth(). До появления в C# необязательных аргументов нам пришлось бы создать три разных варианта методаOptArgMeth (), чтобы добиться таких же функциональных возможностей, как и у рассмотренного выше варианта этого метода. Все эти варианты пришлось бы объявить следующим образом.
static void OptArgMeth(int alpha)
static void OptArgMeth(int alpha, int beta)
static void OptArgMeth(int alpha, int beta, int gamma)
Эти перегружаемые варианты методаOptArgMeth () позволяют вызывать его с од
ним, двумя или тремя аргументами. (Если значения параметровbetaиgammaне передаются, то они предоставляются в теле перегружаемых вариантов данного метода.) Безусловно, в такой реализации функциональных возможностей методаOptArgMeth() с помощью перегрузки нет ничего дурного. Но в данном случае целесообразнее все же воспользоваться необязательными аргументами, хотя такой подход не всегда оказывается более совершенным, чем перегрузка метода.
Необязательные аргументы и неоднозначность
При использовании необязательных аргументов может возникнуть такое затруднение, как неоднозначность. Нечто подобное может произойти при перегрузке метода с необязательными параметрами. В некоторых случаях компилятор может оказаться не в состоянии определить, какой именно вариант метода следует вызывать, когда необязательные аргументы не заданы. В качестве примера рассмотрим два следующих варианта методаOptArgMeth ().
static void OptArgMeth(int alpha, int beta=10, int gamma = 20) {
Console.WriteLine("Это аргументы alpha, beta и gamma: " + alpha + " " + beta + " " + gamma);
}
static void OptArgMeth(int alpha, double beta=10.0, double gamma =20.0) {
Console.WriteLine("Это аргументы alpha, beta и gamma: " + alpha + " " + beta + " " + gamma);
}
Обратите внимание на то, что единственное отличие в обоих вариантах рассматриваемого здесь метода состоит в типах параметровbetaиgamma,которые оказываются необязательными. В первом варианте оба параметра относятся к типуint,а во втором — к типуdouble.С учетом этих вариантов перегрузки методаOptArgMeth() следующий его вызов приводит к неоднозначности.
OptArgMeth(1); // Ошибка из-за неоднозначности!
Этот вызов приводит к неоднозначности потому, что компилятору неизвестно, какой именно вариант данного метода использовать: тот, где параметрыbetaиgammaимеют типint,или же тот, где они имеют типdouble.Но самое главное, что конкретный вызов методаOptArgMeth() может привести к неоднозначности, даже если она и не присуща его перегрузке.
В связи с тем что перегрузка методов, допускающих применение необязательных аргументов, может привести к неоднозначности, очень важно принимать во внимание последствия такой перегрузки. В некоторых случаях, возможно, придется отказаться от применения необязательных аргументов, чтобы исключить неоднозначность и тем самым предотвратить использование метода непреднамеренным образом.
Практический пример использования необязательных аргументов
Длятого чтобы показать на практике, насколько необязательные аргументы упрощают вызовы некоторых типов методов, рассмотрим следующий пример программы. В этой программе объявляется методDisplay (), выводящий на экран символьную строку полностью или частично.
// Использовать необязательный аргумент, чтобы упростить вызов метода.
using System;
class UseOptArgs {
// Вывести на экран символьную строку полностью или частично, static void Display(string str, int start = 0, int stop = -1) {
if(stop < 0)
stop = str.Length;
// Проверить условие выхода за заданные пределы, if(stop > str.Length | start > stop | start < 0) return;
for (int i=start; i < stop; i++)
Console.Write(str[i] ) ;
Console.WriteLine ();
}
static void Main() {
Display("это простой тест");
Display("это простой тест", 12);
Display("3TO простой тест", 4, 14);
}
}
Выполнение этой программы дает следующий результат.
это простой тест тест
простой те
Внимательно проанализируем методDisplay(). Выводимая на экран символьная строка передается в первом аргументе данного метода. Это обязательный аргумент, а два других аргумента — необязательные. Они задают начальный и конечный индексы для вывода части символьной строки. Если параметруstopне передается значение, то по умолчанию он принимает значение -1, указывающее на то, что конечной точкой вывода служит конец символьной строки. Если же параметруstartне передается значение, то по умолчанию он принимает значение 0. Следовательно, в отсутствие одного из необязательных аргументов символьная строка выводится на экран полностью. В противном случае она выводится на экран частично. Это означает, что если вызвать методDisplay() с одним аргументом (т.е. с выводимой строкой), то символьная строка будет выведена на экран полностью. Если же вызвать методDisplay() с двумя аргументами, то на экран будут выведены символы, начиная с позиции, определяемой аргументомstart,и до самого конца строки. А если вызвать методDisplay() с тремя аргументами, то на экран будут выведены символы из строки, начиная с позиции, определяемой аргументомstart,и заканчивая позицией, определяемой аргументомstop.
Несмотря на всю простоту данного примера, он, тем не менее, демонстрирует значительное преимущество, которое дают необязательные аргументы. Это преимущество заключается в том, что1при вызове метода можно указывать только те аргументы, которые требуются. А передавать явным образом устанавливаемые по умолчанию значения не нужно.
Прежде чем переходить к следующей теме, остановимся на следующем важном моменте. Необязательные аргументы оказываются весьма эффективным средством лишь в том случае, если они используются правильно. Они предназначены для того, чтобы метод выполнял свои функции эффективно, а пользоваться им можно было бы просто и удобно. В этом отношении устанавливаемые по умолчанию значения всех аргументов должны упрощать обычное применение метода. В противном случае необязательные аргументы способны нарушить структуру кода и ввести в заблуждение тех, кто им пользуется. И наконец, устанавливаемое по умолчанию значение необязательного параметра не должно наносить никакого вреда. Иными словами, неумышленное использование необязательного аргумента не должно приводить к необратимым, отрицательным последствиям. Так, если забыть указать аргумент при вызове метода, то это не должно привести к удалению важного файла данных!
Именованные аргументы
Еще одним средством, связанным с передачей аргументов методу, являетсяименованный аргумент.Именованные аргументы были внедрены в версии C# 4.0. Как вам должно быть уже известно, при передаче аргументов методу порядок их следования, как правило, должен совпадать с тем порядком, в котором параметры определены в самом методе. Иными словами, значение аргумента присваивается параметру по его позиции в списке аргументов. Данное ограничение призваны преодолеть именованные аргументы. Именованный аргумент позволяет указать имя того параметра, которому присваивается его значение. И в этом случае порядок следования аргументов уже не имеет никакого значения. Таким образом, именованные аргументы в какой-то степени похожи на упоминавшиеся ранее инициализаторы объектов, хотя и отличаются от них своим синтаксисом.
Для указания аргумента по имени служит следующая форма синтаксиса.
имя_параметра : значение
Здесьимя_параметраобозначает имя того параметра, которому передаетсязначение.Разумеется,имя_параметрадолжно обозначать имя действительного параметра для вызываемого метода.
Ниже приведен простой пример, демонстрирующий применение именованных аргументов. В этом примере создается метод IsFactor (), возвращающий логическое значениеtrue,если первый его параметр нацело делится на второй параметр.
// Применить именованные аргументы, using System;
class NamedArgsDemo {
// Выяснить, делится ли одно значение нацело на другое, static bool IsFactor(int val, int divisor) { if((val % divisor) == 0) return true; return false;
}
static void Main() {
// Ниже демонстрируются разные способы вызова метода IsFactor().
// Вызов с использованием позиционных аргументов, if(IsFactor(10, 2))
Console.WriteLine("2 - множитель 10.");
// Вызов с использованием именованных аргументов, if(IsFactor(val: 10, divisor: 2))
Console.WriteLine("2 - множитель 10.");
// Для именованного аргумента порядок указания не имеет значения, if(IsFactor(divisor: 2, val: 10))
Console.WriteLine("2 - множитель 10.");
// Применить как позиционный, так и именованный аргумент, if(IsFactor(10, divisor: 2))
Console.WriteLine("2 - множитель 10.");
}
}
Выполнение этого кода дает следующий результат.
2 - множитель 10.
2 - множитель 10.
2 - множитель 10.
2 - множитель 10.
Как видите, при каждом вызове методаIsFactor() получается один и тот же результат.
Помимо демонстрации именованного аргумента в действии, приведенный выше пример кода иллюстрирует две важные особенности именованных аргументов. Во-первых, порядок следования аргументов не имеет никакого значения. Например, два следующих вызова методаIsFactor() совершенно равнозначны.
IsFactor(val :10, divisor: 2)
IsFactor(divisor: 2, val: 10)
Независимость от порядка следования является главным преимуществом именованных аргументов. Это означает, что запоминать (или даже знать) порядок следования параметров в вызываемом методе совсем не обязательно. Для работы с СОМ-интерфейсами это может быть очень удобно. И во-вторых, позиционные аргументы можно указывать вместе с именованными в одном и том же вызове, как показано в следующем примере.
IsFactor(10, divisor: 2)
Следует, однако, иметь в виду, что при совместном использовании именованных и позиционных аргументов все позиционные аргументы должны быть указаны перед любыми именованными аргументами.
Именованные аргументы можно также применять вместе с необязательными аргументами. Покажем это на примере вызова метода Display (), рассматривавшегося в предыдущем разделе.
// Указать все аргументы по имени.
Display(stop: 10, str: "это простой тест", start: 0);
// Сделать аргумент start устанавливаемым по умолчанию.
Display(stop: 10, str: "это простой тест") ;
// Указать строку по позиции, аргумент stop — по имени by name,
// тогда как аргумент start — устанавливаемым по умолчанию Display("это простой тест", stop: 10);
Вообще говоря, комбинация именованных и необязательных аргументов позволяет упростить вызовы сложных методов со многими параметрами.
Синтаксис именованных аргументов более многословен, чем у обычных позиционных аргументов, и поэтому для вызова методов чаще всего применяются позиционные аргументы. Но в тех случаях, когда это уместно, именованные аргументы могут быть использованы довольно эффективно.
ПРИМЕЧАНИЕ
Помимо методов, именованные и необязательные аргументы могут применяться в конструкторах, индексаторах и делегатах. (06 индексаторах и делегатах речь пойдет далее в этой книге.)
Метод Main ()
В представленных до сих пор примерах программ использовалась одна форма методаMain (). Но у него имеется также целый ряд перегружаемых форм. Одни из них могут служить для возврата значений, другие — для получения аргументов. В этом разделе рассматриваются и те и другие формы.
Возврат значений из метода Main ()
По завершении программы имеется возможность возвратить конкретное значение из методаMain() вызывающему процессу (зачастую операционной системе). Для этой цели служит следующая форма методаMain ().
static int Main()
Обратите внимание на то, что в этой форме методаMain() объявляется возвращаемый типintвместо типаvoid.
Как правило, значение, возвращаемое методомMain (), указывает на нормальное завершение программы или на аварийное ее завершение из-за сложившихся ненормальных условий выполнения. Условно нулевое возвращаемое значение обычно указывает на нормальное завершение программы, а все остальные значения обозначают тип возникшей ошибки.
Передача аргументов методу Main ()
Многие программы принимают так называемыеаргументы командной строки,т.е. информацию, которая указывается в командной строке непосредственно после имени программы при ее запуске на выполнение. В программах на C# такие аргументы передаются затем методу Main (). Для получения аргументов служит одна из приведенных ниже форм метода Main ().
static void Main(string[ ] args) static int Main(string[ ] args)
В первой форме метод Main () возвращает значение типа void, а во второй — целое значение, как пояснялось выше. Но в обеих формах аргументы командной строки сохраняются в виде символьных строк в массиве типа string, который передается методу Main (). Длина этого массива (args) должна быть равна числу аргументов командной строки, которое может быть и нулевым.
В качестве примера ниже приведена программа, выводящая все аргументы командной строки, вместе с которыми она вызывается.
// Вывести все аргументы командной строки.
using System;
class CLDemo {
static void Main(string[] args) {
Console.WriteLine("Командная строка содержит " + args.Length +
" аргумента.");
Console.WriteLine("Вот они: ");
for(int i=0; i < args.Length; i++)
Console.WriteLine(args[i]);
}
}
Если программа CLDemo запускается из командной строки следующим образом:CLDemo один два три
то ее выполнение дает такой результат.
Командная строка содержит 3 аргумента.
Вот они: один два три
Для того чтобы стало понятнее, каким образом используются аргументы командной строки, рассмотрим еще один пример программы, в которой применяется простой подстановочный шифр для шифровки или расшифровки сообщений. Шифруемое или расшифровываемое сообщение указывается в командной строке. Применяемый шифр действует довольно просто. Для шифровки слова значение каждой его буквы инкрементируется на 1. Следовательно, Буква"А"становится буквой "Б" и т.д. А для расшифровки слова значение каждой его буквы декрементируется на 1. Разумеется, такой шифр не имеет никакой практической ценности, поскольку его нетрудно разгадать. Тем не менее он может стать приятным развлечением для детей.
// Зашифровать и расшифровать сообщение, используя // простой подстановочный шифр.
using System;
class Cipher {
static int Main(string[] args) {
// Проверить наличие аргументов, if(args.Length < 2) {
Console.WriteLine("ПРИМЕНЕНИЕ: " +
"слово1: <зашифровать>/<расшифровать> " +
"[слово2... словоЫ]"); return 1; // возвратить код неудачного завершения программы
}
// Если аргументы присутствуют, то первым аргументом должно быть // слово <зашифровать> или же слово <расшифровать>. if(args[0] != "зашифровать" & args[0] != "расшифровать") {
Console.WriteLine("Первым аргументом должно быть слово " + "<зашифровать> или <расшифровать>."); return 1; // возвратить код неудачного завершения программы
}
}
Console.Write(" ");
}
Console.WriteLine() ; return 0;
}
}
Для того чтобы воспользоваться этой программой, укажите в командной строке имя программы, затем командное слово "зашифровать" или "расшифровать" и далее сообщение, которое требуется зашифровать или расшифровать. Ниже приведены два примера выполнения данной программы, при условии, что она называется Cipher.
C:\Cipher зашифровать один два
пейо егб
C:\Cipher расшифровать пейо егб
один два
Данная программа отличается двумя интересными свойствами. Во-первых, обратите внимание на то, как в ней проверяется наличие аргументов командной строки перед тем, как продолжить выполнение. Это очень важное свойство, которое можно
обобщить. Если в программе принимается во внимание наличие одного или более аргументов командной строки, то в ней должна быть непременно организована проверка факта передачи ей предполагаемых аргументов, иначе программа будет работать неправильно. Кроме того, в программе должна быть организована проверка самих аргументов перед тем, как продолжить выполнение. Так, в рассматриваемой здесь программе проверяется наличие командного слова "зашифровать" или "расшифровать" в качестве первого аргумента командной строки.
И во-вторых, обратите внимание на то, как программа возвращает код своего завершения. Если предполагаемые аргументы командной строки отсутствуют или указаны неправильно, программа возвращает код 1, указывающий на ее аварийное завершение. В противном случае возвращается код 0, когда программа завершается нормально.
Рекурсия
В C# допускается, чтобы метод вызывал самого себя. Этот процесс называетсярекурсией,а метод, вызывающий самого себя, —рекурсивным.Вообще, рекурсия представляет собой процесс, в ходе которого нечто определяет самое себя. В этом отношении она чем-то напоминает циклическое определение. Рекурсивный метод отличается главным образом тем, что он содержит оператор, в котором этот метод вызывает самого себя. Рекурсия является эффективным механизмом управления программой.
Классическим примером рекурсии служит вычисление факториала числа. Факториал числаNпредставляет собой произведение всех целых чисел от 1 доN.Например, факториал числа 3 равен 1х2><3, или 6. В приведенном ниже примере программы демонстрируется рекурсивный способ вычисления факториала числа. Для сравнения в эту программу включен также нерекурсивный вариант вычисления факториала числа.
// Простой пример рекурсии.
using System;
class Factorial {
// Это рекурсивный метод, public int FactR(int n) { int result;
if(n==l) return 1; result = FactR(n-l) * n; return result;
}
// Это итерационный метод, public int FactI(int n) { int t, result;
result = 1;
for(t=l; t <= n; t++) result *= t; return result;
class Recursion {
static void Main() { Factorial f = new
Factorial ();
Console.WriteLine("Факториалы, рассчитанные рекурсивным методом. Console.WriteLine("Факториал числа 3 равен " + f.FactR(3));
Факториал числа 4 равен " + f.FactR(4));
Console.WriteLine( Console.WriteLine(
Факториал числа 5 равен " + f.FactR(5));
Console.WriteLine() ;
Console.WriteLine("Факториалы, рассчитанные итерационным методом Console.WriteLine("Факториал числа 3 равен " + f.FactR(3));
• Console.WriteLine("Факториал числа 4 равен " + f.FactR(4));
Console.WriteLine("Факториал числа 5 равен " + f.FactR(5));
При выполнении этой программы получается следующий результат.
Факториалы, рассчитанные рекурсивным методом.
Факториал числа 3 равен 6 Факториал числа 4 равен 24 Факториал числа 5 равен 120
Факториалы, рассчитанные итерационным методом.
Принцип действия нерекурсивного методаFactI() вполне очевиден. В нем используется цикл, в котором числа, начиная с 1, последовательно умножаются друг на друга, постепенно образуя произведение, дающее факториал.
А рекурсивный методFactR() действует по более сложному принципу. Если методFactR() вызывается с аргументом 1, то он возвращает значение 1. В противном случае он возвращает произведениеFactR(п-1) *п. Для вычисления этого произведения методFactR() вызывается с аргументомп-1.Этот процесс повторяется до тех пор, пока значение аргументапне станет равным 1, после чего из предыдущих вызовов данного метода начнут возвращаться полученные значения. Например, когда вычисляется факториал числа 2, то при первом вызове методаFactR() происходит второй его вызов с аргументом 1. Из этого вызова возвращается значение 1, которое затем умножается на 2 (первоначальное значение аргументап).В итоге возвращается результат 2, равный факториалу числа 2 (1x2). Было бы любопытно ввести в методFactR() операторы, содержащие вызовы методаWriteLineO,чтобы наглядно показать уровень рекурсии при каждом вызове методаFactR(), а также вывести промежуточные результаты вычисления факториала заданного числа.
Когда метод вызывает самого себя, в системном стеке распределяется память для новых локальных переменных и параметров, и код метода выполняется с этими новыми переменными и параметрами с самого начала. При рекурсивном вызове метода не создается его новая копия, а лишь используются его новые аргументы. А при возврате из каждого рекурсивного вызова старые локальные переменные и параметры извлекаются из стека, и выполнение возобновляется с точки вызова в методе. Рекурсивные методы можно сравнить по принципу действия с постепенно сжимающейся и затем распрямляющейся пружиной.
Ниже приведен еще один пример рекурсиидлявывода символьной строки в обратном порядке. Эта строка задается в качестве аргумента рекурсивного метода
DisplayRev().
// Вывести символьную строку в обратном порядке, используя рекурсию.
using System;
class RevStr {
// Вывести символьную строку в обратном порядке, public void DisplayRev(string str) { if (str.Length > 0)
DisplayRev(str.Substring(1, str.Length-1)); else
return;
Console.Write(str[0]);
}
}
class RevStrDemo { static void Main() {
string s = "Это тест"; ,
RevStr rsOb = new RevStr ();
Console.WriteLine("Исходная строка: " + s);
Console.Write("Перевернутая строка: "); rsOb.DisplayRev(sf;
Console.WriteLine();
}
}
Вот к какому результату приводит выполнение этого кода.
Исходная строка: Это тест Перевернутая строка: тсет отЭ
Всякий раз, когда вызывается метод DisplayRev (), в нем происходит проверка длины символьной строки, представленной аргументом str. Если длина строки не равна нулю, то метод DisplayRev () вызывается рекурсивно с новой строкой, которая меньше исходной строки на один символ. Этот процесс повторяется до тех пор, пока данному методу не будет передана строка нулевой длины. После этого начнется раскручиваться в обратном порядке механизм всех рекурсивных вызовов метода DisplayRev (). При возврате из каждого такого вызова выводится первый символ строки, представленной аргументом s t г, а в итоге вся строка выводится в обратном порядке.
Рекурсивные варианты многих процедур могут выполняться немного медленнее, чем их итерационные эквиваленты из-за дополнительных затрат системных ресурсов на неоднократные вызовы метода. Если же таких вызовов окажется слишком много, то в конечном итоге может быть переполнен системный стек. А поскольку параметры и локальные переменные рекурсивного метода хранятся в системном стеке и при каждом новом вызове этого метода создается их новая копия, то в какой-то момент стек может оказаться исчерпанным. В этом случае возникает исключительная ситуация, и общеязыковая исполняющая среда (CLR) генерирует соответствующее исключение. Но беспокоиться об этом придется лишь в том случае, если рекурсивная процедура выполняется неправильно.
Главное преимущество рекурсии заключается в том, что она позволяет реализовать некоторые алгоритмы яснее и проще, чем итерационным способом. Например, алгоритм быстрой сортировки довольно трудно реализовать итерационным способом. А некоторые задачи, например искусственного интеллекта, очевидно, требуют именно рекурсивного решения.
При написании рекурсивных методов следует непременно указать в соответствующем месте условный оператор, напримерi f, чтобы организовать возврат из метода без рекурсии. В противном случае возврата из вызванного однажды рекурсивного метода может вообще не произойти. Подобного рода ошибка весьма характерна для реализации рекурсии в практике программирования. В этом случае рекомендуется пользоваться операторами, содержащими вызовы методаWriteLine (), чтобы следить за происходящим в рекурсивном методе и прервать его выполнение, если в нем обнаружится ошибка.
Применение ключевого слова static
Иногда требуется определить такой член класса, который будет использоваться независимо от всех остальных объектов этого класса. Как правило, доступ к члену класса организуется посредством объекта этого класса, но в то же время можно создать член класса для самостоятельного применения без ссылки на конкретный экземпляр объекта. Для того чтобы создать такой член класса, достаточно указать в самом начале его объявления ключевое словоstatic.Если член класса объявляется какstatic,то он становится доступным до создания любых объектов своего класса и без ссылки на какой-нибудь объект. С помощью ключевого словаstaticможно объявлять как переменные, так и методы. Наиболее характерным примером члена типаstaticслужит методMain (), который объявляется таковым потому, что он должен вызываться операционной системой в самом начале выполняемой программы.
Для того чтобы воспользоваться членом типаstaticза пределами класса, достаточно указать имя этого класса с оператором-точкой. Но создавать объект для этого не нужно. В действительности член типаstaticоказывается доступным не по ссылке на объект, а по имени своего класса. Так, если требуется присвоить значение 10 переменнойcountтипаstatic,являющейся членом классаTimer,то для этой цели можно воспользоваться следующей строкой кода.
Timer.count = 10;
Эта форма записи подобна той, что используется для доступа к обычным переменным экземпляра посредством объекта, но в ней указывается имя класса, а не объекта. Аналогичным образом можно вызвать метод типаstatic,используя имя класса и оператор-точку. ,
Переменные, объявляемые какstatic,по существу, являются глобальными. Когда же объекты, объявляются в своем классе, то копия переменной типаstaticне создается. Вместо этого все экземпляры класса совместно пользуются одной и той же переменной типа static. Такая переменная инициализируется перед ее применением в классе. Когда же ее инициализатор не указан явно, то она инициализируется нулевым значением, если относится к числовому типу данных, пустым значением, если относится к ссылочному типу, или же логическим значением false, если относится к типу bool. Таким образом, переменные типа static всегда имеют какое-то значение.
Метод типа static отличается от обычного метода тем, что его можно вызывать по имени его класса, не создавая экземпляр объекта этого класса. Пример такого вызова уже приводился ранее. Это был метод Sqrt () типа static, относящийся к классу System.Math из стандартной библиотеки классов С#.
Ниже приведен пример программы, в которой объявляются переменная и метод типа static.
// Использовать модификатор static.
using System;
class StaticDemo {
// Переменная типа static, public static int Val = 100;
// Метод типа static, public static int ValDiv2() { return Val/2;
}
}
class SDemo {
static void Main() {
Console.WriteLine("Исходное значение переменной " +
"StaticDemo.Val равно " + StaticDemo.Val);
StaticDemo.Val = 8;
Console.WriteLine("Текущее значение переменной" +
"StaticDemo.Val равно " + StaticDemo.Val);
Console.WriteLine("StaticDemo.ValDiv2(): " + StaticDemo.ValDiv2());
}
}
Выполнение этой программы приводит к следующему результату.
Исходное значение переменной StaticDemo.Val равно 100 Текущее значение переменной StaticDemo.Val равно 8 StaticDemo.ValDiv2(): 4
Как следует из приведенного выше результата, переменная типа static инициализируется до создания любого объекта ее класса.
На применение методов типа static накладывается ряд следующих ограничений.
• В методе типа static должна отсутствовать ссылка this, поскольку такой метод не выполняется относительно какого-либо объекта.
• В методе типаstaticдопускается непосредственный вызов только других методов типаstatic,но не метода экземпляра из того самого же класса. Дело в том, что методы экземпляра оперируют конкретными объектами, а метод типаstaticне вызывается для объекта. Следовательно, у такого метода отсутствуют объекты, которыми он мог бы оперировать.
• Аналогичные ограничения накладываются на данные типаstatic.Дляметода типаstaticнепосредственно доступными оказываются только другие данные типаstatic,определенные в его классе. Он, в частности, не может оперировать переменной экземпляра своего класса, поскольку у него отсутствуют объекты, которыми он мог бы оперировать.
Ниже приведен пример класса, в котором недопустим методValDivDenom() типаstatic.
class StaticError {
public int Denom =3; // обычная переменная экземпляра public static int Val = 1024; // статическая переменная
/* Ошибка! Непосредственный доступ к нестатической переменной из статического метода недопустим. */ static int ValDivDenom() {
return Val/Denom; // не подлежит компиляции!
}
}
В данном примере кодаDenomявляется обычной переменной, которая недоступна из метода типаstatic.Но в то же время в этом методе можно воспользоваться переменнойVal,поскольку она объявлена какstatic.
Аналогичная ошибка возникает при попытке вызвать нестатический метод из статического метода того же самого класса, как в приведенном ниже примере.
using System;
class AnotherStaticError {
// Нестатический метод, void NonStaticMeth() {
Console.WriteLine("В методе NonStaticMeth().");
}
/* Ошибка! Непосредственный вызов нестатического метода из статического метода недопустим. */ static void staticMeth() {
NonStaticMeth(); // не подлежит компиляции!
}
}
В данномслучаепопытка вызвать нестатический метод (т.е. метод экземпляра) из статического метода приводит к ошибке во время компиляции.
Следует особо подчеркнуть, что из метода типаstaticнельзя вызывать методы экземпляра и получать доступ к переменным экземпляра его класса, как это обычно делается посредством объектов данного класса. И объясняется это тем, что без указания конкретного объекта переменная или метод экземпляра оказываются недоступными. Например, приведенный ниже фрагмент кода считается совершенно верным.
class MyClass -{
// Нестатический метод, void NonStaticMeth() {
Console.WriteLine("В методе NonStaticMeth().");
}
/* Нестатический метод может быть вызван из статического метода по ссылке на объект. */ public static void staticMeth(MyClass ob) { ob.NonStaticMeth(); // все верно!
}
}
В данном примере метод NonStaticMeth () вызывается из метода staticMeth () по ссылке на объект ob типа MyClass.
Поля типа static не зависят от конкретного объекта, и поэтому они удобны для хранения информации, применимой ко всему классу. Ниже приведен пример программы, демонстрирующей подобную ситуацию. В этой программе поле типа static служит для хранения количества существующих объектов.
// Использовать поле типа static для подсчета // экземпляров существующих объектов.
using System;
class Countlnst {
static int count = 0;
// Инкрементировать подсчет, когда создается объект.
public Countlnst () {
count++;
}
// Декрементировать подсчет, когда уничтожается объект.
~CountInst() { count—;
}
public static int GetCountO { return count;
}
}
class CountDemo {
static void Main() {
Countlnst ob;
for(int i=0; i < 10; i++) {
ob = new CountlnstO;
Console.WriteLine("Текущий подсчет: " + Countlnst.GetCount());
}
}
}
Выполнение этой программы приводит к следующему результату.
Текущий подсчет: 1 Текущий подсчет: 2 Текущий подсчет: 3 Текущий подсчет: 4 Текущий подсчет: 5 Текущий подсчет: 6 Текущий подсчет: 7 Текущий подсчет: 8 Текущий подсчет: 9 Текущий подсчет: 10
Всякий раз, когда создается объект типа Countlnst, инкрементируется поле count типа static. Но всякий раз, когда такой объект утилизируется, поле count декрементируется. Следовательно, поле count всегда содержит количество существующих в настоящий момент объектов. И это становится возможным только благодаря использованию поля типа static. Аналогичный подсчет нельзя организовать с помощью переменной экземпляра, поскольку он имеет отношение ко всему классу, а не только к конкретному экземпляру объекта этого класса.
Ниже приведен еще один пример применения статических членов класса. Ранее в этой главе было показано, как объекты создаются с помощью фабрики класса. В том примере фабрика была нестатическим методом, а это означало, что фабричный метод можно было вызывать только по ссылке на объект, который нужно было предварительно создать. Но фабрику класса лучше реализовать как метод типа static, что даст возможность вызывать этот фабричный метод, не создавая ненужный объект. Именно это улучшение и отражено в приведенном ниже измененном примере программы, реализующей фабрику класса.
// Использовать статическую фабрику класса.
using System;
class MyClass { int a, b;
// Создать фабрику для класса MyClass. static public MyClass Factory(int i, int j) {
MyClass t = new MyClassO;
t.a = i; t.b = j;
return t; // возвратить объект
}
public void Show() {
Console.WriteLine("а и b: " + a + " " + b);
}
}
class MakeObjects { static void Main() { int i, j;
// Сформировать объекты, используя фабрику. for(i=0, j = 10; i < 10; i++, j —) {
MyClass ob = MyClass.Factory(i, j); // создать объект ob.Show();
}
Console.WriteLine ();
}
}
В этом варианте программы фабричный метод Factory () вызывается по имени его класса в следующей строке кода.
MyClass ob = MyClass.Factory(i, j); // создать объект
Теперь нет необходимости создавать объект классаMyClass,перед тем как пользоваться фабрикой этого класса.
Статические конструкторы
Конструктор можно также объявить как static. Статический конструктор, как правило, используется для инициализации компонентов, применяемых ко всему классу, а не к отдельному экземпляру объекта этого класса. Поэтому члены класса инициализируются статическим конструктором до создания каких-либо объектов этого класса. Ниже приведен простой пример применения статического конструктора.
// Применить статический конструктор.
using System;
class Cons {
public static int alpha; public int beta;
// Статический конструктор, static Cons() {
alpha = 99;
Console.WriteLine("В статическом конструкторе.");
}
// Конструктор экземпляра, public Cons() {
beta = 100;
Console.WriteLine("В конструкторе экземпляра.");
class ConsDemo {
static void Main() {
Cons ob = new Cons();
Console.WriteLine("Cons.alpha: " + Cons.alpha);
Console.WriteLine("ob.beta: " + ob.beta);
}
}
При выполнении этого кода получается следующий результат.
В статическом конструкторе.
В конструкторе экземпляра.
Cons.alpha: 99 ob.beta: 100
Обратите внимание на то, что конструктор типаstaticвызывается автоматически, когда класс загружается впервые, причем до конструктора экземпляра. Из этого можно сделать более общий вывод: статический конструктор должен выполняться до любого конструктора экземпляра. Более того, у статических конструкторов отсутствуют модификаторы доступа — они пользуются доступом по умолчанию, а следовательно, их нельзя вызывать из программы.
Статические классы
Класс можно объявлять какstatic.Статический класс обладает двумя основными свойствами. Во-первых, объекты статического класса создавать нельзя. И во-вторых, статический класс должен содержать только статические члены. Статический класс создается по приведенной ниже форме объявления класса, видоизмененной с помощью ключевого словаstatic.
static classимя_класса{11...
В таком классе все члены должны быть объявлены какstatic.Ведь если класс становится статическим, то это совсем не означает, что статическими становятся и все его члены.
Статические классы применяются главным образом в двух случаях. Во-первых, статический класс требуется при созданииметода расширения.Методы расширения связаны в основном с языком LINQ и поэтому подробнее рассматриваются в главе 19. И во-вторых, статический класс служит для хранения совокупности связанных друг с другом статических методов. Именно это его применение и рассматривается ниже.
В приведенном ниже примере программы классNumericFnтипаstaticслужит для хранения ряда статических методов, оперирующих числовым значением. А поскольку все члены классаNumericFnобъявлены какstatic,то этот класс также объявлен какstatic,чтобы исключить получение экземпляров его объектов. Таким образом, классNumericFnвыполняет организационную роль, предоставляя удобные средства для группирования логически связанных методов.
// Продемонстрировать применение статического класса.
static class NumericFn {
// Возвратить обратное числовое значение, static public double Reciprocal(double num) { return 1/num;
}
// Возвратить дробную часть числового значения, static public double FracPart(double num) { return num - (int) num;
}
// Возвратить логическое значение true, если числовое // значение переменной num окажется четным, static public bool IsEven(double num) { return (num % 2) ==0 ? true : false;
}
// Возвратить логическое значение true, если числовое // значение переменной num окажется нечетным, static public bool IsOdd(double num) { return !IsEven(num);
}
}
class StaticClassDemo { static void Main() {
Console.WriteLine("Обратная величина числа 5 равна " +
NumericFn.Reciprocal(5.0) ) ;
Console.WriteLine("Дробная часть числа 4.234 равна " +
NumericFn.FracPart(4.234));
if(NumericFn.IsEven(10))
Console.WriteLine("10 — четное число.");
if(NumericFn.IsOdd(5))
Console.WriteLine("5 — нечетное число.");
// Далее следует попытка создать экземпляр объекта класса NumericFn, // что может стать причиной появления ошибки.
// NumericFn ob = new NumericFn(); // Ошибка!
}
}
Вот к какому результату приводит выполнение этой программы.
Обратная величина числа 5 равна 0.2 Дробная часть числа 4.234 равна 0.234
10 — четное число.
5 — нечетное число.
Обратите внимание на то, что последняя строка приведенной выше программы закомментирована. Класс NumericFn является статическим, и поэтому любая попытка создать объект этого класса может привести к ошибке во время компиляции. Ошибкой будет также считаться попытка сделать нестатическим член класса NumericFn.
И последнее замечание: несмотря на то, что для статического класса не допускается наличие конструктора экземпляра, у него может быть статический конструктор.
ГЛАВА 9 Перегрузка операторов
Вязыке C# допускается определять назначение оператора по отношению к создаваемому классу. Этот процесс называетсяперегрузкой операторов.Благодаря перегрузке расширяется сфера применения оператора в классе. При этом действие оператора полностью контролируется и может меняться в зависимости от конкретного класса. Например, оператор + может использоваться для ввода объекта в связный список в одном классе, где определяется такой список, тогда как в другом классе его назначение может оказаться совершенно иным.
Когда оператор перегружается, ни одно из его первоначальных назначений не теряется. Он просто выполняет еще одну, новую операцию относительно конкретного объекта. Поэтому перегрузка оператора +, например, для обработки связного списка не меняет его назначение по отношению к целым числам, т.е. к их сложению.
Главное преимущество перегрузки операторов заключается в том, что она позволяет плавно интегрировать класс нового типа в среду программирования. Подобного ро^а расширяемость типов является важной составляющей эффективности такого объектно-ориентированного языка программирования, как С#. Как только для класса определяются операторы, появляется возможность оперировать объектами этого класса, используя обычный синтаксис выражений в С#. Перегрузка операторов является одной из самых сильных сторон языка С#.
Основы перегрузки операторов
Перегрузка операторов тесно связана с перегрузкой методов. Для перегрузки оператора служит ключевое словоoperator,определяющееоператорный метод, который, в свою очередь, определяет действие оператора относительно своего класса.
Существуют две формы операторных методов(operator):одна — для унарных операторов, другая — для бинарных. Ниже приведена общая форма для каждой разновидности этих методов.
// Общая форма перегрузки унарного оператора.
public staticвозвращаемый_типoperator ор{тип_параметра операнд)
{
// операции
}
// Общая форма перегрузки бинарного оператора.
public staticвозвращаемый_типoperatorор(тип_параметра1 операнд1,
тип_параметра1 операнд2)
{
11операции
}
Здесь вместоорподставляется перегружаемый оператор, например + или /; авоз-вращаемый_типобозначает конкретный тип значения, возвращаемого указанной операцией. Это значение может быть любого типа, но зачастую оно указывается такого же типа, как и у класса, для которого перегружается оператор. Такая корреляция упрощает применение перегружаемых операторов в выражениях. Для унарных операторовоперандобозначает передаваемый операнд, а для бинарных операторов то же самое обозначаютоперанд1иоперанд2.Обратите внимание на то, что операторные методы должны иметь оба типа,publicиstatic.
Тип операнда унарных операторов должен быть таким же, как и у класса, для которого перегружается оператор. А в бинарных операторах хотя бы один из операндов должен быть такого же типа, как и у его класса. Следовательно, в C# не допускается перегрузка любых операторов для объектов, которые еще не были созданы. Например, назначение оператора + нельзя переопределить для элементов типаintилиstring.
И еще одно замечание: в параметрах оператора нельзя использовать модификаторrefилиout.
Перегрузка бинарных операторов
Для того чтобы продемонстрировать принцип действия перегрузки операторов, начнем с простого примера, в котором перегружаются два оператора — + и -. В приведенной ниже программе создается классThreeD,содержащий координаты объекта в трехмерном пространстве. Перегружаемый оператор + складывает отдельные координаты одного объекта типаThreeDс координатами другого. А перегружаемый оператор - вычитает координаты одного объекта из координат другого.
// Пример перегрузки бинарных операторов.
11Класс для хранения трехмерных координат, class ThreeD {
int х, у, z; // трехмерные координаты
public ThreeD() { х = у = z = 0; }
public ThreeD(int i, int j, int k) { x = i; у = j; z = k; }
// Перегрузить бинарный оператор +.
public static ThreeD operator +(ThreeD opl, ThreeD op2)
{
ThreeD result = new ThreeD();
/* Сложить координаты двух точек и возвратить результат. */
result.х = opl.x + ор2.х; // Эти операторы выполняют
result.у = opl.y + ор2.у; // целочисленное сложение,
result.z = opl.z + op2.z; //сохраняя свое исходное назначение.
return result;
}
// Перегрузить бинарный оператор -.
public static ThreeD operator -(ThreeD opl, ThreeD op2)
{
ThreeD result = new ThreeD();
/* Обратите внимание на порядок следования операндов: opl — левый операнд, а ор2 — правый операнд. */ result.х = opl.x - ор2.х; // Эти операторы
result.у = opl.y - ор2.у; // выполняют целочисленное
result.z = opl.z - op2.z; // вычитание
return result;
}
// Вывести координаты X, Y, Z. public void Show()
{
Console.WriteLine(x + ", " + у + ", " + z) ;
}
• }
class ThreeDDemo { static void Main() {
ThreeD a = new ThreeD(1, 2, 3) ;
ThreeD b = new ThreeD(10, 10, 10);
ThreeD c;
Console.Write("Координаты точки a: ");
a.Show();
Console.WriteLine ();
Console.Write("Координаты точки b: ");
b.Show();
Console.WriteLine() ;
с = а + b; // сложить координаты точек а и b Console.Write("Результат сложения а + Ь: "); с.Show();
Console.WriteLine() ;
c=a+b+c; // сложить координаты точек а, b и с Console.Write("Результат сложения а + b + с: "); с.Show();
Console.WriteLine() ;
с = с - а; // вычесть координаты точки а Console.Write("Результат вычитания с - а: ") ; с.Show();
Console.WriteLine() ;
с = с - b; // вычесть координаты точки b Console.Write("Результат вычитания с - Ь: "); с.Show();
Console.WriteLine() ;
}
}
При выполнении этой программы получается следующий результат.
Координаты точки а: 1, 2, 3
Координаты точки Ь: 10, 10, 10 Результат сложения а + Ь: 11, 12, 13 Результат сложения а+Ь+с: 22, 24, 26 Результат вычитания с - а: 21,22,23
Результат вычитания с - b: 11, 12, 13
Внимательно проанализируем приведенную выше программу, начиная с перегружаемого оператора +. Когда оператор + оперирует двумя объектами типа ThreeD, то величины их соответствующих координат складываются, как показано в объявлении операторного метода operator+(). Следует, однако, иметь в виду, что этот оператор не видоизменяет значения своих операндов, а лишь возвращает новый объект типа ThreeD, содержащий результат операции сложения координат. Для того чтобы стало понятнее, почему операция + не меняет содержимое объектов, выступающих в роли ее операндов, обратимся к примеру обычной операции арифметического сложения: 10 + 12. Результат этой операции равен 22, но она не меняет ни число 10, ни число 12. Несмотря на то что ни одно из правил не препятствует перегруженному оператору изменить значение одного из своих операндов, все же лучше, чтобы действия этого оператора соответствовали его обычному назначению.
Обратите внимание на то, что методoperator+() возвращает объект типаThreeD.Этот метод мог бы возвратить значение любого допустимого в C# тип£, но благодаря тому что он возвращает объект типаThreeD,оператор + можно использовать в таких составных выражениях, какa+b+с.В данном случае выражениеа+bдает результат типаThreeD,который можно затем сложить с объектомстого же типа. Если бы
выражение а+b давало результат другого типа, то вычислить составное выражениеa+b+сбыло бы просто невозможно.
Следует также подчеркнуть, что когда отдельные координаты точек складываются в оператореoperators- (), то в результате такого сложения получаются целые значения, поскольку отдельные координаты х, у и z представлены целыми величинами. Но сама перегрузка оператора + для объектов типаThreeDне оказывает никакого влияния на операцию сложения целых значений, т.е. она не меняет первоначальное назначение этого оператора.
А теперь проанализируем операторный методoperator-(). Оператор - действует так же, как и оператор +, но для него важен порядок следования операндов. Напомним, что сложение носит коммутативный характер (от перестановки слагаемых сумма не меняется), чего нельзя сказать о вычитании: А - В не то же самое, что и В - А! Для всех двоичных операторов первым параметром операторного метода является левый операнд, а вторым параметром — правый операнд. Поэтому, реализуя перегружаемые варианты некоммутативных операторов, следует помнить, какой именно операнд должен быть указан слева и какой — справа.
Перегрузка унарных операторов
Унарные операторы перегружаются таким же образом, как и бинарные. Главное отличие заключается, конечно, в том, что у них имеется лишь один операнд. В качестве примера ниже приведен метод, перегружающий оператор унарного минуса для классаThreeD.
// Перегрузить оператор унарного минуса, public static ThreeD operator -(ThreeD op)
{
ThreeD result = new ThreeD();
result.x = -op.x;
result.у = -op.у;
result.z = -op.z;
return result; 1
}
В данном примере создается новый объект, в полях которого сохраняются отрицательные значения операнда перегружаемого унарного оператора, после чего этот объект возвращается операторным методом. Обратите внимание на то, что сам операнд не меняется. Это означает, что и в данном случае обычное назначение оператора унарного минуса сохраняется. Например, результатом выражения
а = -Ь
является отрицательное значение операнда Ь, но сам операндbне меняется.
В C# перегрузка операторов ++ и -- осуществляется довольно просто. Для этого достаточно возвратить инкрементированное или декрементированное значение, но не изменять вызывающий объект. А все остальное возьмет на себя компилятор С#, различая префиксные и постфиксные формы этих операторов. В качестве примера ниже приведен операторный методoperator++() для классаThreeD.
// Перегрузить унарный оператор ++. public static ThreeD operator ++(ThreeD op)
{
ThreeD result = new ThreeD();
return result;
}
Ниже приведен расширенный вариант предыдущего примера программы, в кото ром демонстрируется перегрузка унарных операторов - и ++.
// Пример перегрузки бинарных и унарных операторов, using System;
// Класс для хранения трехмерных координат, class ThreeD {
int х, у, z; // трехмерные координаты
public ThreeD() { х = у = z = 0; }
public ThreeD(int i, int j, int k) { x = i; у = j; z = k; }
// Перегрузить бинарный оператор +.
public static ThreeD operator +(ThreeD opl, ThreeD op2)
{
ThreeD result = new ThreeD();
/* Сложить координаты двух точек и возвратить результат. */
result.х = opl.x + ор2.х;
result.у = opl.у + ор2.у;
result.z = opl.z + op2.z;
return result;
}
// Перегрузить бинарный оператор -.
public static ThreeD operator -(ThreeD opl, ThreeD op2)
{
ThreeD result = new ThreeD();
/* Обратить внимание на порядок следования операндов: opl — левый операнд, ор2 — правый операнд. */ result.х = opl.x - ор2.х; result.у = opl.у - ор2.у; result.z = opl.z - op2.z;
return result;
}
// Перегрузить унарный оператор -. public static ThreeD operator -(ThreeD op)
{
ThreeD result = new ThreeD();
result.x = -op.x; result, у-= -op.y; result.z = -op.z;
return result;
}
// Перегрузить унарный оператор ++. public static ThreeD operator ++(ThreeD op)
{
ThreeD result = new ThreeD();
// Возвратить результат инкрементирования, result.x = op.x + 1; result.у = op.y + 1; result.z = op.z + 1;
return result;
}
// Вывести координаты X, Y, Z. public void Show()
{
Console.WriteLine(x + ", " + у + ", " + z);
}
}
class ThreeDDemo { static void Main() {
ThreeD a = new ThreeD(1, 2, 3);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD с = new ThreeD();
Console.Write("Координаты точки a: ") ;
a.Show();
Console.WriteLine();
Console.Write("Координаты точки b: ");
b.Show () ;
Console.WriteLine();
c=a+b; // сложить координаты точек а и b
Console.Write("Результат сложения a + b: ");
c.Show();
Console.WriteLine();
c=a+b+c; // сложить координаты точек a, b и с Console.Write("Результат сложения a + b + с: ");
с.Show();
Console.WriteLine();
с = с - а; // вычесть координаты точки а Console.Write("Результат вычитания с - а: ") ; с.Show();
Console.WriteLine();
с = с - b; // вычесть координаты точки b Console.Write("Результат вычитания с - Ь: ") ; с.Show();
Console.WriteLine();
с = -а; // присвоить точке с отрицательные координаты точки Console.Write("Результат присваивания -а: "); с.Show();
Console.WriteLine();
с = а++; // присвоить точке с координаты точки а,
// а затем инкрементировать их Console.WriteLine("Если с = а++");
Console.Write("то координаты точки с равны "); с.Show();
Console.Write("а координаты точки а равны ");
а.Show();
// Установить исходные координаты (1,2,3) точки а а = new ThreeD(1, 2, 3);
Console.Write("ХпУстановка исходных координат точки а: ");
а.Show();
с = ++а; // инкрементировать координаты точки а,
// а затем присвоить их точке с Console.WriteLine("\пЕсли с = ++а");
Console.Write("то координаты точки с равны "); с.Show();
Console.Write("а координаты точки а равны "); а . Show ( ) ;
}
}
Вот к какому результату приводит выполнение данной программы.
Координаты точки а: 1, 2, 3
Координаты точки Ь: 10, 10, 10
Результат сложения а + Ь: 11, 12, 13
Результат сложения а+Ь+с: 22, 24, 26
Результат вычитания с - а: 21, 22, 23
Результат вычитания с -Ь: 11, 12, 13
Результат присваивания -а: -1, -2, -3 Если с = а++
то координаты точки с равны 1, 2, 3
а координаты точки а равны 2, 3, 4 Установка исходных координат точки а: 1,2,3 Еслис= ++а _
то координаты точки с равны2,3, 4 а координаты точки а равны2,3, 4
Выполнение операций со встроенными в C# типами данных
Длялюбого заданного класса и оператора имеется также возможность перегрузить сам операторный метод. Это, в частности, требуется для того, чтобы разрешить операции с типом класса и другими типами данных, в том числе и встроенными. Вновь обратимся к классуThreeD.На примере этого класса ранее было показано, как оператор + перегружается для сложения координат одного объекта типаThreeDс координатами другого. Но это далеко не единственный способ определения операции сложения для классаThreeD.Так, было бы не менее полезно прибавить целое значение к каждой 'координате объекта типаThreeD.Подобная операция пригодилась бы для переноса осей координат. Но для ее выполнения придется перегрузить оператор + еще раз, как показано ниже.
// Перегрузить бинарный оператор + для сложения объекта
// типа ThreeD и целого значения типа int.
public static ThreeD operator +(ThreeD opl, int op2)
{
ThreeD result = new ThreeD(); result.x = opl.x + op2; result.у = opl.y + op2; result.z = opl.z + op2;
return result;
}
Как видите, второй параметр операторного метода имеет типint.Следовательно, в этом методе разрешается сложение целого значения с каждым полем объекта типаThreeD.Такая операция вполне допустима, потому что, как пояснялось выше, при перегрузке бинарного оператора один из его операндов должен быть того же типа, что и класс, для которого этот оператор перегружается. Но у второго операнда этого оператора может быть любой другой тип.
Ниже приведен вариант классаThreeDс двумя перегружаемыми методами оператора +.
// Перегрузить бинарный оператор + дважды:
// один раз — для сложения объектов класса ThreeD,
// а другой раз — для сложения объекта типа ThreeD и целого значения типа int. using System;
// Класс для хранения трехмерных координат, class ThreeD {
int х, у, z; // трехмерные координаты
{
ThreeD result = new ThreeD () ;
/* Сложить координаты двух точек и возвратить результат. */ result.х = opl.x + ор2.х;
result.у = opl.у + ор2.у;
result.z = opl.z + op2.z;
return result;
}
// Перегрузить бинарный оператор + для сложения // объекта типа ThreeD и целого значения типа int. public static ThreeD operator +(ThreeD opl, int op2)
{
ThreeD result = new ThreeD();
return result;
}
// Вывести координаты X, Y, Z. public void Show()
{
Console.WriteLine(x + ", " + у + ", " + z);
■ }
}
class ThreeDDemo { static void Main() {
ThreeD a = new ThreeD(1, 2, 3);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD с = new ThreeD();
Console.Write("Координаты точки a: ");
a.Show();
Console.WriteLine();
Console.Write("Координаты точки b: ");
b.Show ();
Console.WriteLine();
с = a + b; // сложить объекты класса ThreeD Console.Write("Результат сложения a + b: ");
c.Show();
Console.WriteLine();
Console.Write("Результат сложения b + 10: "); с.Show();
}
}
При Быполнении этого кода получается следующий результат.
Координаты точки а: 1, 2, 3
Координаты точки Ь: 10, 10, 10 Результат сложения а+Ь: 11, 12, 13
Результат сложения b + 10: 20, 20, 20
Как подтверждает приведенный выше результат, когда оператор + применяется к двум объектам классаThreeD,то складываются их координаты. А когда он применяется к объекту типаThreeDи целому значению, то координаты этого объекта увеличиваются на заданное целое значение.
Продемонстрированная выше перегрузка оператора +, безусловно, расширяет полезные функции классаThreeD,тем не менее, она делает это не до конца. И вот почему. Методoperator + (ThreeD, int)позволяет выполнять операции, подобные следующей.
оЫ = оЬ2 + 10;
Но, к сожалению, он не позволяет выполнять операции, аналогичные следующей.
оЫ = 10 + оЬ2;
Дело в том, что второй целочисленный аргумент данного метода обозначает правый операнд бинарного оператора +, но в приведенной выше строке кода целочисленный аргумент указывается слева. Для того чтобы разрешить выполнение такой операции сложения, придется перегрузить оператор + еще раз. В этомслучаепервый параметр операторного метода должен иметь типint,а второй параметр — типThreeD.Таким образом, в одном варианте методаoperator+() выполняется сложение объекта типаThreeDи целого значения, а во втором — сложение целого значения и объекта типаThreeD.Благодаря такой перегрузке оператора + (или любого другого бинарного оператора) допускается появление встроенного типа данных как с левой, так и с правой стороны данного оператора. Ниже приведен еще один вариант классаThreeD,в котором бинарный оператор + перегружается описанным выше образом.
// Перегрузить бинарный оператор + трижды:
// один -раз — для сложения объектов класса ThreeD,
// второй раз — для сложения объекта типа ThreeD и целого значения типа int,
// а третий раз — для сложения целого значения типа int и объекта типа ThreeD.
using System;
// Класс для хранения трехмерных координат, class ThreeD {
int х, у, z; // трехмерные координаты public ThreeD() { х = у = z = 0; }
public ThreeD(int i, int j, int k) { x = i; у = j; z = k; }
// Перегрузить бинарный оператор + для сложения объектов класса ThreeD. public static ThreeD operator +(ThreeD opl, ThreeD op2)
{
ThreeD result = new ThreeD();
/* Сложить координаты двух точек и возвратить результат. */ result, х ='"opl.x + ор2.х; result.у = opl.y + ор2.у; result.z = opl.z + op2.z;
return result;
}
// Перегрузить бинарный оператор + для сложения // объекта типа ThreeD и целого значения типа int. public static ThreeD operator +(ThreeD opl, int op2)
{
ThreeD result = new ThreeD();
result.x = opl.x + op2; result.у = opl.y + op2; result.z = opl.z + op2;
return result;
}
// Перегрузить бинарный оператор + для сложения // целого значения типа int и объекта типа ThreeD. public static ThreeD operator +(int opl, ThreeD op2)
{
ThreeD result = new ThreeD();
result.x = op2.x + opl; result.у = op2.y + opl; result.z = op2.z + opl;
return result;
}
// Вывести координаты X, Y, Z. public void Show()
{
Console.WriteLine(x + ", " + у + ", " + z);
}
}
class ThreeDDemo { static void Main() {
ThreeD a = new ThreeD(1, 2, 3) ;
ThreeD b = new ThreeD(10, 10, 10); ThreeD с = new ThreeD();
Console.Write("Координаты точки a: ");
a.Show() ;
Console.WriteLine ();
Console.Write("Координаты точки b: ");
b.Show();
Console.WriteLine();
с = a + b; // сложить объекты класса ThreeD Console.Write("Результат сложения a + b: ");
c.Show();
Console.WriteLine ();
c=b+10; // сложить объект типа ThreeD и целое значение типа int Console.Write("Результат сложения b + 10: ");
с.Show();
Console.WriteLine() ;
c=15+b; // сложить целое значение типа int и объект типа ThreeD Console.Write("Результат сложения 15 + b: ");
с.Show();
}
}
Выполнение этого кода дает следующий результат.
Координаты точки а: 1, 2, 3
Координаты точки b: 10, 10, 10 Результат сложения а + Ь: 11, 12, 13 Результат сложения b + 10: 20, 20, 20 Результат сложения 15 + Ь: 25, 25, 25
Перегрузка операторов отношения
Операторы отношения, например == и <, могут также перегружаться, причем очень просто. Как правило, перегруженный оператор отношения возвращает логическое значениеtrueиfalse.Это вполне соответствует правилам обычного применения подобных операторов и дает возможность использовать их перегружаемые разновидности в условных выражениях. Если же возвращается результат другого типа, то тем самым сильно ограничивается применимость операторов отношения.
Ниже приведен очередной вариант классаThreeD,в котором перегружаются операторы < и >. В данном примере эти операторы служат для сравнения объектовThreeD,исходя из их расстояния до начала координат. Один объект считается больше другого, если он находится дальше от начала координат. А кроме того, один объект считается меньше другого, если он находится ближе к началу координат. Такой вариант реализации позволяет, в частности, определить, какая из двух заданных точек находится на большей сфере. Если же ни один из операторов не возвращает логическое значениеtrue,то обе точки находятся на одной и той же сфере. Разумеется, возможны и другие алгоритмы упорядочения.
I/Перегрузить операторы < и >. using System;
//Класс для хранения трехмерных координат, class ThreeD {
int x, у, z; // трехмерные координаты
public ThreeD() { x = у = z = 0; }
public ThreeD(int i, int j, int k) { x = i; у = j; z = k; }
// Перегрузить оператор <.
public static bool operator < (ThreeD opl, ThreeD op2)
{
if(Math.Sqrt(opl.x * opl.x + opl.у * opl.у + opl.z * opl.z) <
Math.Sqrt(op2.x * op2.x + op2.у * op2.y + op2.z * op2.z))
return true; else
return false;
}
// Перегрузить оператор >.
public static bool operator >(ThreeD opl, ThreeD op2)
{
if(Math.Sqrt(opl.x * opl.x + opl.у * opl.у + opl.z * opl.z) >
Math.Sqrt(op2.x * op2.x + op2.у * op2.y + op2.z * op2.z))
return true; else
return false;
}
// Вывести координаты X, Y, Z. public void Show()
{
Console.WriteLine(x + ”, " + у + ", " + z) ;
}
}
class ThreeDDemo { static void Main() {
Console.Write("Координаты точки a: ")
a.Show();
Console.Write("Координаты точки b: ")
b.Show();
Console.Write("Координаты точки с: ")
c.Show();
Console.Write("Координаты точки d: ")
d. Show();
Console.WriteLine();
if(а > с) Console.WriteLine("а > с истинно");
if(а < с) Console.WriteLine("а < с истинно");
if(а > b) Console.WriteLine("а > b истинно");
if (а < b)-Console.WriteLine("а < b истинно");
if(а > d) Console.WriteLine("а > d истинно");
else if(а < d) Console.WriteLine("a < d истинно");
else Console.WriteLine("Точки and находятся на одном расстоянии " +
"от начала отсчета");
}
' }
Вот к какому результату приводит выполнение этого кода.
а > с истинно а < b истинно
Точки and находятся на одном расстоянии от начала отсчета
На перегрузку операторов отношения накладывается следующее важное ограничение: они должны перегружаться попарно. Так, если перегружается оператор <, то следует перегрузить и оператор >, и наоборот. Ниже приведены составленные в пары перегружаемые операторы отношения.
==
I =
<
>
<=
>=
И еще одно замечание: если перегружаются операторы == и ! =, то для этого обычно требуется также переопределить методыObject.EqualsO nObject. GetHashCode (). Эти методы и способы их переопределения подробнее рассматриваются в главе 11.
Перегрузка операторов true и false
Ключевые слова true и false можно также использовать в качестве унарных операторов для целей перегрузки. Перегружаемые варианты этих операторов позволяют определить назначение ключевых слов true и false специально для создаваемых классов. После перегрузки этих ключевых слов в качестве унарных операторов для конкретного класса появляется возможность использовать объекты этого класса для управления операторами if, while, for и do-while или же в условном выражении ?.
Операторы true и false должны перегружаться попарно, а не раздельно. Ниже приведена общая форма перегрузки этих унарных операторов.
public static bool operator true(тип_параметра операнд)
{
// Возврат логического значения true или false.
}
public static bool operator false(тип_параметра операнд)
{
// Возврат логического значения true или false.
}
Обратите внимание на то, что и в том и в другом случае возвращается результат типаbool.
Ниже приведен пример программы, демонстрирующий реализацию операторовtrueиfalseв классеThreeD.В каждом из этих операторов проверяется следующее условие: если хотя бы одна из координат объекта типаThreeDравна нулю, то этот объект истинен, а если все три его координаты равны нулю, то такой объект ложен. В данном примере программы реализован также оператор декремента исключительно в целях демонстрации.
// Перегрузить операторы true и false для класса ThreeD. using System;
// Класс для хранения трехмерных координат, class ThreeD {
int х, у, z; // трехмерные координаты
public ThreeD() { х = у = z = 0; }
public ThreeD(int i, int j, int k) { x = i; у = j; z = k; }
// Перегрузить оператор true.
public static bool operator true(ThreeD op) { if((op.x != 0) M (op.y != 0) || (op.z != 0))
return true; // хотя бы одна координата не равна нулю else
return false;
}
// Перегрузить оператор false.
public static bool operator false(ThreeD op) { if((op.x == 0) && (op.y == 0) && (op.z == 0))
return true; // все координаты равны нулю
else
return false;
}
// Перегрузить унарный оператор —. public static ThreeD operator —(ThreeD op)
{
ThreeD result = new ThreeD();
// Возвратить результат декрементирования, result.x = op.x - 1; result.у = op.y - 1; result.z = op.z - 1;
return result;
}
// Вывести координаты X, Y, Z. •
public void Show ()
{
Console.WriteLine(х + ", " + у + ", " + z);
}
}
class TrueFalseDemo { static void Main() {
ThreeD a = new ThreeD (5, 6, 7);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD с = new ThreeD(0, 0, 0);
Console.Write("Координаты точки a: ");
a.Show();
Console.Write("Координаты точки b: ");
b.Show();
Console.Write("Координаты точки с: ");
c.Show() ;
Console.WriteLine();
if(a) Console.WriteLine("Точка а истинна."); else Console.WriteLine("Точка а ложна.");
if(b) Console.WriteLine("Точка b истинна."); else Console.WriteLine("Точка b ложна.");
if(с) Console.WriteLine("Точка с истинна."); else Console.WriteLine("Точка с ложна.");
Console.WriteLine();
Console.WriteLine("Управление циклом с помощью объекта класса ThreeD.") ; do {
b.Show(); b—;
} while(b);
}
}
Выполнение этой программы приводит к следующему результату.
Координаты точки а: 5, 6, 7 Координаты точки Ь: 10, 10, 10 Координаты точки с: 0, 0, 0
Точка а истинна Точка b истинна Точка с ложна
Управление циклом с помощью объекта класса ThreeD.
10, 10, 10
9, 9, 98, 8, 87,1,7 б, 6, б 5, 5, 5 4, 4, 4 3, 3, 3 2, 2 , 2 1, 1, 1
Обратите внимание на то, как объекты классаThreeDиспользуются для управления условным операторомifи оператором циклаdo-while.Так, в операторахifобъект типаThreeDпроверяется с помощью оператораtrue.Если результат этой проверки оказывается истинным, то операторi fвыполняется. А в операторе циклаdo-whileобъектbдекрементируется на каждом шаге цикла. Следовательно, цикл повторяется до тех пор, пока проверка объекта b дает истинный результат, т.е. этот объект содержит хотя бы одну ненулевую координату. Если же окажется, что объектbсодержит все нулевые координаты, его проверка с помощью оператораtrueдаст ложный результат и цикл завершится.
Перегрузка логических операторов
Как вам должно быть уже известно, в C# предусмотрены следующие логические операторы: &, |, !, & & и | |. Из них перегрузке, безусловно, подлежат только операторы &, [ и !. Тем не менее, соблюдая определенные правила, можно извлечь также пользу из укороченных логических операторов & & и | |. Все эти возможности рассматриваются ниже.
Простой способ перегрузки логических операторов
Рассмотрим сначала простейший случай. Если не пользоваться укороченными логическими операторами, то перегрузку операторов & и | можно выполнять совершенно естественным путем, получая в каждом случае результат типа bool. Аналогичный результат, как правило, дает и перегружаемый оператор !.
Ниже приведен пример программы, в которой демонстрируется перегрузка логических операторов !, & и | для объектов типаThreeD.Как и в предыдущем примере, объект типаThreeDсчитается истинным, если хотя бы одна из его координат не равна нулю. Если же все три координаты объекта равны нулю, то он считается ложным.
// Простой способ перегрузки логических операторов // !, | и & для объектов класса ThreeD.
using System;
// Класс для хранения трехмерных координат. class ThreeD {
int х, у, z; // трехмерные координаты
public ThreeD() { х = у = z = 0; }
public ThreeD(int i, int j, int k) { x = i; у = j; z = k; }
// Перегрузить логический оператор |.
public static bool operator |(ThreeD ‘opl, ThreeD op2)
{
if( ((opl.x != 0) M (opl.у != 0) M (opl.z != 0)) I ( (op2.-x!= 0) || (op2. у != 0) || (op2.z != 0)) )
return true; else
return false;
}
// Перегрузить логический оператор &.
public static bool operator &(ThreeD opl, ThreeD op2)
{
if ( ((opl.x != 0) && (opl.у != 0) && (opl.z != 0)) & ((op2.x != 0) && (op2.y != 0) && (op2.z != 0)) )
return true; else
return false;
}
11Перегрузить логический оператор !. public static bool operator ! (ThreeD op)
{
if ( (op.x != 0) M (op.у != 0) || (op.z != 0))
return false; else return true;
}
// Вывести координаты X, Y, Z. public void Show()
{
Console.WriteLine(x + ", " + у + ", " + z) ;
}
}
class TrueFalseDemo { static void Main() {
ThreeD a = new ThreeD(5, 6, 7);
ThreeD b = new ThreeD(10, 10, 10); ThreeD с = new ThreeD (0, 0, 0);
Console.Write("Координаты точки a: ");
a.Show ();
Console.Write("Координаты точки b: ");
b.Show();
Console.Write("Координаты точки с: ");
c.Show();
Console.WriteLine();
if(!a) Console.WriteLine("Точка а ложна.") if(!b) Console.WriteLine("Точка b ложна.") if(!c) Console.WriteLine("Точка с ложна.")
if(а & b) Console.WriteLine("a & b истинно."); else Console.WriteLine("a & b ложно.");
if(a & c) Console.WriteLine("a & с истинно."); else Console.WriteLine("a & с ложно.");
if(a | b) Console.WriteLine("a | b истинно."); else Console.WriteLine("a | b ложно.");
if (a | c) Console.WriteLine("a | с истинно."); else Console.WriteLine("a | с ложно.");
}
}
При выполнении этой программы получается следующий результат.
Координаты точки а: 5, 6,1
Координаты точки Ь: 10, 10, 10
Координаты точки с: 0, 0, 0
Точка с ложна.
а & b истинно, а & с ложно. а | b истинно, а | с истинно.
При таком способе перегрузки логических операторов &, | и ! методы каждого из них возвращают результат типа bool. Это необходимо для того, чтобы использовать рассматриваемые операторы обычным образом, т.е. в тех выражениях, где предполагается результат типа bool. Напомним, что для всех встроенных в C# типов данных результатом логической операции должно быть значение типа bool. Поэтому вполне разумно предусмотреть возврат значения типа bool и в перегружаемых вариантах этих логических операторов. Но, к сожалению, такой способ перегрузки пригоден лишь в том случае, если не требуются укороченные логические операторы.
Как сделать укороченные логические операторы доступными для применения
Длятогочтобы применение укороченных логических операторов & & и | | стало возможным, необходимо соблюсти следующие четыре правила. Во-первых, в классе должна быть произведена перегрузка логических операторов & и |. Во-вторых, перегружаемые методы операторов & и | должны возвращать значение того же типа, что и у класса, для которого эти операторы перегружаются. В-третьих, каждый параметр должен содержать ссылку на объект того класса, для которого перегружается логический оператор. И в-четвертых, для класса должны быть перегружены операторы true и false. Если все эти условия выполняются, то укороченные логические операторы автоматически становятся пригодными для применения.
В приведенном ниже примере программы показано, как правильно реализовать логические операторы & и | в классе ThreeD, чтобы сделать доступными для применения укороченные логические операторы & & и | |.
/* Более•совершенный способ перегрузки логических операторов !, | и & для объектов класса ThreeD.
В этом варианте укороченные логические операторы && и || становятся доступными для применения автоматически. */
using System;
// Класс для хранения трехмерных координат, class ThreeD {
int х, у, z; // трехмерные координаты
public ThreeD() { х = у = z = 0; }
public ThreeD(int i, int j, int k) { x = i; у = j; z = k; }
// Перегрузить логический оператор | для укороченного вычисления, public static ThreeD operator |(ThreeD opl, ThreeD op2)
{
else
return new ThreeD(0, 0, 0) ;
}
// Перегрузить логический оператор & для укороченного вычисления, public static ThreeD operator & (ThreeD opl, ThreeD op2)
{
if( ((opl.x != 0) && (opl.у != 0) && (opl.z != 0)) &
((op2.x != 0) && (op2.y != 0) && (op2.z != 0)) )
return new ThreeD(1, 1, 1); else
return new ThreeD(0, 0, 0);
}
// Перегрузить логический оператор !. public static bool operator !(ThreeD op)
{
if(op) return false; else return true;
}
// Перегрузить оператор true.
public static bool operator true(ThreeD op) { if((op.x != 0) И (op.у != 0) || (op.z != 0))
return true; // хотя бы одна координата не равна нулю
else
return false;
}
// Перегрузить оператор false.
public static bool operator false(ThreeD op) { if((op.x == 0) && (op.y == 0) && (op.z == 0))
return true; // все координаты равны нулю
else
return false;
}
// Ввести координаты X, Y, Z. public void Show()
{
Console.WriteLine(x+ ", " + у + ", " + z) ;
}
}'
class TrueFalseDemo { static void Main() {
ThreeD a = new ThreeD(5, 6, 7);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD с = new ThreeD(0, 0, 0) ;
Console.Write("Координаты точки a: ");
a.Show();
Console.Write("Координаты точки b: ");
b.Show();
Console.Write("Координаты точки с: ");
c.Show();
Console.WriteLine() ;
if (a) Console.WriteLine("Точка а истинна."); if(b) Console.WriteLine("Точка b истинна."); if(с) Console.WriteLine("Точка с истинна.");
if(!a) Console.WriteLine("Точка а ложна."); if(!b) Console.WriteLine("Точка b ложна."); if(!c) Console.WriteLine("Точка с ложна.");
Console.WriteLine ();
Console.WriteLine("Применение логических операторов & и |"); if(а & b) Console.WriteLine("а & b истинно."); else Console.WriteLine("а & b ложно.");
if(а & с) Console.WriteLine("а & с истинно."); else Console.WriteLine("а & с ложно.");
if(а | b) Console.WriteLine("а | b истинно."); else Console.WriteLine("а | b ложно.");
if(а | с) Console.WriteLine("а | с истинно."); else Console.WriteLine("а | с ложно.");
Console.WriteLine();
// А теперь применить укороченные логические операторы. Console.WriteLine("Применение укороченных" +
"логических операторов && и И"); if(а && b) Console.WriteLine("а && b истинно."); else Console.WriteLine("а && b ложно.");
if(а && с) Console.WriteLine("а && с истинно."); else Console.WriteLine("а && с ложно.");
if(а И b) Console.WriteLine ("а || b истинно."); else- Console.WriteLine("а || b ложно.");
if (а | | с) Console.WriteLine("а | | с истинно."); else Console.WriteLine("а || с ложно.");
}
}
Выполнение этой программы приводит к следующему результату.
Координаты точки а: 5, 6, 7 Координаты точки Ь: 10, 10, 10 Координаты точки с: 0, 0, 0
Точка а истинна Точка b истинна Точка с ложна.
Применение логических операторов & и | а & b истинно, а & с ложно, а | b истинно, а | с истинно.
Применение укороченных логических операторов && и || а && b истинно, а && с ложно, а И Ь истинно, а И с истинно.
Рассмотрим более подробно, каким образом реализуются логические операторы & и |. Они представлены в следующем фрагменте кода.
// Перегрузить логический оператор | для укороченного вычисления, public static ThreeD operator | (ThreeD opl, ThreeD op2)
{
else
return new ThreeD(0, 0, 0);
}
// Перегрузить логический оператор & для укороченного вычисления, public static ThreeD operator & (ThreeD opl, ThreeD op2)
{
if ( ((opl.x != 0) && (opl.y != 0) && (opl.z != 0)) &
((op2.x != 0) && (op2.y != 0) && (op2.z != 0)) )
return new ThreeD(1, 1,1); else
return new ThreeD (0, 0, 0);
Прежде всего обратите внимание на то, что методы обоих перегружаемых логических операторов теперь возвращают объект типаThreeD.И особенно обратите внимание на то, как формируется этот объект. Если логическая операция дает истинный результат, то создается и возвращается истинный объект типаThreeD,у которого хотя бы одна координата не равна нулю. Если же логическая операция дает ложный результат, то соответственно создается и возвращается ложный объект. Таким образом, результатом вычисления логического выражения а & b в следующем фрагменте кода:
if(а & b) Console.WriteLine("а & b истинно."); else Console.WriteLine("а & b ложно.");
является объект типаThreeD,который в данном случае оказывается истинным. А поскольку операторыtrueиfalseуже определены, то созданный объект типаThreeDподвергается действию оператораtrueи в конечном итоге возвращается результат типаbool.В данном случае он равенtrue,а следовательно, условный операторifуспешно выполняется.
Благодаря тому что все необходимые правила соблюдены, укороченные операторы становятся доступными для применения к объектамThreeD.Они действуют следующим образом. Первый операнд проверяется с помощью операторного методаoperator true(для оператора | |) или же с помощью операторного методаoperator false(для оператора &&). Если удается определить результат данной операции, то соответствующий перегружаемый оператор (& или |) далее не выполняется. В противном случае перегружаемый оператор (& или | соответственно) используется для определения конечного результата. Следовательно, когда применяется укороченный логический оператор & & или I |, то соответствующий логический оператор & или | вызывается лишь в том случае, если по первому операнду невозможно определить результат вычисления выражения. В качестве примера рассмотрим следующую строку кода из приведенной выше программы.
if(а И с) Console.WriteLine("а || с истинно.");
В этой строке кода сначала применяется оператор true к объекту а. В данном случае объект а истинен, и поэтому использовать далее операторный метод | нет необходимости. Но если переписать данную строку кода следующим образом:
if(с И a) Console.WriteLine ("с || а истинно.");
то оператор true был бы сначала применен к объекту с, который в данном случае ложен. А это означает, что для определения истинности объекта а пришлось бы далее вызывать операторный метод |.
Описанный выше способ применения укороченных логических операторов может показаться, на первый взгляд, несколько запутанным, но если подумать, то в таком применении обнаруживается известный практический смысл. Ведь благодаря перегрузке операторов true и false для класса компилятор получает разрешение на применение укороченных логических операторов, не прибегая к явной их перегрузке. Это дает также возможность использовать объекты в условных выражениях. И вообще, логические операторы & и | лучше всего реализовывать полностью, если, конечно, не требуется очень узко направленная их реализация.
Операторы преобразования
Иногда объект определенного класса требуется использовать в выражении, включающем в себя данные других типов. В одних случаях для этой цели оказывается пригодной перегрузка одного или более операторов, а в других случаях — обыкновенное преобразование типа класса в целевой тип. Для подобных ситуаций в C# предусмотрена специальная разновидность операторного метода, называемаяоператором преобразования.Такой оператор преобразует объект исходного класса в другой тип. Операторы преобразования помогают полностью интегрировать типы классов в среду программирования на С#, разрешая свободно пользоваться классами вместе с другими типами данных, при условии, что определен порядок преобразования в эти типы.
Существуют две формы операторов преобразования: явная и неявная. Ниже они представлены в общем виде:
public static explicit operatorцелевой_тип{исходный_тип v){returnзначение;}public static implicit operatorцелевой_тип(исходный_тип v){returnзначение;}
гдецелевой_типобозначает тот тип, в который выполняется преобразование;ис-ходный_тип— тот тип, который преобразуется;значение— конкретное значение, приобретаемое классом после преобразования. Операторы преобразования возвращают данные, имеющиецелевой_тип,причем указывать другие возвращаемые типы данных не разрешается.
Если оператор преобразования указан в неявной форме(implicit),то преобразование вызывается автоматически, например, в том случае, когда объект используется в выражении вместе со значением целевого типа. Если же оператор преобразования указан в явной форме(explicit),то преобразование вызывается в том случае, когда выполняется приведение типов. Для одних и тех же исходных и целевых типов данных нельзя указывать оператор преобразования одновременно в явной и неявной форме.
Создадим оператор преобразования специально для классаThreeD,чтобы продемонстрировать его применение. Допустим, что требуется преобразовать объект типаThreeDв целое значение, чтобы затем использовать его в целочисленном выражении. Такое преобразование требуется, в частности, для получения произведения всех трех координат объекта. С этой целью мы воспользуемся следующей неявной формой оператора преобразования.
public static implicit operator int(ThreeD opl)
{
return opl.x * opl.у * opl.z;
}
Ниже приведен пример программы, демонстрирующей применение этого оператора преобразования.
// Пример применения оператора неявного преобразования, using System;
// Класс для хранения трехмерных координат, class ThreeD {
int х, у, z; // трехмерные координаты
public ThreeD() { х = у = z = 0; }
public ThreeD(int i, int j, int k) { x = i; у = j; z = k; }
// Перегрузить бинарный оператор +.
public static ThreeD operator +(ThreeD opl, ThreeD op2)
{
ThreeD result = new ThreeD();
result.x = opl.x + op2.x;
result.у = opl.y + op2.y;
result.z = opl.z + op2.z;
return result;
}
// Неявное преобразование объекта типа ThreeD к типу int. public static implicit operator int(ThreeD opl)
{
return opl.x * opl.y * opl.z;
}
// Вывести координаты X, Y, Z. public void Show()
{
Console.WriteLine(x + ", " + у + ", " + z) ;
}
}
class ThreeDDemo { static void Main() {
ThreeD a = new ThreeD(1, 2, 3);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD с = new ThreeD(); int i;
Console.Write("Координаты точки a: ");
a.Show();
Console.WriteLine() ;
Console.Write("Координаты точки b: ");
b.Show();
Console.WriteLine();
с = a + b; // сложить координаты точек а и b Console.Write("Результат сложения a + b: ");
c.Show ();
Console.WriteLine(); i = a; // преобразовать в тип int
Console.WriteLine("Результат присваивания i = a: " + i) ; Console.WriteLine();
i=a*2-b; // преобразовать в тип int
Console.WriteLine("Результат вычисления выражения a * 2 -
}
}
Вот к какому результату приводит выполнение этой программы.
Координаты точки а:1, 2,3
Координаты точки Ь: 10, 10, 10
Результат сложения а+Ь: 11, 12, 13 Результат присваивания i = а: 6
Результат вычисления выражения а * 2 - Ь: -988
Как следует из приведенного выше примера программы, когда объект типаThreeDиспользуется в таком целочисленном выражении, какi=а,происходит его преобразование. В этом конкретном случае преобразование приводит к возврату целого значения 6, которое является произведением координат точкиа,хранящихся в объекте того же названия. Но если для вычисления выражения преобразование в типintне требуется, то оператор преобразования не вызывается. Именно поэтому операторный методoperator int ()невызывается при вычислении выраженияс = а + Ь.
Но для различных целей можно создать разные операторы преобразования. Так, для преобразования объекта типаThreeDв типdoubleможно было бы определить второй оператор преобразования. При этом каждый вид преобразования выполнялся бы автоматически и независимо от другого.
Оператор неявного преобразования применяется автоматически в следующих случаях: когда в выражении требуется преобразование типов; методу передается объект; осуществляется присваивание и производится явное приведение к целевому типу. С другой стороны, можно создать оператор явного преобразования, вызываемый только тогда, когда производится явное приведение типов. В таком случае оператор явного преобразования не вызывается автоматически. В качестве примера ниже приведен вариант предыдущей программы, переделанный для демонстрации явного преобразования в типint.
// Применить явное преобразование, using System;
// Класс для хранения трехмерных координат, class ThreeD {
int х, у, z; // трехмерные координаты public ThreeD() { х = у = z = 0; }
public ThreeD(int i, int j, int k) { x = i; у = j; z = k; }
// Перегрузить бинарный оператор +.
public static ThreeD operator +(ThreeD opl, ThreeD op2)
{
ThreeD result = new ThreeD();
result.x = opl.x + op2.x; result.у = opl.у + op2.y; result.z = opl.z + op2.z;
return result;
}
// Выполнить на этот раз явное преобразование типов, public static explicit operator int(ThreeD opl)
{
return opl.x * opl.у * opl.z;
}
// Вывести координаты X, Y, Z. public void Show ()
{
Console.WriteLine(x+ ", " + у + ", " + z) ;
}
}
class ThreeDDemo { static void Main() {
ThreeD a = new ThreeD(1, 2, 3) ;
ThreeD b = new ThreeD(10, 10, 10);
ThreeD с = new ThreeD(); int i;
Console.Write("Координаты точки a: ");
a.Show();
Console.WriteLine() ;
Console.Write("Координаты точки b: ");
b.Show() ;
Console.WriteLine() ;
с = a + b; // сложить координаты точек а и b Console.Write("Результат сложения a + b: ");
c.Show();
Console.WriteLine() ;
i = (int) a; // преобразовать в тип int явно,
// поскольку указано приведение типов Console.WriteLine("Результат присваивания i = а: " + i) ;
Console.WriteLine();
i = (int)a * 2 - (int)b; // явно требуется приведение типов Console.WriteLine("Результат вычисления выражения а * 2 - b: " + i) ;
}
}
Оператор преобразования теперь указан в явной форме, и поэтому преобразование должно быть явно приведено к типу int. Например, следующая строка кода не будет скомпилирована, если исключить приведение типов.
i = (int) а; // преобразовать в тип int явно,
// поскольку указано приведение типов
На операторы преобразования накладывается ряд следующих ограничений.
• Исходный или целевой тип преобразования должен относиться к классу, для которого объявлено данное преобразование. В частности, нельзя переопределить преобразование в тип int, если оно первоначально указано как преобразование в тип double.
• Нельзя указывать преобразование в класс ob j ect или же из этого класса.
• Для одних и тех же исходных и целевых типов данных нельзя указывать одновременно явное и неявное преобразование.
• Нельзя указывать преобразование базового класса в производный класс. (Подробнее о базовых и производных классах речь пойдет в главе 11.)
• Нельзя указывать преобразование в интерфейс или же из него. (Подробнее об интерфейсах — в главе 12.)
Помимо указанных выше ограничений, имеется ряд рекомендаций, которыми обычно руководствуются при выборе операторов явного или неявного преобразования. Несмотря на все преимущества неявных преобразований, к ним следует прибегать только в тех случаях, когда преобразованию не свойственны ошибки. Во избежание подобных ошибок неявные преобразования должны быть организованы только в том случае, если удовлетворяются следующие условия. Во-первых, информация не теряется, например, в результате усечения, переполнения или потери знака. И во-вторых, преобразование не приводит к исключительной ситуации. Если же неявное преобразование не удовлетворяет этим двум условиям, то следует выбрать явное преобразование.
Рекомендации и ограничения по перегрузке операторов
Действие перегружаемого оператора распространяется на класс, для которого он определяется, и никак не связано с его первоначальным применением к данным встроенных в C# типов. Но ради сохранения ясности структуры и удобочитаемости исходного кода перегружаемый оператор должен, по возможности, отражать основную суть своего первоначального назначения. Например, назначение оператора + для классаThreeDпо сути не должно заметно отличаться от его назначения для целочисленных типов данных. Если бы, например, определить оператор + относительно некоторого класса таким образом, чтобы по своему действию он стал больше похожим на оператор /, то вряд ли от этого было бы много проку. Главный принцип перегрузки операторов заключается в следующем: несмотря на то, что перегружаемый оператор может получить любое назначение, ради ясности новое его назначение должно быть так или иначе связано с его первоначальным назначением.
На перегрузку операторов накладывается ряд ограничений. В частности, нельзя изменять приоритет любого оператора или количество операндов, которое требуется для оператора, хотя в операторном методе можно и проигнорировать операнд. Кроме того, имеется ряд операторов, которые нельзя перегружать. А самое главное, что перегрузке не подлежит ни один из операторов присваивания, в том числе и составные, как, например, оператор +=. Ниже перечислены операторы, которые нельзя перегружать. Среди них имеются и такие операторы, которые будут рассматриваться далее в этой книге.
&&
0
9
? ?
[]
1 1
=
=>
->
as
checked
default
is
new
sizeof
typeof
unchecked
Несмотря на то что оператор приведения () нельзя перегружать явным образом, имеется все же возможность создать упоминавшиеся ранее операторы преобразования, выполняющие ту же самую функцию.
Ограничение, связанное с тем, что некоторые операторы, например +=, нельзя перегружать, на самом деле не является таким уж непреодолимым. Вообще говоря, если оператор определен как перегружаемый и используется в составном операторе присваивания, то обычно вызывается метод этого перегружаемого оператора. Следовательно, при обращении к оператору += в программе автоматически вызывается заранее объявленный вариант метода opera tor + (). Например, в приведенном ниже фрагменте кода метод operator+ () автоматически вызывается для класса ThreeD, а в итоге объект b будет содержать координаты 11,12,13.
ThreeD а = new ThreeD(1, 2, 3) ;
ThreeD b = new ThreeD(10, 10, 10);
b += a; // сложить координаты точек а и b
И последнее замечание: несмотря на то, что оператор индексации массива [ ] нельзя перегружать с помощью операторного метода, имеется возможность создать индексаторы, о которых речь пойдет в следующей главе.
Еще один пример перегрузки операторов
Во всех предыдущих примерах программ, представленных в этой главе, для демонстрации перегрузки операторов использовался классThreeD,и этой цели он служил исправно. Но прежде чем завершить эту главу, было бы уместно рассмотреть еще один пример перегрузки операторов. Общие принципы перегрузки операторов остаются неизменными независимо от применяемого класса, тем не менее, в рассматриваемом ниже примере наглядно демонстрируются сильные стороны такой перегрузки, особенно если это касается расширяемости типов.
В данном примере разрабатывается 4-разрядный целочисленный тип данных и для него определяется ряд операций. Вам, вероятно, известно, что на ранней стадии развития вычислительной техники широко применялся тип данных для обозначения 4-разрядных двоичных величин, называвшихсяполубайтами, поскольку они составляли половину байта, содержали одну шестнадцатеричную цифру и были удобны для ввода кода полубайтами с пульта ЭВМ, что в те времена считалось привычным занятием для программистов! В наше время этот тип данных применяется редко, но он по-прежнему является любопытным дополнением целочисленных типов данных в С#. По традиции полубайт обозначает целое значение без знака.
В приведенном ниже примере программы тип полубайтовых данных реализуется с помощью классаNybble.В качестве базового для него используется типint,но с ограничением на хранение данных от 0 до 15. В классеNybbleопределяются следующие операторы.
• Сложение двух объектов типаNybble.
• Сложение значения типа int с объектом типа Nybble.
• Сложение объекта типаNybbleсо значением типаint.
• Операции сравнения: больше (>) и меньше (<).
• Операция инкремента.
• Преобразование значения типаintв объект типаNybble.
• Преобразование объекта типаNybbleв значение типаint.
Перечисленных выше операций достаточно, чтобы показать, каким образом тип классаNybbleинтегрируется в систему типов С#. Но для полноценной реализации этого типа данных придется определить все остальные доступные для него операции. Попробуйте сделать это сами в качестве упражнения.
Ниже полностью приводится классNybble,а также классNybbleDemo,демонстрирующий его применение.
// Создать полубайтовый тип 4-разрядных данных под названием Nybble.
using System;
// тип4-разрядных данных.
class Nybble {
int val; // базовый тип для хранения данных
public Nybble() { val =0; }
public Nybble(int i) { val = i;
val = val & OxF; // сохранить 4 младших разряда} •
// Перегрузить бинарный оператор + для сложения двух объектов типа Nybble, public static Nybble operator +(Nybble opl, Nybble op2)
{
Nybble result = new Nybble (); result.val = opl.val + op2.val;
result.val = result.val & OxF; // сохранить 4 младших разряда return result;
}
// Перегрузить бинарный оператор + для сложения
// объекта типа Nybble и значения типа int.
public static Nybble operator + (Nybble opl, int op2)
{
Nybble result = new Nybble (); result.val = opl.val + op2;
result.val = result.val & OxF; // сохранить 4 младших разряда return result;
}
// Перегрузить бинарный оператор + для сложения // значения типа int и объекта типа Nybble, public static Nybble operator +(int opl, Nybble op2)
{
Nybble result = new Nybble();
result.val = opl + op2.val;
result.val = result.val & OxF; // сохранить 4 младших разряда return result;
}
// Перегрузить оператор ++.
public static Nybble operator ++(Nybble op)
{
Nybble result = new Nybble(); result.val = op.val + 1;
result.val = result.val & OxF; // сохранить 4 младших разряда return result;
}
// Перегрузить оператор >.
public static bool operator >(Nybble opl, Nybble op2)
{
if(opl.val > op2.val) return true; else return false;
}
// Перегрузить оператор <.
public static bool operator <(Nybble opl, Nybble op2)
{
if(opl.val < op2.val) return true; else return false;
}
// Преобразовать тип Nybble в тип int. public static implicit operator int (Nybble op)
{
return op.val;
}
// Преобразовать тип int в тип Nybble, public static implicit operator Nybble (int op)
{
return new Nybble(op);
}
}
class NybbleDemo { static void Main() {
Nybble a = new Nybble(1);
Nybble b = new Nybble(10);
Nybble с = new Nybble(); int t;
Console.WriteLine("a: " + (int) a); Console.WriteLine("b: " + (int) b);
if(а < b) Console.WriteLine("а меньше Ь\п");
//Сложить два объекта типаNybble, с =а+ b;
Console.WriteLine("с после операции с = а + b: " + (int) с);
// Сложить значение типа int с объектом типа Nybble, а += 5;
Console.WriteLine("а после операции а += 5: " + (int) а);
Console.WriteLine() ;
// Использовать тип Nybble в выражении типа int. t = а * 2 + 3;
Console.WriteLine("Результат вычисления выражения а * 2 + 3: " + t); Console.WriteLine();
// Продемонстрировать присваивание значения типа int и переполнение, а = 19;
Console.WriteLine("Результат присваивания а = 19: " + (int) а);
Console.WriteLine() ;
// Использовать тип Nybble для управления циклом.
Console.WriteLine("Управление циклом for " +
"с помощью объекта типа Nybble."); for(а =0; а < 10; а++)
Console.Write((int) а + " ");
Console.WriteLine();
}
}
При выполнении этой программы получается следующий результат.
а: 1 Ь: 10
а меньше b
с после операции с = а + Ь: 11 а после операции а += 5: 6
Результат вычисления выражения а * 2 + 3: 15 Результат присваивания а = 19: 3
Управление циклом for с помощью объекта типа Nybble.
0123456789
Большая часть функций класса Nybble не требует особых пояснений. Тем не менее необходимо подчеркнуть ту особую роль, которую операторы преобразования играют в интегрировании класса типа Nybble в систему типов С#. В частности, объект типа Nybble можно свободно комбинировать с данными других типов в арифметических выражениях, поскольку определены преобразования объекта этого типа в тип int и обратно. Рассмотрим для примера следующую строку кода из приведенной выше программы.
t = а * 2 + 3;
В этом выражении переменнаяtи значения2и3относятся к типуint,но в ней присутствует также объект типаNybble.Оба типа оказываются совместимыми благодаря неявному преобразованию типаNybbleв типint.В данном случае остальная часть выражения относится к типуint,поэтому объектапреобразуется в типintс помощью своего метода преобразования.
А благодаря преобразованию типаintв типNybbleзначение типаintможет быть присвоено объекту типаNybble.Например, в следующей строке из приведенной выше программы:
а = 19;
сначала выполняется оператор Преобразования типаintв типNybble.Затем создается новый объект типаNybble,в котором сохраняются 4 младших разряда целого значения 19, а по существу, число 3, поскольку значение 19 превышает диапазон представления чисел для типаNybble.Далее этот объект присваивается переменной экземпляраа.Без операторов преобразования подобные выражения были бы просто недопустимы.
Кроме того, преобразование типаNybbleв типNybbleиспользуется в циклеfor.Без такого преобразования организовать столь простой циклforбыло бы просто невозможно.
ПРИМЕЧАНИЕ
В качестве упражнения попробуйте создать вариант полубайтового типа Nybble, предотвращающий переполнение, если присваиваемое значение оказывается за пределами допустимого диапазона чисел. Для этой цели лучше всего сгенерировать исключение. (Подробнее об исключениях — в главе 13.)
ГЛАВА 10 Индексаторы и свойства
В этой главе рассматриваются две особые и тесно связанные друг с другом разновидности членов класса: индексаторы и свойства. Каждый из них по-своему расширяет возможности класса, способствуя более полной его интеграции в систему типов C# и повышая его гибкость.
В частности, индексаторы предоставляют механизм для индексирования объектов подобно массивам, а свойства — рациональный способ управления доступом к данным экземпляра класса. Эти члены класса тесно связаны друг с другом, поскольку оба опираются на еще одно доступное в C# средство: аксессор.
Индексаторы
Как вам должно быть уже известно, индексирование массива осуществляется с помощью оператора [ ].Длясоздаваемых классов можно определить оператор [ ], но с этой целью вместо операторного метода создается индексатор, который позволяет индексировать объект, подобно массиву. Индексаторы применяются, главным образом, в качестве средства, поддерживающего создание специализированных массивов, на которые накладывается одно или несколько ограничений. Тем не менее индексаторымогут служить практически любым целям,для которыхвыгодным оказывается такой же синтаксис, как и у массивов. Индексаторы могут быть одно- или многомерными.
Рассмотрим сначала одномерные индексаторы.
Создание одномерных индексаторов
Ниже приведена общая форма одномерного индексатора:
тип_элементаthis[intиндекс] {
// Аксессор для получения данных, get {
// Возврат значения, которое определяетиндекс.
}
IIАксессор для установки данных, set {
// Установка значения, которое определяетиндекс.
}
}
гдетип_элементаобозначает конкретный тип элемента индексатора. Следовательно, у каждого элемента, доступного с помощью индексатора, должен быть определенныйтип_элемента.Этот тип соответствует типу элемента массива. Параметриндексполучает конкретный индекс элемента, к которому осуществляется доступ. Формально этот параметр совсем не обязательно должен иметь типint,но поскольку индексаторы, как правило, применяются для индексирования массивов, то чаще всего используется целочисленный тип данного параметра.
В теле индексатора определены два аксессора (т.е. средства доступа к данным):getиset.Аксессор подобен методу, за исключением того, что в нем не объявляется тип возвращаемого значения или параметры. Аксессоры вызываются автоматически при использовании индексатора, и оба получают:индексв качестве параметра. Так, если индексатор указывается в левой части оператора присваивания, то вызывается аксессорsetи устанавливается элемент, на который указывает параметриндекс.В противном случае вызывается аксессорgetи возвращается значение, соответствующее параметруиндекс.Кроме того, аксессорsetполучает неявный параметрvalue,содержащий значение, присваиваемое по указанному индексу.
Преимущество индексатора заключается, в частности, в том, что он позволяет полностью управлять доступом к массиву, избегая нежелательного доступа. В качестве примера рассмотрим программу, в которой создается классFail So f t Array,реализующий массив для выявления ошибок нарушения границ массива, а следовательно, для предотвращения исключительных ситуаций, возникающих во время выполнения в связи с индексированием массива за его границами. Для этого массив инкапсулируется в качестве закрытого члена класса, а доступ к нему осуществляется только с помощью индексатора. При таком подходе исключается любая попытка получить доступ к массиву за его границами, причем эта попытка пресекается без катастрофических последствий для программы. А поскольку в классеFailSof tArrayиспользуется индексатор, то к массиву можно обращаться с помощью обычной формы записи.
// Использовать индексатор для создания отказоустойчивого массива.
using System;
class FailSoftArray {
int[] a; // ссылка на базовый массив
public bool ErrFlag; // обозначает результат последней операции
// Построить массив заданного размера, public FailSoftArray(int size) { a = new irrt [size] ;
Length = size;
}
// Это индексатор для класса FailSoftArray. public int this[int index] {
// Это аксессор get. get {
if (ok(index)) {
ErrFlag = false; return a[index];
} else {
ErrFlag = true; return 0;
}
}
// Это аксессор set. set {
. if(ok(index)) {
a[index] = value;
ErrFlag = false;
}
else ErrFlag = true;
}
}
// Возвратить логическое значение true, если // индекс находится в установленных границах, private bool ok(int index) {
if(index >= 0 & index < Length) return true; return false;
}
}
// Продемонстрировать применение отказоустойчивого массива, class FSDemo {
static void Main() {
FailSoftArray fs = new FailSoftArray(5); int x;
// Выявить скрытые сбои.
Console.WriteLine("Скрытый сбой."); for(int i=0; i < (fs.Length * 2); i++) fs[i] = i*10;
for(int i=0; i < (fs.Length * 2); i++) {
x = fs[i] ;
if (x != -1) Console.Write(x + " ") ;
Console.WriteLine ();
//А теперь показать сбои.
Console.WriteLine("ХпСбой с уведомлением об ошибках."); for(int i=0; i < (fs.Length * 2); i++) {
fs[i] = i * 10; if(fs.ErrFlag)
Console.WriteLine("fs[" + i + "] вне границ");
}
for(int i=0; i < (fs.Length * 2); i++) { N
x = f s [ i ] ;
if(!fs.ErrFlag) Console.Write(x + " "); else
Console.WriteLine("fs[" + i + "] вне границ");
}
}
}
Вот к какому результату приводит выполнение этой программы.
Скрытый сбой.
О 10 20 30 40 О О О О О
Сбой с уведомлением об ошибках.
fs[5] вне границ
fs[6] вне границ
fs[7] вне границ
fs[8] вне границ
fs[9] вне границ
О 10 20 30 40 fs[5] вне границ
fs[6] вне границ
fs[7] вне границ
fs[8] вне границ
fs[9] вне границ
Индексатор препятствует нарушению границ массива. Внимательно проанализируем каждую часть кода индексатора. Он начинается со следующей строки.
public int this[int index] {
В этой строке кода объявляется индексатор, оперирующий элементами типаint.Ему передается индекс в качестве параметраindex.Кроме того, индексатор объявляется открытым(public),что дает возможность использовать этот индексатор в коде за пределами его класса.
Рассмотрим следующий код аксессораget.
get {
if (ok(index) ) {
ErrFlag = false; return a[index];
} else {
ErrFlag = true; return 0;
Аксессорgetпредотвращает ошибки нарушения границ массива, проверяя в первую очередь, находитсялииндекс в установленных границах. Эта проверка границ выполняется в методеok (), который возвращает логическое значениеtrue,если индекс правильный, а_ртначе — логическое значениеfalse.Так, если указанный индекс находитсяъустановленных границах, то по этому индексу возвращается соответствующий элемент. А если индекс оказывается вне установленных границ, то никаких операций не выполняется, но в то же время не возникает никаких ошибок переполнения. В данном варианте классаFailSof tArrayпеременнаяErrFlagсодержит результат каждой операции. Ее содержимое может быть проверено после каждой операции на предмет удачного или неудачного выполнения последней. (В главе 13 будет представлен более совершенный способ обработки ошибок с помощью имеющейся в C# подсистемы обработки исключительных ситуаций, а до тех пор можно вполне обойтись установкой и проверкой признака ошибки.)
А теперь рассмотрим следующий код аксессора set, предотвращающего ошибки нарушения границ массива.
set {
if(ok(index) ) {
a[index] = value;
ErrFlag = false;
}
else ErrFlag = true;
}
Если параметрindexметодаok() находится в установленных пределах, то соответствующему элементу массива присваивается значение, передаваемое из параметраvalue.В противном случае устанавливается логическое значениеtrueпеременнойErrFlag.Напомним, чтоvalueв любом аксессорном методе является неявным параметром, содержащим присваиваемое значение. Его не нужно (да и нельзя) объявлять отдельно.
Наличие обоих аксессоров, get и set, в индексаторе не является обязательным. Так, можно создать индексатор только для чтения, реализовав в нем один лишь аксессор get, или же индексатор только для записи с единственным аксессором set.
Перегрузка индексаторов
Индексатор может быть перегружен. В этом случае для выполнения выбирается тот вариант индексатора, в котором точнее соблюдается соответствие его параметра и аргумента, указываемого в качестве индекса. Ниже приведен пример программы, в которой индексатор массива классаFailSof tArrayперегружается для индексов типаdouble.При этом индексатор типаdoubleокругляет свой индекс до ближайшего целого значения.
// Перегрузить индексатор массива класса FailSoftArray.
using System;
class FailSoftArray {
int[] a; // ссылка на базовый массив
public bool ErrFlag; // обозначает результат последней операции
// Построить массив заданного размера, public FailSoftArray(int size) { a = new int[size];
Length = size;
}
// Это индексатор типа int для массива FailSoftArray. public int this[int index] {
// Это аксессор get. get {
if(ok(index)) {
ErrFlag = false; return a[index];
} else {
ErrFlag = true; return 0;
}
}
// Это аксессор set. set {
if(ok(index)) {
a[index] = value;
ErrFlag = false;
}
else ErrFlag = true;
}
}
/* Это еще один индексатор для массива FailSoftArray.
Он округляет свой аргумент до ближайшего целого индекса. */ public int this[double idx] {
// Это аксессор get. get {
int index;
// Округлить до ближайшего целого.
if( (idx - (int) idx) < 0.5) index = (int) idx;
else index = (int) idx + 1;
if(ok(index)) {
ErrFlag = false; return a[index];
} else {
ErrFlag = true; return 0;
}
}
// Это аксессор set. set {
int index;
// Округлить до ближайшего целого.
if( (idx - (int) idx) < 0.5) index = (int) idx;
else index = (int) idx + 1;
if (ok (index) ) {
a[index] = value;
ErrFlag = false;
}
else ErrFlag = true;
}
}
// Возвратить логическое значение true, если // индекс находится в установленных границах, private bool ok(int index) {
if(index >= 0 & index < Length) return true; return false;
}
}
// Продемонстрировать применение отказоустойчивого массива, class FSDemo {
static void Main() {
FailSoftArray fs = new FailSoftArray(5);
// Поместить ряд значений в массив fs. for(int i=0; i < fs.Length; i++) fs[i] = i;
// А теперь воспользоваться индексами // типа int и double для обращения к массиву.
Console.WriteLine("fs[1]: " + fs[1]);
Console.WriteLine("fs[2]: " + fs[2]);
Console.WriteLine("fs[1.1]: " + fs[l.l]);
Console.WriteLine("fs[1.6]: " + fs[1.6]);
}
}
При выполнении этой программы получается следующий результат.
f S [ 1 ] : 1
fs [2] : 2 fs[1 -1 ] : 1 f s [ 1. 6 ] : 2
Как показывает приведенный выше результат, индексы типа double округляются до ближайшего целого значения. В частности, индекс 1.1 округляется до 1, а индекс 1.6 — до 2.
Представленный выше пример программы наглядно демонстрирует правомочность перегрузки индексаторов, но на практике она применяется нечасто. Как правило, индексаторы перегружаются для того, чтобы использовать объект определенного класса в качестве индекса, вычисляемого каким-то особым образом.
Индексаторы без базового массива
Следует особо подчеркнуть, что индексатор совсем не обязательно должен оперировать массивом. Его основное назначение — предоставить пользователю функциональные возможности, аналогичные массиву. В качестве примера в приведенной ниже программе демонстрируется индексатор, выполняющий роль массива только для чтения, содержащего степени числа 2 от 0 до 15. Обратите внимание на то, что в этой программе отсутствует конкретный массив. Вместо этого индексатор просто вычисляет подходящее значение для заданного индекса.
// Индексаторы совсем не обязательно должны оперировать отдельными массивами.
using System;
class PwrOfTwo {
/* Доступ к логическому массиву, содержащему степени числа 2 от 0 до 15. */ public int this[int index] {
// Вычислить и возвратить степень числа 2. get {
if((index >= 0) && (index < 16)) return pwr(index);
else return -1;
}
// Аксессор set отсутствует.
}
int pwr(int p) { int result = 1;
for(int i=0; i < p; i++) result *= 2;
return result;
}
}
class UsePwrOfTwo { static void Main() {
PwrOfTwo pwr = new PwrOfTwo();
Console.Write("Первые 8 степеней числа 2: "); for(int i=0; i < 8; i++)
Console.Write(pwr[i] + " ");
Console.WriteLine();
Console.Write("А это некоторые ошибки: ");
Console.Write(pwr[-1] + " " + pwr[17]);
Вот к какому результату приводит выполнение этой программы.
Первые 8 степеней числа 2: 1 2 4 8 16 32 64 128 А это некоторые ошибки: -1 -1
Обратите вйимание на то, что в индексатор классаPwrOf Twoвключен только аксессорget,но в нем отсутствует аксессорset.Как пояснялось выше, такой индексатор служит только для чтения. Следовательно, объект классаPwrOf Twoможет указываться только в правой части оператора присваивания, но не в левой его части. Например, попытка ввести следующую строку кода в приведенную выше программу не приведет к желаемому результату.
pwr[0] =11; //не подлежит компиляции
Такой оператор присваивания станет причиной появления ошибки во время компиляции, поскольку для индексатора не определен аксессорset.
На применение индексаторов накладываются два существенных ограничения. Во-первых, значение, выдаваемое индексатором, нельзя передавать методу в качестве параметраrefилиout,поскольку в индексаторе не определено место в памяти для его хранения. И во-вторых, индексатор должен быть членом своего класса и поэтому не может быть объявлен какstatic.
Многомерные индексаторы
Индексаторы можно создавать и для многомерных массивов. В качестве примера ниже приведен двумерный отказоустойчивый массив. Обратите особое внимание на объявление индексатора в этом примере.
// Двумерный отказоустойчивый массив.
using System;
class FailSoftArray2D {
int[,] a; // ссылка на базовый двумерный массив int rows, cols; // размеры массива
public int Length; // открытая переменная длины массива public bool ErrFlag; // обозначает результат последней операции
// Построить массив заданных размеров, public FailSoftArray2D(int г, int с) { rows = г; cols = с;
а = new int[rows, cols];
Length = rows * cols;
}
// Это индексатор для класса FailSoftArray2D. public int this[int indexl, int index2] {
// Это аксессор get. get {
if(ok(indexl, index2)) {
ErrFlag = false;
return a[indexl, index2];
} else {
ErrFlag = true; return 0;
}
}
// Это аксессор set. set {
if(ok(indexl, index2)) {
a[indexl, index2] = value;
ErrFlag = false;
}
else ErrFlag = true;
}
}
// Возвратить логическое значение true, если // индексы находятся в установленных пределах, private bool ok(int indexl, int index2) {
•if (indexl >= 0 & indexl < rows & index2 >= 0 & index2 < cols) return true;
return false;
}
}
// Продемонстрировать применение двумерного индексатора, class TwoDIndexerDemo { static void Main() {
FailSoftArray2D fs = new FailSoftArray2D(3, 5); int x;
// Выявить скрытые сбои.
Console.WriteLine("Скрытый сбой."); for (int i=0; i < 6; i++) fs[i, i]=i*10;
for(int i=0; i < 6; i++) {
x = f s [ i, i ] ;
if(x != -1) Console.Write (x + " ");
}
Console.WriteLine ();
// А теперь показать сбои.
Console.WriteLine("\пСбой с уведомлением об ошибках."); for(int i=0; i < 6; i++) {
fs[i,i] = i *10; if(fs.ErrFlag)
Console.WriteLine("fs[" + i + ", " + i + "] вне границ
}
for(int i=0; i < 6; i++) {
x = f s [ i, i ] ;
if(!fs.ErrFlag) Console.Write(x + " ");
else
Console.WriteLine("fs[" + i + ", " + i + "] вне границ");
}
}
}
Вот к какому результату приводит выполнение этого кода:
Скрытый сбой.
0 10 20 0 0 0
Сбой с уведомлением об ошибках.
fs[3, 3] вне границ
fs[4, 4] вне границ
fs[5, 5] вне границ
0 10 20 fs[3, 3] вне границ
fs[4, 4] вне границ
fs[5, 5] вне границ
Свойства
Еще одной разновидностью члена класса являетсясвойство.Как правило, свойство сочетает в себе поле с методами доступа к нему. Как было показано в приведенных ранее примерах программ, поле зачастую создается, чтобы стать доступным для пользователей объекта, но при этом желательно сохранить управление над операциями, разрешенными для этого поля, например, ограничить диапазон значений, присваиваемых данному полю. Этой цели можно, конечно, добиться и с помощью закрытой переменной, а также методов доступа к ее значению, но свойство предоставляет более совершенный и рациональный путь для достижения той же самой цели.
Свойства очень похожи на индексаторы. В частности, свойство состоит из имени и аксессоров get и set. Аксессоры служат для получения и установки значения переменной. Главное преимущество свойства заключается в том, что его имя может быть использовано в выражениях и операторах присваивания аналогично имени обычной переменной, но в действительности при обращении к свойству по имени автоматически вызываются его аксессоры get и set. Аналогичным образом используются аксессоры get и set индексатора.
Ниже приведена общая форма свойства:
ТИПимя{ *
get {
// код аксессора для чтения из поля
}
set {
// код аксессора для записи в поле
}
гдетипобозначает конкретный тип свойства, например int, аимя —присваиваемое свойству имя. Как только свойство будет определено, любое обращение к свойству по имени приведет к автоматическому вызову соответствующего аксессора. Кроме того, аксессор set принимает неявный параметр value, который содержит значение, присваиваемое свойству.
Следует, однако, иметь в виду, что свойства не определяют место в памяти для хранения полей, а лишь управляют доступом к полям. Это означает, что само свойство не предоставляет поле, и поэтому поле должно быть определено независимо от свойства. (Исключение из этого правила составляетавтоматически реализуемоесвойство, рассматриваемое далее.)
Ниже приведен простой пример программы, в которой определяется свойствоMy Prop,предназначенное для доступа к полюprop.В данном примере свойство допускает присваивание только положительных значений.
// Простой пример применения свойства.
using System;
class SimpProp {
int prop; // поле, управляемое свойством MyProp
public SimpProp() { prop =0; }
/* Это свойство обеспечивает доступ к закрытой переменной экземпляра prop. Оно допускает присваивание только положительных значений. */ public int MyProp { get {
return prop;
}
set {
if(value >= 0) prop = value;
}
}
}
// Продемонстрировать применение свойства, class PropertyDemo { static void Main() {
SimpProp ob = new SimpProp();
Console.WriteLine("Первоначальное значение ob.MyProp: " + ob.MyProp);
ob.MyProp = 100; // присвоить значение Console.WriteLine("Текущее значение ob.MyProp: " + ob.MyProp);
// Переменной prop нельзя присвоить отрицательное значение.
Console.WriteLine("Попытка присвоить значение " +
"-10 свойству ob.MyProp");
ob.MyProp = -10;
Console.WriteLine("Текущее значение ob.MyProp: " + ob.MyProp);
}
}
Вот к какому результату приводит выполнение этого кода.
Первоначальное значение ob.MyProp: 0 Текущее значение ob.MyProp: 100
Попытка присвоить значение -10 свойству ob.MyProp Текущее значение ob.MyProp: 100
Рассмотрим приведенный выше код более подробно. В этом коде определяется одно закрытое полеpropи свойствоМуРгор,управляющее доступом к полюprop.Как пояснялось выше, само свойство не определяет место в памяти для хранения поля, а только управляет доступом к полю. Кроме того, полеpropявляется закрытым, а значит, оно доступнотолькочерез свойствоМуРгор.
СвойствоМуРгоруказано какpublic,а следовательно, оно доступно из кода за пределами его класса. И в этом есть своя логика, поскольку данное свойство обеспечивает доступ к полюprop,которое является закрытым. Аксессорgetэтого свойства просто возвращает значение из поляprop,тогда как аксессорsetустанавливает значение в полеpropв том и только в том случае, если это значение оказывается положительным. Таким образом, свойствоМуРгорконтролирует значения, которые могут храниться в полеprop.В этом, собственно, и состоит основное назначение свойств.
Тип свойстваМуРгоропределяется как для чтения, так и для записи, поскольку оно позволяет читать и записывать данные в базовое поле. Тем не менее свойства можно создавать доступными только для чтения или только для записи. Так, если требуется создать свойство, доступное только для чтения, то достаточно определить единственный аксессорget.А если нужно создать свойство, доступное только для записи, то достаточно определить единственный аксессорset.
Воспользуемся свойством для дальнейшего усовершенствования отказоустойчивого массива. Как вам должно быть уже известно, у всех массивов имеется соответствующее свойство длины(Length).До сих пор в классеFailSoftArrayдля этой цели использовалось открытое целочисленное полеLength.Но это далеко не самый лучший подход, поскольку он допускает установку значений, отличающихся от длины отказоустойчивого массива. (Например, программист, преследующий злонамеренные цели, может умышленно ввести неверное значение в данном поле.) Для того чтобы исправить это положение, превратим полеLengthв свойство "только для чтения", как показано в приведенном ниже, измененном варианте классаFailSoftArray.
// Добавить свойство Length в класс FailSoftArray.
using System;
class FailSoftArray {
int[] a; // ссылка на базовый массив
int len; // длина массива — служит основанием для свойства Length
public bool ErrFlag; // обозначает результат последней операции
// Построить массив заданного размера, public FailSoftArray(int size) { a = new int[size]; len = size;
}
// Свойство Length только для чтения, public int Length { get {
return len;
// Это индексатор для класса FailSoftArray. public int this[int index] {
// Это аксессор get. get {
if(ok(index)) {
ErrFlag = false; return a[index];
} else {
ErrFlag = true; return 0;
}
}
// Это аксессор set. set {
if(ok(index)) {
a[index] = value;
ErrFlag = false;
}
else ErrFlag = true;
}
}
// Возвратить логическое значение true, если // индекс находится в установленных границах, private bool ok(int index) {
if(index >= 0 & index < Length) return true; return false;
}
}
// Продемонстрировать применение усовершенствованного // отказоустойчивого массива, class ImprovedFSDemo { static void Main() {
FailSoftArray fs = new FailSoftArray(5); int x;
// Разрешить чтение свойства Length, for (int i=0; i < fs.Length; i++) fs[i] = i*10;
for (int i=0; i < fs.Length; i++) {
x = f s [ i ] ;
if(x != -1) Console.Write(x + " ");
}
Console.WriteLine ();
// fs.Length = 10; // Ошибка, запись запрещена!
}
}
Теперь Length — это свойство, в котором местом для хранения данных служит закрытая переменная 1еп. А поскольку в этом свойстве определен единственный ак-сессорget,то оно доступно только для чтения. Это означает, что значение свойстваLengthможно только читать, но не изменять. Для того чтобы убедиться в этом, попробуйте удалить символы комментария в начале следующей строки из приведенного выше кода.
// fs.Length =10; // Ошибка, запись запрещена!
При попытке скомпилировать данный код вы получите сообщение об ошибке, уведомляющее о том, чтоLengthявляется свойством, доступным только для чтения.
Добавлением свойстваLengthв классFailSoftArrayусовершенствование рассматриваемого здесь примера кода с помощью свойств далеко не исчерпывается. Еще одним членом данного класса, подходящим для превращения в свойство, служит переменнаяErrFlag,поскольку ее применение должно быть ограничено только чтением. Ниже приведен окончательно усовершенствованный вариант классаFailSoftArray,в котором создается свойствоError,использующее в качестве места для хранения данных исходную переменнуюErrFlag,ставшую теперь закрытой.
// Превратить переменную ErrFlag в свойство.
using System;
class FailSoftArray {
int[] a; // ссылка на базовый массив int len; // длина массива
bool ErrFlag; // теперь это частная переменная,
// обозначающая результат последней операции
// Построить массив заданного размера, public FailSoftArray(int size) { a = new int[size]; len = size;
}
// Свойство Length только для чтения, public int Length { get {
return len;
}
}
// Свойство Error только для чтения, public bool Error { get {
return ErrFlag;
}
}
// Это индексатор для класса FailSoftArray. public int this[int index] {
// Это аксессор get. get {
if(ok(index)) {
ErrFlag = false; return a[index];
} else {
ErrFlag = true; return 0;
}
}
// Это аксессор set. set {
if(ok(index)) {
a[index] = value;
ErrFlag = false;
}
else ErrFlag = true;
}
}
// Возвратить логическое значение true, если // индекс находится в установленных границах, private bool ok(int index) {
if(index >= 0 & index < Length) return true; return false;
}
}
// Продемонстрировать применение отказоустойчивого массива, class FinalFSDemo { static void Main() {
FailSoftArray fs = new FailSoftArray(5);
// Использовать свойство Error, for(int i=0; i < fs.Length + 1; i++) {
fs[i] = i * 10; if(fs.Error)
tonsole.WriteLine("Ошибка в индексе " + i);
}
}
}
Создание свойстваErrorстало причиной двух следующих изменений в классеFailSoftArray.Во-первых, переменнаяErrFlagбыла сделана закрытой, поскольку теперь она служит базовым местом хранения данных для свойстваError,а следовательно, она не должна быть доступна непосредственно. И во-вторых, было введено свойствоError"только для чтения". Теперь свойствоErrorбудет опрашиваться в тех программах, где требуется организовать обнаружение ошибок. Именно это и было продемонстрировано выше в методеMain (), где намеренно сгенерирована ошибка нарушения границ массива, а для ее обнаружения использовано свойствоError.
Автоматически реализуемые свойства
Начиная с версии C# 3.0, появилась возможность для реализации_очень простых свойств, не прибегая к явному определению переменной, которой управляет свойство. Вместо этого базовую переменную для свойства автоматически предоставляет компилятор. Такое свойство называетсяавтоматически реализуемыми принимает следующую общую форму:
тип имя {get; set; }
гдетипобозначает конкретный тип свойства, аимя —присваиваемое свойству имя. Обратите внимание на то, что после обозначений аксессоровgetиsetсразу же следует точка с запятой, а тело у них отсутствует. Такой синтаксис предписывает компилятору создать автоматически переменную, иногда еще называемуюподдерживающим полем,для хранения значения. Такая переменная недоступна непосредственно и не имеет имени. Но в то же время она может быть доступна через свойство.
Ниже приведен пример объявления свойства, автоматически реализуемого под именемUserCount.
public int UserCount { get; set; }
Как видите, в этой строке кода переменная явно не объявляется. И как пояснялось выше, компилятор автоматически создает анонимное поле, в котором хранится значение. А в остальном автоматически реализуемое свойствоUserCountподобно всем остальным свойствам.
Но в отличие от обычных свойств автоматически реализуемое свойство не может быть доступным только для чтения или только для записи. При объявлении этого свойства в любом случае необходимо указывать оба аксессора —getиset.Хотя добиться желаемого (т.е. сделать автоматически реализуемое свойство доступным только -для чтения или только для записи) все же можно, объявив ненужный аксессор какprivate(подробнее об этом — в разделе "Применение модификаторов доступа в аксессорах").
Несмотря на очевидные удобства автоматически реализуемых свойств, их применение ограничивается в основном теми ситуациями, в которых не требуется управление установкой или получением значений из поддерживающих полей. Напомним, что поддерживающее поле недоступно напрямую. Это означает, что на значение, которое может иметь автоматически реализуемое свойство, нельзя наложить никаких ограничений. Следовательно, имена автоматически реализуемых свойств просто заменяют собой имена самих полей, а зачастую именно это и требуется в программе. Автоматически реализуемые свойства могут оказаться полезными и в тех случаях, когда с помощью свойств функциональные возможности программы открываются для сторонних пользователей, и для этой цели могут даже применяться специальные средства проектирования.
Применение инициализаторов объектов в свойствах
Как пояснялось в главе 8,инициализатор объектаприменяется в качестве альтернативы явному вызову конструктора при создании объекта. С помощью инициализаторов объектов задаются начальные значения полей или свойств, которые требуется инициализировать. При этом синтаксис инициализаторов объектов оказывается одинаковым как для свойств, так и для полей. В качестве примера ниже приведена программа из главы 8, измененная с целью продемонстрировать применение инициализаторов объектов в свойствах. Напомним, что в версии этой программы из главы 8 использовались поля, а приведенная ниже версия отличается лишь тем, что в ней поляCountиStrпревращены в свойства. В то же время синтаксис инициализаторов объектов не изменился.
// Применить инициализаторы объектов в свойствах.
class MyClass {
11Теперь это свойства, public int Count { get; set; } public string Str { get; set; }
}
class ObjlnitDemo { static void Main() {
// Сконструировать объект типа MyClass с помощью инициализаторов объектов.
MyClass obj =
new MyClass { Count = 100, Str = "Тестирование" };
Console .WriteLine (obj .Cour^t + " " + obj.Str);
}
}
Как видите, свойстваCountиStrустанавливаются в выражениях с инициализатором объекта. Приведенная выше программа дает такой же результат, как и программа из главы 8, а именно:
100 Тестирование
Как пояснялось в главе 8, синтаксис инициализатора объекта оказывается наиболее пригодным для работы с анонимными типами, формируемыми в LINQ-выражениях. А в остальных случаях чаще всего используется синтаксис обычных конструкторов.
Ограничения, присущие свойствам
Свойствам присущ ряд существенных ограничений. Во-первых, свойство не определяет место для хранения данных, и поэтому не может быть передано методу в качестве параметраrefилиout.Во-вторых, свойство не подлежит перегрузке. Наличие двух разных свойств с доступом к одной и той же переменной допускается, но это, скорее, исключение, чем правило. И наконец, свойство не должно изменять состояние базовой переменной при вызове аксессораget.И хотя это ограничительное правило не соблюдается компилятором, его нарушение считается семантической ошибкой. Действие аксессораgetне должно носить характер вмешательства в функционирование переменной.
Применение модификаторов доступа в аксессорах
По умолчанию доступность аксессоровsetиgetоказывается такой же, как и у индексатора и свойства, частью которых они являются. Так, если свойство объявляется какpublic,то по умолчанию его аксессорыsetиgetтакже становятся открытыми(public).Тем не менее для аксессораsetилиgetможно указать собственный модификатор доступа, напримерprivate.Но в любом случае доступность аксессора, определяемая таким модификатором, должна быть более ограниченной, чем доступность, указываемая для его свойства или индексатора.
Существует целый ряд причин, по которым требуется ограничить доступность аксессора. Допустим, что требуется предоставить свободный доступ к значению свойства, но вместе с тем дать возможность устанавливать это свойство только членам его класса. Для этого достаточно объявить аксессор данного свойства какprivate.В приведенном ниже примере используется свойствоMy Prop,аксессорsetкоторого указан какprivate.
// Применить модификатор доступа в аксессоре.
using System;
class PropAccess {
int prop; // поле, управляемое свойством MyProp
public PropAccess() { prop = 0; }
/* Это свойство обеспечивает доступ к закрытой переменной экземпляра prop. Оно разрешает получать значение переменной prop из любого кода, но устанавливать его — только членам своего класса. */ public int MyProp { get {
return prop;
}
private set { // теперь это закрытый аксессор prop = value;
}
}
// Этот член класса инкрементирует значение свойства MyProp. public void IncrPropO {
MyProp++; // Допускается в. том же самом классе.
}
}
// Продемонстрировать применение модификатора доступа в аксессоре свойства, class PropAccessDemo { static void Main() {
PropAccess ob = new PropAccess() ;
Console.WriteLine("Первоначальное значение ob.MyProp: " + ob.MyProp);
// ob.MyProp = 100; // недоступно для установки ob.IncrProp();
Console.WriteLine("Значение ob.MyProp после инкрементирования: " + ob.MyProp);
}
}
В классеPropAccessаксессорsetуказан какprivate.Это означает, что он доступен только другим членам данного класса, например методуIncrProp(), но недоступен для кода за пределами классаPropAccess.Именно поэтому попытка Присвоить свойствуob. My Propзначение в классеPropAccessDemoзакомментирована.
Вероятно, ограничение доступа к аксессорам оказывается наиболее важным для работы с автоматически реализуемыми свойствами. Как пояснялось выше, создать
автоматически реализуемое свойство только для чтения или же только для записи нельзя, поскольку оба аксессора,getиset,должны быть указаны при объявлении такого свойства. Тем не менее добиться желаемого результата все же можно, объявив один из аксессоров автоматически реализуемого свойства какprivate.В качестве примера ниже приведено объявление автоматически реализуемого свойстваLengthдля классаFailSoftArray,которое фактически становится доступным только для чтения.
public int Length { get; private set; }
СвойствоLengthможет быть установлено только из кода в его классе, поскольку его аксессорsetобъявлен какprivate.А изменять свойствоLengthза пределами его класса не разрешается. Это означает, что за пределами своего класса свойство, по существу, оказывается доступным только для чтения. Аналогичным образом можно объявить и свойствоError,как показано ниже.
public bool Error { get; private set; }
Благодаря этому свойствоErrorстановится доступным для чтения, но не для установки за пределами классаFailSoftArray.
Для опробования автоматически реализуемых вариантов свойствLengthиErrorв классеFailSoftArrayудалим сначала переменныеlenиErrFlag,поскольку они больше не нужны, а затем заменим каждое применение переменныхlenиErrFlagсвойствамиLengthиErrorв классеFailSoftArray.Ниже приведен обновленный вариант классаFailSoftArrayвместе с методомMain(), демонстрирующим его применение.
// 'Применить автоматически реализуемые и доступные // только для чтения свойства Length и Error.
using System;
class FailSoftArray {
int[] a; // ссылка на базовый массив
// Построить массив по заданному размеру, public FailSoftArray(int size) { a = new int [size];
Length = size;
}
// Автоматически реализуемое и доступное только для чтения свойство Length, public int Length { get; private set; }
// Автоматически реализуемое и доступное только для чтения свойство Error, public bool Error { get; private set; }
// Это индексатор для массива FailSoftArray. public int this[int index] {
// Это аксессор get. get {
if(ok(index) ) {
Error = false; return a[index];
} else {
Error "= true; return 0;
}
}
// Это аксессор set. set {
if(ok(index)) {
a[index] = value;
Error = false;
}
else Error = true;
}
}
// Возвратить логическое значение true, если // индекс находится в установленных границах, private bool ok(int index) {
if(index >= 0 & index < Length) return true; return false;
}
}
// Продемонстрировать применение усовершенствованного // отказоустойчивого массива, class FinalFSDemo { static void Main() {
FailSoftArray fs = new FailSoftArray(5);
// Использовать свойство Error, for(int i=0; i < fs.Length + 1; i++) {
fs[i] = i * 10; if(fs.Error)
Console.WriteLine("Ошибка в индексе " + i);
}
}
}
Этот вариант классаFailSoftArrayдействует таким же образом, как и предыдущий, но в нем отсутствуют поддерживающие поля, объявляемые явно.
На применение модификаторов доступа в аксессорах накладываются следующие ограничения. Во-первых, действию модификатора доступа подлежит только один аксессор:setилиget,но не оба сразу. Во-вторых, модификатор должен обеспечивать более ограниченный доступ к аксессору, чем доступ на уровне свойства или индексатора. И наконец, модификатор доступа нельзя использовать при объявлении аксессора в интерфейсе или же при реализации аксессора, указываемого в интерфейсе. (Подробнее об интерфейсах речь пойдет в главе 12.)
Применение индексаторов и свойств
В предыдущих примерах программ был продемонстрирован основной принцип действия индексаторов и свойств, но их возможности не были раскрыты в полную силу. Поэтому в завершение этой главы обратимся к примеру классаRangeArray,в котором индексаторы и свойства используются для создания типа массива с пределами индексирования, определяемыми пользователем.
Как вам должно быть уже известно, индексирование всех массивов в C# начинается с нуля. Но в некоторых приложениях индексирование массива удобнее начинать с любой произвольной точки отсчета: с 1 или даже с отрицательного числа, например от -5 и до 5. Рассматриваемый здесь классRangeArrayразработан таким образом, чтобы допускать подобного рода индексирование массйвов.
Используя классRangeArray,можно написать следующий фрагмент кода.
RangeArray га = new RangeArray(-5, 10); // массив'с индексами от -5 до 10 for(int i=-5; i <= 10; i++) ra[i] = i; // индексирование массива от -5 до
10
Нетрудно догадаться, что в первой строке этого кода конструируется объект классаRangeArrayс пределами индексирования массива от-5до 10 включительно. Первый аргумент обозначает начальный индекс, а второй — конечный индекс. Как только объект га будет сконструирован, он может быть проиндексирован как массив в пределах от -5 до 10.
Ниже приведен полностью классRangeArrayвместе с классомRangeArrayDemo,в котором демонстрируется индексирование массива в заданных пределах. КлассRangeArrayреализован таким образом, чтобы поддерживать массивы типаint,но при желании вы можете изменить этот тип на любой другой.
/* Создать класс со специально указываемыми пределами индексирования массива. Класс RangeArray допускает индексирование массива с любого значения, а не только с нуля. При создании объекта класса RangeArray указываются начальный и конечный индексы. Допускается также указывать отрицательные индексы. Например, можно создать массивы, индексируемые от -5 до 5, от 1 до 10 или же от 50 до 56. */
using System;
class RangeArray {
// Закрытые данные.
int[] а; // ссылка на базовый массив int lowerBound; // наименьший индекс int upperBound; // наибольший индекс
// Автоматически реализуемое и доступное только для чтения свойство Length, public int Length { get; private set; }
// Автоматически реализуемое и доступное только для чтения свойство Error, public bool Error { get; private set; }
// Построить массив по заданному размеру, public RangeArray(int low, int high) { high++;
if (high <= low) {
Console.WriteLine("Неверные индексы");
high =1; // создать для надежности минимально допустимый массив low = 0;
}
а = new int[high - low];
Length = high - low;
lowerBound = low; upperBound = --high;
// Это индексатор для класса RangeArray. public int this[int index] {
// Это аксессор get. get {
if(ok(index)) {
Error = false;
return a[index - lowerBound];
} else {
Error = true; return 0;
}
}
// Это аксессор set. set {
if(ok(index)) {
a[index - lowerBound] = value;
Error = false;
}
else Error = true;
}
}
// Возвратить логическое значение true, если // индекс находится в установленных границах, private bool ok(int index) {
if(index >= lowerBound & index <= upperBound) return true; return false;
}
}
// Продемонстрировать применение массива с произвольно // задаваемыми пределами индексирования, class RangeArrayDemo { static void Main() {
RangeArray ra = new RangeArray(-5, 5);
RangeArray ra2 = new RangeArray(1, 10);
RangeArray ra3 = new RangeArray(-20, -12);
// Использовать объект га в качестве массива.
Console.WriteLine("Длина массива га: " + га.Length); for(int i = -5; i <= 5; i++)
га[i] = i;
Console.Write("Содержимое массива га: "); for(int i = -5; i <= 5; i++)
Console.Write(ra[i] + " ");
Console.WriteLine("\n");
// Использовать объект ra2 в качестве массива.
Console.WriteLine("Длина массива га2: " + ra2.Length); for(int i = 1; i <= 10; i++) ra2[i] = i;
Console.Write("Содержимое массива ra2: "); for(int i = 1; i <= 10; i++)
Console.Write(ra2[i] + " ") ;
Console.WriteLine("\n") ;
// Использовать объект гаЗ в качестве массива.
Console.WriteLine("Длина массива гаЗ: " + гаЗ.Length); for(int i = -20; i <= -12; i++) ra3[i] = i; >
Console.Write("Содержимое массива гаЗ: "); for (int i = -20; i <= -12; i++)
Console.Write(ra3[i] + " ");
Console.WriteLine("\n") ;
}
}
При выполнении этого кода получается следующий результат.
Длина массива га: 11
Содержимое массива га: -5-4-3-2-1012345 Длина массива га2: 10
Содержимое массива га2: 12345678910 Длина массива гаЗ: 9
Содержимое массива гаЗ: -20 -19 -18 -17 -16 -15 -14 -13 -12
Как следует из результата выполнения приведенного выше кода, объекты типаRangeArrayможно индексировать в качестве массивов, начиная с любой точки отсчета, а не только с нуля. Рассмотрим подробнее саму реализацию классаRangeArray.
В начале классаRangeArrayобъявляются следующие закрытые переменные экземпляра.
// Закрытые данные.
int[] а; // ссылка на базовый массив int lowerBound; // наименьший индекс •int upperBound; // наибольший индекс
Переменнаяаслужит для обращения к базовому массиву по ссылке. Память для него распределяется конструктором классаRangeArray.Нижняя граница индексирования массива хранится в переменнойlowerBound,а верхняя граница — в переменнойupperBound.
Далее объявляются автоматически реализуемые свойстваLengthиError.
// Автоматически реализуемое и доступное только для чтения свойство Length, public int Length { get; private set; }
// Автоматически реализуемое и доступное только для чтения свойство Error, public bool Error { get; private set; }
Обратите внимание на то, что в обоих свойства аксессорsetобозначен какprivate.Как пояснялось выше, такое объявление автоматически реализуемого свойства, по существу, делает его доступным только для чтения.
Ниже приведен конструктор классаRangeArray.
// Построить массив по заданному размеру, public RangeArray(int low, int high) { high++;
if(high <= low) {
Console.WriteLine("Неверные индексы");
high = 1; // создать для надежности минимально допустимый массив
low = 0;
}
а = new int[high - low];
Length = high - low;
lowerBound = low; upperBound = —high;
}
При конструировании объекту классаRangeArrayпередается нижняя граница массива в качестве параметраlow,а верхняя граница — в качестве параметраhigh.Затем значение параметраhighинкрементируется, поскольку пределы индексирования массива изменяются отlowдоhighвключительно. Далее выполняется следующая проверка: является ли верхний индекс больше нижнего индекса. Если это не так, то выдается сообщение об ошибке и создается массив, состоящий из одного элемента. После этого для массива распределяется память, а ссылка на него присваивается переменнойа.Затем свойствоLengthустанавливается равным числу элементов массива. И наконец, устанавливаются переменныеlowerBoundиupperBound.
Далее в классе RangeArray реализуется его индексатор, как показано ниже.
// Это индексатор для класса RangeArray. public int this[int index] {
// Это аксессор get. get {
if(ok(index) ) {
Error = false;
return a[index - lowerBound];
} else {
Error = true; return 0;
}
// Это аксессор set. set {
if(ok(index)) {
a[index - lowerBound] = value;
Error = false;
}
else Error = true;
}
}
Этот индексатор подобен тому, что использовался в классеFailSof tArray,за одним существенным исключением. Обратите внимание на следующее выражение, в котором индексируется массива.
index - lowerBound
В этом выражении индекс, передаваемый в качестве параметраindex,преобразуется в индекс с отсчетом от нуля, пригодный для индексирования массиваа.Данное выражение действует при любом значении переменнойlowerBound:положительном, отрицательном или нулевом.
Ниже приведен методok ().
// Возвратить логическое значение true, если // индекс находится в установленных границах, private bool ok(int index) {
if(index >= lowerBound & index <= upperBound) return true; return false;
}
Этот метод аналогичен использовавшемуся в классеFailSof tArray,за исключением того, что в нем контроль границ массива осуществляется по значениям переменныхlowerBoundиupperBound.
КлассRangeArrayдемонстрирует лишь одну разновидность специализированного массива, который может быть создан с помощью индексаторов и свойств. Существуют, конечно, и другие. Аналогичным образом можно, например, создать динамические массивы, которые расширяются или сужаются по мере надобности, ассоциативные и разреженные массивы. Попробуйте создать один из таких массивов в качестве упражнения.
ГЛАВА 11 Наследование Наследование является одним из трех основополагающих принципов объектно-ориентированного программирования, поскольку оно допускает создание иерархических классификаций. Благодаря наследованию можно создать общий класс, в котором определяются характерные особенности, присущие множеству связанных элементов. От этого класса могут затем наследовать другие, более конкретные классы, добавляя в него свои индивидуальные особенности. В языке C# класс, который наследуется, называетсябазовым, а класс, который наследует, —производным.Следовательно, производный класс представляет собой специализированный вариант базового класса. Он наследует все переменные, методы, свойства и индексаторы, определяемые в базовом классе, добавляя к ним свои собственные элементы. Основы наследования
Поддержка наследования в C# состоит в том, что в объявление одного класса разрешается вводить другой класс. Для этого при объявлении производного класса указывается базовый класс. Рассмотрим для начала простой пример. Ниже приведен класс TwoDShape,
содержащий ширину и высоту двухмерного объекта, например квадрата, прямоугольника, треугольника и т.д. // Класс для двумерных объектов, class TwoDShape { public double Width; public double Height; public void ShowDimO { Console.WriteLine("Ширина и высота равны " + Width + " и " + Height); } } КлассTwoDShapeможет стать базовым, т.е. отправной точкой для создания классов, описывающих конкретные типы двумерных объектов. Например, в приведенной ниже программе классTwoDShapeслужит для порождения производного классаTriangle.Обратите особое внимание на объявление классаTriangle. // Пример простой иерархии классов, using System; // Класс для двумерных объектов. class TwoDShape { public double Width; public double Height; public void ShowDimO { Console.WriteLine("Ширина и высота равны " + Width + " и " + Height); } } // Класс Triangle, производный от класса TwoDShape. class Triangle : TwoDShape { public string Style; // тип треугольника // Возвратить площадь треугольника, public double Area() { return Width * Height / 2; } // Показать тип треугольника, public void ShowStyleO { Console.WriteLine("Треугольник " + Style); } } class Shapes { static void Main() { Triangle tl = new Triangle(); Triangle t2 = new Triangle(); tl.Width = 4.0; tl.Height = 4.0; tl.Style = "равнобедренный"; t2.Width = 8.0; t2.Height = 12.0; t2.Style = "прямоугольный"; tl.ShowStyle (); tl.ShowDim(); Console .WriteLine ("Площадь равна " + tl.AreaO); Console.WriteLine(); Console.WriteLine("Сведения об объекте t2: "); t2.ShowStyle(); t2.ShowDim(); Console.WriteLine("Площадь равна " + t2.Area()); } } При выполнении этой программы получается следующий результат. Сведения об объекте tl: Треугольник равнобедренный Ширина и высота равны 4 и 4 Площадь равна 8 Сведения об объекте t2: Треугольник прямоугольный Ширина и высота равны 8 и 12 Площадь равна 48 В классеTriangleсоздается особый тип объекта классаTwoDShape(в данном случае — треугольник). Кроме того, в классTriangleвходят все члены классаTwoDShape,к которым, в частности, добавляются методыArea() иShowStyle(). Так, описание типа треугольника сохраняется в переменнойStyle,методArea() рассчитывает и возвращает площадь треугольника, а методShowStyle() отображает тип треугольника. Обратите внимание на синтаксис, используемый в классеTriangleдля наследования классаTwoDShape. class Triangle : TwoDShape { Этот синтаксис может быть обобщен. Всякий раз, когда один класс наследует от другого, после имени базового класса указывается имя производного класса, отделяемое двоеточием. В C# синтаксис наследования класса удивительно прост и удобен в использовании. В классTriangleвходят все члены его базового классаTwoDShape,и поэтому в нем переменныеWidthиHeightдоступны для методаArea(). Кроме того, объектыtlиt2в методеMain() могут обращаться непосредственно к переменнымWidthиHeight,как будто они являются членами классаTriangle.На рис. 11.1 схематически показано, каким образом классTwoDShapeвводится в классTriangle. 
Рис. 11.1. Схематическое представление класса Triangle
Несмотря на то что классTwoDShapeявляется базовым для классаTriangle,в то же время он представляет собой совершенно независимый и самодостаточный класс. Если класс служит базовым для производного класса, то это совсем не означает, что он не может быть использован самостоятельно. Например, следующий фрагмент кода считается вполне допустимым. TwoDShape shape = new TwoDShape (); shape.Width = 10; shape.Height = 20; shape.ShowDim(); Разумеется, объект классаTwoDShapeникак не связан с любым из классов, производных от классаTwoDShape,и вообще не имеет к ним доступа. Ниже приведена общая форма объявления класса, наследующего от базового класса. classимя_производного_класса : имя_базового_класса{ // тело класса } Для любого производного класса можно указать только один базовый класс. В C# не предусмотрено наследование нескольких базовых классов в одном производном классе. (В этом отношении C# отличается от C++, где допускается наследование нескольких базовых классов. Данное обстоятельство следует принимать во внимание при переносе кода C++ в С#.) Тем не менее можно создать иерархию наследования, в которой производный класс становится базовым для другого производного класса. (Разумеется, ни один из классов не может быть базовым для самого себя как непосредственно, так и косвенно.) Но в любом случае производный класс наследует все члены своего базового класса, в том числе переменные экземпляра, методы, свойства и индексаторы. Главное преимущество наследования заключается в следующем: как только будет создан базовый класс, в котором определены общие для множества объектов атрибуты, он может быть использован для создания любого числа более конкретных производных классов. А в каждом производном классе может быть точно выстроена своя собственная классификация. В качестве примера ниже приведен еще один класс, производный от классаTwoDShapeи инкапсулирующий прямоугольники. // Класс для прямоугольников, производный от класса TwoDShape. class Rectangle : TwoDShape { // Возвратить логическое значение true, если // прямоугольник является квадратом, public bool IsSquare() { if(Width == Height) return true; return false; } // Возвратить площадь прямоугольника, public double Area() { return Width * Height; } } В классRectangleвходят все члены классаTwoDShape,к которым добавлен методIs Square (), определяющий, является ли прямоугольник квадратом, а также методArea (), вычисляющий площадь прямоугольника. Доступ к членам класса и наследование
Как пояснялось в главе 8, члены класса зачастую объявляются закрытыми, чтобы исключить их несанкционированное или незаконное использование. Но наследование класса не отменяет ограничения, накладываемые на доступ к закрытым членам класса. Поэтому если в производный класс и входят все члены его базового класса, в нем все равно оказываются недоступными те члены базового класса, которые являются закрытыми. Так, если сделать закрытыми переменные классаTwoDShape,они станут недоступными в классеTriangle,как показано ниже. // Доступ к закрытым членам класса не наследуется. // Этот пример кода не подлежит компиляции. using System; // Класс для двумерных объектов, class TwoDShape { double Width; // теперь это закрытая переменная double Height; // теперь это закрытая переменная public void ShowDimO { Console.WriteLine("Ширина и высота равны " + Width + " и " + Height); } } // Класс Triangle, производный от класса TwoDShape. class Triangle : TwoDShape { public string Style; // тип треугольника // Возвратить площадь треугольника, public double Area() { return Width * Height /2; // Ошибка, доступ к закрытому // члену класса запрещен } // Показать тип треугольника, public void ShowStyle() { Console.WriteLine("Треугольник " + Style); } } КлассTriangleне будет компилироваться, потому что обращаться к переменнымWidthиHeightиз методаArea() запрещено. А поскольку переменныеWidthиHeightтеперь являются закрытыми, то они доступны только длядругихчленов своего класса, но не для членов производных классов. ПРИМЕЧАНИЕ
Закрытый член класса остается закрытым в своем классе. Он не доступен из кода за пределами своего класса, включая и производные классы.
На первый взгляд, ограничение на доступ к частным членам базового класса из производного класса кажется трудно преодолимым, поскольку оно не дает во многих случаях возможности пользоваться частными членами этого класса. Но на самом деле это не так.Дляпреодоления данного ограничения в C# предусмотрены разные способы. Один из них состоит в использовании защищенных(protected)членов класса, рассматриваемых в следующем разделе, а второй — в применении открытых свойств для доступа к закрытым данным. Как пояснялось в предыдущей главе, свойство позволяет управлять доступом к переменной экземпляра. Например, с помощью свойства можно ввести ограничения на доступ к значению переменной или же сделать ее доступной только для чтения. Так, если сделать свойство открытым, но объявить его базовую переменную закрытой, то этим свойством можно будет воспользоваться в производном классе, но нельзя будет получить непосредственный доступ к его базовой закрытой переменной. Ниже приведен вариант классаTwoDShape,в котором переменныеWidthиHeightпревращены в свойства. По ходу дела в этом классе выполняется проверка: являются ли положительными значения свойствWidthиHeight.Это дает, например, возможность указывать свойстваWidthиHeightв качестве координат формы в любом квадранте прямоугольной системы координат, не получая заранее их абсолютные значения. // Использовать открытые свойства для установки и // получения .значений закрытых членов класса. using System; // Класс для двумерных объектов, class TwoDShape { double pri_width; // теперь это закрытая переменная double pri_height; // теперь это закрытая переменная // Свойства ширины и высоты двумерного объекта, public double Width { get { return pri_width; } set { pri_width = value < 0 ? -value : value; } } public double Height { get { return pri_height; } set { pri_height = value < 0 ? -value : value; } } public void ShowDim() { Console.WriteLine("Ширина и высота равны " + Width + " и " + Height); } } // Класс для треугольников, производный от // класса TwoDShape. class Triangle : TwoDShape { public string Style; // тип треугольника // Возвратить площадь треугольника, public double Area() { return Width * Height / 2; } // Показать тип треугольника, public void ShowStyleO { Console.WriteLine("Треугольник " + Style); } } class Shapes2 { static void Main() { Triangle tl = new Triangle (); Triangle t2 = new Triangle (); tl.Width = 4.0; tl.Height = 4.0; tl.Style = "равнобедренный"; t2.Width = 8.0; t2.Height = 12.0; t2.Style = "прямоугольный"; Console.WriteLine("Сведения об объекте tl: "); tl.ShowStyle(); tl.ShowDim(); Console . WriteLine ("Площадь равна " + tl.AreaO); Console.WriteLine (); Console.WriteLine("Сведения об объекте t2: "); t2.ShowStyle(); t2.ShowDim(); Console.WriteLine("Площадь равна " + t2.Area()); } } В этом варианте свойстваWidthиHeightпредоставляют доступ к закрытым членамpri_widthиpri_heightклассаTwoDShape,в которых фактически хранятся значения ширины и высоты двумерного объекта. Следовательно, значения членовpri_widthиpri_heightклассаTwoDShapeмогут быть установлены и получены с помощью соответствующих открытых свойств, несмотря на то, что сами эти члены по-прежнему остаются закрытыми. Базовый и производный классы иногда еще называютсуперклассомиподклассомсоответственно. Эти термины происходят из практики программирования на Java. То, что в Java называется суперклассом, в C# обозначается как базовый класс. А то, что в Java называется подклассом, в C# обозначается как производный класс. Оба ряда терминов часто применяются к классу в обоих языках программирования, но в этой книге по-прежнему употребляются общепринятые в C# термины базового и производного классов, которые принято употреблять и в C++. Организация защищенного доступа
Как пояснялось выше, открытый член базового класса недоступен для производного класса. Из этого можно предположить, что для доступа к некоторому члену базового класса из производного класса этот член необходимо сделать открытым. Но если сделать член класса открытым, то он станет доступным для всего кода, что далеко не всегда желательно. Правда, упомянутое предположение верно лишь отчасти, поскольку в C# допускается созданиезащищенногочлена класса. Защищенный член является открытым в пределах иерархии классов, но закрытым за пределами этой иерархии. Защищенный член создается с помощью модификатора доступа protected. Если член класса объявляется как protected, он становится закрытым, но за исключением одного случая, когда защищенный член наследуется. В этом случае защищенный член базового класса становится защищенным членом производного класса, а значит, доступным для производного класса. Таким образом, используя модификатор доступа protected, можно создать члены класса, являющиеся закрытыми для своего класса, но все же наследуемыми и доступными для производного класса. Ниже приведен простой пример применения модификатора доступа protected. // Продемонстрировать применение модификатора доступа protected, using System; class В { protected int i, j; // члены, закрытые для класса В, // но доступные для класса D public void Set (int a, int b) { i = a; j = b; } public void Show() { Console.WriteLine (i + " " + j); } } class D : В { int k; // закрытый член // члены i и j класса В доступны для класса D public void Setk() { k = i * j; } public void Showk() { Console.WriteLine(k) ; }' } class ProtectedDemo { static void Main() { D ob = new D (); ob.Set(2, 3); // допустимо, поскольку доступно для класса D ob.Show(); // допустимо, поскольку доступно для класса D ob.Setk(); // допустимо, поскольку входит в класс D ob.ShowkO; // допустимо, поскольку входит в класс D } } В данном примере классВнаследуется классомD,а его членыiиjобъявлены какprotected,и поэтому они доступны для методаSetk(). Если бы членыiиjклассаВбыли объявлены какprivate,то они оказались бы недоступными для классаD,и приведенный выше код нельзя было бы скомпилировать. Аналогично состояниюpublicиprivate,состояниеprotectedсохраняется за членом класса независимо от количества уровней наследования. Поэтому когда производный класс используется в качестве базового для другого производного класса, любой защищенный член исходного базового класса, наследуемый первым производным классом, наследуется как защищенный и вторым производным классом. Несмотря на всю свою полезность, защищенный доступ пригоден далеко не для всех ситуаций. Так, в классеTwoDShapeиз приведенного ранее примера требовалось, чтобы значения его членовWidthиHeightбыли доступными открыто, поскольку нужно было управлять значениями, которые им присваивались, что было бы невозможно, если бы они были объявлены какprotected.В данном случае более подходящим решением оказалось применение свойств, чтобы управлять доступом, а не предотвращать его. Таким образом, модификатор доступаprotectedследует применять в том случае, если требуется создать член класса, доступный для всей иерархии классов, но для остального кода он должен быть закрытым. А для управления доступом к значению члена класса лучше воспользоваться свойством. Конструкторы и наследование
В иерархии классов допускается, чтобы у базовых и производных классов были свои собственные конструкторы. В связи с этим возникает следующий резонный вопрос: какой конструктор отвечает за построение объекта производного класса: конструктор базового класса, конструктор производного класса или же оба? На этот вопрос можно ответить так: конструктор базового класса конструирует базовую часть объекта, а конструктор производного класса — производную часть этого объекта. И в этом есть своя логика, поскольку базовому классу неизвестны и недоступны любые элементы производного класса, а значит, их конструирование должно происходить раздельно. В приведенных выше примерах данный вопрос не возникал, поскольку они опирались на автоматическое создание конструкторов, используемых в C# по умолчанию. Но на практике конструкторы определяются в большинстве классов. Ниже будет показано, каким образом разрешается подобная ситуация. Если конструктор определен только в производном классе, то все происходит очень просто: конструируется объект производного класса, а базовая часть объекта автоматически конструируется его конструктором, используемым по умолчанию. В качестве примера ниже приведен переработанный вариант классаTriangle,в котором определяется конструктор, а членStyleделается закрытым, так как теперь он устанавливается конструктором. // Добавить конструктор в класс Triangle, using System; 11Класс для двумерных объектов. • class TwoDShape { double pri_width; double pr.i_height; // Свойства ширины и длины объекта, public double Width { get { return pri_width; } set { pri_width = value < 0 ? -value : value; } } public double Height { get { return pri_height; } set { pri_height = value < 0 ? -value : value; } } public void ShowDim() { Console.WriteLine("Ширина и длина равны " + Width + " и " + Height); } } // Класс для треугольников, производный от класса TwoDShape. class Triangle : TwoDShape { string Style; // Конструктор. public Triangle(string s, double w, double h) { Width = w; // инициализировать член базового класса Height = h; // инициализировать член базового класса Style = s; // инициализировать член производного класса } // Возвратить площадь треугольника, public double Area() { return Width * Height / 2; } // Показать тип треугольника, public void ShowStyle() { Console.WriteLine("Треугольник " + Style); } } class Shapes3 { static void Main() { Triangle tl = new Triangle("равнобедренный", 4.0, 4.0); Triangle t2 = new Triangle("прямоугольный", 8.0, 12.0); Console.WriteLine("Сведения об объекте tl: "); tl.ShowStyle(); tl.ShowDim(); Console . WriteLine ("Площадь равна " + tl.AreaO); Console.WriteLine (); Console.WriteLine("Сведения об объекте t2: "); t2.ShowStyle(); t2 .-ShowDim () ; Console.WriteLine("Площадь равна " + t2.Area()); } } В данном примере конструктор классаTriangleинициализирует наследуемые члены классаTwoDShapeвместе с его собственным полемStyle. Когда конструкторы определяются как в базовом, так и в производном классе, процесс построения объекта несколько усложняется, поскольку должны выполняться конструкторы обоих классов. В данном случае приходится обращаться к еще одному ключевому слову языка С#:base,которое находит двоякое применение: во-первых, для вызова конструктора базового класса; и во-вторых, для доступа к члену базового класса, скрывающегося за членом производного класса. Ниже будет рассмотрено первое применение ключевого словаbase. Вызов конструкторов базового класса
С помощью формы расширенного объявления конструктора производного класса и ключевого словаbaseв производном классе может быть вызван конструктор, определенный в его базовом классе. Ниже приведена общая форма этого расширенного объявления: конструктор_производного_класса{список_параметров): base(список_аргументов){ // тело конструктора } гдесписок_аргументовобозначает любые аргументы, необходимые конструктору в базовом классе. Обратите внимание на местоположение двоеточия. Для того чтобы продемонстрировать применение ключевого словаbaseна конкретном примере, рассмотрим еще один вариант классаTwoDShapeв приведенной ниже программе. В данном примере определяется конструктор, инициализирующий свойстваWidthиHeight.Затем этот конструктор вызывается конструктором классаTriangle. // Добавить конструктор в класс TwoDShape. using System; // Класс для двумерных объектов, class TwoDShape { double pri_width; double pri_height; // Конструктор класса TwoDShape. public TwoDShape(double w, double h) { Width = w; Height = h; } public double Width { get { return pri_width; } set { pri_width = value < 0 ? -value : value; } } public double Height { get { return pri_height; } set { pri_height = value < 0 ? -value : value; } } public void ShowDim() { Console.WriteLine("Ширина и высота равны " + Width + " и " + Height); } } // Класс для треугольников, производный от класса TwoDShape. class Triangle : TwoDShape { string Style; // Вызвать конструктор базового класса. public Triangle(string s, double w, double h) : base(w, h) Style = s; } // Возвратить площадь треугольника, public double Area() { return Width * Height / 2; } // Показать тип треугольника, public void ShowStyleO { Console.WriteLine("Треугольник " + Style); } } class Shapes4 { static void Main() { Triangle tl = new Triangle("равнобедренный", 4.0, 4.0); Triangle t2 = new Triangle("прямоугольный", 8.0, 12.0); Console.WriteLine("Сведения об объекте tl: "); tl.ShowStyle(); tl.ShowDim(); Console.WriteLine("Площадь равна " + tl.AreaO); Console.WriteLine(); Console.WriteLine("Сведения об объекте t2: "); t2.ShowStyle(); t2.ShowDim(); Console.WriteLine("Площадь равна " + t2.Area()); Теперь конструктор классаTriangleобъявляется следующим образом. public Triangle( string s, double w, double h) : base(w, h) { В данном варианте конструкторTriangle() вызывает методbaseс параметрами w иh.Это, в свою очередь, приводит к вызову конструктораTwoDShape (), инициализирующего свойстваWidthиHeightзначениями параметровwиh.Они больше не инициализируются средствами самого классаTriangle,где теперь остается инициализировать только его собственный членStyle,определяющий тип треугольника. Благодаря этому классTwoDShapeвысвобождается для конструирования своего по-добъекта любым избранным способом. Более того, в классTwoDShapeможно ввести функции, о которых даже не будут подозревать производные классы, что предотвращает нарушение существующего кода. С помощью ключевого словаbaseможно вызвать конструктор любой формы, определяемой в базовом классе, причем выполняться будет лишь тот конструктор, параметры которого соответствуют переданным аргументам. В качестве примера ниже приведены расширенные варианты классовTwoDShapeиTriangle,в которые включены как используемые по умолчанию конструкторы, так и конструкторы, принимающие один аргумент. // Добавить дополнительные конструкторы в класс TwoDShape. using System; class TwoDShape { double pri_width; double pri_height; // Конструктор, вызываемый по умолчанию, public TwoDShape() { Width = Height = 0.0; } // Конструктор класса TwoDShape. public TwoDShape(double w, double h) { Width = w; Height = h; } // Сконструировать объект равной ширины и высоты, public TwoDShape(double х) { Width = Height = x; } '// Свойства ширины и высоты объекта, public double Width { get { return pri_width; } set { pri_width = value < 0 ? -value : value; } } public double Height { get { return pri_height; } set { pri_height = value < 0 ? -value : value; } } public void ShowDimO { Console.WriteLine("Ширина и высота равны " + Width + " и " + Height); } } // Класс для треугольников, производный от класса TwoDShape. class Triangle : TwoDShape { string Style; /* Конструктор, используемый по умолчанию. Автоматически вызывает конструктор, доступный по умолчанию в классе TwoDShape. */ public Triangle() { Style = "null"; } // Конструктор, принимающий три аргумента, public Triangle( string s, double w, double h) : base(w, h) { Style = s; } // Сконструировать равнобедренный треугольник, public Triangle(double x) : base(x) { Style = "равнобедренный"; } // Возвратить площадь треугольника, public double Area() { return Width * Height / 2; } // Показать тип треугольника, public void ShowStyleO { Console.WriteLine("Треугольник " + Style); } } class Shapes5 { static void Main() { Triangle tl = new Triangle(); Triangle t2 = new Triangle("прямоугольный", 8.0, 12.0); Triangle t3 = new Triangle(4.0); tl = t2; Console.WriteLine("Сведения об объекте tl: "); tl.ShowStyle(); tl.ShowDim(); Console.WriteLine("Площадь равна " + tl.AreaO); Console.WriteLine(); Console.WriteLine("Сведения об объекте t2: "); t2.ShowStyle (); t2.ShowDim(); Console.WriteLine("Площадь равна " + t2.Area()); Console.WriteLine(); Console.WriteLine("Сведения об объекте t3: "); t3.ShowStyle(); t3.ShowDim(); Console.WriteLine("Площадь равна " + t3.Area()); Console.WriteLine(); } } Вот к какому результату приводит выполнение этого кода. Сведения об объекте tl: Треугольник прямоугольный Ширина и высота равны 8 и 12 Площадь равна 48 Сведения об объекте t2: Треугольник прямоугольный Ширина и высота равны 8 и 12 Площадь равна 48 Сведения об объекте t3: Треугольник равнобедренный Ширина и высота равны 4 и 4 Площадь равна 8 А теперь рассмотрим вкратце основные принципы действия ключевого слова base. Когда в производном классе указывается ключевое слово base, вызывается конструктор из его непосредственного базового класса. Следовательно, ключевое слово base всегда обращается к базовому классу, стоящему в иерархии непосредственно над вызывающим классом. Это справедливо даже для многоуровневой иерархии классов. Аргументы передаются базовому конструктору в качестве аргументов метода base (). Если же ключевое слово отсутствует, то автоматически вызывается конструктор, используемый в базовом классе по умолчанию. Наследование и сокрытие имен
В производном классе можно определить член с таким же именем, как и у члена его базового класса. В этом случае член базового класса скрывается в производном классе. И хотя формально в C# это не считается ошибкой, компилятор все же выдаст сообщение, предупреждающее о том, что имя скрывается. Если член базового класса требуется скрыть намеренно, то перед его именем следует указать ключевое слово new, чтобы избежать появления подобного предупреждающего сообщения. Следует, однако, иметь в виду, что это совершенно отдельное применение ключевого словаnew,не похожее на его применение при создании экземпляра объекта. Ниже приведен пример сокрытия имени. // Пример сокрытия имени с наследственной связью. using System; class А { public int i = 0; } // Создать производный класс. j class В : A { * new int i; // этот член скрывает член i из класса А public В(int b) { i = Ь; // член i в классе В } public void Show() { Console.WriteLine("Член i в производном классе: " + i) ; } } class NameHiding { static void Main() { В ob = new В(2); ob.Show() ; } } Прежде всего обратите внимание на использование ключевого словаnewв следующей строке кода. new int i; // этот член скрывает член i из класса А В этой строке компилятору, по существу, сообщается о том, что вновь создаваемая переменнаяiнамеренно скрывает переменнуюiиз базового классаАи что автору программы об этом известно. Если же опустить ключевое словоnewв этой строке кода, то компилятор выдаст предупреждающее сообщение. Вот к какому результату приводит выполнение приведенного выше кода. Член i в производном классе: 2 В классеВопределяется собственная переменная экземпляраi,которая скрывает переменнуюiиз базового классаА.Поэтому при вызове методаShow() для объекта типаВвыводится значение переменнойi,определенной в классеВ,а не той, что определена в классеА. Применение ключевого слова base для доступа к скрытому имени
Имеется еще одна форма ключевого словаbase,которая действует подобно ключевому словуthis,за исключением того, что она всегда ссылается на базовый класс в том производном классе, в котором она используется. Ниже эта форма приведена в общем виде: base.член гдечленможет обозначать метод или переменную экземпляра. Эта форма ключевого словаbaseчаще всего применяется в тех случаях, когда под именами членов производного класса скрываются члены базового класса с теми же самыми именами. В качестве примера ниже приведен другой вариант иерархии классов из предыдущего примера. // Применение ключевого слова base для преодоления // препятствия, связанного с сокрытием имен. using System; class А { public int i = 0; } // Ссзздать производный класс, class В : А { new int i; // этот член скрывает член i из класса А public В(int a, int b) { base.i = а; // здесь обнаруживается скрытый член из класса А i = Ь; // член i из класса В } « public void Show() { // Здесь выводится член i из класса А. Console.WriteLine("Член i в базовом классе: " + base.i); // А здесь выводится член i из класса В. Console.WriteLine("Член i в производном классе: " + i); } } class UncoverName { static void Main() { В ob = new В(1, 2) ; ob.Show (); } } Выполнение этого кода приводит к следующему результату. Член i в базовом классе: 1 Член i в производном классе: 2 Несмотря на то что переменная экземпляраiв производном классеВскрывает переменнуюiиз базового классаА,ключевое словоbaseразрешает доступ к переменнойi,определенной в базовом классе. С помощью ключевого словаbaseмогут также вызываться скрытые методы. Например, в приведенном ниже коде классВнаследует класс Айв обоих классах объявляется методShow(). А затем в методеShow() классаВс помощью ключевого словаbaseвызывается вариант методаShow (), определенный в классеА. // Вызвать скрытый метод. using System; class А { public int i = 0; 11Метод Show() в классе A public void Show() { Console.WriteLine("Член i в базовом классе: " + i); } } // Создать производный класс, class В : А { new int i; // этот член скрывает член i из класса А public В(int a, int b) { base.i = а; // здесь обнаруживается скрытый член из класса А i = Ь; // член i из класса В } // Здесь скрывается метод Show() из класса А. Обратите // внимание на применение ключевого слова new. new public void Show() { base.Show (); // здесь вызывается метод Show() из класса А // далее выводится член i из класса В Console.WriteLine("Член i в производном классе: " + i); } } class UncoverName { static void Main() { В ob = new В (1, 2); ob.Show (); } } Выполнение этого кода приводит к следующему результату. Член i в базовом классе: 1 Член i в производном классе: 2 Как видите, в выраженииbase. Show() вызывается вариант методаShow() из базового класса. Обратите также внимание на следующее: ключевое слово new используется в приведенном выше коде с целью сообщить компилятору о том, что методShow(), вновь объявляемый в производном классеВ,намеренно скрывает другой методShow (),определенный в базовом классеА. Создание многоуровневой иерархии классов
В представленных до сих пор примерах программ использовались простые иерархии классов, состоявшие только из базового и производного классов. Но в C# мож но также строить иерархии, состоящие из любого числа уровней наследования. Как упоминалось выше, многоуровневая иерархия идеально подходит для использования одного производного класса в качестве базового для другого производного класса. Так, если имеются хри класса,А, ВиС,то классСможет наследовать от классаВ,а тот, в свою очередь, от классаА.В таком случае каждый производный класс наследует характерные особенности всех своих базовых классов. В частности, классСнаследует все члены классовВиА. Для того чтобы показать, насколько полезной может оказаться многоуровневая иерархия классов, рассмотрим следующий пример программы. В ней производный классTriangleслужит в качестве базового для создания другого производного класса —ColorTriangle.При этом классColorTriangleнаследует все характерные особенности, а по существу, члены классовTriangleиTwoDShape,к которым добавляется полеcolor,содержащее цвет треугольника. // Пример построения многоуровневой иерархии классов. using System; class TwoDShape { double pri_width; double pri_height; // Конструктор, используемый по умолчанию, public TwoDShape() { Width = Height = 0.0; } // Конструктор для класса TwoDShape. public TwoDShape(double w, double h) { Width = w; Height = h; } // Сконструировать объект равной ширины и высоты, public TwoDShape(double х) { Width = Height = x; } // Свойства ширины и высоты объекта, public double Width { get { return pri_width; } set { pri_width = value < 0 ? -value : value; } } public double Height { get { return pri_height; } set { pri_height = value < 0 ? -value : value; } } public void ShowDim() { Console.WriteLine("Ширина и высота равны " + Width + " и " + Height); } // Класс для треугольников, производный от класса TwoDShape. class Triangle : TwoDShape { string Style; // закрытый член класса /* Конструктор, используемый по умолчанию. Автоматически вызывает конструктор, доступный по умолчанию в классе TwoDShape. */ public Triangle () { Style = "null"; } // Конструктор. public Triangle(string s, double w, double h) : base(w, h) { Style = s; } // Сконструировать равнобедренный треугольник, public Triangle(double x) : base(x) { Style = "равнобедренный"; } // Возвратить площадь треугольника, public double Area() { return Width * Height / 2; } // Показать тип треугольника, public void ShowStyle() { Console.WriteLine("Треугольник " + Style); } } // Расширить класс Triangle, class ColorTriangle : Triangle { string color; fc
public ColorTriangle(string c, string s, double w, double h) : base(s, w, h) { color = c; } // Показать цвет треугольника, public void ShowColor() { Console.WriteLine("Цвет " + color); } } class Shapes6 { static void Main() { ColorTriangle tl = new ColorTriangle("синий", "прямоугольный", 8.0, 12.0); ColorTriangle t2 = new ColorTriangle("красный", "равнобедренный", 2.0, 2.0); Console.WriteLine("Сведения об объекте tl: "); tl.ShowStyle(); tl. ShowDinv() ; tl.ShowColor (); Console .WriteLine ("Площадь равна " + tl.AreaO); Console.WriteLine () ; Console.WriteLine("Сведения об объекте t2: "); t2.ShowStyle(); t2.ShowDim(); t2.ShowColor() ; Console.WriteLine("Площадь равна " + t2.Area()); } } При выполнении этой программы получается следующей результат. Сведения об объекте tl: Треугольник прямоугольный Ширина и высота равны 8 и 12 Цвет синий Площадь равна 48 Сведения об объекте t2: Треугольник равнобедренный Ширина и высота равны 2 и 2 Цвет красный Площадь равна 2 Благодаря наследованию в классеColorTriangleмогут использоваться определенные ранее классыTriangleиTwoDShape,к элементам которых добавляется лишь та информация, которая требуется для конкретного применения данного класса. В этом отчасти и состоит ценность наследования, поскольку оно допускает повторное использование кода. Приведенный выше пример демонстрирует еще одно важное положение: ключевое словоbaseвсегда обозначает ссылку на конструктор ближайшего по иерархии базового класса. Так, ключевое словоbaseв классеColorTriangleобозначает вызов конструктора из классаTriangle,а ключевое словоbaseв классеTriangle— вызов конструктора из классаTwoDShape.Если же в иерархии классов конструктору базового класса требуются параметры, то все производные классы должны предоставлять эти параметры вверх по иерархии, независимо от того, требуются они самому производному классу или нет.
Порядок вызова конструкторов
В связи с изложенными выше в отношении наследования и иерархии классов может возникнуть следующий резонный вопрос: когда создается объект производного класса и какой конструктор выполняется первым — тот, что определен в производном классе, или же тот, что определен в базовом классе? Так, если имеется базовый классАи производный классВ,то вызывается ли конструктор классаАраньше конструктора классаВ?Ответ на этот вопрос состоит в том, что в иерархии классов конструкторы вызываются по порядку выведения классов: от базового к производному. Более того, этот порядок остается неизменным независимо от использования ключевого слова base. Так, если ключевое слово base не используется, то выполняется конструктор по умолчанию, т.е. конструктор без параметров. В приведенном ниже примере программы демонстрируется порядок вызова и выполнения конструкторов.
// Продемонстрировать порядок вызова конструкторов.
using System;
// Создать базовый класс, class А {
public А() {
Console.WriteLine("Конструирование класса А.");
}
}
// Создать класс, производный от класса А. class В : А { public В() {
Console.WriteLine("Конструирование класса В.");
}
}
/
// Создать класс, производный от класса В. class С : В { public С() {
Console.WriteLine("Конструирование класса С.");
}
}
class OrderOfConstruction { static void Main() {
С с = new С();
}
}
Вот к какому результату приводит выполнение этой программы.
Конструирование класса А.
Конструирование класса В.
Конструирование класса С.
Как видите, конструкторы вызываются по порядку выведения их классов.
Если хорошенько подумать, то в вызове конструкторов по порядку выведения их классов можно обнаружить определенный смысл. Ведь базовому классу ничего не известно ни об одном из производных от него классов, и поэтому любая инициализация, которая требуется его членам, осуществляется совершенно отдельно от инициализации членов производного класса, а возможно, это и необходимое условие. Следовательно, она должна выполняться первой.
Ссылки на базовый класс и объекты производных классов
Как вам должно быть уже известно, C# является строго типизированным языком программирования. Помимо стандартных преобразований и автоматического продвижения простых типов значений, в этом языке строго соблюдается принцип совместимости типов. Это означает, что переменная ссылки на объект класса одного типа, как правило, не может ссылаться на объект класса другого типа. В качестве примера рассмотрим следующую программу, в которой объявляются два класса одинаковой структуры.
// Эта программа не подлежит компиляции.
class X { int а;
public X(int i) { a = i; }
}
class Y { int a;
public Y(int i) { a = i; }
}
class IncompatibleRef { static void Main() {
X x = new X (10);
X x2;
Y у = new Y (5);
x2 = x; // верно, поскольку оба объекта относятся к одному и тому же типу х2 = у; // ошибка, поскольку это разнотипные объекты
}
}
Несмотря на то что классы X иYв данном примере совершенно одинаковы по своей структуре, ссылку на объект типаYнельзя присвоить переменной ссылки на объект типа X, поскольку типы у них разные. Поэтому следующая строка кода оказывается неверной и может привести к ошибке из-за несовместимости типов во время компиляции.
х2 = у; // неверно, поскольку это разнотипные объекты
Вообще говоря, переменная ссылки на объект может ссылаться только на объект своего типа.
Но из этого принципа строгого соблюдения типов в C# имеется одно важное исключение: переменной ссылки на объект базового класса может быть присвоена ссылка на объект любого производного от него класса. Такое присваивание считается вполне допустимым, поскольку экземпляр объекта производного типа инкапсулирует экземпляр объекта базового типа. Следовательно, по ссылке на объект базового класса можно обращаться к объекту производного класса. Ниже приведен соответствующий пример.
// По ссылке на объект базового класса можно обращаться // к объекту производного класса.
using System;
class X {
public int a;
public X(int i) { a = i;
}
}
class Y : X { public int b;
public Y(int i, int j) : base(j) { b = i;
•}
}
class BaseRef {
static void Main() {
X x = new X(10);
X x2;
Y у = new Y (5, 6);
x2 = x; // верно, поскольку оба объекта относятся к одному и тому же типу Console.WriteLine ("х2.а: " + х2.а);
х2 = у; // тоже верно, поскольку класс Y является производным от класса X Console.WriteLine ("х2.а: " + х2.а);
// ссылкам на объекты класса X известно только о членах класса X х2.а = 19; // верно // х2.Ь = 27; // неверно, поскольку член b отсутствует у класса X}
}
В данном примере класс Y является производным от класса X. Поэтому следующая операция присваивания:
х2 = у; // тоже верно, поскольку класс Y является производным от класса X
считается вполне допустимой. Ведь по ссылке на объект базового класса (в данном случае — это переменная х2 ссылки на объект класса X) можно обращаться к объекту производного класса, т.е. к объекту, на который ссылается переменная у.
Следует особо подчеркнуть, что доступ к конкретным членам класса определяется типом переменной ссылки на объект, а не типом объекта, на который она ссылается. Это означает, что если ссылка на объект производного класса присваивается переменной ссылки на объект базового класса, то доступ разрешается только к тем частям этого объекта, которые определяются базовым классом. Именно поэтому переменной х2 недоступен член b класса Y, когда она ссылается на объект этого класса. И в этом есть своя логика, поскольку базовому классу ничего не известно о тех членах, которые до-
бавлены в производный от него класс. Именно поэтому последняя строка кода в приведенном выше примере была закомментирована.
Несмотря на кажущийся несколько отвлеченным характер приведенных выше рас-суждений, им можно найти ряд важных применений на практике. Одно из них рассматривается ниже, а другое — далее в этой главе, когда речь пойдет о виртуальных методах.
Один из самых важных моментов для присваивания ссылок на объекты производного класса переменным базового класса наступает тогда, когда конструкторы вызываются в иерархии классов. Как вам должно быть уже известно, в классе нередко определяется конструктор, принимающий объект своего класса в качестве параметра. Благодаря этому в классе может быть сконструирована копия его объекта. Этой особенностью можно выгодно воспользоваться в классах, производных от такого класса. В качестве примера рассмотрим очередные варианты классовTwoDShapeиTriangle.В оба класса добавлены конструкторы, принимающие объект в качестве параметра.
// Передать ссылку на объект производного класса // переменной ссылки на объект базового класса.
using System;
class TwoDShape { double pri_width; double pri_height;
// Конструктор по умолчанию, public TwoDShape() {
Width = Height = 0.0;
}
// Конструктор для класса TwoDShape. public TwoDShape(double w, double h) {
Width = w;
Height = h;
}
// Сконструировать объект равной ширины и высоты, public TwoDShape(double х) {
Width = Height = x;
}
// Сконструировать копию объекта TwoDShape. public TwoDShape(TwoDShape ob) {
Width = ob.Width;
Height = ob.Height;
}
// Свойства ширины и высоты объекта, public double Width {
get { return pri_width; }
set { pri_width = value < 0 ? -value : value; }
}
get { return pri_height; }
set { pri_height = value < 0 ? -value : value; }
}
public void ShowDim() {
Console.WriteLine("Ширина и высота равны " +
Width + " и " + Height);
}
}
// Класс для треугольников, производный от класса TwoDShape. class Triangle : TwoDShape { string Style;
// Конструктор, используемый по умолчанию, public Triangle() {
Style = "null";
}
// Конструктор для класса Triangle.
public Triangle(string s, double w, double h) : base(w, h) { Style = s;
}
// Сконструировать равнобедренный треугольник, public Triangle(double x) : base (x) {
Style = "равнобедренный";
}
// Сконструировать копию объекта типа Triangle, public Triangle(Triangle ob) : base (ob) {
Style = ob.Style;
}
// Возвратить площадь треугольника, public double Area() {
return Width * Height / 2;
}
// Показать тип треугольника, public void ShowStyle() {
Console.WriteLine("Треугольник " + Style);
}
}
class Shapes7 {
static void Ma^n() {
Triangle tl = new Triangle("прямоугольный", 8.0, 12.0);
// Сделать копию объекта tl.
Triangle t2 = new Triangle ('t*L) ;
Console.WriteLine("Сведения об объекте tl: "); tl.ShowStyle();
tl.ShowDim();
Console.WriteLine ("Площадь равна " + tl.AreaO);
Console.WriteLine ();
Console.WriteLine("Сведения об объекте t2: "); t2.ShowStyle(); t2.ShowDim();
Console.WriteLine("Площадь равна " + t2.Area());
}
}
В представленном выше примере объектt2конструируется из объектаtinпоэтому подобен ему. Ниже приведен результат'выполнения кода из данного примера.
Сведения об объекте tl:
Треугольник прямоугольный Ширина и высота равны 8 и 12 Площадь равна 48
Сведения об объекте t2:
Треугольник прямоугольный Ширина и высота равны 8 и 12 Площадь равна 48
Обратите особое внимание на следующий конструктор классаTriangle:
public Triangle(Triangle ob) : base(ob) {
Style = ob.Style;
}
Он принимает объект типаTriangleв качестве своего параметра и передает его (с помощью ключевого словаbase)следующему конструктору классаTwoDShape.
public TwoDShape(TwoDShape ob) {
Width = ob.Width;
Height = ob.Height;
}
Самое любопытное, что конструкторTwoDShape() предполагает получить объект классаTwoDShape,тогда как конструкторTriangle() передает ему объект классаTriangle.Как пояснялось выше, такое вполне допустимо, поскольку по ссылке на объект базового класса можно обращаться к объекту производного класса. Следовательно, конструкторуTwoDShape() можно на совершенно законных основаниях передать ссылку на объект класса, производного от классаTwoDShape.А поскольку конструкторTwoDShape() инициализирует только те части объекта производного класса, которые являются членами классаTwoDShape,то для него не имеет никакого значения, содержит ли этот объект другие члены, добавленные в производном классе.
Виртуальные методы и их переопределение
Виртуальнымназывается такой метод, который объявляется какvirtualв базовом классе. Виртуальный метод отличается тем, что он может быть переопределен в одном или нескольких производных классах. Следовательно, у каждого производного класса
может быть свой вариант виртуального метода. Кроме того, виртуальные методы интересны тем, что именно происходит при их вызове по ссылке на базовый класс. В этом случае средствами языка C# определяется именно тот вариант виртуального метода, который следует вызывать, исходя изтипаобъекта, к которому происходит обращениепо ссылке, причем это делаетсяво время выполнения.Поэтому при ссылке на разные типы объектов выполняются разные варианты виртуального метода. Иными словами, вариант выполняемого виртуального метода выбирается по типу объекта, а не по типу ссылки на этот объект. Так, если базовый класс содержит виртуальный метод и от него получены производные классы, то при обращении к разным типам объектов по ссылке на базовый класс выполняются разные варианты этого виртуального метода.
Метод объявляется как виртуальный в базовом классе с помощью ключевого словаvirtual,указываемого перед его именем. Когда же виртуальный метод переопределяется в производном классе, то для этого используется модификаторoverride.А сам процесс повторного определения виртуального метода в производном классе называетсяпереопределением метода.При переопределении имя, возвращаемый тип и сигнатура переопределяющего метода должны быть точно такими же, как и у того виртуального метода, который переопределяется. Кроме того, виртуальный метод не может быть объявлен какstaticилиabstract(подробнее данный вопрос рассматривается далее в этой главе).
Переопределение метода служит основанием для воплощения одного из самых эффективных в C# принципов:динамической диспетчеризации методов, которая представляет собой механизм разрешения вызова во время выполнения, а не компиляции. Значение динамической диспетчеризации методов состоит в том, что именно благодаря ей в C# реализуется динамический полиморфизм.
Ниже приведен пример, демонстрирующий виртуальные методы и их переопределение.
// Продемонстрировать виртуальный метод.
using System;
class Base {
// Создать виртуальный метод в базовом классе, public virtual void Who() {
Console.WriteLine("Метод Who() в классе Base");
}
}
class Derivedl : Base {
// Переопределить метод Who() в производном классе, public override void Who() {
Console.WriteLine("Метод Who() в классе Derivedl");
}
}
class Derived2 : Base {
// Вновь переопределить метод Who() в еще одном производном классе, public override void Who() {
Console.WriteLine("Метод Who() в классе Derived2");
class OverrideDemo { static void Main() {
Base baseOb = new Base();
Derivedl dObl = new DerivedlO;
Deri'ved2 dOb2 = new Derived2();
Base baseRef; // ссылка на базовый класс
baseRef = baseOb; baseRef.Who() ;
baseRef = dObl; baseRef.Who();
baseRef = d0b2; baseRef.Who();
}
}
Вот к какому результату приводит выполнение этого кода.
Метод Who() в классе Base.
Метод Who() в классе Derivedl Метод Who() в классе Derived2
В коде из приведенного выше примера создаются базовый классBaseи два производных от него класса —DerivedlиDerived2.В классеBaseобъявляется виртуальный методWho (), который переопределяется в обоих производных классах. Затем в методеMain() объявляются объекты типаBase, DerivedlиDerived2.Кроме того, объявляется переменнаяbaseRefссылочного типаBase.Далее ссылка на каждый тип объекта присваивается переменнойbaseRefи затем используется для вызова методаWho (). Как следует из результата выполнения приведенного выше кода, вариант выполняемого методаWho() определяется по типу объекта, к которому происходит обращение по ссылке во время вызова этого метода, а не по типу класса переменнойbaseRef.
Но переопределять виртуальный метод совсем не обязательно. Ведь если в производном классе не предоставляется собственный вариант виртуального метода, то используется его вариант из базового класса, как в приведенном ниже примере.
/* Если виртуальный метод не переопределяется, то используется его вариант из базового класса. */
using System;
class Base {
// Создать виртуальный метод в базовом классе. public virtual void Who() {
Console.WriteLine("Метод Who() в классе Base");
}
}
class Derivedl : Base {
// Переопределить метод Who() в производном классе.
public override void Who() {
Console.WriteLine("Метод Who() в классе Derivedl");
}
}
class Derived2 : Base {
// В этом классе метод Who() не переопределяется.
}
class NoOverrideDemo { static void Main() {
Base baseOb = new Base();
Derivedl dObl = new Derivedl();
Derived2 d0b2 = new Derived2();
Base baseRef; // ссылка на базовый класс
baseRef = baseOb; baseRef.Who();
baseRef = dObl ; baseRef.Who() ;
baseRef = d0b2;
• baseRef.Who(); // вызывается метод Who() из класса Base}
}
Выполнение этого кода приводит к следующему результату.
Метод Who() в классе Base.
Метод Who() в классе Derivedl Метод Who() в классе Base
В данном примере методWho() не переопределяется в классеDerived2.Поэтому для объекта классаDerived2вызывается методWho() из классаBase.
Если при наличии многоуровневой иерархии виртуальный метод не переопределяется в производном классе, то выполняется ближайший его вариант, обнаруживаемый вверх по иерархии, как в приведенном ниже примере.
/* В многоуровневой иерархии классов выполняется тот переопределенный вариант виртуального метода, который обнаруживается первым при продвижении вверх по иерархии. */
using System;
class Base {
// Создать виртуальный метод в базовом классе, public virtual void Who() {
Console.WriteLine("Метод Who() в классе Base");
}
}
class Derivedl : Base {
// Переопределить метод Who() в производном классе. public override void Who() {
Console.WriteLine("Метод Who() в классе Derivedl");
}
}
class Derived2 : Derivedl {
// В этом классе метод Who() не переопределяется.
}
class Derived3 : Derived2 {
//Ив этом классе метод Who() не переопределяется.
}
class No0verrideDemo2 { static void Main() {
Derived3 dOb = new Derived3();
Base baseRef; // ссылка на базовый класс
baseRef = dOb;
baseRef.Who(); // вызов метода Who() из класса Derivedl
}
}
Вот к какому результату приводит выполнение этого кода.
Метод Who() в классе Derivedl
В данном примере классDerived3наследует классDerived2,который наследует классDerivedl,а тот, в свою очередь, — классBase.Как показывает приведенный выше результат, выполняется методWho (), переопределяемый в классеDerivedl,поскольку это первый вариант виртуального метода, обнаруживаемый при продвижении вверх по иерархии от классовDerived3иDerived2,где методWho() не переопределяется, к классуDerivedl.
И еще одно замечание: свойства также подлежат модификации ключевым словомvirtualи переопределению ключевым словомoverride.Это же относится и к индексаторам.
Что дает переопределение методов
Благодаря переопределению методов в C# поддерживается динамический полиморфизм. В объектно-ориентированном программировании полиморфизм играет очень важную роль, потому что он позволяет определить в общем классе методы, которые становятся общими для всех производных от него классов, а в производных классах — определить конкретную реализацию некоторых или же всех этих методов. Переопределение методов — это еще один способ воплотить в C# главный принцип полиморфизма: один интерфейс — множество методов.
Удачное применение полиморфизма отчасти зависит от правильного понимания той особенности, что базовые и производные классы образуют иерархию, которая продвигается от меньшей к большей специализации. При надлежащем применении базовый класс предоставляет все необходимые элементы, которые могут использоваться в производном классе непосредственно. А с помощью виртуальных методов в базовом классе определяются те методы, которые могут быть самостоятельно реализованы в производном классе. Таким образом, сочетая наследование с виртуальными методами, можно определить в базовом классе общую форму методов, которые будут использоваться во всех его производных классах.
Применение виртуальных методов
Для тогочтобы стали понятнее преимущества виртуальных методов, применим их в классеTwoDShape.В предыдущих примерах в каждом классе, производном от классаTwoDShape,определялся методArea(). Но, по-видимому, методArea() лучше было бы сделать виртуальным в классеTwoDShapeи тем самым предоставить возможность переопределить его в каждом производном классе с учетом особенностей расчета площади той двумерной формы, которую инкапсулирует этот класс. Именно это и сделано в приведенном ниже примере программы. Ради удобства демонстрации классов в этой программе введено также свойствопашев классеTwoDShape.
// Применить виртуальные методы и полиморфизм-.
using System;
class TwoDShape { double pri_width; double pri_height;
// Конструктор по умолчанию, public TwoDShape() {
Width = Height = 0.0; name = "null";
}
// Параметризированный конструктор.'
public TwoDShape(double w, double h, string n) {
Width = w;
Height = h; name = n;
}
// Сконструировать объект равной ширины и высоты, public TwoDShape(double х, string n) {
Width = Height = x; name = n;
}
// Сконструировать копию объекта TwoDShape. public TwoDShape(TwoDShape ob) {
Width = ob.Width;
Height = ob.Height; name = ob.name;
}
// Свойства ширины и высоты объекта, public double Width {
get { return pri_width; }
set { pri_width = value < 0 ? -value : value; }
}
public double Height {
get { return pri_height; }
set -{ pri_height = value < 0 ? -value : value; }
}
public string name { get; set; }
public void ShowDim() {
Console.WriteLine("Ширина и высота равны " +
Width + " и " + Height);
}
public virtual double Area() {
Console.WriteLine("Метод Area() должен быть переопределен"); return 0.0;
}
}
// Класс для треугольников, производный от класса TwoDShape.
class Triangle : TwoDShape { string Style;
// Конструктор, используемый по умолчанию, public Triangle() {
Style = "null";
}
// Конструктор для класса Triangle, public Triangle(string s, double w, double h) : base (w, h, "треугольник") {
Style = s;
}
// Сконструировать равнобедренный треугольник, public Triangle(double x) : base(x, "треугольник") {
Style = "равнобедренный";
}
// Сконструировать копию объекта типа Triangle, public Triangle(Triangle ob) : base(ob) {
Style = ob.Style;
}
// Переопределить метод Area() для класса Triangle, public override double Area() { return Width * Height / 2;
}
// Показать тип треугольника, public void ShowStyle() {
Console.WriteLine("Треугольник " + Style);
}
I/Класс для прямоугольников, производный от класса TwoDShape. class Rectangle : TwoDShape {
//Конструктор для класса Rectangle, public Rectangle(double w, double h) : base (w, h, "прямоугольник") { }
// Сконструировать квадрат, public Rectangle(double x) : base(x, "прямоугольник") { }
// Сконструировать копию объекта типа Rectangle, public Rectangle(Rectangle ob) : base(ob) { }
// Возвратить логическое значение true, если // прямоугольник окажется квадратом, public bool IsSquareO {
if(Width == Height) return true; return false;
}
// Переопределить метод Area() для класса Rectangle, public override double Area(){return Width * Height;
}
}
class DynShapes {
static void Main() {
TwoDShape[] shapes = new TwoDShape[5] ;
shapes[0] = new Triangle("прямоугольный", 8.0, 12.0); shapes[1] = new Rectangle(10);
shapes[2] = new Rectangle(10, 4);
shapes[3] = new Triangle(7.0);
shapes[4] = new TwoDShape(10, 20, "общая форма");
for (int i=0; i < shapes.Length; i++) {
Console.WriteLine("Объект — " + shapes[i].name);
Console.WriteLine("Площадь равна " + shapes[i].Area());
Console.WriteLine();
}
}
}
При выполнении этой программы получается следующий результат.
Объект — треугольник Площадь равна 48
Объект — прямоугольник Площадь равна 100
Площадь равна 40
Объект — треугольник Площадь равна 24.5
Объект — общая форма
Метод Area() должен быть переопределен Площадь равна 0
Рассмотрим данный пример программы более подробно. Прежде всего, методArea() объявляется какvirtualв классеTwoDShapeи переопределяется в классахTriangleиRectangleпо объяснявшимся ранее причинам. В классеTwoDShapeметодArea() реализован в виде заполнителя, который сообщает о том, что пользователь данного метода должен переопределить его в производном классе. Каждое переопределение методаArea() предоставляет конкретную его реализацию, соответствующую типу объекта, инкапсулируемого в производном классе. Так, если реализовать класс для эллипсов, то методArea() должен вычислять площадь эллипса.
У программы из рассматриваемого здесь примера имеется еще одна примечательная особенность. Обратите внимание на то, что в методеMain() двумерные формы объявляются в виде массива объектов типаTwoDShape,но элементам этого массива присваиваются ссылки на объекты классовTriangle, RectangleиTwoDShape.И это вполне допустимо, поскольку по ссылке на базовый класс можно обращаться к объекту прризводного класса. Далее в программе происходит циклическое обращения к элементам данного массива для вывода сведений о каждом объекте. Несмотря на всю свою простоту, данный пример наглядно демонстрирует преимущества наследования и переопределения методов. Тип объекта, хранящийся в переменной ссылки на базовый класс, определяется во время выполнения и соответственно обусловливает дальнейшие действия. Так, если объект является производным от классаTwoDShape,то для получения его площади вызывается методArea (). Но интерфейс для выполнения этой операции остается тем же самым независимо от типа используемой двумерной формы.
Применение абстрактных классов
Иногда требуется создать базовый класс, в котором определяется лишь самая общая форма для всех его производных классов, а наполнение ее деталями предоставляется каждому из этих классов. В таком классе определяется лишь характер методов, которые должны быть конкретно реализованы в производных классах, а не в самом базовом классе. Подобная ситуация возникает, например, в связи с невозможностью получить содержательную реализацию метода в базовом классе. Именно такая ситуация была продемонстрирована в варианте классаTwoDShapeиз предыдущего примера, где методArea() был просто определен как заполнитель. Такой метод не вычисляет и не выводит площадь двумерного объекта любого типа.
Создавая собственные библиотеки классов, вы можете сами убедиться в том, что у метода зачастую отсутствует содержательное определение в контексте его базового класса. Подобная ситуация разрешается двумя способами. Один из них, как показано в предыдущем примере, состоит в том, чтобы просто выдать предупреждающее сообщение. Такой способ может пригодиться в определенных ситуациях, например при отладке, но в практике программирования он обычно не применяется. Ведь в базовом классе могут быть объявлены методы, которые должны быть переопределены в производном классе, чтобы этот класс стал содержательным. Рассмотрим для примера классTriangle.Он был бы неполным, если бы в нем не был переопределен методArea ().В подобных случаях требуется какой-то способ, гарантирующий, что в производном классе действительно будут переопределены все необходимые методы. И такой способ в C# имеется. Он состоит в использовании абстрактного метода.
Абстрактный методсоздается с помощью указываемого модификатора типа abstract. У абстрактного метода отсутствует тело, и поэтому он не реализуется в базовом классе. Это означает, что он должен быть переопределен в производном классе, поскольку его вариант из базового класса просто непригоден для использования. Нетрудно догадаться, что абстрактный метод автоматически становится виртуальным и не требует указания модификатора virtual. В действительности совместное использование модификаторов virtual и abstract считается ошибкой.
Для определения абстрактного метода служит приведенная ниже общая форма.
abstractтип имя{список_параметров);
Как видите, у абстрактного метода отсутствует тело. Модификатор abstract может применяться только в методах экземпляра, но не в статических методах (static). Абстрактными могут быть также индексаторы и свойства.
Класс, содержащий один или больше абстрактных методов, должен быть также объявлен как абстрактный, и для этого перед его объявлением class указывается модификатор abstract. А поскольку реализация абстрактного класса не определяется полностью, то у него не может быть объектов. Следовательно, попытка создать объект абстрактного класса с помощью оператораnewприведет к ошибке во время компиляции.
Когда производный класс наследует абстрактный класс, в нем должны быть реализованы все абстрактные методы базового класса. В противном случае производный класс должен быть также определен как abstract. Таким образом, атрибут abstract наследуется до тех пор, пока не будет достигнута полная реализация класса.
Используя абстрактный класс, мы можем усовершенствовать рассматривавшийся ранее классTwoDShape.Для неопределенной двухмерной фигуры понятие площади не имеет никакого смысла, поэтому в приведенном ниже варианте классаTwoDShapeметодArea() и сам классTwoDShapeобъявляются какabstract.Это, конечно, означает, что во всех классах, производных от классаTwoDShape,должен быть переопределен методArea ().
// Создать абстрактный класс, using System;
abstract class TwoDShape { double pri_width; double pri_height;
// Конструктор, используемый по умолчанию, public TwoDShape() {
Width = Height =0.0; name = "null";
}
// Параметризированный конструктор.
public TwoDShape(double w, double h, string n) {
Width = w;
Height = h; name = n;
}
// Сконструировать объект равной ширины и высоты, public TwoDShape(double х, string n) {
Width = Height = x; name = n;
}
// Сконструировать копию объекта TwoDShape. public TwoDShape(TwoDShape ob) {
Width = ob.Width;
Height = ob.Height; name = ob.name;
}
// Свойства ширины и высоты объекта, public double Width {
get { return pri_width; }
set { pri_width = value < 0 ? -value : value; }
}
public double Height {
get { return pri_height; }
set { pri_height = value < 0 ? -value : value; }
}
public string name { get; set; } public void ShowDimO {
Console.WriteLine("Ширина и высота равны " +
Width + " и " + Height);
}
// Теперь метод Area() является абстрактным, public abstract double Area();
}
// Класс для треугольников, производный от класса TwoDShape. class Triangle : TwoDShape { string Style;
// Конструктор, используемый по умолчанию, public Triangle() {
Style = "null";
}
// Конструктор для класса Triangle, public Triangle(string s, double w, double h) : base(w, h, "треугольник") {
Style = s;
I/Сконструировать равнобедренный треугольник, public Triangle(double x) : base(x, "треугольник") { Style = "равнобедренный";
}
// Сконструировать копию объекта типа Triangle, public Triangle(Triangle ob) : base(ob) {
Style = ob.Style;
}
// Переопределить метод Area() для класса Triangle, public override double Area() {
return Width * Height / 2;
}
// Показать тип треугольника, public void ShowStyle() {
Console.WriteLine("Треугольник " + Style);
}
}
// Класс для прямоугольников, производный от класса TwoDShape class Rectangle : TwoDShape {
// Конструктор для класса Rectangle, public Rectangle(double w, double h) : base(w, h, "прямоугольник"){ }
// Сконструировать квадрат, public Rectangle(double x) : base (x, "прямоугольник")- { }
// Сконструировать копию объекта типа Rectangle, public Rectangle(Rectangle ob) : base(ob) { }
// Возвратить логическое значение true, если // прямоугольник окажется квадратом, public bool IsSquare() {
if(Width == Height) return true; return false;
}
// Переопределить метод Area() для класса Rectangle, public override double Area() { return Width * Height;
}
}
class AbsShape {
static void Main() {
TwoDShape[] shapes = new TwoDShape[4];
shapes[0] = new Triangle("прямоугольный", 8.0, 12.0); shapes[1] = new Rectangle(10) ;
shapes[2] = new Rectangle(10, 4); shapes[3] = new Triangle(7.0);
for(int i=0; i < shapes.Length; i++) {
Console.WriteLine("Объект — " + shapes[i].name);
Console.WriteLine("Площадь равна " + shapes[i].Area());
Console.WriteLine() ;
}
}
> t
Как показывает представленный выше пример программы, во всех производных классах методArea() должен быть непременно переопределен, а также объявлен абстрактным. Убедитесь в этом сами, попробовав создать производный класс, в котором не переопределен методArea (). В итоге вы получите сообщение об ошибке во время компиляции. Конечно, возможность создавать ссылки на объекты типаTwoDShapeпо-прежнему существует, и это было сделано в приведенном выше примере программы, но объявлять объекты типаTwoDShapeуже нельзя. Именно поэтому массивshapesсокращен в методеMain() до 4 элементов, а объект типаTwoDShapeдля общей двухмерной формы больше не создается.
Обратите также внимание на то, что в классTwoDShapeпо-прежнему входит методShowDim() и что он не объявляется с модификаторомabstract.В абстрактные классы вполне допускается (и часто практикуется) включать конкретные методы, которые могут быть использованы в своем исходном виде в производном классе. А переопределению в производных классах подлежат только те методы, которые объявлены какabstract.
Предотвращение наследования с помощью ключевого слова sealed
Несмотря на всю эффективность и полезность наследования, иногда возникает потребность предотвратить его. Допустим, что имеется класс, инкапсулирующий последовательность инициализации некоторого специального оборудования, например медицинского монитора. В этом случае требуется, чтобы пользователи данного класса не могли изменять порядок инициализации монитора, чтобы исключить его неправильную настройку. Но независимо от конкретных причин в C# имеется возможность предотвратить наследование класса с помощью ключевого слова sealed.
Для того чтобы предотвратить наследование класса, достаточно указать ключевое слово sealed перед определением класса. Как и следовало ожидать, класс не допускается объявлять одновременно как abstract и sealed, поскольку сам абстрактный класс реализован не полностью и опирается в этом отношении на свои производные классы, обеспечивающие полную реализацию.
Ниже приведен пример объявления класса типа sealed.
sealed class А {
// . . .
}
// Следующий класс недопустим.
class В : A { // ОШИБКА! Наследовать класс А нельзя/ /...
}
Как следует из комментариев в приведенном выше фрагменте кода, классВне может наследовать классА,потому что последний объявлен какsealed.
И еще одно замечание: ключевое словоsealedможет быть также использовано в виртуальных методах для предотвращения их дальнейшего переопределения. Допустим, что имеется базовый классВи производный классD.Метод, объявленный в классеВкакvirtual,может быть объявлен в классеDкакsealed.Благодаря этому в любом классе, наследующем от класса % предотвращается переопределение данного метода. Подобная ситуация демонстрируется в приведенном ниже фрагменте кода,class В {
public virtual void MyMethodO { /* ... */ }
}
class D : В {
// Здесь герметизируется метод MyMethodO и // предотвращается его дальнейшее переопределение, sealed public override void MyMethodO { /* ••• */ }
}
class X : D {
// Ошибка! Метод MyMethodO герметизирован! public override void MyMethodO { /* ••• */ }
}
МетодMyMethod() герметизирован в классеD,и поэтому не может быть переопределен в классе X.
Класс object
В C# предусмотрен специальный классobject,который неявно считается базовым классом для всех остальных классов и типов, включая и типы значений. Иными словами, все остальные типы являются производными отobj ect.Это, в частности, означает, что переменная ссылочного типаobjectможет ссылаться на объект любого другого типа. Кроме того, переменная типаobjectможет ссылаться на любой массив, поскольку в C# массивы реализуются как объекты. Формально имяobjectсчитается в C# еще одним обозначением классаSystem. Object,входящего в библиотеку классов для среды .NET Framework.
В классеobj ectопределяются методы, приведенные в табл. 11.1. Это означает, что они доступны для каждого объекта.
Некоторые из этих методов требуют дополнительных пояснений. По умолчанию методEquals (object)определяет, ссылается ли вызывающий объект на тот же самый объект, что и объект, указываемый в качества аргумента этого метода, т.е. он определяет, являются ли обе ссылки одинаковыми. МетодEquals (object)возвращает логическое значениеtrue,если сравниваемые объекты одинаковы, в противном случае — логическое значениеfalse.Он может быть также переопределен в создаваемых классах. Это позволяет выяснить, что же означает равенство объектов для создаваемого класса. Например, методEquals (object)можно определить таким образом, чтобы в нем сравнивалось содержимое двух объектов.
Метод
Назначение
public virtual bool
Определяет, является ли вызывающий объект таким же,
Equals(objectob)
как и объект, доступный по ссылке оЬ
public static bool
Определяет, является ли объект, доступный по ссылке
Equals(objectobjA,
objA, таким же, как и объект, доступный по ссылке
objectobjB)
objB
protected Finalize()
Выполняет завершающие действия перед “сборкой му
сора". В C# метод Finalize () доступен посредством
деструктора
public virtual int
Возвращает хеш-код, связанный с вызывающим
GetHashCode()
объектом
public Type GetType()
Получает тип объекта во время выполнения программы
protected object
Выполняет неполное копирование объекта, т.е. копиру
MemberwiseClone()
ются только члены, но не объекты, на которые ссылают
ся эти члены
public static bool
Определяет, делаются ли ссылки objA и objB на один
ReferenceEquals(objobjA,
и тот же объект
objectobjB)
public virtual string
Возвращает строку, которая описывает объект
ToString()
МетодGetHashCode() возвращает хеш-код, связанный с вызывающим объектом. Этот хеш-код можно затем использовать в любом алгоритме, где хеширование применяется в качестве средства доступа к хранимым объектам. Следует, однако, иметь в виду, что стандартная реализация методаGetHashCode() не пригодна на все случаи применения.
Как упоминалось в главе 9, если перегружается оператор ==, то обычно приходится переопределять методы Equals (object) и GetHashCode (), поскольку чаще всего требуется, чтобы метод Equals (object) и оператор == функционировали одинаково. Когда же переопределяется метод Equals (object), то следует переопределить и метод GetHashCode (), чтобы оба метода оказались совместимыми.
МетодToStringOвозвращает символьную строку, содержащую описание того объекта, для которого он вызывается. Кроме того, методToStringOавтоматически вызывается при выводе содержимого объекта с помощью методаWriteLine (). Этот метод переопределяется во многих классах, что позволяет приспосабливать описание к конкретным типам объектов, создаваемых в этих классах. Ниже приведен пример применения данного метода.
// Продемонстрировать применение метода ToStringO
using System;
class MyClass {
static int count = 0; int id;
id = count; count++;
}
public override string ToStringO {
return "Объект #" + id + " типа MyClass";
}
}
class Test {
static void Main() {
MyClass obi = new MyClass();
MyClass ob2 = new MyClass();
MyClass ob3 = new MyClass();
Console.WriteLine(obi);
Console.WriteLine(ob2);
Console.WriteLine(ob3) ;
}
}
При выполнении этого кода получается следующий результат.
Объект #0 типа MyClass Объект #1 типа MyClass Объект #2 типа MyClass
Упаковка и распаковка
Как пояснялось выше, все типы в С#, включая и простые типы значений, являются производными от класса object. Следовательно, ссылкой типа object можно воспользоваться для обращения к любому другому типу, в том числе и к типам значений. Когда ссылка на объект класса ob j ect используется для обращения к типу значения, то такой процесс называетсяупаковкой. Упаковка приводит к тому, что значение простого типа сохраняется в экземпляре объекта, т.е. "упаковывается" в объекте, который затем используется как и любой другой объект. Но в любом случае упаковка происходит автоматически. Для этого достаточно присвоить значение переменной ссылочного типа object, а об остальном позаботится компилятор С#.
Распаковкапредставляет собой процесс извлечения упакованного значения из объекта. Это делается с помощью явного приведения типа ссылки на объект класса ob j ect к соответствующему типу значения. Попытка распаковать объект в другой тип может привести к ошибке во время выполнения.
Ниже приведен простой пример, демонстрирующий упаковку и распаковку.
// Простой пример упаковки и распаковки.
using System;
class BoxingDemo { static void Main() {
int x; object obj ;
X = 10;
obj = х; // упаковать значение переменной х в объект
int у = (int)obj; // распаковать значение из объекта, доступного по // ссылке obj, в переменную типа int Console.WriteLine(у);
}
}
В этом примере кода выводится значение 10. Обратите внимание на то, что значение переменной х упаковывается в объект простым его присваиванием переменнойobj,ссылающейся на этот объект. А затем это значение извлекается из объекта, доступного по его ссылкеobj,и далее приводится к типуint.
Ниже приведен еще один, более интересный пример упаковки. В данном случае значение типаintпередается в качестве аргумента методуSqr (), который, в свою очередь, принимает параметр типаobject.
// Пример упаковки при передаче значения методу.
using System;
class BoxingDemo { static void Main() { int x; x = 10;
Console.WriteLine("Значение x равно: " + x);
// значение переменной x автоматически упаковывается // когда оно передается методу Sqr(). х = BoxingDemo.Sqr(х) ;
Console.WriteLine("Значение x в квадрате равно: " + х);
}
static int Sqr(object о) { return (int)о * (int)о;
}
}
Вот к какому результату приводит выполнение этого кода.
Значение х равно: 10
Значение х в квадрате равно: 100
В данном примере значение переменной х автоматически упаковывается при передаче методуSqr ().
Упаковка и распаковка позволяют полностью унифицировать систему типов в С#. Благодаря тому что все типы являются производными от классаobject,ссылка на значение любого типа может быть просто присвоена переменной ссылочного типаobject,а все остальное возьмут на себя упаковка и распаковка. Более того, методы классаobjectоказываются доступными всем типам, поскольку они являются производными от этого класса. В качестве примера рассмотрим довольно любопытную программу.
// Благодаря упаковке становится возможным вызов методов по значению! using System;
/
class MethOnValue { static void Main() {
Console.WriteLine(10.ToString() ) ;
}
}
В результате выполнения этой программы выводится значение 10. Дело в том, что метод ToString () возвращает строковое представление объекта, для которого он вызывается. В данном случае строковым представлением значения 10 как вызывающего объекта является само значение 10!
Класс object как универсальный тип данных
Если obj ect является базовым классом для всех остальных типов и упаковка значений простых типов происходит автоматически, то класс object можно вполне использовать в качестве "универсального" типа данных. Для примера рассмотрим программу, в которой сначала создается массив типа object, элементам которого затем присваиваются значения различных типов данных.
// Использовать класс object для создания массива "обобщенного" типа.
using System;
class GenericDemo { static void Main() {
object[] ga. = new object[10];
// Сохранить целые значения, for (int i=0; i < 3; i++) ga[i] = i;
ga[9] = "Конец";
for(int i = 0; i < ga.Length; i++)
Console.WriteLine("ga[" + i + "]: " + ga[i] + " ");
}
}
Выполнение этой программы приводит к следующему результату.
да [ 0] : 0 да[1] : 1
да
[2] :
2
да[3] :
1.5
да [4] :
2
да[5] :
2.5
да[6] :
Привет
да
[7] :
True
да
[8] :
X
да
[9] :
Конец
Как показывает данный пример, по ссылке на объект класса object можно обращаться к данным любого типа, поскольку в переменной ссылочного типа object допускается хранить ссылку на данные всех остальных типов. Следовательно, в массиве типа object из рассматриваемого здесь примера можно сохранить данные практически любого типа. В развитие этой идеи можно было бы, например, без особого труда создать класс стека со ссылками на объекты класса object. Это позволило бы хранить в стеке данные любого типа.
Несмотря на то что универсальный характер класса object может быть довольно эффективно использован в некоторых ситуациях, было бы ошибкой думать, что с помощью этого класса стоит пытаться обойти строго соблюдаемый в C# контроль типов. Вообще говоря, целое значение следует хранить в переменной типа int, строку — в переменной ссылочного типа string и т.д.
А самое главное, что начиная с версии 2.0 для программирования на C# стали доступными подлинно обобщенные типы данных — обобщения (более подробно они рассматриваются в главе 18). Внедрение обобщений позволило без труда определять классы и алгоритмы, автоматически обрабатывающие данные разных типов, соблюдая типовую безопасность. Благодаря обобщениям отпала необходимость пользоваться классом object как универсальным типом данных при создании нового кода..Универсальный характер этого класса лучше теперь оставить для применения в особых случаях.
ГЛАВА 12 Интерфейсы, структуры и перечисления
В этой главе рассматривается одно из самых важных в C# средств:интерфейс, определяющий ряд методов для реализации в классе. Но поскольку в самом интерфейсе ни один из методов не реализуется, интерфейс представляет собой чисто логическую конструкцию, описывающую функциональные возможности без конкретной их реализации.
Кроме того, в этой главе представлены еще два типа данных С#: структуры и перечисления.Структурыподобны классам, за исключением того, что они трактуются как типы значений, а не ссылочные типы. Аперечисленияпредставляют собой перечни целочисленных констант. Структуры и перечисления расширяют богатый арсенал средств программирования на С#.
Интерфейсы
Иногда в объектно-ориентированном программировании полезно определить, что именно должен делать класс, но не как он должен это делать. Примером тому может служить упоминавшийся ранее абстрактный метод.
В абстрактном методе определяются возвращаемый тип и сигнатура метода, но не предоставляется его реализация.
А в производном классе должна быть обеспечена своя собственная реализация каждого абстрактного метода, определенного в его базовом классе. Таким образом, абстрактный метод определяетинтерфейс,но не реализацию метода. Конечно, абстрактные классы и методы приносят известную пользу, но положенный в их основу принцип может быть
развит далее. В C# предусмотрено разделение интерфейса класса и его реализации с помощью ключевого слова interface.
С точки зрения синтаксиса интерфейсы подобны абстрактным классам. Но в интерфейсе ни у одного из методов не должно быть тела. Это означает, что в интерфейсе вообще не предоставляется никакой реализации. В нем указывается только, что именно следует делать, но не как это делать. Как только интерфейс будет определен, он может быть реализован в любом количестве классов. Кроме того, в одном классе может быть реализовано любое количество интерфейсов.
Для реализации интерфейса в классе должны быть предоставлены тела (т.е. конкретные реализации) методов, описанных в этом интерфейсе. Каждому классу предоставляется полная свобода для определения деталей своей собственной реализации интерфейса. Следовательно, один и тот же интерфейс может быть реализован в двух классах по-разному. Тем не менее в каждом из них должен поддерживаться один и тот же набор методов данного интерфейса. А в том коде, где известен такой интерфейс, могут использоваться объекты любого из этих двух классов, поскольку интерфейс для всех этих объектов остается одинаковым. Благодаря поддержке интерфейсов в C# может быть в полной мере реализован главный принцип полиморфизма: один интерфейс — множество методов.
Интерфейсы объявляются с помощью ключевого слова interface. Ниже приведена упрощенная форма объявления интерфейса.
interfaceимя{
возвращаемый_тип имя_метода1(список_параметров);возвращаемый_тип ммя_метода2 [список_параметров);
// ...
возвращаемый_тип имя_методаЫ(список_параметров);
}
гдеимя— это конкретное имя интерфейса. В объявлении методов интерфейса используются только ихвозвращаемый_типи сигнатура. Они, по существу, являются абстрактными методами. Как пояснялось выше, в интерфейсе не может быть никакой реализации. Поэтому все методы интерфейса должны быть реализованы в каждом классе, включающем в себя этот интерфейс. В самом же интерфейсе методы неявно считаются открытыми, поэтому доступ к ним не нужно указывать явно.
Ниже приведен пример объявления интерфейса для класса, генерирующего последовательный ряд чисел.
public interface ISeries {
int GetNextO; // возвратить следующее по порядку число void Reset(); // перезапустить
void SetStart(int х); // задать начальное значение
}
Этому интерфейсу присваивается имя ISeries. Префикс I в имени интерфейса указывать необязательно, но это принято делать в практике программирования, чтобы как-то отличать интерфейсы от классов. Интерфейс ISeries объявляется как public и поэтому может быть реализован в любом классе какой угодно программы.
Помимо методов, в интерфейсах можно также указывать свойства, индексаторы и события. Подробнее о событиях речь пойдет в главе 15, а в этой главе-основное внимание будет уделено методам, свойствам и индексаторам. Интерфейсы не могут содержать члены данных. В них нельзя также определить конструкторы, деструкторы или операторные методы. Кроме того, ни один из членов интерфейса не может быть объявлен как static.
Реализация интерфейсов
Как только интерфейс будет определен, он может быть реализован в одном или нескольких классах. Для реализации интерфейса достаточно указать его имя после имени класса, аналогично базовому классу. Ниже приведена общая форма реализации интерфейса в классе.
classимя_класса:имя_мнтерфемса{
// тело класса
}
гдеимя_интерфейса —это конкретное имя реализуемого интерфейса. Если уж интерфейс реализуется в классе, то это должно быть сделано полностью. В частности, реализовать интерфейс выборочно и только по частям нельзя.
В классе допускается реализовывать несколько интерфейсов. В этом случае все реализуемые в классе интерфейсы указываются списком через запятую. В классе можно наследовать базовый класс и в тоже время реализовать один или более интерфейс. В таком случае имя базового класса должно быть указано перед списком интерфейсов, разделяемых запятой.
Методы, реализующие интерфейс, должны быть объявлены как public. Дело в том, что в самом интерфейсе эти методы неявно подразумеваются как открытые, поэтому их реализация также должна быть открытой. Кроме того, возвращаемый тип и сигнатура реализуемого метода должны точно соответствовать возвращаемому типу и сигнатуре, указанным в определении интерфейса.
Ниже приведен пример программы, в которой реализуется представленный ранее интерфейс ISeries. В этой программе создается класс ByTwos, генерирующий последовательный ряд чисел, в котором каждое последующее число на два больше предыдущего.
// Реализовать интерфейс ISeries, class ByTwos : ISeries { int start; int val;
public ByTwos () {
start = 0; val = 0;
}
public int GetNext() { val += 2; return val;
}
public void Reset() {
val = start;
}
public void SetStart(int x) { start = x; val = start;
Как видите, в классе ByTwos реализуются три метода, определяемых в интерфейсе ISeries. Как пояснялось выше, это приходится делать потому, что в классе нельзя реализовать интерфейс частично.
Ниже приведен код класса, в котором демонстрируется применение класса ByTwos, реализующего интерфейс ISeries.
// Продемонстрировать применение класса ByTwos, реализующего интерфейс, using System;
class SeriesDemo { static void Main() {
ByTwos ob = new ByTwos(); /
for (int i=0; i < 5; i++)
Console .WriteLine ("Следующее число равно " + ob. GetNext () ) ;
Console.WriteLine("ХпСбросить") ; ob.Reset();
for(int i=0; i < 5; i++)
Console.WriteLine("Следующее число равно " + ob.GetNext());
Console.WriteLine("ХпНачать с числа 100");
ob.SetStart(100);
for(int i=0; i < 5; i++)
Console.WriteLine("Следующее число равно " + ob.GetNext());
}
}
Для того чтобы скомпилировать код класса SeriesDemo, необходимо включить в компиляцию файлы, содержащие интерфейс ISeries, а также классы ByTwos и SeriesDemo. Компилятор автоматически скомпилирует все три файла и сформирует из них окончательный исполняемый файл. Так, если эти файлы называются ISeries . cs, ByTwos . cs и SeriesDemo. cs, то программа будет скомпилирована в следующей командной строке:
>csc SeriesDemo.cs ISeries.cs ByTwos.cs
В интегрированной среде разработки Visual Studio для этой цели достаточно ввести все три упомянутых выше файла в конкретный проект С#. Кроме того, все три компилируемых элемента (интерфейс и оба класса) допускается включать в единый файл. Ниже приведен результат выполнения скомпилированного кода.
Следующее число равно 2 Следующее число равно 4 Следующее число равно 6 Следующее число равно 8 Следующее число равно 10
Сбросить.
Следующее число равно 2 Следующее число равно 4 Следующее число равно 6 Следующее число равно 8 Следующее число равно 10
Начать с числа 100.
Следующее число равно 102 Следующее число равно 104 Следующее число равно 106 Следующее число равно 108 Следующее число равно 110
В классах, реализующих интерфейсы, разрешается и часто практикуется определять их собственные дополнительные члены. В качестве примера ниже приведен другой вариант классаByTwos,в который добавлен методGetPrevious (), возвращающий предыдущее значение.
// Реализовать интерфейс ISeries и добавить в // класс ByTwos метод GetPrevious().
class ByTwos : ISeries {
int start;
int val;
int prev;
public ByTwos() {
start = 0; val = 0; prev = -2;
}
public int GetNextO { prev = val; val += 2; return val;
}
public void Reset() {
val = start; prev = start - 2;
}
public void SetStart(int x) { start = x; val = start; prev = val - 2;
}
// Метод, не указанный в интерфейсе ISeries.
public int GetPrevious() {
return prev;
}
}
Как видите, для того чтобы добавить методGetPrevious (), потребовалось внести изменения в реализацию методов, определяемых в интерфейсеISeries.Но поскольку интерфейс для этих методов остается прежним, то такие изменения не вызывают никаких осложнений и не нарушают уже существующий код. В этом и заключается одно из преимуществ интерфейсов.
Как пояснялось выше, интерфейс может быть реализован в любом количестве классов. В качестве примера ниже приведен классPrimes,генерирующий ряд простых чисел. Обратите внимание на то, реализация интерфейсаISeriesв этом классе коренным образом отличается от той, что предоставляется в классеByTwos.
// Использовать интерфейс ISeries для реализации // процесса генерирования простых чисел, class Primes : ISeries { int start; int val;
public Primes() {
start = 2; val = 2;
}
public int GetNextO { int i, j; bool isprime;
val++;
for(i = val; i < 1000000; i++) {
isprime = true; for(j = 2; j <= i/j; j++) {
if( (i%j ) ==0) {isprime = false; break;
}
}
if (isprime) { val = i; break;
}
}
return val;
}
public void Reset() {
val = start;
}
public void SetStart(int x) { start = x; val = start;
}
}
Самое любопытное, что в обоих классах,ByTwosиPrimes,реализуется один и тот же интерфейс, несмотря на то, что в них генерируются совершенно разные ряды чисел. Как пояснялось выше, в интерфейсе вообще отсутствует какая-либо реализация, поэтому он может быть свободно реализован в каждом классе так, как это требуется для самого класса.
Применение интерфейсных ссылок
Как это ни покажется странным, но в C# допускается объявлять переменные ссылочного интерфейсного типа, т.е. переменные ссылки на интерфейс. Такая переменная может ссылаться на любой объект, реализующий ее интерфейс. При вызове метода для объекта посредством интерфейсной ссылки выполняется его вариант, реализованный в классе данного объекта. Этот процесс аналогичен применению ссылки на базовый класс для доступа к объекту производного класса, как пояснялось в главе 11.
В приведенном ниже примере программы демонстрируется применение интерфейсной ссылки. В этой программе переменная ссылки на интерфейс используется с целью вызвать методы для объектов обоих классов —ByTwosиPrimes.Для ясности в данном примере показаны все части программы, собранные в единый файл.
// Продемонстрировать интерфейсные ссылки, using System;
// Определить интерфейс, public interface ISeries {
int GetNext(); // возвратить следующее по порядку число void Reset(); // перезапустить
void SetStart(int х); // задать начальное значение
}
// Использовать интерфейс ISeries для реализации процесса // генерирования последовательного ряда чисел, в котором каждое // последующее число на два больше предыдущего, class ByTwos : ISeries { int start; int val;
val = start;
}
public void SetStart(int x) { start = x; val = start;
}
}
// Использовать интерфейс ISeries для реализации // процесса генерирования простых чисел.
class Primes : ISeries { int start; int val;
public Primes() {
start = 2; val = 2;
}
public int GetNextO { int i, j; bool isprime;
val++;
for(i = val; i < 1000000; i++) {
isprime = true; for (j = 2; j <= i/j; j++) {
if ( (i % j)==0) { isprime = false; break;
}
}
if (isprime) { val = i; break;
}
}
return val;
}
public void Reset() {
val = start;
}
public void SetStart(int x) { start = x; val = start;
}
}
class SeriesDemo2 { static void Main() {
ByTwos twoOb = new ByTwos();
Primes primeOb = new Primes();
ISeries ob;
for(int i=0; i < 5; i++) {
ob = twoOb;
Console.WriteLine("Следующее четное число равно " + ob.GetNext()) ; ob = primeOb;
Console.WriteLine("Следующее простое число " + "равно " + ob.GetNext());
Вот к какому результату приводит выполнение этой программы:
Следующее четное число равно 2 Следующее простое число -равно 3 Следующее четное число равно 4 Следующее простое число равно- 5 Следующее четное число равно 6 Следующее простое число равно 7 Следующее четное число равно 8 Следующее простое число равно 11 Следующее четное число равно 10 Следующее простое число равно 13
В методеMain() переменнаяobобъявляется для ссылки на интерфейсISeries.Это означает, что в ней могут храниться ссылки на объект любого класса, реализующего интерфейсISeries.В данном случае она служит для ссылки на объектыtwoObиprimeObклассовByTwosиPrimesсоответственно, в которых реализован интерфейсISeries.
И еще одно замечание: переменной ссылки на интерфейс доступны только методы, объявленные в ее интерфейсе. Поэтому интерфейсную ссылку нельзя использовать для доступа к любым другим переменным и методам, которые не поддерживаются объектом класса, реализующего данный интерфейс.
Интерфейсные свойства
Аналогично методам, свойства указываются в интерфейсе вообще без тела. Ниже приведена общая форма объявления интерфейсного свойства.
// Интерфейсное свойствотип имя{get; set;
}
Очевидно, что в определении интерфейсных свойств, доступных только для чтения или только для записи, должен присутствовать единственный аксессор:getилиsetсоответственно.
Несмотря на то что объявление свойства в интерфейсе очень похоже на объявление автоматически реализуемого свойства в классе, между ними все же имеется отличие. При объявлении в интерфейсе свойство не становится автоматически реализуемым. В этом случае указывается только имя и тип свойства, а его реализация предоставляется каждому реализующему классу. Кроме того, при объявлении свойства в интерфейсе не разрешается указывать модификаторы доступа для аксессоров. Например, аксессорsetне может быть указан в интерфейсе какprivate.
Ниже в качестве примера приведен переделанный вариант интерфейсаISeriesи классаByTwos,в котором свойствоNextиспользуется для получения и установки следующего по порядку числа, которое больше предыдущего на два.
public interface ISeries {
// Интерфейсное свойство, int Next {
get; // возвратить следующее по порядку число set; // установить следующее число
}
}
// Реализовать интерфейс ISeries, class ByTwos : ISeries { int val;
public ByTwos() {
val = 0;
}
// Получить или установить значение, public int Next { get {
val += 2; return val;
}
set {
val = value;
}
}
}
// Продемонстрировать применение интерфейсного свойства, class SeriesDemo3 { static void Main() {
ByTwos ob = new ByTwos();
// Получить доступ к последовательному ряду чисел с помощью свойства, for(int i=0; i < 5; i++)
Console.WriteLine("Следующее число равно " + ob.Next);
Console.WriteLine("ХпНачать с числа 21");
ob.Next = 21;
for (int i=0; i <5; i++)
Console.WriteLine("Следующее число равно " + ob.Next);
} .
}
При выполнении этого кода получается следующий результат.
Следующее число равно 2 Следующее число равно 4 Следующее число равно 6 Следующее число равно 8 Следующее число равно 10
Начать с числа 21 Следующее число равно 23 Следующее число равно 25
Следующее число равно 27 Следующее число равно 2 9 Следующее число равно 31
Интерфейсные индексаторы
В интерфейсе можно также указывать индексаторы. Ниже приведена общая форма объявления интерфейсного индексатора.
// Интерфейсный индексатортип_элементаthis[intиндекс]{get; set;
}
Как и прежде, в объявлении интерфейсных индексаторов, доступных только для чтения или только для записи, должен присутствовать единственный аксессор:getилиsetсоответственно.
Ниже в качестве примера приведен еще один вариант реализации интерфейсаISeries,в котором добавлен индексатор только для чтения, возвращающийi-uэлемент числового ряда.
// Добавить индексатор в интерфейс, using System;
public interface ISeries {
// Интерфейсное свойство, int Next {
get; // возвратить следующее по порядку число set; // установить следующее число
}
// Интерфейсный индексатор, int this[int index] {
get; // возвратить указанное в ряду число
}
}
// Реализовать интерфейс ISeries, class ByTwos : ISeries { int val;
public ByTwos() {
val = 0;
}
// Получить или установить значение с помощью свойства, public int Next { get {
val += 2; return val;
set {
val = value;
}
}
//Получить значение по индексу, public int this[int index] { get {
val = 0;
for(int i=0; i < index; i++) val += 2; return val;
}
}
}
// Продемонстрировать применение интерфейсного индексатора, class SeriesDemo4 { static void Main() {
ByTwos ob = new ByTwos();
// Получить доступ к последовательному ряду чисел с помощью свойства, for (int i=0; i < 5; i++)
Console.WriteLine("Следующее число равно " + ob.Next);
Console.WriteLine("ХпНачать с числа 21");
ob.Next = 21;
for (int i=0; i < 5; i++)
Console.WriteLine("Следующее число равно " + ob.Next);
Console.WriteLine("ХпСбросить в 0"); ob.Next = 0;
// Получить доступ к последовательному ряду чисел с помощью индексатора for (int i=0; i < 5; i++)
Console.WriteLine("Следующее число равно " + ob[i]);
}
}
Вот к какому результату приводит выполнение этого кода.
Следующее число равно 2 Следующее число равно 4 Следующее число равно 6 Следующее число равно 8 Следующее число равно 10
Начать с числа 21 Следующее число равно 23 Следующее число равно 25 Следующее число равно 27 Следующее число равно 2 9 Следующее число равно 31
Сбросить в О Следующее число равно О Следующее число равно 2 Следующее число равно 4 Следующее число равно 6 Следующёе число равно 8
Наследование интерфейсов
Один интерфейс может наследовать другой. Синтаксис наследования интерфейсов такой же, как и у классов. Когда в классе реализуется один интерфейс, наследующий другой, в нем должны быть реализованы все члены, определенные в цепочке наследования интерфейсов, как в приведенном ниже примере.
// Пример наследования интерфейсов, using System;
public interface IA { void Methl(); void Meth2() ;
}
// В базовый интерфейс включены методы Methl() и Meth2(),
// а в производный интерфейс добавлен еще один метод — Meth3(). public interface IB : IA { void Meth3();
}
// В этом классе должны быть реализованы все методы интерфейсов IA и IB. class MyClass : IB { public void Methl() {
Console.WriteLine("Реализовать метод Methl().");
}
public void Meth2() {
Console.WriteLine("Реализовать метод Meth2().");
}
public void Meth3() {
Console.WriteLine("Реализовать метод Meth3().");
}
}
class IFExtend {
static void Main() {
MyClass ob = new MyClass();
ob.Methl() ; ob.Meth2 (); ob.M^th3();
Ради интереса попробуйте удалить реализацию метода Methl () из класса MyClass. Это приведет к ошибке во время компиляции. Как пояснялось ранее, в любом классе, реализующем интерфейс, должны быть реализованы все методы, определенные в этом интерфейсе, в том числе и те, что наследуются из другцх интерфейсов.
Сокрытие имен при наследовании интерфейсов
Когда один интерфейс наследует другой, то в производном интерфейсе может быть объявлен член, скрывающий член с аналогичным именем в базовом интерфейсе. Такое сокрытие имен происходит в том случае, если член в производном интерфейсе объявляется таким же образом, как и в базовом интерфейсе. Но если не указать в объявлении члена производного интерфейса ключевое слово new, то компилятор выдаст соответствующее предупреждающее сообщение.
Явные реализации
При реализации члена интерфейса имеется возможность указать его имяполностьювместе с именем самого интерфейса. В этом случае получаетсяявная реализация члена интерфейса, или простоявная реализация.Так, если объявлен интерфейс IMylF
interface IMylF { int MyMeth(int x) ;
}
то следующая его реализация считается вполне допустимой:
class MyClass : IMylF { int IMylF.MyMeth(int x) { return x / 3;
}
}
Как видите, при реализации члена MyMeth () интерфейса IMylF указывается его полное имя, включающее в себя имя его интерфейса.
Для явной реализации интерфейсного метода могут быть две причины. Во-первых, когда интерфейсный метод реализуется с указанием его полного имени, то такой метод оказывается доступным не посредством объектов класса, реализующего данный интерфейс, а по интерфейсной ссылке. Следовательно, явная реализация позволяет реализовать интерфейсный метод таким образом, чтобы оннестал открытым членом класса, предоставляющего его реализацию. И во-вторых, в одном классе могут быть реализованы два интерфейса с методами, объявленными с одинаковыми именами и сигнатурами. Но неоднозначность в данном случае устраняется благодаря указанию в именах этих методов их соответствующих интерфейсов. Рассмотрим каждую из этих двух возможностей явной реализации на конкретных примерах.
В приведенном ниже примере программы демонстрируется интерфейсIEven,в котором объявляются два метода:IsEven () иIsOdd (). В первом из них определяется четность числа, а во втором—его нечетность. ИнтерфейсIEvenзатем реализуется в классеMyClass.При этом методIsOdd() реализуется явно.
// Реализовать член интерфейса явно, using System;
interface IEven { bool IsOdd(int x); bool IsEven(int x);
}
class MyClass : IEven {
// Явная реализация. Обратите внимание на то, что // этот член является закрытым по умолчанию, bool IEven.IsOdd(int x) { if((x%2) != 0) return true;
else return false;
}
// Обычная реализация, public bool IsEven(int x) {
IEven о = this; // Интерфейсная ссылка на вызывающий объект, return !о.IsOdd(х);
}
}
class Demo {
static void Main() {
MyClass ob = new MyClass(); bool result;
result = ob.IsEven (4);
if(result) Console.WriteLine("4 — четное число.");
// result = ob.IsOdd(4); // Ошибка, член IsOdd интерфейса IEven недоступен
// Но следующий код написан верно, поскольку в нем сначала создается // интерфейсная ссылка типа IEven на объект класса MyClass, а затем по // этой ссылке вызывается метод IsOdd().
IEven iRef = (IEven) ob; result = iRef.IsOdd(3);
if(result) Console.WriteLine("3 — нечетное число.");
}
}
В приведенном выше примере метод IsOdd () реализуется явно, а значит, он недоступен как открытый член класса MyClass. Напротив, он доступен только по интерфейсной ссылке. Именно поэтому он вызывается посредством переменной о ссылочного типа IEven в реализации метода IsEven ().
Ниже приведен пример программы, в которой реализуются два интерфейса, причем в обоих интерфейсах объявляется метод Meth (). Благодаря явной реализации исключается неоднозначность, характерная для подобной ситуации.
interface IMyIF_A { int Meth(int x) ;
}
interface IMyIF_B { int Meth(int x) ;
}
// Оба интерфейса реализуются в классе MyClass. class MyClass : IMyIF_A, IMyIF_B {
// Реализовать оба метода Meth() явно, int IMyIF_A.Meth(int x) { return x + x;
}
int IMyIF_B.Meth(int x) { return x * x;
}
// Вызывать метод Meth() по интерфейсной ссылке. public int MethA(int x){
IMyIF_A a_ob; a_ob = this;
return a_ob.Meth(x); // вызов интерфейсного метода IMyIF_A
}
public int MethB(int x){
IMyIF_B b_ob; b_ob = this;
return b_ob.Meth(x); // вызов интерфейсного метода IMyIF_B
}
}
class FQIFNames {
static void Main() {
MyClass ob = new MyClassO;
Console.Write("Вызов метода IMyIF_A.Meth(): ");
Console.WriteLine(ob.MethA(3));
Console.Write("Вызов метода IMyIF_B.Meth(): ");
Console.WriteLine(ob.MethB(3)) ;
}
}
Вот к какому результату приводит выполнение этой программы.
Вызов метода IMyIF_A.Meth(): 6 Вызов метода IMyIF_B.Meth(): 9
Анализируя приведенный выше пример программы, обратим прежде всего внимание на одинаковую сигнатуру метода Meth () в обоих интерфейсах, IMyIF_A и IMyIF_B. Когда оба этих интерфейса реализуются в классе MyClass, для каждого из них в отдельности это делается явно, т.е. с указанием полного имени метода Meth (). А поскольку явно реализованный метод может вызываться только по интерфейсной
ссылке, то в классеMyClassсоздаются две такие ссылки: одна — для интерфейсаIMyIF_A,а другая — для интерфейсаIMyIF_B.Именно по этим ссылкам происходит обращение к объектам данного класса с целью вызвать методы соответствующих интерфейсов, благодаря чему и устраняется неоднозначность.
Выбор между интерфейсом и абстрактным классом
Одна из самых больших трудностей программирования на C# состоит в правильном выборе между интерфейсом и абстрактным классом в тех случаях, когда требуется описать функциональные возможности, но не реализацию. В подобных случаях рекомендуется придерживаться следующего общего правила: если какое-то понятие можно описать с точки зрения функционального назначения, не уточняя конкретные детали реализации, то следует использовать интерфейс. А если требуются некоторые детали реализации, то данное понятие следует представить абстрактным классом.
Стандартные интерфейсы для среды .NET Framework
Длясреды .NET Framework определено немало стандартных интерфейсов, которыми можно пользоваться в программах на С#. Так, в интерфейсеSystem. IComparableопределен методCompareTo (), применяемый для сравнения объектов, когда требуется соблюдать отношение порядка. Стандартные интерфейсы являются также важной частью классов коллекций, предоставляющих различные средства, в том числе стеки и очереди, для хранения целых групп объектов. Так, в интерфейсеSystem. Collections . ICollectionопределяются функции для всей коллекции, а в интерфейсеSystem.Collections . IEnumerator— способ последовательного обращения к элементам коллекции. Эти и многие другие интерфейсы подробнее рассматриваются в части II данной книги.
Структуры
Как вам должно быть уже известно, классы относятся к ссылочным типам данных. Это означает, что объекты конкретного класса доступны по ссылке, в отличие от значений простых типов, доступных непосредственно. Но иногда прямой доступ к рбъектам как к значениям простых типов оказывается полезно иметь, например, ради повышения эффективности программы. Ведь каждый доступ к объектам (даже самым мелким) по ссылке связан с дополнительными издержками на расход вычислительных ресурсов и оперативной памяти. Для разрешения подобных затруднений в C# предусмотренаструктура,которая подобна классу, но относится к типу значения, а не к ссылочному типу данных.
Структуры объявляются с помощью ключевого словаstructи с точки зрения синтаксиса подобны классам. Ниже приведена общая форма объявления структуры:
structимя : интерфейсы {
IIобъявления членов
}
гдеимяобозначает конкретное имя структуры.
Одни структуры не могут наследовать другие структуры и классы или служить в качестве базовых для других структур и классов. (Разумеется, структуры, как и все остальные типы данных в С#, наследуют класс obj ect.) Тем не менее в структуре можно реализовать один или несколько интерфейсов, которые указываются после имени структуры списком через запятую. Как и у классов, у каждой структуры имеются свои члены: методы, поля, индексаторы, свойства, операторные методы и события. В структурах допускается также определять конструкторы, но не деструкторы. В то же время для структуры нельзя определить конструктор, используемый по умолчанию (т.е. конструктор без параметров). Дело в том, что конструктор, вызываемый по умолчанию, определяется для всех структур автоматически и не подлежит изменению. Такой конструктор инициализирует поля структуры значениями, задаваемыми по умолчанию. А поскольку структуры не поддерживают наследование, то их члены нельзя указывать как abstract, virtual или protected.
Объект структуры может быть создан с помощью оператора new таким же образом, как и объект класса, но в этом нет особой необходимости. Ведь когда используется оператор new, то вызывается конструктор, используемый по умолчанию. А когда этот оператор не используется, объект по-прежнему создается, хотя и не инициализируется. В этом случае инициализацию любых членов структуры придется выполнить вручную.
В приведенном ниже примере программы демонстрируется применение структуры для хранения информации о книге.
// Продемонстрировать применение структуры.
using System;
// Определить структуру, struct Book {
public string Author; public string Title; public int Copyright;
public Book(string a, string t, int c) {
Author = a;
Title = t;
Copyright = c;
}
}
// Продемонстрировать применение структуры Book, class StructDemo { static void Main() {
Book bookl = new Book("Герберт Шилдт",
"Полный справочник ho C# 4.0",
2010); // вызов явно заданного конструктора Book book2 = new Book(); // вызов конструктора по умолчанию Book ЬоокЗ; // конструктор не вызывается
Console.WriteLine(bookl.Author + ", " +
bookl.Title + ", (c) " + bookl.Copyright);
Console.WriteLine();
if (book2.Title == null)
Console.WriteLine("Член book2.Title пуст.");
// А теперь ввести информацию в структуру book2. book2.Title = "О дивный новый мир"; book2.Author = "Олдос Хаксли"; book2.Copyright = 1932;
Console.Write("Структура book2 теперь содержит:\n");
Console.WriteLine(book2.Author + ", " +
book2.Title + ", (c) " + book2.Copyright);
Console.WriteLine() ;
// Console.WriteLine(ЬоокЗ.Title); // неверно, этот член структуры
// нужно сначала инициализировать
ЬоокЗ.Title = "Красный шторм";
Console.WriteLine(ЬоокЗ.Title); // теперь верно
}
}
При выполнении этой программы получается следующий результат.
Герберт Шилдт, Полный справочник по C# 4.0, (с) 2010
Член book2.Title пуст.
Структура Ьоок2 теперь содержит:
Олдос Хаксли, О дивный новый мир, (с) 1932
Красный шторм
Как демонстрирует приведенный выше пример программы, структура может быть инициализирована с помощью оператора new для вызова конструктора или же путем простого объявления объекта. Так, если используется оператор new, то поля структуры инициализируются конструктором, вызываемым по умолчанию (в этом случае во всех полях устанавливается задаваемое по умолчанию значение), или же конструктором, определяемым пользователем. А если оператор new не используется, как это имеет место для структуры ЬоокЗ, то объект структуры не инициализируется, а его поля должны быть установлены вручную перед тем, как пользоваться данным объектом.
Когда одна структура присваивается другой, создается копия ее объекта. В этодо заключается одно из главных отличий структуры от класса. Как пояснялось ранее в этой книге, когда ссылка на один класс присваивается ссылке на другой класс, в итоге ссылка в левой части оператора присваивания указывает на тот же самый объект, что и ссылка в правой его части. А когда переменная одной структуры присваивается переменной другой структуры, создаетсякопияобъекта структуры из правой части оператора присваивания. Рассмотрим в качестве примера следующую программу.
// Скопировать структуру.
using System;
// Определить структуру, struct MyStruct { public int x;
11Продемонстрировать присваивание структуры, class StructAssignment { static void Main() {
MyStruct a;
MyStruct b;
a.x = 10;
b.x = 20;
Console.WriteLine("a.x {0}, b.x {1}", a.x, b.x);
a = b;
b.x = 30;
Console.WriteLine("a.x {0}, b.x {1}", a.x, b.x);
}
}
Вот к какому результату приводит выполнение этой программы.
а.х 10, b.x 20 а.х 20, b.x 30
Как показывает приведенный выше результат, после присваивания
а = Ь;
переменные структуры а и b по-прежнему остаются совершенно обособленными, т.е. переменная а не указывает на переменную b и никак не связана с ней, помимо того, что она содержит копию значения переменной Ь. Ситуация была бы совсем иной, если бы переменные а и b были ссылочного типа, указывая на объекты определенного класса. В качестве примера ниже приведен вариант предыдущей программы, где демонстрируется присваивание переменных ссылки на объекты определенного класса.
// Использовать ссылки на объекты определенного класса, using System;
// Создать класс, class MyClass { public int x;
}
// Показать присваивание разных объектов данного класса, class ClassAssignment { static void Main() {
MyClass a = new MyClass();
MyClass b = new MyClass();
a.x = 10;
b.x = 20;
Console.WriteLine("a.x {0}, b.x {1}", a.x, b.x);
a = b;
b.x = 30;
Console.WriteLine("а.х {0}, b.x {1}", а.х, Ь.х);
}
}
Выполнение этой программы приводит к следующему результату.
а.х 10, Ь.х 20 а.х 30, Ь.х 30
Как видите, после того как переменная b будет присвоена переменной а, обе переменные станут указывать на один и тот же объект, т.е. на тот объект, на который первоначально указывала переменная Ь.
О назначении структур
В связи с изложенным выше возникает резонный вопрос: зачем в C# включена структура, если она обладает более скромными возможностями, чем класс? Ответ на этот вопрос заключается в повышении эффективности и производительности программ. Структуры относятся к типам значений, и поэтому ими можно оперировать непосредственно, а не по ссылке. Следовательно, для работы со структурой вообще не требуется переменная ссылочного типа, а это означает в ряде случаев существенную экономию оперативной памяти. Более того, работа со структурой не приводит кухудшениюпроизводительности, столь характерному для обращения к объекту класса. Ведь доступ к структуре осуществляется непосредственно, а к объектам — по ссылке, поскольку классы относятся к данным ссылочного типа. Косвенный характер доступа к объектам подразумевает дополнительные издержки вычислительных ресурсов на каждый такой доступ, тогда как обращение к структурам не влечет за собой подобные издержки. И вообще, если нужно просто сохранить группу связанных вместе данных, не требующих наследования и обращения по ссылке, то с точки зрения производительности для них лучше выбрать структуру.
Ниже приведен еще один пример, демонстрирующий применение структуры на практике. В этом примере из области электронной коммерции имитируется запись транзакции. Каждая такая транзакция включает в себя заголовок пакета, содержащий номер и длину пакета. После заголовка следует номер счета и сумма транзакции. Заголовок пакета представляет собой самостоятельную единицу информации, и поэтому он организуется в отдельную структуру, которая затем используется для создания записи транзакции или же информационного пакета любого другого типа.
// Структуры удобны для группирования небольших объемов данных, using System;
// Определить структуру пакета, struct PacketHeader {
public uint PackNum; // номер пакета public ushort PackLen; // длина пакета}
// Использовать структуру PacketHeader для создания записи транзакции (
// в сфере электронной коммерции, class Transaction {
static uint transacNum = 0;
PacketHeader ph; // ввести структуру PacketHeader в класс Transaction string accountNum; double amount;
public Transaction(string acc, double val) {
// создать заголовок пакета
ph.PackNum = transacNum++;
ph.PackLen =512; // произвольная длина
accountNum = acc; amount = val;
}
// Сымитировать транзакцию, public void sendTransaction() {
Console.WriteLine("Пакет #: " + ph.PackNum +
", Длина: " + ph.PackLen +
",\n Счет #: " + accountNum +
", Сумма: {0:C}\n", amount);
}
}
// Продемонстрировать применение структуры в виде пакета транзакции, class PacketDemo { static void Main() {
Transaction t = new Transaction("31243", -100.12);
Transaction t2 = new Transaction("AB4655", 345.25);
Transaction t3 = new Transaction ("8475-09", 9800.00);
t.sendTransaction (); t2.sendTransaction (); t3.sendTransaction ();
}
}
Вот к какому результату может привести выполнение этого кода.
Счет #: 8475-09, Сумма: $9,800.00
СтруктураPacketHeaderоказывается вполне пригодной для формирования заголовка пакета транзакции, поскольку в ней хранится очень небольшое количество данных, не используется наследование и даже не содержатся методы. Кроме того, работа со структуройPacketHeaderне влечет за собой никаких дополнительных издержек, связанных со ссылками на объекты, что весьма характерно для класса. Следовательно, структуруPacketHeaderможно использовать для записи любой транзакции, не снижая эффективность данного процесса.
Любопытно, что в C++ также имеются структуры и используется ключевое словоstruct.Но эти структуры отличаются от тех, что имеются в С#. Так, в C++ структура относится к типу класса, а значит, структура и класс в этом языке практически равноценны и отличаются друг от друга лишь доступом по умолчанию к их членам, которые оказываются закрытыми для класса и открытыми для структуры. А в C# структура относится к типу значения, тогда как класс — к ссылочному типу.
Перечисления
Перечислениепредставляет собой множество именованных целочисленных констант. Перечислимый тип данных объявляется с помощью ключевого словаenum.Ниже приведена общая форма объявления перечисления:
enumимя{список_перечисления} ;
гдеимя— это имя типа перечисления, асписок_перечисления— список идентификаторов, разделяемый запятыми.
В приведенном ниже примере объявляется перечислениеAppleразличных сортов яблок.
enum Apple { Jonathan, GoldenDel, RedDel, Winesap,
Cortland, McIntosh };
Следует особо подчеркнуть, что каждая символически обозначаемая константа в перечислении имеет целое значение. Тем не менее неявные преобразования перечислимого типа во встроенные целочисленные типы и обратно в C# не определены, а значит, в подобных случаях требуется явное приведение типов. Кроме того, приведение типов требуется при преобразовании двух перечислимых типов. Но поскольку перечисления обозначают целые значения, то их можно, например, использовать для управления оператором выбораswitchили же оператором циклаfor.
Для каждой последующей символически обозначаемой константы в перечислении задается целое значение, которое на единицу больше, чем у предыдущей константы. По умолчанию значение первой символически обозначаемой константы в перечислении равно нулю. Следовательно, в приведенном выше примере перечисленияAppleконстантаJonathanравна нулю, константаGoldenDel— 1, константаRedDel— 2 и т.д.
Доступ к членам перечисления осуществляется по имени их типа, после которого следует оператор-точка. Например, при выполнении фрагмента кода
Console.WriteLine(Apple.RedDel + " имеет значение " +
(int)Apple.RedDel) ;
выводится следующий результат.
RedDel имеет значение 2
Как показывает результат выполнения приведенного выше фрагмента кода, для вывода перечислимого значения используется его имя. Но для получения этого значения требуется предварительно привести его к типуint.
Ниже приведен пример программы, демонстрирующий применение перечисленияApple.
11Продемонстрировать применение перечисления.
using System;
class EnumDemo {
enum Apple { Jonathan, GoldenDel, RedDel, Winesap,
Cortland, McIntosh };
static void Main() { string[] color = {
"красный",
"желтый",
"красный",
"красный",
"красный",
"красновато-зеленый"
};
Apple i;11объявить переменную перечислимого типа
// Использовать переменную i для циклического
// обращения к членам перечисления.
for(i = Apple.Jonathan; i <= Apple.McIntosh; i++)
Console.WriteLine(i + " имеет значение " + (int)i);
Console.WriteLine ();
// Использовать перечисление для индексирования массива. for(i = Apple.Jonathan; i <= Apple.McIntosh; i++)
Console.WriteLine("Цвет сорта " + i + " — " + color[ (int)i]);
}
}
Ниже приведен результат выполнения этой программы.
Jonathan имеет значение О GoldenDel имеет значение 1 RedDel имеет значение 2 Winsap имеет- значение 3 Cortland имеет значение 4 McIntosh имеет значение 5
Цвет сорта Jonathan - красный
Цвет сорта GoldenDel - желтый
Цвет сорта RedDel - красный
Цвет сорта Winsap - красный
Цвет сорта Cortland - красный
Цвет сорта McIntosh - красновато-зеленый
Обратите внимание на то, как переменная типа Apple управляёт циклами for. Значения символически обозначаемых констант в перечислении Apple начинаются с нуля, поэтому их можно использовать для индексирования массива, чтобы получить цвет каждого сорта яблок. Обратите также внимание на необходимость производить приведение типов, когда перечислимое значение используется для индексирования массива. Как упоминалось выше, в C# не предусмотрены неявные преобразования перечислимых типов в целочисленные и обратно, поэтому для этой цели требуется явное приведение типов.
И еще одно замечание: все перечисления неявно наследуют от классаSystem. Enum,который наследует от классаSystem. ValueType,а тот, в свою очередь, — от классаobject.
Инициализация перечисления
Значение одной или нескольких символически обозначаемых констант в перечислении можно задать с помощью инициализатора. Для этого достаточно указать после символического обозначения отдельной константы знак равенства и целое значение. Каждой последующей константе присваивается значение, которое на единицу больше значения предыдущей инициализированной константы. Например, в приведенном ниже фрагменте кода константеRedDelприсваивается значение 10.
enum Apple { Jonathan, GoldenDel, RedDel = 10, Winesap,
Cortland, McIntosh };
В итоге все константы в перечислении принимают приведенные ниже значения.
Jonathan
0
GoldenDel
1
RedDel
10
Winesap
11
Cortland
12
McIntosh
13
Указание базового типа перечисления
По умолчанию в качестве базового для перечислений выбирается типint,тем не менее перечисление может быть создано любого целочисленного типа, за исключениемchar.Для того чтобы указать другой тип, кромеint,достаточно поместить этот тип после имени перечисления, отделив его двоеточием. В качестве примера ниже задается типbyteдля перечисленияApple.
enum Apple : byte { Jonathan, GoldenDel, RedDel,
Winesap, Cortland, McIntosh };
Теперь константаApple . Winesap,например, имеет количественное значение типаbyte.
Применение перечислений
На первый взгляд перечисления могут показаться любопытным, но не очень нужным элементом С#, но на самом деле это не так. Перечисления очень полезны, когда в программе требуется одна или несколько специальных символически обозначаемых констант. Допустим, что требуется написать программу для управления лентой конвейера на фабрике. Для этой цели можно создать методConveyor(), принимающий в качестве параметров следующие команды: "старт", "стоп", "вперед" и "назад". Вместо того чтобы передавать методуConveyor() целые значения, например, 1 — в качестве команды "старт", 2 — в качестве команды "стоп" и так далее, что чревато ошибками, можно создать перечисление, чтобы присвоить этим значениям содержательные символические обозначения. Ниже приведен пример применения такого подхода.
// Сымитировать управление лентой конвейера, using System;
class ConveyorControl {
// Перечислить команды конвейера.
public enum Action { Start, Stop, Forward, Reverse };
public void Conveyor(Action com) { switch(com) {
case Action.Start:
Console.WriteLine("Запустить конвейер."); break; case Action.Stop:
Console.WriteLine("Остановить конвейер."); break; case Action.Forward:
* Console.WriteLine("Переместить конвейер вперед.");
break; case Action.Reverse:
Console.WriteLine ("Переместить конвейер назад."); break;
}
}
}
class ConveyorDemo { static void Main() {
ConveyorControl с = new ConveyorControl();
с.Conveyor(ConveyorControl.Action.Start);
с.Conveyor(ConveyorControl.Action.Forward);
с.Conveyor(ConveyorControl.Action.Reverse);
с.Conveyor(ConveyorControl.Action.Stop);
}
}
Вот к какому результату приводит выполнение этого кода.
Запустить конвейер.
Переместить конвейер вперед.
Переместить конвейер назад.
Остановить конвейер.
МетодConveyor() принимает аргумент типаAction,и поэтому ему могут быть переданы только значения, определяемые в перечисленииAction.Например, ниже приведена попытка передать методуConveyor() значение 22.
с.Conveyor(22); // Ошибка!
Эта строка кода не будет скомпилирована, поскольку отсутствует предварительно заданное преобразование типаintв перечислимый типAction.Именно это и препятствует передаче неправильных команд методуConveyor (). Конечно, такое
преобразование можно организовать принудительно с помощью приведения типов, но это было бы преднамеренным, а не случайным или неумышленным действием. Кроме того, вероятность неумышленной передачи пользователем неправильных команд методу Conveyor () сводится с минимуму благодаря тому, что эти команды обозначены символическими именами в перечислении.
В приведенном выше примере обращает на себя внимание еще одно интересное обстоятельство: перечислимый тип используется для управления оператором switch. Как упоминалось выше, перечисления относятся к целочисленным типам данных, и поэтому их вполне допустимо использовать в операторе switch.
ГЛАВА 13 Обработка исключительных ситуаций
Исключительная ситуация, или просто исключение, происходит во время выполнения. Используя подсистему обработки исключительных ситуаций в С#, можно обрабатывать структурированным и контролируемым образом ошибки, возникающие при выполнении программы. Главное преимущество обработки исключительных ситуаций заключается в том, что она позволяет автоматизировать получение большей части кода, который раньше приходилось вводить в любую крупную программу вручную для обработки ошибок. Так, если программа написана на языке программирования без обработки исключительных ситуаций, то при неудачном выполнении методов приходится возвращать коды ошибок, которые необходимо проверять вручную при каждом вызове метода.
Это не только трудоемкий, но и чреватый ошибками процесс. Обработка исключительных ситуаций рационализирует весь процесс обработки ошибок, позволяя определить в программе блок кода, называемыйобработчиком исключенийи выполняющийся автоматически, когда возникает ошибка. Это избавляет от необходимости проверять вручную, насколько удачно или неудачно завершилась конкретная операция либо вызов метода. Если возникнет ошибка, она будет обработана соответствующим образом обработчиком ошибок.
Обработка исключительных ситуаций важна еще и потому, что в C# определены стандартные исключения для типичных программных ошибок, например деление на нуль или выход индекса за границы массива.Дляреагирования на подобные ошибки в программе должно быть организовано отслеживание и обработка соответствующих
исключительных ситуаций. Ведь в конечном счете для успешного программирования на C# необходимо научиться умело пользоваться подсистемой обработки исключительных ситуаций.
Класс System. Exception
В C# исключения представлены в виде классов. Все классы исключений должны быть производными от встроенного в C# классаException,являющегося частью пространства именSystem.Следовательно, все исключения являются подклассами классаException.
К числу самых важных подклассовExceptionотносится классSystemException.Именно от этого класса являются производными все исключения, генерируемые исполняющей системой C# (т.е. системой CLR). КлассSystemExceptionничего не добавляет к классуException,а просто определяет вершину иерархии стандартных исключений.
В среде .NET Framework определено несколько встроенных исключений, являющихся производными от классаSystemException.Например, при попытке выполнить деление на нуль генерируется исключениеDivideByZeroException.Как будет показано далее в этой главе, в C# можно создавать собственные классы исключений, производные от классаException.
Основы обработки исключительных ситуаций
Обработка исключительных ситуаций в C# организуется с помощью четырех ключевых слов:try, catch, throwиfinally.Они образуют взаимосвязанную подсистему, в которой применение одного из ключевых слов подразумевает применение другого. На протяжении всей этой главы назначение и применение каждого из упомянутых выше ключевых слов будет рассмотрено во всех подробностях. Но прежде необходимо дать общее представление о роли каждого из них в обработке исключительных ситуаций. Поэтому ниже кратко описан принцип их действия.
Операторы программы, которые требуется контролировать на появление исключений, заключаются в блокtry.Если внутри блокаtryвозникает исключительная ситуация,генерируетсяисключение. Это исключение может быть перехвачено и обработано каким-нибудь рациональным способом в коде программы с помощью оператора, обозначаемого ключевым словомcatch.Исключения, возникающие на уровне системы, генерируются исполняющей системой автоматически. А для генерирования исключений вручную служит ключевое словоthrow.Любой код, который должен быть непременно выполнен после выхода из блокаtry,помещается в блокfinally.
Применение пары ключевых слов try и catch
Основу обработки исключительных ситуаций в C# составляет пара ключевых словtryиcatch.Эти ключевые слова действуют совместно и не могут быть использованы порознь. Ниже приведена общая форма определения блоковtry/catchдля обработки исключительных ситуаций:
try {
// Блок кода, проверяемый на наличие ошибок.
}catch(ExcepTypel exOb) {
// Обработчик исключения типаExcepTypel.}
catch (ЕхсерТуре2 exOb){
// Обработчик исключения типаЕхсерТуре2.}
гдеЕхсерТуре— это тип возникающей исключительной ситуации. Когда исключение генерируется операторомtry,оно перехватывается составляющим ему пару операторомcatch,который затем обрабатывает это исключение. В зависимости от типа исключения выполняется и соответствующий операторcatch.Так, если типы генерируемого исключения и того, что указывается в оператореcatch,совпадают, то выполняется именно этот оператор, а все остальные пропускаются. Когда исключение перехватывается, переменная исключенияexObполучает свое значение.
На самом деле указывать переменнуюexObнеобязательно. Так, ее необязательно указывать, если обработчику исключений не требуется доступ к объекту исключения, что бывает довольно часто. Для обработки исключения достаточно и его типа. Именно поэтому во многих примерах программ, приведенных в этой главе, переменнаяexObопускается.
Следует, однако, иметь в виду, что если исключение не генерируется, то блок оператораtryзавершается как обычно, и все его операторыcatchпропускаются. Выполнение программы возобновляется с первого оператора, следующего после завершающего оператораcatch.Таким образом, операторcatchвыполняется лишь в том случае, если генерируется исключение.
Простой пример обработки исключительной ситуации
Рассмотрим простой пример, демонстрирующий отслеживание и перехватывание исключения. Как вам должно быть уже известно, попытка индексировать массив за его границами приводит к ошибке. Когда возникает подобная ошибка, система CLR генерирует исключениеIndexOutOfRangeException,которое определено как стандартное для среды .NET Framework. В приведенной ниже программе такое исключение генерируется намеренно и затем перехватывается.
// Продемонстрировать обработку исключительной ситуации.
using System;
class ExcDemol {
static void Main() {
int[] nums = new int [4];
try {
Console.WriteLine("До генерирования исключения.");
// Сгенерировать исключение в связи с выходом индекса за границы массива.
for(int i=0; i < 10; i++) {
nums[i] = i;
Console.WriteLine("nums[{0}]: {1}", i, nums[i]);
}
Console.WriteLine("He подлежит выводу");
}
catch (IndexOutOfRangeException) {
// Перехватить исключение.
Console.WriteLine("Индекс вышел за границы массива!");
}
Console.WriteLine("После блока перехвата исключения.");
}
}
При выполнении этой программы получается следующий результат.
До генерирования исключения.
nums[0]: О
nums[1]: 1
nums[2]: 2
nums[3]: 3
Индекс вышел за границы массива!
После блока перехвата исключения.
В данном примере массивnumsтипаintсостоит из четырех элементов. Но в циклеforпредпринимается попытка проиндексировать этот массив от 0 до 9, что и приводит к появлению исключенияIndexOutOfRangeException,когда происходит обращение к элементу массива по индексу 4.
Несмотря на всю свою краткость, приведенный выше пример наглядно демонстрирует ряд основных моментов процесса обработки исключительных ситуаций. Во-первых, код, который требуется контролировать на наличие ошибок, содержится в блокеtry.Во-вторых, когда возникает исключительная ситуация (в данном случае — при попытке проиндексировать массивnumsза его границами в циклеfor),в блокеtryгенерируется исключение, которое затем перехватывается в блокеcatch.В этот момент выполнение кода в блокеtryзавершается и управление передается блокуcatch.Это означает, что операторcatchневызывается специально, а выполнение кода переходит к нему автоматически. Следовательно, оператор, содержащий методWriteLine() и следующий непосредственно за цикломfor,где происходит выход индекса за границы массива, вообще не выполняется. А в задачу обработчика исключений входит исправление ошибки, приведшей к исключительной ситуации, чтобы продолжить выполнение программы в нормальном режиме.
Обратите внимание на то, что в оператореcatchуказан только тип исключения (в данном случае —IndexOutOfRangeException),а переменная исключения отсутствует. Как упоминалось ранее, переменную исключения требуется указывать лишь в том случае, если требуется доступ к объекту исключения. В ряде случаев значение объекта исключения может быть использовано обработчиком исключений для получения дополнительной информации о самой ошибке, но зачастую для обработки исключительной ситуации достаточно просто знать, что она произошла. Поэтому переменная исключения нередко отсутствует в обработчиках исключений, как в рассматриваемом здесь примере.
Как пояснялось ранее, если исключение не генерируется в блокеtry,то блокcatchне выполняется, а управление программой передается оператору, следующему после блокаcatch.Для того чтобы убедиться в этом, замените в предыдущем примере программы строку кода
for(int i=0; i < 10; i++) {
на строку
for(int i=0; i < nums.Length; i++) {
Теперь индексирование массива не выходит за его границы в циклеfor.Следовательно, никакого исключения не генерируется и блокcatchне выполняется.
Второй пример обработки исключительной ситуации
Следует особо подчеркнуть, что весь код, выполняемый в блокеtry,контролируется на предмет исключительных ситуаций, в том числе и тех, которые могут возникнуть в результате вызойа метода из самого блокаtry.Исключение, генерируемое методом в блокеtry,может быть перехвачено в том же блоке, если, конечно, этого не будет сделано в самом методе.
В качестве еще одного примера рассмотрим следующую программу, где блокtryпомещается в методеMain (). Из этого блока вызывается методGenException (), в котором и генерируется исключениеIndexOutOfRangeException.Это исключение не перехватывается методомGenException(). Но поскольку методGenException() вызывается из блокаtryв методеMain (), то исключение перехватывается в блокеcatch,связанном непосредственно с этим блокомtry.
/* Исключение может быть сгенерировано одним методом и перехвачено другим. */
using System;
class ExcTest {
// Сгенерировать исключение, public static void GenException() {
int[] nums = new int [4];
Console.WriteLine("До генерирования исключения.");
// Сгенерировать исключение в связи с выходом индекса за границы массива.
for(int i=0; i < 10; i++) {
nums[i] = i;
Console.WriteLine("nums[{0}]: {1}", i, nums[i]);
}
Console.WriteLine("He подлежит выводу");
}
}
class ExcDemo2 {
static void Main() {
try {
ExcTest.GenException() ;
}
catch (IndexOutOfRangeException) {
// Перехватить исключение. 9
Console.WriteLine("Индекс вышел за границы массива!");
}
Console.WriteLine("После блока перехвата исключения.");
}
}
Выполнение этой программы дает такой же результат, как и в предыдущем примере.
До генерирования исключения.
nums[0]: О
nums[1]: 1
nums[2]: 2
nums[3]: 3
Индекс вышел за границы массива!
После блока перехвата исключения.
Как пояснялось выше, методGenException() вызывается из блокаtry,и поэтому генерируемое им исключение перехватывается не в нем, а в блокеcatchвнутри методаMain(). А если бы исключение перехватывалось в методеGenException(), оно не было бы вообще передано обратно методуMain ().
Последствия неперехвата исключений
Перехват одного из стандартных исключений, как в приведенных выше примерах, дает еще одно преимущество: он исключает аварийное завершение программы. Как только исключение будет сгенерировано, оно должно быть перехвачено каким-то фрагментом кода в определенном месте программы. Вообще говоря, если исключение не перехватывается в программе, то оно будет перехвачено исполняющей системой. Но дело в том, что исполняющая система выдаст сообщение об ошибке и прервет выполнение программы. Так, в приведенном ниже примере программы исключение в связи с выходом индекса за границы массива не перехватывается.
// Предоставить исполняющей системе C# возможность самой обрабатывать ошибки.
using System;
class NotHandled { static void Main() {
int[] nums = new int[4];
Console.WriteLine("До генерирования исключения.");
// Сгенерировать исключение в связи с выходом индекса за границы массива, for (int i=0; i < 10; i++) {
nums[i] = i;
Console.WriteLine("nums[{0}] : {1}", i, nums[i]);
Когда возникает ошибка индексирования массива, выполнение программы прерывается и выдается следующее сообщение об ошибке.
Необработанное исключение: System.IndexOutOfRangeException:
Индекс находился вне границ массива, в NotHandled.Main() в<имя_файла>:строка 16
Это сообщение уведомляет об обнаружении в методеNotHandled. Main() необработанного исключения типаSystem. IndexOutOfRangeException,которое связано с выходом индекса за границы массива.
Такие сообщения об ошибках полезны для отладки программы, но, по меньше мере, нежелательны при ее использовании на практике! Именно поэтому так важно организовать обработку исключительных ситуаций в самой программе.
Как упоминалось ранее, тип генерируемого исключения должен соответствовать типу, указанному в оператореcatch.В противном случае исключение не будет перехвачено. Например, в приведенной ниже программе предпринимается попытка перехватить ошибку нарушения границ массива в блокеcatch,реагирующем на исключениеDivideByZeroException,связанное с делением на нуль и являющееся еще одним стандартным исключением. Когда индексирование массива выходит за его границы, генерируется исключениеIndexOutOfRangeException,но оно не будет перехвачено блокомcatch,что приведет к аварийному завершению программы.
// Не сработает!
using System;
class ExcTypeMismatch { static void Main() {
int[] nums = new int [4];
try {
Console.WriteLine("До генерирования исключения.");
// Сгенерировать исключение в связи с выходом индекса за границы массива, for(int i=0; i < 10; i++) {
nums[i] = i;
Console.WriteLine("nums[{0}]: {1}", i, nums[i]);
}
Console.WriteLine("He подлежит выводу");
}
/* Если перехват рассчитан на исключение DivideByZeroException, то перехватить ошибку нарушения границ массива не удастся. */ catch (DivideByZeroException) {
// Перехватить исключение.
Console.WriteLine("Индекс вышел за границы массива!");
}
Console.WriteLine("После блока перехвата исключения.");
}
}
Вот к какому результату приводит выполнение этой программы.
До генерирования исключения.
nums[0]: О
nums[1]: 1
nums[2]: 2
nums[3]: 3
Необработанное исключение: System.IndexOutOfRangeException:
Индекс находился вне границ массива в ExcTypeMismatch.Main() в<имя_файла>:строка 18
Как следует из приведенного выше результата, в блокеcatch,реагирующем на исключениеDivideByZeroException,не удалось перехватить исключениеIndexOutOfRangeException.
Обработка исключительных ситуаций - “изящный” способ устранения программных ошибок
Одно из главных преимуществ обработки исключительных ситуаций заключается в том, что она позволяет вовремя отреагировать на ошибку в программе и затем продолжить ее выполнение. В качестве примера рассмотрим еще одну программу, в которой элементы одного массива делятся на элементы другого. Если при этом происходит деление на нуль, то генерируется исключениеDivideByZeroException.Обработка подобной исключительной ситуации заключается в том, что программа уведомляет об ошибке и затем продолжает свое выполнение. Таким образом, попытка деления на нуль не приведет к аварийному завершению программы из-за ошибки при ее выполнении. Вместо этого ошибка обрабатывается "изящно", не прерывая выполнение программы.
// Изящно обработать исключительную ситуацию и продолжить выполнение программы.
using System;
class ExcDemo3 {
static void Main() {
int[] numer = { 4, 8, 16, 32, 64, 128 };
int[] denom = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i < numer.Length; i++) {
try {
Console.WriteLine(numer[i] + " / M +
denom[i] + м равно M +
numer[i]/denom[i]);
}
catch (DivideByZeroException) {
// Перехватить исключение.
Console.WriteLine("Делить на нуль нельзя!");
}
}
}
}
Ниже приведен результат выполнения этой программы.
4/2 равно 2
Делить на нуль нельзя!
16/4 равно 4 32/4 равно 8 Делить на нуль-нельзя!
128 / 8 равно 16
Из данного примера следует еще один важный вывод: как только исключение обработано, оно удаляется из системы. Поэтому в приведенной выше программе проверка ошибок в блоке try начинается снова на каждом шаге цикла for, при условии, что все предыдущие исключительные ситуации были обработаны. Это позволяет обрабатывать в программе повторяющиеся ошибки.
Применение нескольких операторов catch
С одним оператором try можно связать несколько операторов catch. И на практике это делается довольно часто. Но все операторы catch должны перехватывать исключения разного типа. В качестве примера ниже приведена программа, в которой перехватываются ошибки выхода за границы массива и деления на нуль.
// Использовать несколько операторов catch.
using System;
class ExcDemo4 {
static void Main() {
// Здесь массив numer длиннее массива denom.
int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
int[] denom = { 2, 0, 4, 4, 0, 8 };
for(int i=0; i < numer.Length; i++) {
try {
Console.WriteLine(numer[i] + " / " +
denom[i] + " равно " + numer[i]/denom[i ]) ;
}
catch (DivideByZeroException) {
Console.WriteLine("Делить на нуль нельзя!");
}
catch (IndexOutOfRangeException) {
Console.WriteLine("Подходящий элемент не найден.");
}
}
}
}
Вот к какому результату приводит выполнение этой программы.
4/2 равно 2
Делить на нуль нельзя!
16/4 равно 4 32/4 равно 8 Делить на нуль нельзя!
128 / 8 равно 16 Подходящий элемент не найден.
Подходящий элемент не найден.
Как следует из приведенного выше результата, каждый операторcatchреагирует только на свой тип исключения.
Вообще говоря, операторы catch выполняются по порядку их следования в программе. Но при этом выполняется только один блок catch, в котором тип исключения совпадает с типом генерируемого исключения. А все остальные блоки catch пропускаются.
Перехват всех исключений
Время от времени возникает потребность в перехвате всех исключений независимо от их типа. Для этой цели служит операторcatch,в котором тип и переменная исключения не указываются. Ниже приведена общая форма такого оператора.
catch {
// обработка исключений
}
С помощью такой формы создается "универсальный" обработчик всех исключений, перехватываемых в программе.
Ниже приведен пример такого "универсального" обработчика исключений. Обратите внимание на то, что он перехватывает и обрабатывает оба исключения,IndexOutOfRangeExceptionиDivideByZeroException,генерируемых.в программе.
// Использовать "универсальный" обработчик исключений.
using System;
class ExcDemo5 {
static void Main() {
// Здесь массив numer длиннее массива denom.
int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
int[] denom = { 2, 0, 4, 4, 0, 8 };
for (int i=0; i < numer.Length; i++) {
try {
Console.WriteLine(numer[i] + " / " +
denom[i] + " равно " + numer[i]/denom[i]);
}
catch { // "Универсальный" перехват.
Console.WriteLine ("Возникла некоторая исключительная ситуация.");
}
}
}
}
При выполнении этой программы получается следующий результат.
4/2 равно 2
Возникла некоторая исключительная ситуация.
16/4 равно 4 32/4 равно 8
Возникла некоторая исключительная ситуация.
128 / 8 равно 16
Возникла некоторая исключительная ситуация.
Возникла'некоторая исключительная ситуация.
Применяя "универсальный" перехват, следует иметь в виду, что его блок должен располагаться последним по порядку среди всех'блоков catch.
ПРИМЕЧАНИЕ
В подавляющем большинстве случаев “универсальный” обработчик исключений не применяется. Как правило, исключения, которые могут быть сгенерированы в коде, обрабатываются по отдельности. Неправильное использование “универсального" обработчика может привести к тому, что ошибки, перехватывавшиеся при тестировании программы, маскируются. Кроме того, организовать надлежащую обработку всех исключительных ситуаций в одном обработчике не так-то просто. Иными словами, “универсальный" обработчик исключений может оказаться пригодным лишь в особых случаях, например в инструментальном средстве анализа кода во время выполнения.
Вложение блоков try
Один блокtryможет быть вложен в другой. Исключение, генерируемое во внутреннем блокеtryи не перехваченное в соответствующем блокеcatch,передается во внешний блокtry.В качестве примера ниже приведена программа, в которой исключениеIndexOutOfRangeExceptionперехватывается не во внутреннем, а во внешнем блокеtry.
// Использовать вложенный блок try.
using System;
class NestTrys {
static void Main() {
// Здесь массив numer длиннее массива denom.
int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
int[] denom = { 2, 0, 4, 4, 0, 8 };
try { // внешний блок try
for(int i=0; i < numer.Length; i++) {
try { // вложенный блок try
Console.WriteLine(numer[i] + " / " +
denom[i] + " равно " + numer[i]/denom[i]);
}
catch (DivideByZeroException) {
Console.WriteLine("Делить на нуль нельзя!");
catch (IndexOutOfRangeException) {
Console.WriteLine("Подходящий элемент не найден.");
Console.WriteLine("Неисправимая ошибка - программа прервана.");
}
}
}
Выполнение этой программы приводит к следующему результату.
4/2 равно 2 Делить на нуль нельзя!
16/4 равно 4 32/4 равно 8 Делить на нуль нельзя!
128 / 8 равно 16 Подходящий элемент не найден.
Неисправимая ошибка - программа прервана.
В данном примере исключение, обрабатываемое во внутреннем блоке try и связанное с ошибкой из-за деления на нуль, не мешает дальнейшему выполнению программы. Но ошибка нарушения границ массива, обнаруживаемая во внешнем блоке try, приводит к прерыванию программы.
Безусловно, приведенный выше пример демонстрирует далеко не единственное основание для применения вложенных блоков try, тем не менее из него можно сделать важный общий вывод. Вложенные блоки try нередко применяются для обработки различных категорий ошибок разными способами. В частности, одни ошибки считаются неисправимыми и не подлежат исправлению, а другие ошибки незначительны и могут быть обработаны немедленно. Как правило, внешний блок try служит для обнаруже-ния.и обработки самых серьезных ошибок, а во внутренних блоках try обрабатываются менее серьезные ошибки. Кроме того, внешний блок try может стать "универсальным" для тех ошибок, которые не подлежат обработке во внутреннем блоке.
Генерирование исключений вручную
В приведенных выше примерах перехватывались исключения, генерировавшиеся исполняющей системой автоматически. Но исключение может быть сгенерировано и вручную с помощью оператораthrow.Ниже приведена общая форма такого генерирования:
throwexceptOb;
где в качествеexceptObдолжен быть обозначен объект класса исключений, производного от классаException.
Ниже приведен пример программы, в которой демонстрируется применение оператораthrowдля генерирования исключенияDivideByZeroException.
// Сгенерировать исключение вручную.
using System;
class ThrowDemo {
static void Main() { try {
Console.WriteLine("До генерирования исключения."); throw new DivideByZeroException();
}
catch (DivideByZeroException) {
Console.WriteLine("Исключение перехвачено.");
}
Console.WriteLine("После пары операторов try/catch.");
}
}
Вот к какому результату приводит выполнение этой программы.
До генерирования исключения.
Исключение перехвачено.
После пары операторов try/catch.
Обратите внимание на то, что исключениеDivideByZeroExceptionбыло сгенерировано с использованием ключевого словаnewв оператореthrow.Не следует забывать, что в данномслучаегенерируется конкретный объект, а следовательно, он должен быть создан перед генерированием исключения. Это означает, что сгенерировать исключение только по его типу нельзя. В данном примере для создания объектаDivideByZeroExceptionбыл автоматически вызван конструктор, используемый по умолчанию, хотя для генерирования исключений доступны идругиеконструкторы.
Повторное генерирование исключений
Исключение, перехваченное в одном блокеcatch,может быть повторно сгенерировано в другом блоке, чтобы быть перехваченным во внешнем блокеcatch.Наиболее вероятной причиной для повторного генерирования исключения служит предоставление доступа к исключению нескольким обработчикам. Допустим, что один обработчик оперирует каким-нибудь одним аспектом исключения, а другой обработчик — другим его аспектом. Для повторного генерирования исключения достаточно указать операторthrowбез сопутствующего выражения, как в приведенной ниже форме.
throw ;
Не следует, однако, забывать, что когда исключение генерируется повторно, то оно не перехватывается снова тем же самым блокомcatch,а передается во внешний блокcatch.
В приведенном ниже примере программы демонстрируется повторное генерирование исключения. В данном случае генерируется исключение
IndexOutOfRangeException.
// Сгенерировать исключение повторно.
using System;
class Rethrow {
public static void GenException() {
// Здесь массив numer длиннее массива denom.
int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 };
int[] denom = { 2, 0, 4, 4, 0, 8 };
try {
Console.WriteLine(numer[i] + " / " +
denom[i] + " равно " + numer[i]/denom[i]);
}
catch (DivideByZeroException) {
Console.WriteLine("Делить на нуль нельзя!");
}
catch (IndexOutOfRangeException) {
Console.WriteLine("Подходящий элемент не найден."); throw; // сгенерировать исключение повторно
}
}
}
}
class RethrowDemo { static void Main() { try {
Rethrow.GenException ();
}
catch(IndexOutOfRangeException) {
// перехватить исключение повторно
Console.WriteLine("Неисправимая ошибка - программа прервана.");
}
}
}
В этом примере программы ошибки из-за деления на нуль обрабатываются локально в методеGenException (), но ошибка выхода за границы массива генерируется повторно. В данном случае исключениеIndexOutOfRangeExceptionобрабатывается в методеMain ().
Использование блока finally
Иногда требуется определить кодовый блок, который будет выполняться после выхода из блокаtry/catch.В частности, исключительная ситуация может возникнуть в связи с ошибкой, приводящей к преждевременному возврату из текущего метода. Но в этом методе мог быть открыт файл, который нужно закрыть, или же установлено сетевое соединение, требующее разрывания. Подобные ситуации нередки в программировании, и поэтому для их разрешения в C# предусмотрен удобный способ: воспользоваться блокомfinally.
Для того чтобы указать кодовый блок, который должен выполняться после блокаtry/catch,достаточно вставить блокfinallyв конце последовательности операторовtry/catch.Ниже приведена общая форма совместного использования блоковtry/ catchиfinally.
try {
// Блок кода, предназначенный для обработки ошибок.
}
catch(ExcepTypel exOb){
// Обработчик исключения типаExcepTypel.
}
catch (ЕхсерТуре2 ехОЬ) {
// Обработчик исключения типаЕхсерТуре2.}
finally {
// Код завершения обработки исключений.
}
Блок finally будет выполняться всякий раз, когда происходит выход из блока try/ catch, независимо от причин, которые к этому привели. Это означает, что если блок try завершается нормально или по причине исключения, то последним выполняется код, определяемый в блоке finally. Блок finally выполняется и в том случае, если любой код в блоке try или в связанных с ним блоках catch приводит к возврату из метода.
Ниже приведен пример применения блока finally.
// Использовать блок finally.
using System;
class UseFinally {
public static void GenException(int what) { int t;
int[] nums = new int [2];
Console.WriteLine("Получить " + what); try {
switch(what) { case 0:
t = 10 / what; // сгенерировать ошибку из-за деления на нуль break; case 1:
nums[4] =4; // сгенерировать ошибку индексирования массива break; case 2:
return; // возврат из блока try
}
}
catch (DivideByZeroException) {
Console.WriteLine("Делить на нуль нельзя!"); return; // возврат из блока catch
}
catch (IndexOutOfRangeException) {
Console.WriteLine("Совпадающий элемент не найден.");
}
finally {
Console.WriteLine("После выхода из блока try.");
class FinallyDemo { static void Main() {
for(int i=0; i < 3; i++) {
UseFinally.GenException(i);
Console.WriteLine() ;
}
}
}
Вот к какому результату приводит выполнение этой программы.
Получить О
Делить на нуль нельзя После выхода из блока try.
Получить 1
Совпадающий элемент не найден.
После выхода из блока try.
Получить 2
После выхода из блока try.
Как следует из приведенного выше результата, блокfinallyвыполняется независимо от причины выхода из блокаtry.
И еще одно замечание: с точки зрения синтаксиса блокfinallyследует после блокаtry,и формально блокиcatchдля этого не требуются. Следовательно, блокfinallyможно ввести непосредственно после блокаtry,опустив блокиcatch.В этом случае блокfinallyначнет выполняться сразу же после выхода из блокаtry,но исключения обрабатываться не будут.
Подробное рассмотрение класса Exception
В приведенных выше примерах исключения только перехватывались, но никакой существенной обработке они не подвергались. Как пояснялось выше, в оператореcatchдопускается указывать типипеременную исключения. Переменная получает ссылку на объект исключения. Во всех исключениях поддерживаются члены, определенные в классеException,поскольку все исключения являются производными от этого класса. В этом разделе будет рассмотрен ряд наиболее полезных членов и конструкторов классаExceptionи приведены конкретные примеры использования переменной исключения.
В классеExceptionопределяется ряд свойств. К числу самых интересных относятся три свойства:Message, StackTraceиTargetsite.Все эти свойства доступны только для чтения. СвойствоMessageсодержит символьную строку, описывающую характер ошибки; свойствоStackTrace— строку с вызовами стека, приведшими к исключительной ситуации, а свойствоТа г get Siteполучает объект, обозначающий метод, сгенерировавший исключение.
Кроме того, в классеExceptionопределяется ряд методов. Чаще всего приходится пользоваться методомToString (), возвращающим символьную строку с описанием исключения. Этот метод автоматически вызывается, например, при отображении исключения с помощью методаWriteLine ().
Применение всех трех упомянутых выше свойств и метода из класса Exception демонстрируется в приведенном ниже примере программы.
// Использовать члены класса Exception.
using System;
class ExcTest {
public static void GenException() {
int[] nums = new int [4];
Console.WriteLine("До генерирования исключения.");
// Сгенерировать исключение в связи с выходом за границы массива, for(int i=0; i < 10; i++) {
nums[i] = i;
Console.WriteLine("nums[{0}]: {1}", i, nums[i]);
}
Console.WriteLine("He подлежит выводу");
}
}
class UseExcept {
static void Main() { try {
ExcTest.GenException();
}
catch (IndexOutOfRangeException exc) {
Console.WriteLine("Стандартное сообщение таково: ");
Console.WriteLine(exc); // вызвать метод ToStringO Console.WriteLine("Свойство StackTrace: " + exc.StackTrace);
Console.WriteLine("Свойство Message: " + exc.Message);
Console.WriteLine("Свойство TargetSite: " + exc.TargetSite);
}
Console.WriteLine("После блока перехвата исключения.");
}
}
При выполнении этой программы получается следующий результат.
До генерирования исключения.
nums[0]: 0
nums[1]: 1 v
nums[2]: 2
nums[3]: 3
Стандартное сообщение таково: System.IndexOutOfRangeException: Индекс находился
вне границ массива.
в ExcTest.genException() в<имя_файла>:строка 15 в UseExcept.Main()в<имя_файла>:строка 2 9 Свойство StackTrace: в ExcTest.genException()в<имя_файла>:строка 15
в UseExcept.Main()в<имя_файла>:строка 2 9 Свойство Message: Индекс находился вне границ массива.
Свойство TargetSite: Void genException ()
После блока перехвата исключения.
public Exception ()
public Exception(stringсообщение)
public Exception(stringсообщение,Exceptionвнутреннее_исключение)protected Exception(System.Runtime.Serialization.Serializationlnfoинформация,System.Runtime.Serialization.StreamingContextконтекст)
Первый конструктор используется по умолчанию. Во втором конструкторе указывается строкасообщение,связанная со свойствомMessage,которое имеет отношение к генерируемому исключению. В третьем конструкторе указывается так называемоевнутреннее исключение.Этот конструктор используется в том случае, когда одно исключение порождает другое, причемвнутреннее_исключениеобозначает первое исключение, которое будет пустым, если внутреннее исключение отсутствует. (Если внутреннее исключение присутствует, то оно может быть получено из свойстваInnerException,определяемого в классеException.)И последний конструктор обрабатывает исключения, происходящие дистанционно, и поэтому требует десериализации.
Следует также заметить, что в четвертом конструкторе классаExceptionтипыSerializationlnfoиStreamingContextотносятся к пространству именSystem. Runtime.Serialization.
Наиболее часто используемые исключения
В пространстве именSystemопределено несколько стандартных, встроенных исключений. Все эти исключения являются производными от классаSystemException,поскольку они генерируются системой CLR при появлении ошибки во время выполнения. В табл. 13.1 перечислены некоторые наиболее часто используемые стандартные исключения.
Таблица 13.1. Наиболее часто используемые исключения, определенные в пространстве имен System
Исключение
Значение
ArrayTypeMismatchException
Тип сохраняемого значения несовместим с типом массива
DivideByZeroException
Попытка деления на нуль
IndexOutOfRangeException
Индекс оказался за границами массива
InvalidCastException
Неверно выполнено динамическое приведение типов
OutOfMemoryException
Недостаточно свободной памяти для дальнейшего выполнения программы. Это исключение может быть, например, сгенерировано, если для создания объекта с помощью оператора new не хватает памяти
OverflowException
Произошло арифметическое переполнение
NullReferenceException
Попытка использовать пустую ссылку, т.е. ссылку, которая не указывает ни на один из объектов
Большинствоисключений, приведенных в табл. 13.1, не требует особых пояснений, кроме исключенияNullReferenceException.Это исключение генерируется при попытке использовать пустую ссылку на несуществующий объект, например, при вызове метода по пустой ссылке.Пустойназывается такая ссылка, которая не указывает ни на один из объектов. Для того чтобы создать такую ссылку, достаточно, например, присвоить явным образом пустое значение переменной ссылочного типа, используя ключевое словоnull.Пустые ссылки могут также появляться и другими, менее очевидными путями. Ниже приведен пример программы, демонстрирующий обработку исключенияNullReferenceException.
// Продемонстрировать обработку исключения NullReferenceException.
using System;
class X { int x;
public X(int a) { x = a;
}
public int Add(X o) { return x + o.x;
}
}
// Продемонстрировать генерирование и обработку // исключения NullReferenceException. class NREDemo {
static void Main() {
X p = new X(10);
X q = null; // присвоить явным образом пустое значение переменной q int val;
try {
val = p.Add(q); // эта операция приведет к исключительной ситуации } catch (NullReferenceException) {
Console.WriteLine("Исключение NullReferenceException!");
Console.WriteLine("Исправление ошибки...\n");
// А теперь исправить ошибку, q = new X(9); val = p.Add(q);
}
Console.WriteLine("Значение val равно {0}", val);
}
}
Вот к какому результату приводит выполнение этой программы.
Исключение NullReferenceException!
Исправление ошибки...
Значение val равно 19
В приведенном выше примере программы создается класс X, в котором определяются членхи методAdd (), складывающий значение членахв вызывающем объекте со значением члена х в объекте, передаваемом этому методу в качестве параметра. Оба объекта класса X создаются в методеMain (). Первый из них (переменнаяр)инициализируется, а второй (переменнаяq)— нет. Вместо этого переменнойqприсваивается пустое значение. Затем вызывается методр. Add() с переменнойqв качестве аргумента. Но поскольку переменнаяqне ссылается ни на один из объектов, то при попытке получить значение членаq. хгенерируется исключениеNullReferenceException.
Получение производных классов исключений
Несмотря на то что встроенные исключения охватывают наиболее распространенные программные ошибки, обработка исключительных ситуаций в C# не ограничивается только этими ошибками. В действительности одна из сильных сторон принятого в C# подхода к обработке исключительных ситуаций состоит в том, что в этом языке допускается использовать исключения, определяемые пользователем, т.е. тем, кто программирует на С#. В частности, такие специальные исключения можно использовать для обработки ошибок в собственном коде, а создаются они очень просто. Для этого достаточно определить класс, производный от классаException.В таких классах совсем не обязательно что-то реализовывать — одного только их существования в системе типов уже достаточно, чтобы использовать их в качестве исключений.
ПРИМЕЧАНИЕ
В прошлом специальные исключения создавались как производные от класса Application.Exception, поскольку эта иерархия классов была первоначально зарезервирована для исключений прикладного характера. Но теперь корпорация Microsoft не рекомендует этого делать, а вместо этого получать исключения, производные от класса Exception. Именно по этой причине данный подход и рассматривается в настоящей книге.
Создаваемые пользователем классы будут автоматически получать свойства и методы, определенные в классеExceptionи доступные для них. Разумеется, любой из этих членов классаExceptionможно переопределить в создаваемых классах исключений.
Когда создается собственный класс исключений, то, как правило, желательно, чтобы в нем поддерживались все конструкторы, определенные в классеException.В простых специальных классах исключений этого нетрудно добиться, поскольку для этого достаточно передать подходящие аргументы соответствующему конструктору классаException,используя ключевое словоbase.Но формально нужно предоставить только те конструкторы, которые фактически используются в программе.
Рассмотрим пример программы, в которой используется исключение специального типа. Напомним, что в конце главы 10 был разработан классRangeArray,поддерживающий одномерные массивы, в которых начальный и конечный индексы определяются пользователем. Так, например, вполне допустимым считается массив, индексируемый в пределах от -5 до 27. Если же индекс выходил за границы массива, то для обработки этой ошибки в классеRangeArrayбыла определена специальная переменная. Такая переменная устанавливалась и проверялась после каждой операции обращения к массиву в коде, использовавшем классRangeArray.Безусловно, такой подход к обработке ошибок "неуклюж" и чреват дополнительными ошибками. В приведенном ниже улучшенном варианте классаRangeArrayобработка ошибок нарушения границ
массива выполняется более изящным и надежным способом с помощью специально генерируемого исключения.
// Использовать специальное исключение для обработки // ошибок при-обращении к массиву класса RangeArray.
using System;
// Создать исключение для класса RangeArray. class RangeArrayException : Exception {
/* Реализовать все конструкторы класса Exception. Такие конструкторы просто реализуют конструктор базового класса. А поскольку класс исключения RangeArrayException ничего не добавляет к классу Exception, то никаких дополнительных действий не требуется. */ public RangeArrayException() : base() { }
public RangeArrayException(string str) : base(str) { }
public RangeArrayException(
string str, Exception inner) : base(str, inner) { } protected RangeArrayException(
System.Runtime.Serialization.SerializationInfо si,
System.Runtime.Serialization.StreamingContext sc) : base(si, sc) { }
// Переопределить метод ToStringO для класса исключения RangeArrayException. public override string ToStringO { return Message;
}
}
// Улучшенный вариант класса RangeArray. class RangeArray {
// Закрытые данные.
int[] a; // ссылка на базовый массив int lowerBound; // наименьший индекс int upperBound; // наибольший индекс
// Автоматически реализуемое и доступное только для чтения свойство Length, public int Length { get; private set; }
// Построить массив по заданному размеру public RangeArray(int low, int high) { high++;
if(high <= low) {
throw new RangeArrayException("Нижний индекс не меньше верхнего.");
}
а = new int[high - low];
Length = high - low;
lowerBound = low; upperBound = —high;
}
// Это индексатор для класса RangeArray. public int this[int index] {
// Это аксессор get. get {
if(ok(index)) {
return a[index - lowerBound];
} else {
throw new RangeArrayException("Ошибка нарушения границ.");
}
}
// Это аксессор set. set {
if(ok(index)) {
a[index - lowerBound] = value;
}
else throw new RangeArrayException("Ошибка нарушения границ.");}
}
// Возвратить логическое значение true, если // индекс находится в установленных границах, private bool ok(int index) {
if(index >= lowerBound & index <= upperBound) return true; return false;
}
}
// Продемонстрировать применение массива с произвольно // задаваемыми пределами индексирования, class RangeArrayDemo { static void Main() { try {
RangeArray ra = new RangeArray(-5, 5);
RangeArray ra2 = new RangeArray(1, 10);
i
// Использовать объект га в качестве массива.
Console.WriteLine("Длина массива га: и- + га.Length); for(int i = -5; i <= 5; i++) ra[i] = i;
Console.Write("Содержимое массива ra: "); for (int i = -5; i <= 5; i++)
Console.Write(ra[i] + " ");
Console.WriteLine("\n");
// Использовать объект ra2 в качестве массива.
Console.WriteLine("Длина массива га2: " + ra2.Length); for (int i = 1; i <= 10; i++) ra2[i] = i;
Console.Write("Длина массива ra2: "); for (int i = 1; i <= 10; i++)
Console.Write(ra2[i] + " ");
Console.WriteLine("\n") ;
} catch (RangeArrayException exc) {
Console.WriteLine(exc);
}
// А теперь продемонстрировать обработку некоторых ошибок.
Console.WriteLine("Сгенерировать ошибки нарушения границ.");
// Использовать неверно заданный конструктор, try {
RangeArray гаЗ = new RangeArray(100, -10); // Ошибка!
} catch (RangeArrayException exc) {
Console.WriteLine(exc);
}
// Использовать неверно заданный индекс, try {
RangeArray гаЗ = new RangeArray(-2, 2);
for(int i = -2; i <= 2; i++) ra3[i] = i;
Console.Write("Содержимое массива гаЗ: ");
for (int i = -2; i <= 10; i++) // сгенерировать ошибку нарушения границ Console.Write(гаЗ[i] + " ");
} catch (RangeArrayException exc) {
Console.WriteLine(exc);
}
}
}
После выполнения этой программы получается следующий результат.
Длина массива га: 11
Содержимое массива га: -5-4-3-2-1012345 Длина массива га2: 10
Содержимое массива га2: 12345678910
Сгенерировать ошибки нарушения границ.
Нижний индекс не меньше верхнего.
Содержимое массива гаЗ: -2-1012 Ошибка нарушения границ.
Когда возникает ошибка нарушения границ массива классаRangeArray,генерируется объект типаRangeArrayException.В классеRangeArrayэто может произойти в трех следующих местах: в аксессореgetиндексатора, в аксессореsetиндексатора и в конструкторе классаRangeArray.Для перехвата этих исключений подразумевается, что объекты типаRangeArrayдолжны быть сконструированы и доступны из блокаtry,что и продемонстрировано в приведенной выше программе. Используя специальное исключение для сообщения об ошибках, классRangeArrayтеперь действует как один из встроенных в C# типов данных, и поэтому он может быть полностью интегрирован в механизм обработки ошибок, обнаруживаемых в программе.
Обратите внимание на то, что в теле конструкторов класса исключенияRangeArrayExceptionотсутствуют какие-либо операторы, но вместо этого они просто передают свои аргументы классуException,используя ключевое словоbase.Как пояснялось ранее, в тех случаях, когда производный класс исключений не дополняет функции базового класса, весь процесс создания исключений можно поручить конструкторам классаException.Ведь производный класс исключений совсем не обязательно должен чем-то дополнять функции, наследуемые от классаException.
Прежде чем переходить к дальнейшему чтению, попробуйте немного поэкспериментировать с приведенной выше программой. В частности, попробуйте закомментировать переопределение методаToString() и понаблюдайте за результатами. Кроме того, попытайтесь создать исключение, используя конструктор, вызываемый по умолчанию, и посмотрите, какое сообщение при этом сформируется стандартными средствами С#.
Перехват исключений производных классов
При попытке перехватить типы исключений, относящихся как к базовым, так и к производным классам, следует особенно внимательно соблюдать порядок следования операторовcatch,поскольку перехват исключения базового класса будет совпадать с перехватом исключений любых его производных классов. Например, классExceptionявляется базовым для всех исключений, и поэтому вместе с исключением типаExceptionмогут быть перехвачены и все остальные исключения производных от него классов. Конечно, для более четкого перехвата всех исключений можно воспользоваться упоминавшейся ранее формой оператораcatchбез указания конкретного типа исключения. Но вопрос перехвата исключений производных классов становится весьма актуальным и в других ситуациях, особенно при создании собственных исключений.
Если требуется перехватывать исключения базового и производного классов, то первым по порядку должен следовать операторcatch,перехватывающий исключение производного класса. Это правило необходимо соблюдать потому, что при перехвате исключения базового класса будут также перехвачены исключения всех производных от него классов. Правда, это правило соблюдается автоматически: если первым расположить в коде операторcatch,перехватывающий исключение базового класса, то во время компиляции этого кода будет выдано сообщение об ошибке.
В приведенном ниже примере программы создаются два класса исключений:ExceptAиExceptB.КлассExceptAявляется производным от классаException,а классExceptB— производным от классаExceptA.Затем в программе генерируются исключения каждого типа. Ради краткости в классах специальных исключений предоставляется только один конструктор, принимающий символьную строку, описывающую исключение. Но при разработке программ коммерческого назначения в классах специальных исключений обычно требуется предоставлять все четыре конструктора, определяемых в классеException.
// Исключения производных классов должны появляться до // исключений базового класса.
using System;
// Создать класс исключения, class ExceptA : Exception {
public ExceptA(string str) : base(str) { }
public override string ToStringO { return Message;
}
}
// Создать класс исключения, производный от класса ExceptA. class ExceptB : ExceptA {
public ExceptB(string str) : base(str) { }
public override string ToStringO { return Message;
}
}
class OrderMatters { static void Main() {
for(int x = 0; x < 3; x++) {
try {
if (x==0) throw new ExceptA("Перехват исключения типа ExceptA"); else if(x==l) throw new ExceptB("Перехват исключения типа ExceptB");
else throw new Exception();
}
catch (ExceptB exc) {
Console.WriteLine(exc);
}
catch (ExceptA exc) {
Console.WriteLine(exc);
}
catch (Exception exc) {
Console.WriteLine(exc);
}
}
}
}
Вот к какому результату приводит выполнение этой программы.
Перехват исключения типа ExceptA.
Перехват исключения типа ExceptB.
System.Exception: Выдано исключение типа "System.Exception".
в OrderMatters.Main() в<имя_файла>:строка 3 6
Обратите внимание на порядок следования операторовcatch.Именно в таком порядке они и должны выполняться. КлассExceptBявляется производным от классаExceptA,поэтому исключение типаExceptBдолжно перехватываться до исключения типаExceptA.Аналогично, исключение типаException(т.е. базового класса для всех исключений) должно перехватываться последним. Для того чтобы убедиться в этом, измените порядок следования операторовcatch.В итоге это приведет к ошибке во время компиляции.
Полезным примером использования оператора catch, перехватывающего исключения базового класса, служит перехват всей категории исключений. Допустим, что создается ряд исключений для управления некоторым устройством. Если сделать их классы производными от общего базового класса, то в тех приложениях, где необязательно выяснять конкретную причину возникшей ошибки, достаточно перехватывать исключение базового класса и тем самым исключить ненужное дублированиекода.
Применение ключевых слов checked и unchecked
В C# имеется специальное средство, связанное с генерированием исключений, возникающих при переполнении в арифметических вычислениях. Как вам должно быть уже известно, результаты некоторых видов арифметических вычислений могут превышать диапазон представления чисел для типа данных, используемого в вычислении. В этом случае происходит так называемоепереполнение.Рассмотрим в качестве примера следующий фрагмент кода.
byte a, b, result; а = 127;
Ь = 127;
result = (byte)(а * b);
В этом коде произведение значений переменных а и b превышает диапазон представления чисел для типа byte. Следовательно, результат вычисления данного выражения приводит к переполнению для типа данных, сохраняемого в переменной
result.
В C# допускается указывать, будет ли в коде сгенерировано исключение при переполнении, с помощью ключевых слов checked и unchecked. Так, если требуется указать, что выражение будет проверяться на переполнение, следует использовать ключевое слово checked, а если требуется проигнорировать переполнение — ключевое слово unchecked. В последнем случае результат усекается, чтобы не выйти за пределы диапазона представления чисел для целевого типа выражения.
У ключевого слова checked имеются две общие формы. В одной форме проверяется конкретное выражение, и поэтому она называетсяоператорной.А в другой форме проверяется блок операторов, и поэтому она называетсяблочной.Ниже приведены обе формы:
checked (выражение)
checked {
// проверяемые операторы
}
гдевыражениеобозначает проверяемое выражение. Если вычисление проверяемого выражения приводит к переполнению, то генерируется исключение OverflowException.
У ключевого слова unchecked также имеются две общие формы. В первой, операторной форме переполнение игнорируется при вычислении конкретного выражения. А во второй, блочной форме оно игнорируется при выполнении блока операторов:
unchecked (выражение)unchecked {
// операторы, для которых переполнение игнорируется
}
гдевыражениеобозначает конкретное выражение, при вычислении которого переполнение игнорируется. Если же в непроверяемом выражении происходит переполнение, то результат его вычисления усекается.
Ниже приведен пример программы, в котором демонстрируется применение ключевых слов checked и unchecked.
// Продемонстрировать применение ключевых слов checked и unchecked.
using System;"
class CheckedDemo { static void Main() { byte a, b; byte result;
a = 127; b = 127;
try {
result = unchecked((byte) (a * b));
Console.WriteLine("Непроверенный на переполнение результат: " + result);
result = checked((byte)(a * b)); // эта операция приводит к
// исключительной ситуации Console.WriteLine("Проверенный на переполнение результат: " + result); //не подлежит выполнению
}
catch (OverflowException exc) {
Console.WriteLine(exc);
}
}
}
При выполнении этой программы получается следующий результат.
Непроверенный на переполнение результат: 1 System.OverflowException: Переполнение в результате выполнения арифметической операции.
в CheckedDemo.Main() в<имя_файла>:строка 2 0
Как видите, результат вычисления непроверяемого выражения был усечен. А вычисление проверяемого выражения привело к исключительной ситуации.
В представленном выше примере программы было продемонстрировано применение ключевых слов checked и unchecked в одном выражении. А в следующем примере программы показывается, каким образом проверяется и не проверяется на переполнение целый блок операторов.
// Продемонстрировать применение ключевых слов checked // и unchecked в блоке операторов.
using System;
class CheckedBlocks { static void Main() { byte a, b; byte result;
a = 127; b = 127;
try {
unchecked { a = 127; b = 127;
result = unchecked((byte)(a * b));
Console.WriteLine("Непроверенный на переполнение результат: " + result);
а = 125; b = 5;
result = unchecked((byte)(a * b));
Console.WriteLine("Непроверенный на переполнение результат: " + result);
}
checked { a = 2; b = 7;
result = checked((byte)(a * b)); // верно
Console.WriteLine("Проверенный на переполнение результат: " + result);
а = 127; b = 127;
result = checked((byte)(a * b)); // эта операция приводит к
// исключительной ситуации Console.WriteLine("Проверенный на переполнение результат: " + result); //не подлежит выполнению
}
. }
catch (OverflowException exc) {
Console.WriteLine(exc);
}
}
}
Результат выполнения этой программы приведен ниже.
Непроверенный на переполнение результат: 1 Непроверенный на переполнение результат: 113 Проверенный на переполнение результат: 14 System.OverflowException: Переполнение в результате выполнения арифметической операции.
в CheckedDemo.Main() в<имя_файла>:строка 41
Как видите, результаты выполнения непроверяемого на переполнение блока операторов были усечены. Когда же в проверяемом блоке операторов произошло переполнение, то возникла исключительная ситуация.
Потребность в применении ключевого слова checked или unchecked может возникнуть, в частности, потому, что по умолчанию проверяемое или непроверяемое состояние переполнения определяется путем установки соответствующего параметра компилятора и настройки самой среды выполнения. Поэтому в некоторых программах состояние переполнения лучше проверять явным образом.
ГЛАВА 14 Применение средств ввода-вывода
В примерах программ, приводившихся в предыдущих главах, уже применялись отдельные части системы ввода-вывода в С#, например методConsole.
WriteLine(), но делалось это без каких-либо формальных пояснений. Система ввода-вывода основана в C# на иерархии классов, поэтому ее функции и особенности нельзя было представлять до тех пор, пока не были рассмотрены классы, наследование и исключения. А теперь настал черед и для ввода-вывода. В C# применяется система ввода-вывода и классы, определенные в среде .NET Framework, и поэтому рассмотрение ввода-вывода в этом языке относится ко всей системе ввода-вывода среды .NET в целом.
В этой главе речь пойдет о средствах консольного и файлового ввода-вывода. Следует, однако, сразу же предупредить, что система ввода-вывода в C# довольно обширна. Поэтому в этой главе рассматриваются лишь самые важные и наиболее часто используемые ее средства.
Организация системы ввода-вывода в C# на потоках
Ввод-вывод в программах на C# осуществляется посредством потоков.Поток —это некая абстракция производства или потребления информации. С физическим устройством поток связывает система ввода-вывода. Все потоки действуют одинаково — даже если они связаны с разными физическими устройствами. Поэтому классы и методы ввода-вывода могут применяться к самым разным типам устройств. Например, методами вывода на консоль можно пользоваться и для вывода в файл на диске.
Байтовые и символьные потоки
На самом низком уровне ввод-вывод в C# осуществляется байтами. И делается это потому, что многие устройства ориентированы на операции ввода-вывода отдельными байтами. Но человеку больше свойственно общаться символами. Напомним, что в C# типcharявляется 16-разрядным, а типbyte— 8-разрядным. Так, если в целях ввода-вывода используется набор символов в коде ASCII, то для преобразования типаcharв типbyteдостаточно отбросить старший байт значения типаchar.Но это не годится • для набора символов в уникоде (Unicode), где символы требуется представлять двумя, а то и больше байтами. Следовательно, байтовые потоки не совсем подходят для организации ввода-вывода отдельными символами. С целью разрешить это затруднение в среде .NET Framework определено несколько классов, выполняющих превращение байтового потока в символьный с автоматическим преобразованием типаbyteв типcharи обратно.
Встроенные потоки
Длявсех программ, в которых используется пространство именSystem,доступны встроенные потоки, открывающиеся с помощью свойствConsole. In, Console.OutиConsole.Error.В частности, свойствоConsole.Outсвязано со стандартным потоком вывода. По умолчанию это поток вывода на консоль. Так, если вызывается методConsole .WriteLine (), информация автоматически передается свойствуConsole.Out.СвойствоConsole. Inсвязано со стандартным потоком ввода, который по умолчанию осуществляется с клавиатуры. А свойствоConsole.Errorсвязано со стандартным потоком сообщений об ошибках, которые по умолчанию также выводятся на консоль. Но эти потоки могут быть переадресованы на любое другое совместимое устройство ввода-вывода. Стандартные потоки являются символьными. Поэтому в эти потоки выводятся и вводятся из них символы.
Классы потоков
В среде .NET Framework определены классы как для байтовых, так и для символьных потоков. Но на самом деле классы символьных потоков служат лишь оболочками для превращения заключенного в них байтового потока в символьный, автоматически выполняя любые требующиеся преобразования типов данных. Следовательно, символьные потоки основываются на байтовых, хотя они и разделены логически.
Основные классы потоков определены в пространстве именSystem. 10.Длятого чтобы воспользоваться этими классами, как правило, достаточно ввести приведенный ниже оператор в самом начале программы.
using System.10;
Пространство именSystem. 10не указывается для консольного ввода-вывода потому, что для него определен классConsoleв пространстве именSystem.
Класс Stream '
Основным для потоков является классSystem. 10. Stream.Он представляет байтовый поток и является базовым для всех остальных классов потоков. Кроме того, он является абстрактным классом, а это означает, что получить экземпляр объекта классаStreamнельзя. В классеStreamопределяется ряд операций со стандартными потоками, представленных соответствующими методами. В табл. 14.1 перечислен ряд наиболее часто используемых методов, определенных в классеStream.
Таблица 14.1. Некоторые методы, определенные в классе stream
Метод
Описание
void Close ()
Закрывает поток
void Flush()
Выводит содержимое потока на физическое устройство
int ReadByte()
Возвращает целочисленное представление следующего байта, доступного для ввода из потока. При обнаружении конца файла возвращает значение -1
int Read(byte[] buffer,
Делает попытку ввести count байтов в массив
intoffset,intcount)
buffer, начиная с элемента buffer[offset]. Возвращает количество успешно введенных байтов
long Seek(longoffset,
Устанавливает текущее положение в потоке по указан
SeekOriginorigin)
ному смещению offset относительно заданного начала отсчета origin. Возвращает новое положение в потоке
void WriteByte(bytevalue)
Выводит один байт в поток вывода
void Write(byte[]buffer,
Выводит подмножество count байтов из массива
intoffset,
buffer, начиная с элемента buffer'i offset]. Воз
intcount)
вращает количество выведенных байтов
Некоторые из методов, перечисленных в табл. 14.1, генерируют исключениеIOExceptionпри появлении ошибки ввода-вывода. Если же предпринимается попытка выполнить неверную операцию, например вывести данные в поток, предназначенный только для чтения, то генерируется исключениеNotSupportedException.Кроме того, могут быть сгенерированы и другие исключения — все зависит от конкретного метода.
Следует заметить, что в классеStreamопределены методы для ввода (или чтения) и вывода (или записи) данных. Но не все потоки поддерживают обе эти операции, поскольку поток можно открывать только для чтения или только для записи. Кроме того, не все потоки поддерживают запрос текущего положения в потоке с помощью методаSeek(). Для того чтобы определить возможности потока, придется воспользоваться одним, а то и несколькими свойствами классаStream.Эти свойства перечислены в табл. 14.2 наряду со свойствамиLengthиPosition,содержащими длину потока и текущее положение в нем.
Таблица 14.2. Свойства, определенные в классе Stream
Свойство
Описание
bool CanRead bool CanSeek
Принимает значение true, если из потока можно ввести данные. Доступно только для чтения
Принимает значение true, если поток поддерживает запрос текущего положения в потоке. Доступно только для чтения
Свойство
Описание
bool CanWrite
Принимает значение true, если в поток можно вывести данные. До
ступно только для чтения
long Length
Содержит длину потока. Доступно только для чтения
long Position
Представляет текущее положение в потоке. Доступно как для чтения,
так и для записи
int ReadTimeout
Представляет продолжительность времени ожидания в операциях
ввода. Доступно как для чтения, так и для записи
int' WriteTimeout
Представляет продолжительность времени ожидания в операциях
вывода. Доступно как для чтения, так и для записи
Классы байтовых потоков
Производными от классаStreamявляются несколько конкретных классов байтовых потоков. Эти классы определены в пространстве именSystem. 10и перечислены ниже.
Класс потока
Описание
BufferedStream
Заключает в оболочку байтовый поток и добавляет буфериза
цию. Буферизация, как правило, повышает производительность
FileStream
Байтовый поток, предназначенный для файлового ввода-
вывода
MemoryStream
Байтовый поток, использующий память для хранения данных
UnmanagedMemoryStream
Байтовый поток, использующий неуправляемую память для
хранения данных
В среде NET Framework поддерживается также ряд других конкретных классов потоков, в том числе для ввода-вывода в сжатые файлы, сокеты и каналы. Кроме того, можно создать свои собственные производные классы потоков, хотя для подавляющего числа приложений достаточно и встроенных потоков.
Классы-оболочки символьных потоков
Для создания символьного потока достаточно заключить байтовый поток в один из классов-оболочек символьных потоков. На вершине иерархии классов символьных потоков находятся абстрактные классыTextReaderиTextWriter.Так, классTextReaderорганизует ввод, а классTextWriter— вывод. Методы, определенные в обоих этих классах, доступны для всех их подклассов. Они образуют минимальный набор функций ввода-вывода, которыми должны обладать все символьные потоки.
В табл. 14.3 перечислены методы ввода, определенные в классеTextReader.В целом, эти методы способны генерировать исключениеIOExceptionпри появлении ошибки ввода, а некоторые из них — исключения других типов. Особый интерес вызывает методReadLine(), предназначенный для ввода целой текстовой строки, возвращая ее в виде объекта типаstring.Этот метод удобен для чтения входных данных, содержащих пробелы. В классеTextReaderимеется также методClose(), определяемый следующим образом.
Этот метод закрывает считывающий поток и освобождает его ресурсы.
В классеTextWriterопределены также варианты методовWrite() иWriteLine (),предназначенные для вывода данных всех встроенных типов. Ниже в качестве примера перечислены лишь некоторые из перегружаемых вариантов этих методов.
virtual void Close() virtual void Flush()
МетодFlush() организует вывод в физическую среду всех данных, оставшихся в выходном буфере. А методClose() закрывает записывающий поток и освобождает его ресурсы.
КлассыTextReaderиTextWriterреализуются несколькими классами символьных потоков, включая и те, что перечислены ниже. Следовательно, в этих классах потоков предоставляются методы и свойства, определенные в классахTextReaderиTextWriter.
Двоичные потоки
Помимо классов байтовых и символьных потоков, имеются еще два класса двоичных потоков, которые могут служить для непосредственного ввода и вывода двоичных данных —BinaryReaderиBinaryWriter.Подробнее о них речь пойдет далее в этой главе, когда дойдет черед до файлового ввода-вывода.
А теперь, когда представлена общая структура системы ввода-вывода в С#, отведем оставшуюся часть этой главы более подробному рассмотрению различных частей данной системы, начиная с консольного ввода-вывода.
Консольный ввод-вывод
Консольньгй ввод-вывод осуществляется с помощью стандартных потоков, представленных свойствамиConsole. In, Console. OutиConsole. Error.Примеры консольного ввода-вывода были представлены еще в главе 2, поэтому он должен быть вам уже знаком. Как будет показано ниже, он обладает и рядом других дополнительных возможностей.
Но прежде следует еще раз подчеркнуть, что большинство реальных приложений C# ориентированы не на консольный ввод-вывод в текстовом виде, а на графический оконный интерфейс для взаимодействия с пользователем, или же они представляют собой программный код, используемый на стороне сервера. Поэтому часть системы ввода-вывода, связанная с консолью, не находит широкого практического применения. И хотя программы, ориентированные на текстовый ввод-вывод, отлично подходят в качестве учебных примеров, коротких сервйсньгх программ или определенного рода программных компонентов, для большинства реальных приложений они не годятся.
Чтение данных из потока ввода с консоли
ПотокConsole. Inявляется экземпляром объекта классаTextReader,и поэтому для доступа к нему могут быть использованы методы и свойства, определенные в классеTextReader.Но для этой цели чаще все же используются методы, предоставляемые классомConsole,в котором автоматически организуется чтение данных из потокаConsole. In.В классеConsoleопределены три метода ввода. Два первых метода,Read () иReadLine(), были доступны еще в версии .NET Framework 1.0. А третий метод,ReadKey(), был добавлен в версию 2.0 этой среды.
Для чтения одного символа служит приведенный ниже методRead ().
static int Read()
МетодRead() возвращает очередной символ, считанный с консоли. Он ожидает до тех пор, пока пользователь не нажмет клавишу, а затем возвращает результат. Возвращаемый символ относится к типуintи поэтому должен быть приведен к типуchar.Если при вводе возникает ошибка, то методRead() возвращает значение -1. Этот метод сгенерирует исключениеIOExceptionпри неудачном исходе операции ввода. Ввод с консоли с помощью методаRead() буферизуется построчно, поэтому пользователь должен нажать клавишу <Enter>, прежде чем программа получит любой символ, введенный с консоли.
Ниже приведен пример программы, в которой методRead() используется для считывания символа, введенного с клавиатуры.
// Считать символ, введенный с клавиатуры.
using System;
class КЫп {
static void Main() { char ch;
Console.Write("Нажмите клавишу, а затем — <ENTER>: ");
ch = (char) Console.Read(); // получить значение типа char Console.WriteLine("Вы нажали клавишу: " + ch) ;
}
}
Вот, например, к какому результату может привести выполнение этой программы.
Нажмите клавишу, а затем — <ENTER>: t Вы нажали клавишу: t
Необходимость буферизировать построчно ввод, осуществля^емый с консоли посредством методаRead (), иногда может быть досадным препятствием. Ведь при нажатии клавиши <Enter> в поток ввода передается последовательность символов перевода каретки и перевода строки. Более того, эти символы остаются во входном буфере до тех пор, пока они не будут считаны. Следовательно, в некоторых приложениях приходится удалять эти символы (путем их считывания), прежде чем приступать к следующей операции ввода. Впрочем, для чтения введенных с клавиатуры символов без построчной буферизации, можно воспользоваться рассматриваемым далее методомReadKey().Длд считывания строки символов служит приведенный ниже методReadLine ().
static string ReadLine()
Символы считываются методомReadLine() до тех пор, пока пользователь не нажмет клавишу <Enter>, а затем этот метод возвращает введенные символы в виде
объекта типаstring.Кроме того, он сгенерирует исключениеIOExceptionпри неудачном исходе операции ввода.
Ниже приведен пример программы, в которой демонстрируется чтение строки из потокаConsole . Inс помощью методаReadLine ().
// Ввод с консоли с помощью метода ReadLine().
using System;
class ReadString { static void Main() { string str;
Console.WriteLine("Введите несколько символов."); str = Console.ReadLine();
Console.WriteLine("Вы ввели: " + str);
}
}
Выполнение этой программы может привести, например, к следующему результату.
Введите несколько символов.
Это просто тест.
Вы ввели: Это просто тест.
Итак, для чтения данных из потокаConsole. Inпроще всего воспользоваться методами классаConsole.Но для этой цели можно обратиться и к методам базового классаTextReader.В качестве примера ниже приведен переделанный вариант предыдущего примера программы, в котором используется методRea.dLine (), определенный в классеTextReader.
// Прочитать введенную с клавиатуры строку // непосредственно из потока Console.In.
using System;
class ReadChars2 { static void Main() { string str;
Console.WriteLine("Введите несколько символов.");
str = Console.In.ReadLine(); // вызвать метод ReadLine() класса TextReader Console.WriteLine("Вы ввели: " + str);
}
}
Обратите внимание на то, что методReadLine() теперь вызывается непосредственно для потокаConsole . In.Поэтому если требуется доступ к методам, определенным в классеTextReader,который является базовым для потокаConsole. In,то подобные методы вызываются так, как было показано в приведенном выше примере.
Применение метода ReadKey ()
В состав среды .NET Framework включен метод, определяемый в классеConsoleи позволяющий непосредственно считывать отдельно введенные с клавиатуры символы без построчной буферизации. Этот метод называетсяReadKey (). При нажатии клавиши методReadKey() немедленно возвращает введенный с клавиатуры символ. И в этом случае пользователю уже не нужно нажимать дополнительно клавишу <Enter>. Таким образом, методReadKey() позволяет считывать и обрабатывать ввод с клавиатуры в реальном масштабе времени.
Ниже приведены две формы объявления методаReadKey ().
static ConsoleKeylnfo ReadKey()
static ConsoleKeylnfo ReadKey(boolintercept)
В первой форме данного метода ожидается нажатие клавиши. Когда оно происходит, метод возвращает введенный с клавиатуры символ и выводит его на экран. Во второй форме также ожидается нажатие клавиши, и затем возвращается введенный с клавиатуры символ. Но если значение параметраinterceptравноtrue,то введенный символ не отображается. А если значение параметраinterceptравноfalse,то введенный символ отображается.
МетодReadKey() возвращает информацию о нажатии клавиши в объекте типаConsoleKeylnfo,который представляет собой структуру, состоящую из приведенных ниже свойств, доступных только для чтения.
char KeyChar ConsoleKey Key ConsoleModifiers Modifiers
СвойствоKeyCharсодержит эквивалентcharвведенного с клавиатуры символа, свойствоKey— значение из перечисленияConsoleKeyвсех клавиш на клавиатуре, а свойствоModifiers— описание одной из модифицирующих клавиш (<Alt>, <Ctrl> или <Shift>), которые были нажаты, если это действительно имело место, при формировании ввода с клавиатуры. Эти модифицирующие клавиши представлены в перечисленииConsoleModifiersследующими значениями:Control, ShiftиAlt.В свойствеModifiersможет присутствовать несколько значений нажатых модифицирующих клавиш.
Главное преимущество методаReadKey() заключается в том, что он предоставляет средства для организации ввода с клавиатуры в диалоговом режиме, поскольку этот ввод не буферизуется построчно. Для того чтобы продемонстрировать данный метод в действии, ниже приведен соответствующий пример программы.
// Считать символы, введенные с консоли, используя метод ReadKey().
using System;
class ReadKeys {
static void Main() {
ConsoleKeylnfo keypress;
Console.WriteLine("Введите несколько символов, " +
"а по окончании - <Q>.");
do {
keypress = Console.ReadKey(); // считать данные о нажатых клавишах Console.WriteLine(" Вы нажали клавишу: " + keypress.KeyChar);
// Проверить нажатие модифицирующих клавиш.
if((ConsoleModifiers.Alt & keypress.Modifiers) != 0)
Console.WriteLine("Нажата клавиша <Alt>."); if((ConsoleModifiers.Control & keypress.Modifiers) != 0)
Console.WriteLine("Нажата клавиша <Control>."); if((ConsoleModifiers.Shift & keypress.Modifiers) != 0)
Console.WriteLine("Нажата клавиша <Shift>.");
} while(keypress.KeyChar != ' Q');
}
}
Вот, например, к какому результату может привести выполнение этой программы.
Введите несколько символов, а по окончании - <Q>.
а Вы нажали клавишу: а
b Вы нажали клавишу: b
d Вы нажали клавишу: d
А Вы нажали клавишу: А
Нажата клавиша <Shift>.
В Вы нажали клавишу: В
Нажата клавиша <Shift>.
С Вы нажали клавишу: С
Нажата клавиша <Shift>.
• Вы нажали клавишу: •
Нажата клавиша <Control>.
Q Вы нажали клавишу: Q
Нажата клавиша <Shift>.
/
Как следует из приведенного выше результата, всякий раз, когда нажимается клавиша, методReadKey() немедленно возвращает введенный с клавиатуры символ.*Этим он отличается от упоминавшегося ранее методаRead (), в котором ввод выполняется с построчной буферизацией. Поэтому если требуется добиться в программе реакции на ввод с клавиатуры, то рекомендуется выбрать методReadKey ().
Запись данных в поток вывода на консоль
ПотокиConsole . OutиConsole .Errorявляются объектами типаTextWriter.Вывод на консоль проще всего осуществить с помощью методовWrite() иWriteLine (), с которыми вы уже знакомы. Существуют варианты этих методов для вывода данных каждого из встроенных типов. В классеConsoleопределяются его собственные варианты методаWrite() nWriteLine(),nпоэтому они могут вызываться непосредственно для классаConsole,как это было уже не раз показано на страницах данной книги. Но при желании эти и другие методы могут быть вызваны и для классаTextWriter,который является базовым для потоковConsole . OutиConsole . Error.
Ниже приведен пример программы, в котором демонстрируется вывод в потокиConsole . OutиConsole . Error.По умолчанию данные в обоих случаях выводятся на консоль.
// Организовать вывод в потоки Console.Out и Console.Error.
using System;
class ErrOut {
static void Main() { int a=10, b=0; int result;
Console.Out.WriteLine("Деление на нуль приведет " +
"к исключительной ситуации.");
try {
result = а / b; // сгенерировать исключение при попытке деления на нуль } catch(DivideByZeroException exc) {
Console.Error.WriteLine(exc.Message);
}
}
}
При выполнении этой программы получается следующий результат.
Деление на нуль приведет к исключительной ситуации.
Попытка деления на нуль.
Начинающие программисты порой испытывают затруднения при использовании потокаConsole.Error.Перед ними невольно встает вопрос: если оба потока,Console . OutиConsole .Error,по умолчанию выводят результат на консоль, то зачем нужны два разных потока вывода? Ответ на этот вопрос заключается в том, что стандартные потоки могут быть переадресованы на другие устройства. Так, потокConsole .Errorможно переадресовать в выходной файл на диске, а не на экран. Это, например, означает, что сорбщения об ошибках могут быть направлены в файл журнала регистрации, не мешая выводу на консоль. И наоборот, если вывод на консоль пере-адресуется, а вывод сообщений об ошибках остается прежним, то на консоли появятся сообщения об ошибках, а не выводимые на нее данные. Мы еще вернемся к вопросу переадресации после рассмотрения файлового ввода-вывода.
Класс FileStream и байтовый ввод-вывод в файл
В среде .NET Framework предусмотрены классы для организации ввода-вывода в файлы. Безусловно, это в основном файлы дискового типа. На уровне операционной системы файлы имеют байтовую организацию. И, как следовало ожидать, для ввода и вывода байтов в файлы имеются соответствующие методы. Поэтому ввод и вывод в файлы байтовыми потоками весьма распространен. Кроме того, байтовый поток ввода или вывода в файл может быть заключен в соответствующий объект символьного потока. Операции символьного ввода-вывода в файл находят применение при обработке текста. О символьных потоках речь пойдет далее в этой главе, а здесь рассматривается байтовый ввод-вывод.
Для создания байтового потока, привязанного к файлу, служит классFileStream.Этот класс является производным от классаStreamи наследует всего его функции.
Напомним, что классы потоков, в том числе иFileStream,определены в пространстве именSystem. 10.Поэтому в самом начале любой использующей их программы обычно вводится следующая строка кода.
using System.10;
Открытие и закрытие файла
Дляформирования байтового потока, привязанного к файлу, создается объект классаFileStream.В этом классе определено несколько конструкторов. Ниже приведен едва ли не самый распространенный среди них:
FileStream(stringпуть,FileModeрежим)
гдепутьобозначает имя открываемого файла, включая полный путь к нему;а режим— порядок открытия файла. В последнем случае указывается одно из значений, определяемых в перечисленииFileModeи приведенных в табл. 14.4. Как правило, этот конструктор открывает файл для доступа с целью чтения или записи. Исключением из этого правила служит открытие файла в режимеFileMode .Append,когда файл становится доступным только для записи.
Таблица 14.4. Значения из перечисления FileMode
Значение
Описание
FileMode.Append FileMode.Create
FileMode.CreateNew
FileMode.Open FileMode.OpenOrCreate
FileMode.Truncate
Добавляет выводимые данные в конец файла
Создает новый выходной файл. Существующий файл с таким
же именем будет разрушен
Создает новый выходной файл. Файл с таким же именем не должен существовать Открывает существующий файл
Открывает файл, если он существует. В противном случае создает новый файл
Открывает существующий файл, но сокращает его длину до нуля
Еслипопытка открыть файл оказывается неудачной, то генерируется исключение. Если же файл нельзя открыть из-за того что он не существует, генерируется исключениеFileNotFoundException.А если файл нельзя открыть из-за какой-нибудь ошибки ввода-вывода, то генерируется исключениеIOException.К числу других исключений, которые могут быть сгенерированы при открытии файла, относятся следующие:ArgumentNullException(указано пустое имя файла),ArgumentException(указано неверное имя файла),ArgumentOutOfRangeException(указан неверный режим),SeaurityException(у пользователя нет прав доступа к файлу),PathTooLongException(слишком длинное имя файла или путь к нему),NotSupportedException(в имени файла указано устройство, которое не поддерживается), а такжеDirectoryNotFoundException(указан неверный каталог).
ИсключенияPathTooLongException, DirectoryNotFoundExceptionиFileNotFoundExceptionотносятся к подклассам класса исключенийIOException.Поэтому все они могут быть перехвачены, если перехватывается исключениеIOException.
Ниже в качестве примера приведен один из способов открытия файлаtest. datдля ввода.
FileStream fin;
try {
fin = new FileStream("test", FileMode.Open);
}
catch(IOException exc) { // перехватить все исключения, связанные с вводом-выводом Console.WriteLine(exc.Message);
// Обработать ошибку.
}
catch(Exception exc { // перехватить любое другое исключение.
Console.WriteLine(exc.Message);
// Обработать ошибку, если это возможно.
// Еще раз сгенерировать необрабатываемые исключения.
}
В первом блоке catch из данного примера обрабатываются ошибки, возникающие в том случае, если файл не найден, путь к нему слишком длинен, каталог не существует, а также другие ошибки ввода-вывода. Во втором блоке catch, который является "универсальным" для всех остальных типов исключений, обрабатываются другие вероятные ошибки (возможно, даже путем повторного генерирования исключения). Кроме того, каждую ошибку можно проверять отдельно, уведомляя более подробно о ней и принимая конкретные меры по ее исправлению.
Ради простоты в примерах, представленных в этой книге, перехватывается только исключениеIOException,но в реальной программе, скорее всего, потребуется перехватывать и другие вероятные исключения, связанные с вводом-выводом, в зависимости от обстоятельств. Кроме того, в обработчиках исключений, приводимых в качестве примера в этой главе, просто уведомляется об ошибке, но зачастую в них должны быть запрограммированы конкретные меры по исправлению ошибок, если это вообще возможно. Например, можно предложить пользователю еще раз ввести имя файла, если указанный ранее файл не был найден. Возможно, также потребуется сгенерировать исключение повторно.
Как упоминалось выше, конструктор классаFileStreamоткрывает файл, доступный для чтения или записи. Если же требуется ограничить доступ к файлу только для чтения или же только для записи, то в таком случае следует использовать такой конструктор.
FileStream(stringпуть,FileModeрежим,FileAccessдоступ)
Как и прежде,путьобозначает имя открываемого файла, включая и полный путь к нему, арежим —порядок открытия файла. В то же времядоступобозначает конкретный способ доступа к файлу. В последнем случае указывается одно из значений, определяемых в перечисленииFileAccessи приведенных ниже.
FileAccess.Read . FileAccess.Write FileAccess.ReadWrite
Например, в следующем примере кода файлtest. datоткрывается только для чтения.
FileStream fin = new FileStream("test.dat", FileMode.Open, FileAccess.Read);
По завершении работы с файлом его следует закрыть, вызвав методClose (). Ниже приведена общая форма обращения к этому методу.
void Close()
При закрытии файла высвобождаются системные ресурсы, распределенные для этого файла, что дает возможность использовать их для другого файла. Любопытно, что методClose () вызывает, в свою очередь, методDispose (), который, собственно, и высвобождает системные ресурсы.
ПРИМЕЧАНИЕ
Оператор using, рассматриваемый в главе 20, предоставляет еще один способ закрытия файла, который больше не нужен. Такой способ оказывается удобным во многих случаях обращения с файлами, поскольку гарантирует закрытие ненужного больше файла простыми средствами. Но исключительно в целях демонстрации основ обращения с файлами, в том числе и того момента, когда файл может быть закрыт, во всех примерах, представленных в этой главе, используются явные вызовы метода Close ().
Чтение байтов из потока файлового ввода-вывода
В классеFileStreamопределены два метода для чтения байтов из файла:ReadByte ()иRead (). Так, для чтения одного байта из файла используется методReadByte (), общая форма которого приведена ниже.
int ReadByte()
Всякий раз, когда этот метод вызывается, из файла считывается один байт, который затем возвращается в виде целого значения. К числу вероятных исключений, которые генерируются при этом, относятсяNotSupportedException(поток не открыт для ввода) иObjectDisposedException(поток закрыт).
Для чтения блока байтов из файла служит методRead (), общая форма которого выглядит так.
int Read(byte[ ]array,intoffset,intcount)
В методеRead() предпринимается попытка считать количествоcountбайтов в массивarray,начиная с элементаarray[offset].Он возвращает количество байтов, успешно считанных из файла. Если же возникает ошибка ввода-вывода, то генерируется исключениеIOException.К числу других вероятных исключений, которые генерируются при этом, относитсяNotSupportedException.Это исключение генерируется в том случае, если чтение из файла не поддерживается в потоке.
В приведенном ниже примере программы методReadByte() используется для ввода и отображения содержимого текстового файла, имя которого указывается в качестве аргумента командной строки. Обратите внимание на то, что в этой программе проверяется, указано ли имя файла, прежде чем пытаться открыть его.
/* Отобразить содержимое текстового файла.
Чтобы воспользоваться этой программой, укажите имя того файла, содержимое которого требуется отобразить. Например, для просмотра содержимого файла TEST.CS введите в командной строке следующее:
ShowFile TEST.CS
*/
using System; using System.10;
class ShowFile {
static void Main(string[] args) { int i;
FileStream fin; if(args.Length != 1) {
Console.WriteLine("Применение: ShowFile Файл"); return;
}
try {
fin = new FileStream(args[0], FileMode.Open);
} catch(IOException exc) {
Console!WriteLine("He удается открыть файл");
Console.WriteLine(exc.Message);
return; // Файл не открывается, завершить программу
}
// Читать байты до конца файла, try {
• do {
i = fin.ReadByte();
if(i != -1) Console.Write((char) i);
} while(i != -1);
} catch(IOException exc) {
Console.WriteLine("Ошибка чтения файла");
Console.WriteLine(exc.Message) ;
} finally { fin.Close() ;
}
}
}
Обратите внимание на то, что в приведенной выше программе применяются два блокаtry.В первом из них перехватываются исключения, возникающие при вводе-выводе и способные воспрепятствовать открытию файла. Если произойдет ошибка ввода-вывода, выполнение программы завершится. В противномслучаево втором блокеtryбудет продолжен контроль исключений, возникающих в операциях ввода-вывода. Следовательно, второй блокtryвыполняется только в том случае, если в переменнойfinсодержится ссылка на открытый файл. Обратите также внимание на то, что файл закрывается в блокеfinally,связанном со вторым блокомtry.Это означает, что независимо от того, как завершится циклdo-while(нормально или аварийно из-за ошибки), файл все равно будет закрыт. И хотя в данном конкретном примере это и так важно, поскольку программа все равно завершится в данной точке, преимущество такого подхода, вообще говоря, заключается в том, что файл закрывается в завершающем блокеfinallyв любом случае — даже если выполнение кода доступа к этому файлу завершается преждевременно из-за какого-нибудь исключения.
В некоторых случаях оказывается проще заключить те части программы, где осуществляется открытие и доступ к файлу, внутрь блокаtry,вместо того чтобы разделять обе эти операции. В качестве примера ниже приведен другой, более краткий вариант написания представленной выше программыShowFile.
// Отобразить содержимое текстового файла.
using System; using System.10;
class ShowFile {
static void Main(string[] args) { int i;
FileStream fin = null;
if (args.Length != 1)' {
Console.WriteLine("Применение: ShowFile File"); return;
}
try {
fin = new FileStream(args[0], FileMode.Open);
// Читать байты до конца файла, do {
i = fin.ReadByte();
if(i != -1) Console.Write((char) i);
} while (i != -1);
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода:\n" + exc.Message);
} finally {
if(fin != null) fin.Close();
}
}
}
Обратите внимание на то, что в данном варианте программы переменная fin ссылки на объект класса FileStream инициализируется пустым значением. Если файл удастся открыть в конструкторе класса FileStream, то значение переменной fin окажется непустым, а иначе — оно так и останется пустым. Это очень важно, поскольку метод Close () вызывается внутри блока finally только в том случае, если значение переменной fin оказывается непустым. Подобный механизм препятствует любой попытке вызвать метод С lose () для переменной fin, когда она не ссылается на открытый файл. Благодаря своей компактности такой подход часто применяется во многих примерах организации ввода-вывода, приведенных далее в этой книге. Следует, однако, иметь в виду, что он не пригоден в тех случаях, когда ситуацию, возникающую в связи с невозможностью открыть файл, нужно обрабатывать отдельно. Так, если пользователь неправильно введет имя файла, то на экран, возможно, придется вывести приглашение правильно ввести имя файла, прежде чем входить в блок try, где осуществляется проверка правильности доступа к файлу.
В целом, порядок открытия, доступа и закрытия файла зависит от конкретного приложения. То, что хорошо в одном случае, может оказаться неприемлемым в другом. Поэтому данный процесс приходится приспосабливать к конкретным потребностям разрабатываемой программы.
Запись в файл
Длязаписи байта в файл служит метод WriteByte(). Ниже приведена его простейшая форма.
void WriteByte(bytevalue)
Этот метод выполняет запись в файл байта, обозначаемого параметромvalue.Если базовый поток не открывается для вывода, то генерируется исключение NotSupportedException. А если поток закрыт, то генерируется исключение ObjectDisposedException.
Для записи в файл целого массива байтов может быть вызван метод Write О. Ниже приведена его общая форма.
void Write(byte[] array, intoffset,intcount)
В методе Write () предпринимается попытка записать в файл количествоcountбайтов из массиваarray,начиная с элементаarray[offset].Он возвращает количество байтов, успешно записанных в файл. Если во время записи возникает ошибка, то генерируется исключениеIOException.А если базовый поток не открывается для вывода, то генерируется исключениеNotSupportedException.Кроме того, может быть сгенерирован ряд других исключений.
Вам, вероятно, известно, что при выводе в файл выводимые данные зачастую записываются на конкретном физическом устройстве не сразу. Вместо этого они буферизуются на уровне операционной системы до тех пор, пока не накопится достаточный объем данных, чтобы записать их сразу одним блоком. Благодаря этому повышается эффективность системы. Так, на диске файлы организованы по секторам величиной от 128 байтов и более. Поэтому выводимые данные обычно буферизуются до тех пор, пока не появится возможность записать на диск сразу весь сектор. .
Но если данные требуется записать на физическое устройство без предварительного накопления в буфере, то для этой цели можно вызвать методFlush.
void Flush()
При неудачном исходе данной операции генерируется исключениеIOException.Если же поток закрыт, то генерируется исключениеObjectDisposedException.
По завершении вывода в файл следует закрыть его с помощью методаClose ().Этим гарантируется, что любые выведенные данные, оставшиеся в дисковом буфере, будут записаны на диск. В этом случае отпадает необходимость вызывать методFlush() перед закрытием файла.
Ниже приведен простой пример программы, в котором демонстрируется порядок записи данных в файл.
// Записать данные в файл.
using System; using System.10;
class WriteToFile {
static void Main(string[] args) {
FileStream fout = null;
try {
// Открыть выходной файл.
fout = new FileStream("test.txt", FileMode.CreateNew);
// Записать весь английский алфавит в файл, for(char с= 'А';с<= 'Z'; C++)fout.WriteByte((byte) с);
} catch(IOException exc) {
Console .WriteLine (."Ошибка ввода-вывода: \n" + exc .Message) ;
} finally {
if(fout != null) fout.Close();
}
}
}
В данной программе сначала создается выходной файл под названиемtest. txtс помощью перечисляемого значенияFileMode . CreateNew.Это означает, что файл с таким же именем не должен уже существовать. (В противном случае генерируется исключениеIOException.)После открытия выходного файла в него записываются
прописные буквы английского алфавита. По завершении данной программы содержимое файла test. txt оказывается следующим.
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Использование класса FileStream для копирования файла
Преимущество байтового ввода-вывода средствами класса FileS tream заключается, в частности, в том, что его можно применить к файлам практически любого типа, а не только к текстовым файлам. В качестве примера ниже приведена программа, позволяющая копировать файл любого типа, в том числе исполняемый. Имена исходного и выходного файлов указываются в командной строке.
/* Копировать файл.
Чтобы воспользоваться этой программой, укажите имена исходного и выходного файлов. Например, для копирования файла FIR'ST.DAT в файл SECOND.DAT введите в командной строке следующее:
CopyFile FIRST.DAT SECOND.DAT
*/
using System; using System.10;
class CopyFile {
static void Main(string[] args) { int i;
FileStream fin = null;
FileStream fout = null;
if(args.Length != 2) {
Console.WriteLine("Применение: CopyFile Откуда Куда"); return;
}
try { 1
// Открыть файлы.
fin = new FileStream(args[0], FileMode.Open) ; fout = new FileStream(args[1] , FileMode.Create);
// Скопировать файл, do {
i = fin.ReadByte();
if(i != -1) fout.WriteByte((byte)i);
} while (i != -1);
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода:\n" + exc.Message);
} finally {
if(fin != null) fin.Close (); if(fout != null) fout.Close ();
>
Символьный ввод-вывод в файл
Несмотря на то что файлы часто обрабатываются побайтово, для этой цели можно воспользоваться также символьными потоками. Преимущество символьных потоков заключается в том, что они оперируют символами непосредственно в уникоде. Так, если требуется сохранить текст в уникоде, то для этого лучше всего подойдут именно символьные потоки. В целом, для выполнения операций символьного ввода-вывода в файлы объект классаFileStreamзаключается в оболочку классаStreamReaderилиStreamWriter.В этих классах выполняется автоматическое преобразование байтового потока в символьный и наоборот.
Не следует, однако, забывать, что на уровне операционной системы файл представляет собой набор байтов. И применение классаStreamReaderилиStreamWriterникак не может этого изменить.
КлассStreamWriterявляется производным от классаТех tW rite г,а классStreamReader— производным от классаTextReader.Следовательно, в классахStreamReaderиStreamWriterдоступны методы и свойства, определенные в их базовых классах.
Применение класса StreamWriter
Длясоздания символьного потока вывода достаточно заключить объект классаStream,напримерFileStream,в оболочку классаStreamWriter.В классеStreamWriterопределено несколько конструкторов. Ниже приведен едва ли не самый распространенный среди них:
StreamWriter(Streamпоток)
гдепотокобозначает имя открытого потока. Этот конструктор генерирует исключениеArgumentException,еслипотокне открыт для вывода, а также исключениеArgumentNullException,еслипотококазывается пустым. После создания объекта классStreamWriterвыполняет автоматическое преобразование символов в байты.
Ниже приведен простой пример сервисной программы ввода с клавиатуры и вывода на диск набранных текстовых строк, сохраняемых в файлеtest. txt.Набираемый тест вводится до тех пор, пока в нем не встретится строка "стоп". Для символьного вывода в файл в этой программе используется объект классаFileStream,заключенный в оболочку классаStreamWriter.
// Простая сервисная программа ввода с клавиатуры и вывода на диск,
// демонстрирующая применение класса StreamWriter.
using System; using System.10;
class KtoD {
static void Main() { string str;
FileStream fout;
// Открыть сначала поток файлового ввода-вывода, try {
fout = new FileStream("test.txt", FileMode.Create);
catch(IOException exc) {
Console.WriteLine("Ошибка открытия файла:\п" + exc.Message); return ;
}
// Заключить поток файлового ввода-вывода в оболочку класса StreamWriter. StreamWriter fstr_out = new StreamWriter(fout) ;
try {
Console.WriteLine("Введите текст, а по окончании — 'стоп'."); do {
Console.Write (": "); str = Console.ReadLine();
if(str != "стоп") {
str = str + "\r\n"; // добавить новую строку fstr_out.Write(str);
}
} while(str != "стоп");
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода:\n" + exc.Message);
} finally {
fstr_out.Close();
}
}
}
В некоторых случаях файл удобнее открывать средствами самого классаStreamWriter.Для этого служит один из следующих конструкторов:
StreamWriter(stringпуть)
StreamWriter(stringпуть,boolappend)
гдепуть— это имя открываемого файла, включая полный путь к нему. Если во второй форме этого конструктора значение параметраappendравноtrue,то выводимые данные присоединяются в конец существующего файла. В противном случае эти данные перезаписывают содержимое указанного файла. Но независимо от формы конструктора файл создается, если он не существует. При появлении ошибок ввода-вывода в обоих случаях генерируется исключениеIOException.Кроме того, могут быть сгенерированы и другие исключения.
Ниже приведен вариант представленной ранее сервисной программы ввода с клавиатуры и вывода на диск, измененный таким образом, чтобы открывать выходной файл средствами самого классаStreamWriter.
// Открыть файл средствами класса StreamWriter.
using System; using System.10;
class KtoD {
static void Main() { string str;
StreamWriter fstr_out = null; try {
// Открыть файл, заключенный в оболочку класса StreamWriter.
fstr_out = new StreamWriter("test.txt");
Console.WriteLine("Введите текст, а по окончании — 'стоп'."); do {
-Console.Write (": "); str = Console.ReadLine ();
if(str != "стоп") {
str = str + "\r\n"; // добавить новую строку fstr_out.Write(str);
}
} while(str != "стоп");
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода:\n" + exc.Message);
} finally {
if(fstr_out != null) fstr_out.Close() ;
}
}
}
Применение класса StreamReader
Длясоздания символьного потока ввода достаточно заключить байтовый поток в оболочку классаStreamReader.В классеStreamReaderопределено несколько конструкторов. Ниже приведен наиболее часто используемый конструктор:
StreamReader(Streamпоток)
гдепотокобозначает имя открытого потока. Этот конструктор генерирует исключениеArgumentNullException,еслипотококазывается пустым, а также исключениеArgumentException,еслипотокне открыт для ввода. После своего создания объект классаStreamReaderвыполняет автоматическое преобразование байтов в символы. По завершении ввода из потока типаStreamReaderего нужно закрыть. При этом закрывается и базовый поток.
В приведенном ниже примере создается простая сервисная программа ввода с диска и вывода на экран содержимого текстового файлаtest. txt.Она служит дополнением к представленной ранее сервисной программе ввода с клавиатуры и вывода на диск.
// Простая сервисная программа ввода с диска и вывода на экран,
// демонстрирующая применение класса StreamReader.
using System; using System.10;
class DtoS {
static void Main() {
FileStream fin; string s;
try {
fin = new FileStream("test.txt", FileMode.Open);
catch(IOException exc) {
Console.WriteLine("Ошибка открытия файла:\п" + exc.Message); return;
}
StreamReader fstr_in = new StreamReader(fin); try {
while((s = fstr_in.ReadLine()) != null) {
Console.WriteLine(s);
}
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода:\n" + exc.Message);
} finally {
fstr_in.Close();
}
}
}
Обратите внимание на то, как в этой программе определяется конец файла. Когда методReadLine() возвращает пустую ссылку, это означает, что достигнут конец файла. Такой способ вполне работоспособен, но в классеStreamReaderпредоставляется еще одно средство для обнаружения конца потока —EndOfStream.Это доступное для чтения свойство имеет логическое значениеtrue,когда достигается конец потока, в противном случае — логическое значениеfalse.Следовательно, свойствоEndOf Streamможно использовать для отслеживания конца файла. В качестве примера ниже представлен другой способ организации циклаwhileдля чтения из файла.
while(!fstr_in.EndOfStream) { s = fstr_in.ReadLine();
Console.WriteLine(s);
}
В данном случае код немного упрощается благодаря свойствуEndOf Stream,хотя общий порядок выполнения операции ввода из файла не меняется. Иногда такое применение свойстваEndOf Streamпозволяет несколько упростить сложную ситуацию, внося ясность и улучшая структуру кода.
Иногда файл проще открыть, используя непосредственно классStreamReader,аналогично классуStreamWriter.Для этой цели служит следующий конструктор:
StreamReader(stringпуть)
гдепуть— это имя открываемого файла, включая полный путь к нему. Указываемый файл должен существовать. В противном случае генерируется исключениеFileNotFoundException.Еслипутьоказывается пустым, то генерируется исключениеArgumentNullException.А еслипутьсодержит пустую строку, то генерируется исключениеArgumentException.Кроме того, могут быть сгенерированы исключенияIOExceptionиDirectoryNotFoundException.
Переадресация стандартных потоков
Как упоминалось ранее, стандартные потоки, напримерConsole. In,могут быть переадресованы. И чаще всего они переадресовываются в файл. Когда стандартный поток переадресовывается, то вводимые или выводимые данные направляются в новый поток в обход устройств, используемых по умолчанию. Благодаря переадресации стандартных потоков в программе может быть организован ввод команд из дискового файла, создание файлов журнала регистрации и даже чтение входных данных из сетевого соединения.
Переадресация стандартных потоков достигается двумя способами. Прежде всего, это делается при выполнении программы из командной строки с помощью операторов < и >, переадресовывающих потокиConsole . InиConsole . Outсоответственно. Допустим, что имеется следующая программа.
using System;
class Test {
static void Main() {
Console.WriteLine("Это тест.");
}
}
Если выполнить эту программу из командной строкиTest > log
то символьная строка"Это тест."будет выведена в файлlog.Аналогичным образом переадресуется ввод. Но для переадресации ввода указываемый источник входных данных должен удовлетворять требованиям программы, иначе она "зависнет".
Операторы < и >, выполняющие переадресацию из командной строки, не являются составной частью С#, а предоставляются операционной системой. Поэтому если в рабочей среде поддерживается переадресация ввода-вывода, как, например, в Windows, то стандартные потоки ввода и вывода можно переадресовать, не внося никаких изменений в программу. Тем не менее существует другой способ, позволяющий осуществлять переадресацию стандартных потоков под управлением самой программы. Для этого служат приведенные ниже методыSetln (),SetOut ()иSetError (), являющиеся членами классаConsole.
static void Setln(TextReaderновый_поток_ввода)static void SetOut(TextWriterновый_поток_вывода)
static void SetError(TextWriterновый_поток_сообщений_об_ошибках)
Таким образом, для переадресации ввода вызывается методSetln() с указанием требуемого потока. С этой целью может быть использован любой поток ввода, при условии, что он является производным от классаTextReader.А для переадресации вывода вызывается методSetOut() с указанием требуемого потока вывода, который должен быть также производным от классаTextReader.Так, для переадресации вывода в файл достаточно указать объект классаFileStream,заключенный в оболочку классаStreamWriter.Соответствующий пример программы приведен ниже.
// Переадресовать поток Console.Out.
using System; using System.10;
class Redirect {
static void Main() {
StreamWriter log_out = null;
try {
log_out = new StreamWriter("logfile.txt");
// Переадресовать стандартный вывод в файл logfile.txt.
Console.SetOut(log_out);
Console.WriteLine("Это начало файла журнала регистрации.");
for(int i=0; i<10; i++) Console.WriteLine(i);
Console.WriteLine("Это конец файла журнала регистрации.");
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода\п" + exc.Message);
} finally {
if(log_out != null) log_out.Close();
}
}
}
При выполнении этой программы на экран ничего не выводится, но файлlogfile. txt будет содержать следующее.
Это начало файла журнала регистрации.
0
1
2
3
4
5
6
7
8
9
Это конец файла журнала регистрации.
Попробуйте сами поупражняться в переадресации других встроенных потоков.
Чтение и запись двоичных данных
В приведенных ранее примерах демонстрировались возможности чтения и записи байтов или символов. Но ведь имеется также возможность (и ею пользуются часто) читать и записывать другие типы данных. Например, можно создать файл, содержащий данные типаint, doubleилиshort.Для чтения и записи двоичных значений встроенных в C# типов данных служат классы потоковBinaryReader nBinaryWriter.Используя эти потоки, следует иметь в виду, что данные считываются и записываются во внутреннем двоичном формате, а не в удобочитаемой текстовой форме.
Класс BinaryWri ter
КлассBinaryWriterслужит оболочкой, в которую заключается байтовый поток, управляющий выводом двоичных данных. Ниже приведен наиболее часто употребляемый конструктор этого класса:
BinaryWriter(Streamoutput)
гдеoutputобозначает поток, в который выводятся записываемые данные. Для записи в выходной файл в качестве параметраoutputможет быть указан объект, создаваемый средствами классаFileStream.Если же параметрoutputоказывается пустым, то генерируется исключениеArgumentNullException.А если поток, определяемый параметромoutput, небыл открыт для записи данных, то генерируется исключениеArgumentException.По завершении вывода в поток типаBinaryWriterего нужно закрыть. При этом закрывается и базовый поток.
В классеBinaryWriterопределены методы, предназначенные для записи данных всех встроенных в C# типов. Некоторые из этих методов перечислены в табл. 14.5. Обратите внимание на то, что строковые данные типаstringзаписываются во внутреннем формате с указанием длины строки. Кроме того, в классеBinaryWriterопределены стандартные методыClose()иFlush(), действующие аналогично описанному выше.
Таблица 14.5. Наиболее часто используемые методы, определенные в классе BinaryWriter
Метод
Описание
void
Write
(sbytevalue)
Записывает значение типа sbyte со знаком
void
Write
(bytevalue)
Записывает значение типа byte без знака
void
Write
(byte[]buffer)
Записывает массив значений типа byte
void
Write
(shortvalue)
Записывает целочисленное значение типа short (короткое целое)
void
Write
(ushortvalue)
Записывает целочисленное значение типа ushort (короткое целое без знака)
void
Write
(intvalue)
Записывает целочисленное значение типа int
void
Write
(uintvalue)
Записывает целочисленное значение типа uint (целое без знака)
void
Write
(longvalue)
Записывает целочисленное значение типа long (длинное целое)
void
Write
(ulongvalue)
Записывает целочисленное значение типа ulong (длинное целое без знака)
void
Write
(floatvalue)
Записывает значение типа float (с плавающей точкой одинарной точности)
void
Write
(doublevalue)
Записывает значение типа double (с плавающей точкой двойной точности)
void
Write
(decimalvalue)
Записывает значение типа decimal (с двумя десятичными разрядами после запятой)
void
Write
(charch)
Записывает символ
void
Write
(char[]buffer)
Записывает массив символов
void
Write
(stringvalue)
Записывает строковое значение типа string, представленное во внутреннем формате с указа-
Класс BinaryReader служит оболочкой, в которую заключается байтовый поток, управляющий вводом двоичных данных. Ниже приведен наиболее часто употребляемый конструктор этого класса:
BinaryReader(Streaminput)
гдеinputобозначает поток, из которого вводятся считываемые данные. Для чтения из входного файла в качестве параметраinputможет быть указан объект, создаваемый средствами классаFileStream.Если же поток, определяемый параметромinput, не был открыт для чтения данных или оказался недоступным по иным причинам, то генерируется исключениеArgumentException.По завершении ввода из потока типаBinaryReaderего нужно закрыть. При этом закрывается и базовый поток.
В классеBinaryReaderопределены методы, предназначенные для чтения данных всех встроенных в C# типов. Некоторые из этих методов перечислены в табл. 14.6. Следует, однако, иметь в виду, что в методеReadstring() считывается символьная строка, хранящаяся во внутреннем формате с указанием ее длины. Все методы данного класса генерируют исключениеIOException,если возникает ошибка ввода. Кроме того, могут быть сгенерированы и другие исключения.
Таблица 14.6. Наиболее часто используемые методы, определенные в классе BinaryReader
Метод
Описание
bool ReadBoolean ()
Считывает значение логического типа bool
byte ReadByteO
Считывает значение типа byte
sbyte ReadSByteO
Считывает значение типа sbyte
byte[] ReadBytes(intcount)
Считывает количество count байтов и возвращает их в виде массива
char ReadCharO
Считывает значение типа char
char[] ReadChars(intcount)
Считывает количество count символов и возвращает их в виде массива
decimal ReadDecimal()
Считывает значение типа decimal
double ReadDoubleO
Считывает значение типа double
float ReadSingleO
Считывает значение типа float
short Readlntl6()
Считывает значение типа short
int Readlnt32()
Считывает значение типа int
long Readlnt64()
Считывает значение типа long
ushort ReadUIntl6()
Считывает значение типа ushort
uint ReadUInt32()
Считывает значение типа uint
ulong ReadUInt64()
Считывает значение типа ulong
string ReadStringO
Считывает значение типа string, представленное во внутреннем двоичном формате с указанием длины строки. Этот метод следует использовать для считывания строки, которая была записана средствами класса BinaryWriter
В классеBinaryWriterопределены также три приведенных ниже варианта методаRead ().
При неудачном исходе операции чтения эти методы генерируют исключениеIOException.Кроме того, в классеBinaryReaderопределен стандартный метод
Close ().
Метод
Описание
int
Read()
Возвращает целочисленное представление следующего доступного символа из вызывающего потока ввода. При об
-
наружении конца файла возвращает значение -1
int
Read(byte []buffer,
Делает попытку прочитать количество count байтов в
int
offset,intcount)
массив buffer, начиная с элемента buffer[offset], и возвращает количество успешно считанных байтов
int
Read(char[]buffer,
Делает попытку прочитать количество count символов
int
offset,intcount)
в массив buffer, начиная с элемента buffer[offset], и возвоашает количество успешно считанных символов
Демонстрирование двоичного ввода-вывода
Ниже приведен пример программы, в котором демонстрируется применение классовBinaryReaderиBinaryWriterдля двоичного ввода-вывода. В этой программе в файл записываются и считываются обратно данные самых разных типов.
// Записать двоичные данные, а затем считать их обратно.
using System; using System.10;
class RWData {
static void Main() {
BinaryWriter dataOut;
BinaryReader dataln;
int i = 10;
double d = 1023.56;
bool b = true;
string str = "Это тест";
// Открыть файл для вывода, try {
dataOut = new
'BinaryWriter(new FileStream("testdata", FileMode.Create));
}
catch(IOException exc) {
Console.WriteLine("Ошибка открытия файла:\п" + exc.Message); return;
}
// Записать данные в файл, try {
Console.WriteLine("Запись " + i) ; dataOut.Write(i);
Console.WriteLine("Запись " + d) ; dataOut.Write(d);
Console.WriteLine("Запись " + b); dataOut.Write(b);
Console.WriteLine("Запись " + 12.2 * 7.4); dataOut.Write(12.2 * 7.4);
Console.WriteLine("Запись " + str); dataOut.Write(str);
}
catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода:\n" + exc.Message);
} finally {
dataOut.Close();
}
Console.WriteLine();
//А теперь прочитать данные из файла, try {
dataln = new
BinaryReader(new FileStream("testdata", FileMode.Open));
}
catch(IOException exc) {
Console.WriteLine("Ошибка открытия файла:\п" + exc.Message) return;
}
try {
i = dataln.Readlnt32();
Console.WriteLine("Чтение " + i); d = dataln.ReadDouble();
Console.WriteLine("Чтение " + d); b = dataln.ReadBoolean();
Console.WriteLine("Чтение " + b); d = dataln.ReadDouble();
Console.WriteLine("Чтение " + d); str = dataln.ReadString();
Console.WriteLine("Чтение " + str);
}
catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода:\n" + exc.Message);
} finally {
dataln.Close ();
}
}
}
Вот к какому результату приводит выполнение этой программы.
Запись 10 Запись 1023.56 Запись True Запись 90.28 Запись Это тест
Чтение 10 Чтение 1023.56 Чтение True Чтение 90.28 Чтение Это тест
Если просмотреть содержимое файлаtestdata,который получается при выполнении этой программы, то можно обнаружить, что он содержит данные в двоичной, а не в удобочитаемой текстовой форме.
Далее следует более практический пример, демонстрирующий, насколько эффективным может быть двоичный ввод-вывод. Для учета каждого предмета хранения на складе в приведенной ниже программе сначала запоминается наименование предмета, имеющееся в наличии, количество и стоимость, а затем пользователю предлагается ввести наименование предмета, чтобы найти его в базе данных. Если предмет найден, отображаются сведения о его запасах на складе.
/* Использовать классы BinaryReader и BinaryWriter для
реализации простой программы учета товарных запасов. */
using System; using System.10;
class Inventory {
static void Main() {
BinaryWriter dataOut;
BinaryReader dataln;
string item; // наименование предмета
int onhand; // имеющееся в наличии количество
double cost; // цена
try {
dataOut = new
BinaryWriter(new FileStream("inventory.dat", FileMode.Create));
}
catch(IOException exc) {
Console.WriteLine("He удается открыть файл " +
"товарных запасов для вывода");
Console.WriteLine("Причина: " + exc.Message); return;
}
// Записать данные о товарных запасах в файл, try {
dataOut.Write("Молотки"); dataOut.Write(10); dataOut.Write(3.95);
dataOut.Write("Отвертки"); dataOut.Write(18); dataOut.Write(1.50);
dataOut.Write("Плоскогубцы"); dataOut.Write(5);
dataOut.Write (4.95);
dataOut.Write("Пилы"); dataOut.Write (8); dataOut.Write(8.95);
}
catch(IOException exc) {
Console.WriteLine("Ошибка записи в файл товарных запасов");
Console.WriteLine("Причина: " + exc.Message);
} finally {
dataOut.Close();
}
Console.WriteLine() ;
// А теперь открыть файл товарных запасов для чтения, try {
dataln = new
BinaryReader(new FileStream("inventory.dat", FileMode.Open));
}
catch(IOException exc) {
Console.WriteLine("He удается открыть файл " +
"товарных запасов для ввода");
Console.WriteLine("Причина: " + exc.Message); return;
}
// Найти предмет, введенный пользователем.
Console.Write("Введите наименование для поиска: "); string what = Console.ReadLine() ;
Console.WriteLine();
try {
for (;;) {
// Читать данные о предмете хранения, item = dataln.ReadString() ; onhand = dataln.Readlnt32() ; cost = dataln.ReadDouble();
// Проверить, совпадает ли он с запрашиваемым предметом.
// Если совпадает, то отобразить сведения о нем.
if(item.Equals(what, StringComparison.OrdinallgnoreCase)) {
Console.WriteLine(item + ": " + onhand + " штук в наличии. " +
"Цена: {0:С} за штуку", cost);
Console.WriteLine("Общая стоимость по наименованию <{0}>: {1:С}.", item, cost * onhand);
break;
}
}
}
catch(EndOfStreamException) {
Console.WriteLine("Предмет не найден.");
catch(IOException exc) {
Console.WriteLine("Ошибка чтения из файла товарных запасов");
Console.WriteLine("Причина: " + exc.Message);
} finally {
dataln.Close();
} '
}
}
Выполнение этой программы может привести, например, к следующему результату.
Введите наименование для поиска: Отвертки
Отвертки: 18 штук в наличии. Цена: $1.50 за штуку.
Общая стоимость по наименованию <Отвертки>: $27.00.
Обратите внимание на то, что сведения о товарных запасах сохраняются в этой программе в двоичном формате, а не в удобной для чтения текстовой форме. Благодаря этому обработка числовых данных может выполняться без предварительного их преобразования из текстовой формы.
Обратите также внимание на то, как в этой программе обнаруживается конец файла. Методы двоичного ввода генерируют исключениеEndOf StreamExceptionпо достижении конца потока, и поэтому файл читается до тех пор, пока не будет найден искомый предмет или сгенерировано данное исключение. Таким образом, для обнаружения конца файла никакого специального механизма не требуется.
Файлы с произвольным доступом
В предыдущих примерах использовалисьпоследовательные файлы, т.е. файлы со строго линейным доступом, байт за байтом. Но доступ к содержимому файла может быть и произвольным. Для этого служит, в частности, метод Seek (), определенный в классе FileStream. Этот метод позволяет установитьуказатель положения в файле, или так называемыйуказатель файла, на любое место в файле. Ниже приведена общая форма метода Seek ():
long Seek(longoffset,SeekOriginorigin)
гдеoffsetобозначает новое положение указателя файла в байтах относительно заданного начала отсчета(origin).В качествеoriginможет быть указано одно из приведенных ниже значений, определяемых в перечисленииSeekOrigin.
Значение
Описание
SeekOrigin.Begin
Поиск от начала файла
SeekOrigin.Current
Поиск от текущего положения
SeekOrigin.End
Поиск от конца файла
Следующая операция чтения или записи после вызова методаSeek() будет выполняться, начиная с нового положения в файле, возвращаемого этим методом. Если во время поиска в файле возникает ошибка, то генерируется исключениеIOException.Если же запрос положения в файле не поддерживается базовым потоком, то генерируется исключениеNotSupportedException.Кроме того, могут быть сгенерированы и другие исключения.
В приведенном ниже примере программы демонстрируется ввод-вывод в файл с произвольным доступом. Сначала в файл записываются прописные буквы английского алфавита, а затем его содержимое считывается обратно в произвольном порядке.
// Продемонстрировать произвольный доступ к файлу.
using System; using System.10;
class RandomAccessDemo { static void Main() {
FileStream f = null; char ch;
try {
f = new FileStream("random.dat", FileMode.Create);
// Записать английский алфавит в файл, for (int i=0; i < 26; i++) f.WriteByte((byte)('A'+i));
//А теперь считать отдельные буквы английского алфавита. f.Seek(0, SeekOrigin.Begin); // найти первый байт ch = (char) f. ReadByte () ;
Console.WriteLine("Первая буква: " + ch) ;
f.Seek(l, SeekOrigin.Begin); // найти второй байт ch = (char) f. ReadByte () ;
Console.WriteLine("Вторая буква: " + ch);
f.Seek(4, SeekOrigin.Begin); // найти пятый байт ch = (char) f.ReadByte() ;
Console.WriteLine("Пятая буква: " + ch) ;
Console.WriteLine() ;
//А теперь прочитать буквы английского алфавита через одну.
Console.WriteLine("Буквы алфавита через одну: "); for(int i=0; i < 26; i += 2) {
f.Seek(i, SeekOrigin.Begin); // найти i-й символ ch = (char) f.ReadByte() ;
Console.Write(ch + " ") ;
}
}
catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода\п" + exc.Message);
'} finally {
if(f != null) f.Close();
}
Console.WriteLine() ;
}
}
При выполнении этой программы получается следующий результат.
Перзая буква: А Вторая буква: В Пятая буква: Е
Буквы алфавита, через одну:
АСЕ G‘I KMOQSUWY
Несмотря на то что методSeek() имеет немало преимуществ при использовании с файлами, существует и другой способ установки текущего положения в файле с помощью свойстваPosition.Как следует из табл.14.2,свойствоPositionдоступно как для чтения, так и для записи. Поэтому с его помощью можно получить или же установить текущее положение в файле. В качестве примера ниже приведен фрагмент кода из предыдущей программы записи и чтения из файла с произвольным доступомrandom.dat,измененный с целью продемонстрировать применение свойстваPosition.
Console.WriteLine("Буквы алфавита через одну: "); for(int i=0; i < 26; i += 2) {
f.Position = i; // найти i-й символ посредством свойства Position ch = (char) f.ReadByte();
Console.Write(ch + " ");
}
Применение класса MemoryStream
Иногда оказывается полезно читать вводимые данные из массива или записывать выводимые данные в массив, а не вводить их непосредственно из устройства или выводить прямо на него. Для этой цели служит классMemoryStream.Он представляет собой реализацию классаStream,в которой массив байтов используется для ввода и вывода. В классеMemoryStreamопределено несколько конструкторов. Ниже представлен один из них:
MemoryStream(byte[ ]buffer)
гдеbufferобозначает массив байтов, используемый в качестве источника или адресата в запросах ввода-вывода. Используя этот конструктор, следует иметь в виду, что массивbufferдолжен быть достаточно большим для хранения направляемых в него данных.
В качестве примера ниже приведена программа, демонстрирующая применение классаMemoryStreamв операциях ввода-вывода.
// Продемонстрировать применение класса MemoryStream.
using System; using System.10;
class MemStrDemo { static void Main() {
byte[] storage = new byte[255];
// Создать запоминающий поток.
MemoryStream memsttm = new MemoryStream(storage);
// чтения и записи данных в потоки.
StreamWriter memwtr = new StreamWriter(memstrm);
StreamReader memrdr = new StreamReader(memstrm);
try {
// Записать данные в память, используя объект memwtr. for(int i=0; i < 10; i++)
memwtr.WriteLine("byte [" + i + "]: " + i);
// Поставить в конце точку, memwtr.WriteLine(".");
memwtr.Flush() ;
Console.WriteLine("Чтение прямо из массива storage: ");
// Отобразить содержимое массива storage непосредственно. foreach(char ch in storage) { if (ch == '.') break;
Console.Write(ch);
}
Console.WriteLine("ХпЧтение из потока с помощью объекта memrdr: ");
// Читать из объекта memstrm средствами ввода данных из потока, memstrm.Seek(0, SeekOrigin.Begin); // -установить указатель файла
// в исходное положение
string str = memrdr.ReadLine() ; while(str != null) {
str = memrdr.ReadLine() ; if(str[0] == '.') break;
Console.WriteLine(str) ;
}
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода\п" + exc.Message);
} finally {
// Освободить ресурсы считывающего и записывающего потоков, memwtr.Close(); memrdr.Close() ;
}
}
}
Вот к какому результату приводит выполнение этой программы.
Чтение прямо из массива storage:
byte [0]: 0
byte [1]: 1
byte [2]: 2
byte [3]: 3
byte [4] : 4
byte [5] : 5
byte [6]: 6
byte [7]: 7
byte [8] : 8
byte [9]: 9
Чтение из потока с помощью объекта memrdr:
byte [1]: 1
byte [2]: 2
byte [3]: 3
byte [4]: 4
byte [5]: 5
byte [6]: 6
byte [7]: 7
byte [8]: 8
byte [9]: 9
В этой программе сначала создается массив байтов, называемыйstorage.Затем этот массив используется в качестве основной памяти для объектаmemstrmклассаMemoryStream.Из объектаmemstrm,в свою очередь, создаются объектыmemrdrклассаStreamReaderиmemwtrклассаStreamWriter.С помощью объектаmemwtrвыводимые данные записываются в запоминающий поток. Обратите внимание на то, что после записи выводимых данных для объектаmemwtrвызывается методFlush (). Это необходимо для того, чтобы содержимое буфера этого объекта записывалось непосредственно в базовый массив. Далее содержимое базового массива байтов отображается вручную в циклеfor each.После этого указатель файла устанавливается с помощью методаSeek() в начало запоминающего потока, из которого затем вводятся данные с помощью объекта потокаmemrdr.
Запоминающие потоки очень полезны для программирования. С их помощью можно, например, организовать сложный вывод с предварительным накоплением данных в массиве до тех пор, пока они не понадобятся. Этот прием особенно полезен для программирования в такой среде с графическим пользовательским интерфейсом, как Windows. Кроме того, стандартный поток может быть переадресован из массива. Это может пригодиться, например, для подачи тестовой информации в программу.
Применение классов StringReader и StringWriter
Длявыполнения операций ввода-вывода с запоминанием в некоторых приложениях в качестве базовой памяти иногда лучше использовать массив типаstring,чем массив типаbyte.Именно для таких случаев и предусмотрены классыStringReaderиStringWriter.В частности, классStringReaderнаследует от классаTextReader,а классStringWriter— от классаTextWriter.Следовательно, они представляют собой потоки, имеющие доступ к методам, определенным в этих двух базовых классах, что позволяет, например, вызывать методReadLine() для объекта классаStringReader,а методWriteLine() — для объекта классаStringWriter.
Ниже приведен конструктор классаStringReader:
StringReader(strings)
где s обозначает символьную строку, из которой производится чтение.
В классеStringWriterопределено несколько конструкторов. Ниже представлен один из наиболее часто используемых.
StringWriter()
Этот конструктор создает записывающий поток, который помещает выводимые данные в строку. Для получения содержимого этой строки достаточно вызвать методToString().
Ниже приведен пример, демонстрирующий применение классовStringReaderиStringWriter.
// // Продемонстрировать применение классов StringReader и StringWriter.
using System; using System.10;
class StrRdrWtrDemo { static void Main() {
StringWriter strwtr = null;
StringReader strrdr = null;
try {
// Создать объект класса StringWriter. strwtr = new StringWriter();
// Вывести данные в записывающий поток типа StringWriter. for (int i=0; i < 10; i++)
strwtr.WriteLine("Значение i равно: " + i);
// Создать объект класса StringReader. strrdr = new StringReader(strwtr.ToString());
//А теперь ввести данные из считывающего потока типа StringReader. string str = strrdr.ReadLine(); while(str != null) {
str = strrdr.ReadLine();
Console.WriteLine(str);
}
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода\п" + exc.Message);
} finally {
// Освободить ресурсы считывающего и записывающего потоков, if(strrdr != null) strrdr.Close(); if(strwtr != null) strwtr.Close ();
}
}
}
Вот к каком результату приводит выполнение этого кода.
Значение i равно: 1 Значение i равно: 2 Значение i равно: 3 Значение i равно: 4 Значение i равно: 5 Значение i равно: 6 Значение i равно: 7 Значение i равно: 8 Значение i равно: 9
В данном примере сначала создается объектstrwtrклассаStringWriter,Bкоторый выводятся данные с помощью методаWriteLine().Затем создается объект классаStringReaderс использованием символьной строки, содержащейся в объектеstrwtr.Эта строка получается в результате вызова методаToString() для объектаstrwtr.И наконец, содержимое данной строки считывается с помощью методаReadLine ().
Класс File
В среде .NET Framework определен класс File, который может оказаться полезным для работы с файлами, поскольку он содержит несколько статических методов, выполняющих типичные операции над файлами. В частности, в классе File имеются методы для копирования и перемещения, шифрования и расшифровывания, удаления файлов, а также для получения и задания информации о файлах, включая сведения об их существовании, времени создания, последнего доступа и различные атрибуты файлов (только для чтения, скрытых и пр.). Кроме того, в классе File имеется ряд удобных методов для чтения из файлов и записи в них, открытия файла и получения ссылки типа FileStream на него. В классе File содержится слишком много методов для подробного их рассмотрения, поэтому мы уделим внимание только трем из них. Сначала будет представлен методСору(), а затем — методы Exists () и GetLastAccessTime (). На примере этих методов вы сможете получить ясное представление о том, насколько удобны методы, доступные в классе File. И тогда вам станет ясно, что класс File определенно заслуживает более тщательного изучения.
СОВЕТ
Ряд методов для работы с файлами определен также в классе Filelnfo. Этот класс отличается от класса File одним, очень важным преимуществом: для операций над файлами он предоставляет методы экземпляра и свойства, а не статические методы. Поэтому для выполнения нескольких операций над одним и тем же файлом лучше воспользоваться классом Filelnfo.
Копирование файлов с помощью метода Сору ()
Ранее в этой главе демонстрировался пример программы, в которой файл копировался вручную путем чтения байтов из одного файла и записи в другой. И хотя задача копирования файлов не представляет особых трудностей, ее можно полностью автоматизировать с помощью метода Сору (), определенного в классеFile.Ниже представлены две формы его объявления.
static void Copy (stringимя_исходного_файла,stringимя_целевого_файла)static void Copy (stringимя_исходного_файла,stringимя_целевого_файла,booleanoverwrite)
МетодCopy() копирует файл, на который указываетимя_исходного_файла,в файл, на который указываетимя_целевого_файла.В первой форме данный метод копирует файл только в том случае, если файл, на который указываетимя_целево-го_файла,еще не существует. А во второй форме копия заменяет и перезаписывает целевой файл, если он существует и если параметрoverwri teпринимает логическое значениеtrue.Но в обоих случаям может быть сгенерировано несколько видов исключений, включаяIOExceptionиFileNotFoundException.
В приведенном ниже примере программы метод Сору () применяется для копирования файла. Имена исходного и целевого файлов указываются в командной строке. Обратите внимание, насколько эта программа короче демонстрировавшейся ранее. Кроме того, она более эффективна.
/* Скопировать файл, используя метод File.CopyO.
Чтобы воспользоваться этой программой, укажите имя исходного и целевого файлов. Например, чтобы скопировать файл FIRST.DAT в файл SECOND.DAT, введите в командной строке следующее:
CopyFile FIRST.DAT SECOND.DAT
*/
using System; using System.10;
class CopyFile {
static void Main(string[ ] args) { if (args.Length != 2) {
Console.WriteLine("Применение: CopyFile Откуда Куда"); return;
}
// Копировать файлы, try {
File.Copy(args[0], args[l]);
} catch(IOException exc) {
Console.WriteLine("Ошибка копирования файла\п" + exc.Message);
}
}
}
^Сак видите, в этой программе не нужно создавать поток типаFileStreamили освобождать его ресурсы. Все это делается в методе Сору () автоматически. Обратите также внимание на то, что в данной программе существующий файл не перезаписывается. Поэтому если целевой файл должен быть перезаписан, то для* этой цели лучше воспользоваться второй из упоминавшихся ранее форм метода Сору ().
Применение методов Exists () и GetLastAccessTime ()
С помощью методов классаFileочень легко получить нужные сведения о файле. Рассмотрим два таких метода:Exists () иGetLastAccessTime (). МетодExists ()определяет, существует ли файл, а методGetLastAccessTime() возвращает дату и время последнего доступа к файлу. Ниже приведены формы объявления обоих методов.
static bool Exists(stringпуть)
static DateTime GetLastAccessTime(stringпуть)
В обоих методахпутьобозначает файл, сведения о котором требуется получить. МетодExists() возвращает логическое значениеtrue,если файл существует и доступен для вызывающего процесса. А методGetLastAccessTime() возвращает структуруDateTime,содержащую дату и время последнего доступа к файлу. (Структура
DateTimeописывается далее в этой книге, но методToString() автоматически приводит дату и время к удобочитаемому виду.) С указанием недействительных аргументов или прав доступа при вызове обоих рассматриваемых здесь методов может быть связан целый ряд исключений, но в действительности генерируется только исключениеIOException.
В приведенном ниже примере программы методыExists() иGetLastAccessTime() демонстрируются в действии. В этой программе сначала определяется, существует ли файл под названиемtest. txt.Если он существует, то на экран выводит время последнего доступа к нему.
// Применить методы Exists () и GetLastAccessTime() .
using System; using System.10;
class ExistsDemo { static void Main() {
if(File.Exists("test.txt"))
Console.WriteLine("Файл существует. В последний раз он был доступен " + File.GetLastAccessTime("test.txt"));
else
Console.WriteLine("Файл не существует");
}
}
Кроме того, время создания файла можно выяснить, вызвав методGetCreationTime (),а время последней записи в файл, вызвав методGetLastWriteTime (). Имеются также варианты этих методов для представления данных о файле в формате всеобщего скоординированного времени (UTC). Попробуйте поэкспериментировать с ними.
Преобразование числовых строк в их внутреннее представление
Прежде чем завершить обсуждение темы ввода-вывода, рассмотрим еще один способ, который может пригодиться при чтении числовых строк. Как вам должно быть уже известно, методWriteLineOпредоставляет удобные средства для вывода различных типов данных на консоль, включая и числовые значения встроенных типов, напримерintилиdouble.При этом числовые значения автоматически преобразуются методомWriteLineOв удобную для чтения текстовую форму. В то же время аналогичный метод ввода для чтения и преобразования строк с числовыми значениями в двоичный формат их внутреннего представления не предоставляется. В частности, отсутствует вариант методаRead() специально для чтения строки "100", введенной с клавиатуры, и автоматического ее преобразования в соответствующее двоичное значение, которое может быть затем сохранено в переменной типаint.Поэтому данную задачу приходится решать другими способами. И самый простой из них — воспользоваться методомParse (), определенным для всех встроенных числовых типов данных.
Прежде всего необходимо отметить следующий важный факт: все встроенные в C# типы данных, напримерintилиdouble,на самом деле являются не более чемпсевдонимами(т.е. другими именами) структур, определяемых в среде .NET Framework. В действительности тип в C# невозможно отличить от типа структуры в среде .NET Framework, поскольку один просто носит имя другого. В C# для поддержки значений простых типов используются структуры, и поэтому для типов этих значений имеются специально определенные члены структур.
Ниже приведены имена структур .NET и их эквиваленты в виде ключевых слов C# для числовых типов данных.
Имя структуры в .NET
Имя типа данных в C#
Decimal
decimal
Double
double
Single
float
In 116
short
Int32
int
Int64
long
Ulntl6
ushort
UInt32
uint
Uint64
ulong
Byte
byte
Sbyte
sbyte
Эти структуры определены в пространстве именSystem.Следовательно, имя структурыInt32полностью определяется какSystem. Int32.Эти структуры предоставляют обширный ряд методов, помогающих полностью интегрировать значения простых типов в иерархию объектов С#. А кроме тоГо, в числовых структурах определяется статический методParse(), преобразующий числовую строку в соответствующий двоичный эквивалент.
Существует несколько перегружаемых форм методаParse(). Ниже приведены его простейшие варианты для каждой числовой структуры. Они выполняют преобразование с учетом местной специфики представления чисел. Следует иметь в виду, что каждый метод возвращает двоичное значение, соответствующее преобразуемой строке.
Структура Метод преобразования
Decimal
static
decimal Parse(strings)
Double
static
double Parse(strings)
Single
static
float Parse(strings)
Int 64
static
long Parse (strings)
Int32
static
int Parse(strings)
I n 116
static
short Parse(strings)
Uint64
static
ulong Parse(strings)
UInt32
static
uint Parse(strings)
Ulntl6
static
ushort Parse(strings)
Byte
static
byte Parse(strings)
Sbyte
static
sbyte Parse(strings)
Приведенные выше варианты методаParse() генерируют исключениеFormatException,если строкаsне содержит допустимое число, определяемое вызывающим типом данных. А если она содержит пустое значение, то генерируется исключениеArgumentNullException.Когда же значение в строкеsпревышает допустимый диапазон чисел для вызывающего типа данных, то генерируется исключениеOverflowException.
Методы синтаксического анализа позволяют без особого труда преобразовать числовое значение, введенное с клавиатуры или же считанное из текстового файла в виде строки, в соответствующий внутренний формат. В качестве примера ниже приведена программа, в которой усредняется ряд чисел, вводимых пользователем. Сначала пользователю предлагается указать количество усредняемых значений, а затем это количество считывается методомReadLine() и преобразуется из строки в целое число методомInt32.Parse(). Далее вводятся отдельные значения, преобразуемые методомDouble. Parse() из строки в их эквивалент типаdouble.
// Эта программа усредняет ряд чисел, вводимых пользователем.
using System; using System.10;
class AvgNums {
static void Main() { string str; int n;
double sum = 0.0; double avg, t;
Console.Write("Сколько чисел вы собираетесь ввести: "); str = Console.ReadLine (); try {
n = Int32.Parse(str);
} catch(FormatException exc) {
Console.WriteLine(exc.Message); return;
} catch(OverflowException exc) {
Console.WriteLine(exc.Message); return;
}
Console.WriteLine("Введите " + n + " чисел."); for (int i=0; i < n ; i++) {
Console.Write(": "); str = Console.ReadLine (); try {
t = Double.Parse(str);
} catch(FormatException exc) {
Console.WriteLine(exc.Message) ; t = 0.0;
} catch(OverflowException exc) {
Console.WriteLine(exc.Message) ; t = 0;
}
sum += t;
}
avg = sum / n;
Console.WriteLine("Среднее равно " + avg);
Выполнение этой программы может привести, например, к следующему результату.
Сколько чисел вы собираетесь ввести: 5 Введите 5 чисел.
: 1.1 : 2.2 : 3.3 : 4.4 : 5.5
Среднее равно 3.3
Следует особо подчеркнуть, что для каждого преобразуемого значения необходимо выбирать подходящий метод синтаксического анализа. Так, если попытаться преобразовать строку, содержащую значение с плавающей точкой, методомInt32 . Parse (), то искомый результат, т.е. числовое значение с плавающей точкой, получить не удастся.
Как пояснялось выше, при неудачном исходе преобразования методParse() сгенерирует исключение. Для того чтобы избежать генерирования исключений при преобразовании числовых строк, можно воспользоваться методомTryParse (), определенным для всех числовых структур. В качестве примера ниже приведен один из вариантов методаTryParseO,определяемых в структуреInt 32:
static bool TryParse(strings,out intрезультат)
гдеsобозначает числовую строку, передаваемую данному методу, который возвращает соответствующийрезультатпосле преобразования с учетом выбираемой по умолчанию местной специфики представления чисел. (Конкретную местную специфику представления чисел с учетом региональных стандартов можно указать в другом варианте данного метода.) При неудачном исходе преобразования, например, когда параметрsне содержит числовую строку в надлежащей форме, методTryParse() возвращает логическое значениеfalse.В противном случае он возвращает логическое значениеtrue.Следовательно, значение, возвращаемое этим методом, обязательно следует проверить, чтобы убедиться в удачном (или неудачном) исходе преобразования.
ГЛАВА 15 Делегаты, события и лямбда-выражения
В этой главе рассматриваются три новых средства С#: делегаты, события и лямбда-выражения.Делегатпредоставляет возможность инкапсулировать метод, асобытиеуведомляет о том, что произошло некоторое действие. Делегаты и события тесно связаны друг с другом, поскольку событие основывается на делегате. Оба средства расширяют круг прикладных задача, решаемых при программировании на С#. Алямбда-выражениепредставляет собой новое синтаксическое средство, обеспечивающее упрощенный, но в то же время эффективный способ определения того, что по сути является единицей исполняемого кода. Лямбда-выражения обычно служат для работы с делегатами и событиями, поскольку делегат может ссылаться на лямбда-выражение. (Кроме того, лямбда-выражения очень важны для языка LINQ, описываемого в главе 19.) В данной главе рассматриваются также анонимные методы, ковариантность, контравариантность и групповые преобразования методов.
Делегаты
Начнем с определения понятия делегата. Попросту говоря,делегатпредставляет собой объект, который может ссылаться на метод. Следовательно, когда создается делегат, то в итоге получается объект, содержащий ссылку на метод.
Более того, метод можно вызывать по этой ссылке. Иными словами, делегат позволяет вызывать метод, на который он ссылается. Ниже будет показано, насколько действенным оказывается такой принцип.
Следует особо подчеркнуть, что один и тот же делегат может быть использован для вызова разных методов во время выполнения программы, для чего достаточно изменить метод, на который ссылается делегат. Таким образом, метод, вызываемый делегатом, определяется во время выполнения, а не в процессе компиляции. В этом, собственно, и заключается главное преимущество делегата.
ПРИМЕЧАНИЕ
Если у вас имеется опыт программирования на C/C++, то вам полезно будет знать, что делегат в C# подобен указателю на функцию в C/C++.
Тип делегата объявляется с помощью ключевого слова delegate. Ниже приведена общая форма объявления делегата:
delegateвозвращаемый_тип имя(список_параметров);
гдевозвращаемый_типобозначает тип значения, возвращаемого методами, которые будут вызываться делегатом;имя— конкретное имя делегата;список_параметров —параметры, необходимые для методов, вызываемых делегатом. Как только будет создан экземпляр делегата, он может вызывать и ссылаться на те методы, возвращаемый тип и параметры которых соответствуют указанным в объявлении делегата.
Самое главное, что делегат может служить для вызовалюбогометода с соответствующей сигнатурой и возвращаемым типом. Более того, вызываемый метод может быть методом экземпляра, связанным с отдельным объектом, или же статическим методом, связанным с конкретным классом. Значение имеет лишь одно: возвращаемый тип и сигнатура метода должны быть согласованы с теми, которые указаны в объявлении делегата.
Длятого чтобы показать делегат в действии, рассмотрим для начала простой пример его применения.
// Простой пример применения делегата.
using System;
// Объявить тип делегата, delegate string StrMod(string str);
class DelegateTest {
// Заменить пробелы дефисами.
static string ReplaceSpaces(string s) {
Console.WriteLine("Замена пробелов дефисами."); return s.Replace(' ', '-');
}
// Удалить пробелы.
static string RemoveSpaces(string s) { string temp = ""; int i;
Console.WriteLine("Удаление пробелов."); for(i=0; i < s.Length; i++) if(s[i] != ' ') temp += s[i];
return temp;
}
// Обратить^строку. static string Reverse(string s) { string temp = ""; int i, j;
Console.WriteLine("Обращение строки. ") ; for(j=0, i=s.Length-1; i >= 0; i—, j++) temp += s[i];
return temp;
}
static void Main() {
// Сконструировать делегат.
StrMod strOp = new StrMod(ReplaceSpaces) ; string str;
// Вызвать методы с помощью делегата, str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine();
strOp = new StrMod(RemoveSpaces); str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine();
strOp = new StrMod(Reverse); str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
}
}
Вот к какому результату приводит выполнение этого кода.
Замена пробелов дефисами.
Результирующая строка: Это-простой-тест.
Удаление пробелов.
Результирующая строка: Этопростойтест.
Обращение строки.
Результирующая строка: .тсет йотсорп отЭ
Рассмотрим данный пример более подробно. В его коде сначала объявляется делегатStrModтипаstring,как показано ниже.
delegate string StrMod(string str);
Как видите, делегатStrModпринимает один параметр типаstringи возвращает одно значение того же типа.
Далее в классе DelegateTest объявляются три статических метода с одним параметром типа string и возвращаемым значением того же типа. Следовательно, они соответствуют делегату StrMod. Эти методы видоизменяют строку в той или иной форме. Обратите внимание на то, что в методе Rep la се Spaces () для замены пробелов дефисами используется один из методов типа string — Replace ().
В методеMain() создается переменная экземпляраstrOpссылочного типаStrModи затем ей присваивается ссылка на методReplaceSpaces (). Обратите особое внимание на следующую строку кода.
StrMod strOp = new StrMod(ReplaceSpaces);
В этой строке методReplaceSpaces() передается в качестве параметра. При этом указывается только его имя, но не параметры. Данный пример можно обобщить: при получении экземпляра делегата достаточно указать только имя метода, на который должен ссылаться делегат. Ясно, что сигнатура метода должна совпадать с той, что указана в объявлении делегата. В противном случае во время компиляции возникнет ошибка.
Далее методReplaceSpaces() вызывается с помощью экземпляра делегатаstrOp,как показано ниже.
str = strOp("Это простой тест.");
Экземпляр делегатаstrOpссылается на методReplaceSpaces (), и поэтому вызывается именно этот метод.
Затем экземпляру делегатаstrOpприсваивается ссылка на методRemoveSpaces (),и с его помощью вновь вызывается указанный метод — на этот разRemoveSpaces ().
И наконец, экземпляру делегатаstrOpприсваивается ссылка на методReverse().А в итоге вызывается именно этот метод.
Главный вывод из данного примера заключается в следующем: в тот момент, когда происходит обращение к экземпляру делегатаstrOp,вызывается метод, на который он ссылается. Следовательно, вызов метода разрешается во время выполнения, а не в процессе компиляции.
Групповое преобразование делегируемых методов
Еще в версии C# 2.0 было внедрено специальное средство, существенно упрощающее синтаксис присваивания метода делегату. Это так называемоегрупповое преобразование методов,позволяющее присвоить имя метода делегату, не прибегая к операторуnewили явному вызову конструктора делегата.
Ниже приведен методMain() из предыдущего примера, измененный с целью продемонстрировать групповое преобразование методов.
static void Main() {
// Сконструировать делегат, используя групповое преобразование методов. StrMod strOp = ReplaceSpaces; // использовать групповое преобразование методов string str;
// Вызвать методы с помощью делегата, str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine();
strOp = RemoveSpaces; // использовать групповое преобразование методов str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine() ;
strOp -= Reverse; // использовать групповое преобразование методов str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine() ;
}
Обратите особое внимание на то, как создается экземпляр делегатаstrOpи как ему присваивается методRep la се Spacesв следующей строке кода.
strOp = RemoveSpaces; // использовать групповое преобразование методов
В этой строке кода имя метода присваивается непосредственно экземпляру делегатаstrOp,а все заботы по автоматическому преобразованию метода в тип делегата "возлагаются" на средства С#. Этот синтаксис может быть распространен на любую ситуацию, в которой метод присваивается или преобразуется в тип делегата.
Синтаксис группового преобразования методов существенно упрощен по сравнению с прежним подходом к делегированию, поэтому в остальной части книги используется именно он.
Применение методов экземпляра в качестве делегатов
В предыдущем примере использовались статические методы, но делегат может ссылаться и на методы экземпляра, хотя для этого требуется ссылка на объект. Так, ниже приведен измененный вариант предыдущего примера, в котором операции со строками инкапсулируются в классеStringOps.Следует заметить, что в данном случае может быть также использован синтаксис группового преобразования методов.
// Делегаты могут ссылаться и на методы экземпляра.
using System;
// Объявить тип делегата, delegate string StrMod(string str);
class StringOps {
// Заменить пробелы дефисами.
public string ReplaceSpaces(string s) {
Console.WriteLine("Замена пробелов дефисами."); return s.Replace(' '-');
}
// Удалить пробелы.
public string RemoveSpaces(string s) { string temp = ""; int i;
Console.WriteLine("Удаление пробелов."); for(i=0; i < s.Length; i++) if(s[i] != ' ') temp += s[i ] ;
return temp;
}
// Обратить строку, public string Reverse(string s) { string temp = ""; int i, j;
Console.WriteLine("Обращение строки."); for(j=0, i=s.Length-1; i >= 0; i—, j++) temp += s[i];
return temp;
}
}
class DelegateTest { static void Main() {
StringOps so = new StringOpsO; // создать экземпляр
// объекта класса StringOps
// Инициализировать делегат.
StrMod strOp = so.ReplaceSpaces; string str;
// Вызвать методы с помощью делегатов, str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine();
strOp = so.RemoveSpaces;
str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine();
strOp = so.Reverse;
str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
}
}
Результат выполнения этого кода получается таким же, как и в предыдущем примере, но на этот раз делегат обращается к методам по ссылке на экземпляр объекта класса StringOps.
Групповая адресация
Одним из самых примечательных свойств делегата является поддержка групповой адресации. Попросту говоря,групповая адресация— это возможность создатьсписок,илицепочку вызовов,для методов, которые вызываются автоматически при обращении к делегату. Создать такую цепочку нетрудно. Для этого достаточно получить экземпляр делегата, а затем добавить методы в цепочку с помощью оператора + или +=. Для удаления метода из цепочки служит оператор - или -=. Если делегат возвращает значение, то им становится значение, возвращаемое последним методом в списке вызовов. Поэтому делегат, в котором используется групповая адресация, обычно имеет возвращаемый тип void.
Ниже приведен пример групповой адресации. Это переработанный вариант предыдущих примеров, в котором тип значений, возвращаемых методами манипулирования строками, изменен на void, а для возврата измененной строки в вызывающую часть кода служит параметр типа ref. Благодаря этому методы оказываются более приспособленными для групповой адресации.
// Продемонстрировать групповую адресацию.
using System;
// Объявить тип делегата.
delegate void StrMod(ref string str);
class MultiCastDemo {
// Заменить пробелы дефисами.
static void ReplaceSpaces(ref string s) {
Console.WriteLine("Замена пробелов дефисами."); s = s.Replace(' ' , '-');
}
// Удалить пробелы.
static void RemoveSpaces(ref string s) { string temp = ""; int i;
Console.WriteLine("Удаление пробелов."); for(i=0; i < s.Length; i++) if(s[i] != ' ') temp += s[i];
s = temp;
}
// Обратить строку.
static void Reverse(ref string s) { string temp = ""; int i, j;
Console.WriteLine("Обращение строки."); for(j=0, i=s.Length-1; i >= 0; i—, j++) temp += s[i];
s = temp;
}
static void Main() {
// Сконструировать делегаты.
StrMod strOp;
StrMod replaceSp = ReplaceSpaces;
StrMod removeSp = RemoveSpaces;
StrMod reverseStr = Reverse; string str = "Это простой тест.";
// Организовать групповую адресацию. strOp = replaceSp;
strOp += reverseStr;
// Обратиться к делегату с групповой адресацией. strOp(ref str);
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine();
// Удалить метод замены пробелов и добавить метод удаления пробелов. strOp -= replaceSp; strOp += removeSp;
str = "Это простой тест."; // восстановить исходную строку
// Обратиться к делегату с групповой адресацией. strOp(ref str);
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine() ;
}
}
Выполнение этого кода приводит к следующему результату.
Замена пробелов дефисами.
Обращение строки.
Результирующая строка: .тсет-йотсорп-отЭ
Обращение строки.
Удаление пробелов.
Результирующая строка: .тсетйотсорпотЭ
В методеMain() из рассматриваемого здесь примера кода создаются четыре экземпляра делегата. Первый из них,strOp,является пустым, а три остальных ссылаются на конкретные методы видоизменения строки. Затем организуется групповая адресация для вызова методовRemoveSpaces () иReverse (). Это делается в приведенных ниже строках кода.
strOp = replaceSp; strOp += reverseStr
Сначала делегатуstrOpприсваивается ссылкаreplaceSp,а затем с помощью оператора += добавляется ссылкаreverseStr.При обращении к делегатуstrOpвызываются оба метода, заменяя пробелы дефисами и обращая строку, как и показывает приведенный выше результат.
Далее ссылкаreplaceSpудаляется из цепочки вызовов в следующей строке кода:
strOp -= replaceSp;
и добавляется ссылкаremoveSpв строке кода.
strOp += removeSp;
После этого вновь происходит обращение к делегатуstrOp.На этот раз обращается строка с удаленными пробелами.
Цепочки вызовов являются весьма эффективным механизмом, поскольку они позволяют определить ряд методов, выполняемых единым блоком. Благодаря этому улучшается структура некоторых видов кода. Кроме того, цепочки вызовов имеют особое значение для обработки событий, как станет ясно в дальнейшем.
Ковариантность и контравариантность
Делегаты становятся еще более гибкими средствами программирования благодаря двум свойствам:ковариантностииконтравариантности.Как правило, метод, передаваемый делегату,-должен иметь такой же возвращаемый тип и сигнатуру, как и делегат. Но в отношении производных типов это правило оказывается не таким строгим благодаря ковариантности и контравариантности. В частности, ковариантность позволяет присвоить делегату метод, возвращаемым типом которого служит класс, производный от класса, указываемого в возвращаемом типе делегата. А контравариантность позволяет присвоить делегату метод, типом параметра которого служит класс, являющийся базовым для класса, указываемого в объявлении делегата.
Ниже приведен пример, демонстрирующий ковариантность и контравариантность.
// Продемонстрировать ковариантность и контравариантность.
using System;
class X {
public int Val;
}
// Класс Y, производный от класса X. class Y : X { }
// Этот делегат возвращает объект класса X и // принимает объект класса Y в качестве аргумента, delegate X ChangeIt(Y obj);
class CoContraVariance {
// Этот метод возвращает объект класса X и // имеет объект класса X в качестве параметра, static X IncrA(X obj) {
X temp = new X() ; temp.Val = obj.Val + 1; return •■temp;
}
// Этот метод возвращает объект класса Y и // имеет объект класса Y в качестве параметра, static Y IncrB(Y obj) {
Y temp = new Y(); temp.Val = obj.Val + 1; return temp;
}
static void Main() {
Y Yob = new Y();
// В данном случае параметром метода IncrA является объект класса X,
// а параметром делегата Changelt — объект класса Y. Но благодаря // контравариантности следующая строка кода вполне допустима.
Changelt change = IncrA;
X Xob = change(Yob);
Console.WriteLine("Xob: " + Xob.Val);
// В этом случае возвращаемым типом метода IncrB служит объект класса Y, // а возвращаемым типом делегата Changelt — объект класса X. Но благодаря // ковариантности следующая строка кода оказывается вполне допустимой, change = IncrB;
Yob = (Y) change(Yob);
Console.WriteLine("Yob: " + Yob.Val);
}
}
Вот к какому результату приводит выполнение этого кода.
Xob: 1 Yob: 1
В данном примере классYявляется производным от классаX.А делегатChangeltобъявляется следующим образом.
delegate X Changelt(Y obj);
Делегат возвращает объект классаXи принимает в качестве параметра объект классаY.А методыIncrA() иIncrB() объявляются следующим образом.
static X IncrA(X obj) static Y IncrB(Y obj)
МетодIncrA() принимает объект классаXв качестве параметра и возвращает объект того же класса. А методIncrB() принимает в качестве параметра объект классаYи возвращает объект того же класса. Но благодаря ковариантности и контравари-антности любой из этих методов может быть передан делегатуChangelt,что и демонстрирует рассматриваемый здесь пример.
Таким образом, в строке
Changelt change = IncrA;
методIncrA() может быть передан делегату благодаря контравариантности, так как объект классаXслужит в качестве параметра методаIncrA (), а объект классаY —в качестве параметра делегатаChangelt.Но метод и делегат оказываются совместимыми в силу контравариантности, поскольку типом параметра метода, передаваемого делегату, служит класс, являющийся базовым для класса, указываемого в качестве типа параметра делегата.
Приведенная ниже строка кода также является вполне допустимой, но на этот раз благодаря ковариантности.
change = IncrB;
В данном случае возвращаемым типом для методаIncrB() служит классY,а для делегата — класс X. Но поскольку возвращаемый тип метода является производным классом от возвращаемого типа делегата, то оба оказываются совместимыми в силу ковариантности.
Класс System. Delegate
Все делегаты и классы оказываются производными неявным образом от классаSystem. Delegate.Как правило, членами этого класса не пользуются непосредственно, и этоНеделается явным образом в данной книге. Но члены классаSystem. Delegateмогут оказаться полезными в ряде особых случаев.
Назначение делегатов
В предыдущих примерах был наглядно продемонстрирован внутренний механизм действия делегатов, но эти примеры не показывают их истинное назначение. Как правило, делегаты применяются по двум причинам. Во-первых, как упоминалось ранее в этой главе, делегаты поддерживают события. И во-вторых, делегаты позволяют вызывать методы во время выполнения программы, не зная о них ничего определенного в ходе компиляции. Это очень удобно для создания базовой конструкции, допускающей подключение отдельных программных компонентов. Рассмотрим в качестве примера графическую программу, аналогичную стандартной сервисной программе Windows Paint. С помощью делегата можно предоставить пользователю возможность подключать специальные цветные фильтры или анализаторы изображений. Кроме того, пользователь может составлять из этих фильтров или анализаторов целые последовательности. Подобные возможности программы нетрудно обеспечить, используя делегаты.
Анонимные функции
Метод, на который ссылается делегат, нередко используется только для этой цели. Иными словами, единственным основанием для существования метода служит то обстоятельство, что он может быть вызван посредством делегата, но сам он не вызывается вообще. В подобных случаях можно воспользоватьсяанонимной функцией, чтобы не создавать отдельный метод. Анонимная функция, по существу, представляет собой безымянный кодовый блок, передаваемый конструктору делегата. Преимущество анонимной функции состоит, в частности, в ее простоте. Благодаря ей отпадает необходимость объявлять отдельный метод, единственное назначение которого состоит в том, что он передается делегату.
Начиная с версии 3.0, в C# предусмотрены две разновидности анонимных функций:анонимные методыилямбда-выражения.Анонимные методы были внедрены в C# еще в версии 2.0, а лямбда-выражения — в версии 3.0. В целом лямбда-выражение совершенствует принцип действия анонимного метода и в настоящее время считается более предпочтительным для создания анонимной функции. Но анонимные методы широко применяются в существующем коде С# .и поэтому по-прежнему являются важной составной частью С#. А поскольку анонимные методы предшествовали появлению лямбда-выражений, то ясное представление о них позволяет лучше понять особенности лямбда-выражений. К тому же анонимные методы могут быть использованы в целом ряде случаев, где применение лямбда-выражений оказывается невозможным. Именно поэтому в этой главе рассматриваются и анонимные методы, и лямбда-выражения.
Анонимные методы
Анонимный метод — один из способов создания безымянного блока кода, связанного с конкретным экземпляром делегата. Для создания анонимного метода достаточно указать кодовый блок после ключевого словаdelegate.Покажем, как это делается, на конкретном примере. В приведенной ниже программе анонимный метод служит для подсчета от 0 до 5.
// Продемонстрировать применение анонимного метода.
using System;
// Объявить тип делегата, delegate void Countlt();
class AnonMethDemo { static void Main() {
// Далее следует код для подсчета чисел, передаваемый делегату // в качестве анонимного метода.
Countlt count = delegate {
// Этот кодовый блок передается делегату, for (int i=0; i <= 5; i++)
Console.WriteLine(i) ;
}; // обратите внимание на точку с запятой
count();
}
}
В данной программе сначала объявляется тип делегатаCountltбез параметров и с возвращаемым типомvoid.Далее в методеMain() создается экземплярcountделегатаCountlt,которому передается кодовый блок, следующий после ключевого словаdelegate.Именно этот кодовый блок и является анонимным методом, который будет выполняться при обращении к делегатуcount.Обратите внимание на то, что после кодового блока следует точка с запятой, фактически завершающая оператор объявления. Ниже приведен результат выполнения данной программы.
0
1
2
3
4
5
Передача аргументов анонимному методу
Анонимному методу можно передать один или несколько аргументов. Для этого достаточно указать в скобках список параметров после ключевого словаdelegate,а при обращении к экземпляру делегата — передать ему соответствующие аргументы. В качестве примера ниже приведен вариант предыдущей программы, измененный с целью передать в качестве аргумента конечное значение для подсчета.
// Продемонстрировать применение анонимного метода, принимающего аргумент, using System;
// Обратите внимание на то, что теперь у делегата Countlt имеется параметр, delegate void Countlt(int end);
class AnonMethDemo2 {
static void Main() {
// Здесь конечное значение для подсчета передается анонимному методу. Countlt count = delegate (int end) { for(int i=0; i <= end; i++)
Console.WriteLine(i);
};
count (3);
Console.WriteLine (); count (5);
}
}
В этом варианте программы делегат Countlt принимает целочисленный аргумент. Обратите внимание на то, что при создании анонимного метода список параметров указывается после ключевого слова delegate. Параметр end становится доступным для кода в анонимном методе таким же образом, как и при создании именованного метода. Ниже приведен результат выполнения данной программы.
0
1
2 3
0
1
2
3
4
5
Возврат значения из анонимного метода
Анонимный метод может возвращать значение. Для этой цели служит оператор return, действующий в анонимном методе таким же образом, как и в именованном методе. Как и следовало ожидать, тий возвращаемого значения должен быть совместим с возвращаемым типом, указываемым в объявлении делегата. В качестве примера ниже приведен код, выполняющий подсчет с суммированием и возвращающий результат.
// Продемонстрировать применение анонимного метода, возвращающего значение.
// Этот делегат возвращает значение, delegate int Countlt(int end);
class AnonMethDemo3 {
static void Main() { int result;
// Здесь конечное значение для подсчета передается анонимному методу. //А возвращается сумма подсчитанных чисел.
Countlt count = delegate (int end) { int sum = 0;
for(int i=0; i <= end; i++) {
Console.WriteLine (i); sum += i;
}
return sum; // возвратить значение из анонимного метода
};
result = count (3);
Console.WriteLine("Сумма 3 равна " + result);
Console.WriteLine ();
result = count (5);
Console.WriteLine("Сумма 5 равна " + result);
}
}
В этом варианте кода суммарное значение возвращается кодовым блоком, связанным с экземпляром делегата count. Обратите внимание на то, что оператор return применяется в анонимном методе таким же образом, как и в именованном методе. Ниже приведен результат выполнения данного кода.
0
1
2
3
Сумма 3 равна 6
0
1
2
3
4
5
Сумма 5 равна 15
Применение внешних переменных в анонимных методах
Локальная переменная, в область действия которой входит анонимТный метод, называетсявнешней переменной.Такие переменные доступны для использования в анонимном методе. И в этом случае внешняя переменная считаетсязахваченной.Захваченная переменная существует до тех пор, пока захвативший ее делегат не будет собран в "мусор". Поэтому если локальная переменная, которая обычно прекращает свое существование после выхода из кодового блока, используется в анонимном методе, то она продолжает существовать до тех пор, пока не будет уничтожен делегат, ссылающийся на этот метод.
Захват локальной переменной может привести к неожиданным результатам. В качестве примера рассмотрим еще один вариант программы подсчета с суммированием чисел. В данном варианте объектCountltконструируется и возвращается статическим методомCounter (). Этот объект использует переменнуюsum,объявленную в охватывающей области действия методаCounter (), а не самого анонимного метода. Поэтому переменнаяsumзахватывается анонимным методом. МетодCounter() вызывается в методеMain() для получения объектаCountlt,а следовательно, переменнаяsumне уничтожается до самого конца программы.
// Продемонстрировать применение захваченной переменной, using System;
// Этот делегат возвращает значение типа int и принимает аргумент типа int. delegate int Countlt(int end);
class VarCapture {
static Countlt Counter () {
int sum = 0;
// Здесь подсчитанная сумма сохраняется в переменной sum.
Countlt ctObj = delegate (int end) { for(int i=0; i <= end; i++) {
Console.WriteLine(i); sum += i;
}
return sum;
};
return ctObj;
}
static void Main() {
// Получить результат подсчета.
Countlt count = Counter ();
int result;
result = count(3);
Console.WriteLine("Сумма 3 равна " + result);
Console.WriteLine();
result = count(5);
Console.WriteLine("Сумма 5 равна " + result);
}
}
Ниже приведен результат выполнения этой программы. Обратите особое внимание на суммарное значение.
0
1
2
3
Сумма 3 равна 6
0
1
2
3
4
5
Сумма 5 равна 21
Как видите, подсчет по-прежнему выполняется как обычно. Но обратите внимание на то, что сумма 5 теперь равна 21, а не 15! Дело в том, что переменнаяsumзахватывается объектомctOb jпри его создании в методеCounter (). Это означает, что она продолжает существовать вплоть до уничтожения делегатаcountпри "сборке мусо-ра" в самом конце программы. Следовательно, ее значение не уничтожается после возврата из методаCounter() или при каждом вызове анонимного метода, когда происходит обращение к делегатуcountв методеMain().
Несмотря на то что применение захваченных переменных может привести к довольно неожиданным результатам, как в приведенном выше примере, оно все же логически обоснованно. Ведь когда анонимный метод захватывает переменную, она продолжает существовать до тех пор, пока используется захватывающий ее делегат. В противном случае захваченная переменная оказалась бы неопределенной, когда она могла бы потребоваться делегату.
Лямбда-выражения
Несмотря на всю ценность анонимных методов, им на смену пришел более совершенный подход:лямбда-выражение.Не будет преувеличением сказать, что лямбда-выражение относится к одним из самых важных нововведений в С#, начиная с выпуска исходной версии 1.0 этого языка программирования. Лямбда-выражение основывается на совершенно новом синтаксическом элементе и служит более эффективной альтернативой анонимному методу. И хотя лямбда-выражения находят применение главным образом в работе с LINQ (подробнее об этом — в главе 19), они часто используются и вместе с делегатами и событиями. Именно об этом применении лямбда-выражений и пойдет речь в данном разделе.
Лямбда-выражение — это другой собой создания анонимной функции. (Первый ее способ, анонимный метод, был рассмотрен в предыдущем разделе.) Следовательно, лямбда-выражение может быть присвоено делегату. А поскольку лямбда-выражение считается более эффективным, чем эквивалентный ему анонимный метод, то в большинстве случаев рекомендуется отдавать предпочтение именно ему.
Лямбда-оператор
Во всех лямбда-выражениях применяется новый лямбда-оператор =>, который разделяет лямбда-выражение на две части. В левой его части указывается входной параметр (или несколько параметров), а в правой части — тело лямбда-выражения. Оператор => иногда описывается такими словами, как "переходит" или "становится".
В C# поддерживаются две разновидности лямбда-выражений в зависимости от тела самого лямбда-выражения. Так, если тело лямбда-выражения состоит из одного выражения, то образуетсяодиночное лямбда-выражение.В этом случае тело выражения не заключается в фигурные скобки. Если же тело лямбда-выражения состоит из блока операторов, заключенных в фигурные скобки, то образуетсяблочное лямбда-выражение.При этом блочное лямбда-выражение может содержать целый ряд операторов, в том числе циклы, вызовы методов и условные операторы if. Обе разновидности лямбда-выражений рассматриваются далее по отдельности.
Одиночные лямбда-выражения
В одиночном лямбда-выражении часть, находящаяся справа от оператора =>, воздействует на параметр (или ряд параметров), указываемый слева. Возвращаемым результатом вычисления такого выражения является результат выполнения лямбда-оператора.
Ниже приведена общая форма одиночного лямбда-выражения, принимающего единственный параметр.
параметр => выражение
Если же требуется указать несколько параметров, то используется следующая форма.
(список_параметров) =>выражение
Таким образом, когда требуется указать два параметра или более, их следует заключить в скобки. Если жевыражениене требует параметров, то следует использовать пустые скобки.
Ниже приведен простой пример одиночного лямбда-выражения.
count- => count + 2
В этом выражении count служит параметром, на который воздействует выражение count + 2. В итоге значение параметра count увеличивается на 2. А вот еще один пример одиночного лямбда-выражения.
n => п % 2 == О
В данн“ом случае выражение возвращает логическое значение true, если числовое значение параметра п оказывается четным, а иначе — логическое значение false.
Лямбда-выражение применяется в два этапа. Сначала объявляется тип делегата, совместимый с лямбда-выражением, а затем экземпляр делегата, которому присваивается лямбда-выражение. После этого лямбда-выражение вычисляется при обращении к экземпляру делегата. Результатом его вычисления становится возвращаемое значение.
В приведенном ниже примере программы демонстрируется применение двух одиночных лямбда-выражений. Сначала в этой программе объявляются два типа делегатов. Первый из них, Inc г, принимает аргумент типа int и возвращает результат того же типа. Второй делегат, IsEven, также принимает аргумент типа int, но возвращает результат типа bool. Затем экземплярам этих делегатов присваиваются одиночные лямбда-выражения. И наконец, лямбда-выражения вычисляются с помощью соответствующих экземпляров делегатов.
// Применить два одиночных лямбда-выражения.
11Объявить делегат, принимающий аргумент типа int и // возвращающий результат типа int. '
delegate int Incr(int v);
// Объявить делегат, принимающий аргумент типа int и // возвращающий результат типа bool, delegate bool IsEven(int v);
class SimpleLambdaDemo {
static void Main() {
// Создать делегат Incr, ссылающийся на лямбда-выражение,
// увеличивающее свой параметр на 2.
Incr incr = count => count + 2;
// А теперь использовать лямбда-выражение incr.
Console.WriteLine("Использование лямбда-выражения incr: "); int x = -10; while(x <= 0) {
Console.Write(x + " ");
x = incr(x); // увеличить значение x на 2
}
Console.WriteLine ("\n");
// Создать экземпляр делегата IsEven, ссылающийся на лямбда-выражение,
// возвращающее логическое значение true, если его параметр имеет четное // значение, а иначе — логическое значение false.
IsEven isEven = n => n % 2 == 0;
// А теперь использовать лямбда-выражение isEven.
Console.WriteLine("Использование лямбда-выражения isEven: "); for (int i=l; i <= 10; i++)
if(isEven (i)) Console.WriteLine(i + " четное.");
}
}
Вот к какому результату приводит выполнение этой программы.
Использование лямбда-выражения incr:
-10 -8 -6 -4 -2 0
Использование лямбда-выражения isEven:
2 четное.
4 четное.
6 четное.
8 четное.
10 четьюе.
Обратите в данной программе особое внимание на следующие строки объявлений.
Incr incr = count => count +2;
IsEven isEven = n => n % 2 == 0;
В первой строке объявления экземпляру делегата incr присваивается одиночное лямбда-выражение, возвращающее результат увеличения на 2 значения параметраcount.Это выражение может быть присвоено делегатуIncr,поскольку оно совместимо с объявлением данного делегата. Аргумент, указываемый при обращении к экземпляру делегатаincr,передается параметруcount,который и возвращает результат вычисления лямбда-выражения. Во второй строке объявления делегатуisEvenприсваивается выражение, возвращающее логическое значениеtrue,если передаваемый ему аргумент оказывается четным, а иначе — логическое значениеfalse.Следовательно, это лямбда-выражение совместимо с объявлением делегатаIsEven.
В связи со всем изложенным выше возникает резонный вопрос: каким образом компилятору становится известно о типе данных, используемых в лямбда-выражении, например, о типеintпараметраcountв лямбда-выражении, присваиваемом экземпляру делегата incr? Ответить на этот вопрос можно так: компилятор делает заключение о типе параметра и типе результата вычисления выражения по типу делегата. Следовательно, параметры и возвращаемое значение лямбда-выражения должны быть совместимы по типу с параметрами и возвращаемым значением делегата.
Несмотря на всю полезность логического заключения о типе данных, в некоторых случаях приходится явно указывать тип параметра лямбда-выражения. Для этого достаточно ввести конкретное название типа данных. В качестве примера ниже приведен другой способ объявления экземпляра делегатаincr.
Incr incr = (int count) => count + 2;
Как видите,countтеперь явно объявлен как параметр типаint.Обратите также внимание на использование скобок. Теперь они необходимы. (Скобки могут быть опущены только в том случае, если задается лишь один параметр, а его тип явно не указывается.)
В предыдущем примере в обоих лямбда-выражениях использовался единственный параметр, но в целом у лямбда-выражений может быть любое количество параметров, в том числе и нулевое. Если в лямбда-выражении используется несколько параметров, ихнеобходимозаключить в скобки. Ниже приведен пример использования лямбда-выражения с целью определить, находится ли значение в заданных пределах.
(low, high, val) => val >= low && val <= high;
А вот как объявляется тип делегата, совместимого с этим лямбда-выражением.
delegate bool InRange(int lower, int upper, int v);
Следовательно, экземпляр делегатаInRangeможет быть создан следующим образом.
InRange rangeOK = (low, high, val) => val >= low && val <= high;
После этого одиночное лямбда-выражение может быть выполнено так, как показано ниже.
if(rangeOK(1, 5, 3)) Console.WriteLine(
"Число 3 находится в пределах от 1 до 5.");
И последнее замечание: внешние переменные могут использоваться и захватываться в лямбда-выражениях таким же образом, как и в анонимных методах.
Блочные лямбда-выражения
Как упоминалось выше, существуют две разновидности лямбда-выражений. Первая из них, одиночное лямбда-выражение, была рассмотрена в предыдущем разделе. Тело такого лямбда-выражения состоит только из одного выражения. Второй разновидностью являетсяблочное лямбда-выражение. Длятакого лямбда-выражения характерны расширенные возможности выполнения различных операций, поскольку в его теле допускается указывать несколько операторов. Например, в блочном лямбда-выражении можно использовать циклы и условные операторы if, объявлять переменные и т.д. Создать блочное лямбда-выражение нетрудно.Дляэтого достаточно заключить тело выражения в фигурные скобки. Помимо возможности использовать несколько операторов, в остальном блочное лямбда-выражение, практически ничем не отличается от только что рассмотренного одиночного лямбда-выражения.
Ниже приведен пример использования блочного лямбда-выражения для вычисления и возврата факториала целого значения.
// Продемонстрировать применение блочного лямбда-выражения, using System;
// Делегат IntOp принимает один аргумент типа int // и возвращает результат типа int. delegate int IntOp(int end);
class StatementLambdaDemo {
static void Main() {
// Блочное лямбда-выражение возвращает факториал // передаваемого ему значения.
IntOp fact = n => {
int г = 1;
for (int i=l; i <= n; i++) r = i * r; return r;
};
Console.WriteLine("Факториал 3 равен " + fact(3));
Console.WriteLine("Факториал 5 равен " + fact(5));
}
}
При выполнении этого кода получается следующий результат.
Факториал 3 равен 6 Факториал 5 равен 120
В приведенном выше примере обратите внимание на то, что в теле блочного лямбда-выражения объявляется переменная г, организуется цикл for и используется оператор return. Все эти элементы вполне допустимы в блочном лямбда-выражении. И в этом отношении оно очень похоже на анонимный метод. Следовательно, многие анонимные методы могут быть преобразованы в блочные лямбда-выражения при обновлении унаследованного кода. И еще одно замечание: когда в блочном лямбда-выражении встречается оператор return, он просто обусловливает возврат из лямбда-выражения, но не возврат из охватывающего метода.
И в заключение рассмотрим еще один пример, демонстрирующий блочное лямбда-выражение в действии. Ниже приведен вариант первого примера из этой главы, измененного с целью использовать блочные лямбда-выражения вместо автономных методов для выполнения различных операций со строками.
// Первый пример применения делегатов, переделанный с // целью использовать блочные лямбда-выражения.
using System;
// Объявить тип делегата, delegate string StrMod(string s);
class UseStatementLambdas {
static void Main() {
// Создать делегаты, ссылающиеся на лямбда- выражения,
// выполняющие различные операции с символьными строками.
// Заменить пробелы дефисами.
StrMod ReplaceSpaces = s => {
Console.WriteLine("Замена пробелов дефисами."); return s.Replace(' '-');
};
% // Удалить пробелы.
StrMod RemoveSpaces = s => { string temp = ""; int i;
Console.WriteLine("Удаление пробелов."); for(i=0; i < s.Length; i++) if(s[i] != 1 ') temp += s[i];
return temp;
};
// Обратить строку.
StrMod Reverse = s => {
string temp = ""; int i, j;
Console.WriteLine("Обращение строки."); for(j=0, i=s.Length-1; i >= 0; i—, j++) temp += s[i];
return temp;
};
string str;
// Обратиться к лямбда-выражениям с помощью делегатов.
StrMod strOp = ReplaceSpaces;
str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine() ;
strOp = RemoveSpaces;
str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
Console.WriteLine();
strOp = Reverse;
str = strOp("Это простой тест.");
Console.WriteLine("Результирующая строка: " + str);
}
}
Результат выполнения кода этого примера оказывается таким же, как и в первом примере применения делегатов.
Замена пробелов дефисами.
Результирующая строка: Это-простой-тест.
Удаление пробелов.
Результирующая строка: Этопростойтест.
Обращение строки.
Результирующая строка: .тсет йотсорп отЭ
События
Еще одним важным средством С#, основывающимся на делегатах, являетсясобытие.Событие, по существу, представляет собой автоматическое уведомление о том, что произошло некоторое действие. События действуют по следующему принципу: объект, проявляющий интерес к событию, регистрирует обработчик этого события. Когда же событие происходит, вызываются все зарегистрированные обработчики этого события. Обработчики событий обычно представлены делегатами.
События являются членами класса и объявляются с помощью ключевого слова event. Чаще всего для этой цели используется следующая форма:
eventделегат_события имя_события;
гдеделегат_событияобозначает имя делегата, используемого для поддержки события, аммя_событмя— конкретный объект объявляемого события.
Рассмотрим для начала очень простой пример.
// Очень простой пример, демонстрирующий событие, using System;
// Объявить тип делегата для события, delegate void MyEventHandler();
// Объявить класс, содержащий событие, class MyEvent {
public event MyEventHandler SomeEvent;
// Этот метод вызывается для запуска события, public void OnSomeEvent() {
if (SomeEvent != null)
SomeEvent();
}
}
class EventDemo {
// Обработчик события, static void Handler () {
Console.WriteLine("Произошло событие");
}
static void Main() {
MyEvent evt = new MyEvent ();
// Добавить метод Handler() в список событий, evt.SomeEvent += Handler;
// Запустить событие, evt.OnSomeEvent();
}
}
Вот какой результат получается при выполнении этого кода.
Произошло событие
Несмотря на всю свою простоту, данный пример кода содержит все основные элементы, необходимые для обработки событий. Он начинается с объявления типа делегата для обработчика событий, как показано ниже.
delegate void MyEventHandler();
Все события активизируются с помощью делегатов. Поэтому тип делегата события определяет возвращаемый тип и сигнатуру для события. В данном случае параметры события отсутствуют, но их разрешается указывать.
Далее создается класс событияMyEvent.В этом классе объявляется событиеSomeEventв следующей строке кода.
public event MyEventHandler SomeEvent;
Обратите внимание на синтаксис этого объявления. Ключевое словоeventуведомляет компилятор о том, что объявляется событие.
Кроме того, в классеMyEventобъявляется методOnSomeEvent (), вызываемый для сигнализации о запуске события. Это означает, что он вызывается, когда происходит событие. В методеOnSomeEvent() вызывается обработчик событий с помощью делегатаSomeEvent.
if(SomeEvent != null)
SomeEvent();
Как видите, обработчик вызывается лишь в том случае, если событиеSomeEventне является пустым. А поскольку интерес к событию должен быть зарегистрирован в других частях программы, чтобы получать уведомления о нем, то методOnSomeEvent() может быть вызван до регистрации любого обработчика события. Но во избежание
вызова по пустой ссылке делегат события должен быть проверен, чтобы убедиться в том, что он не является пустым.
В классеEvent Demoсоздается обработчик событийHandler (). В данном простом примере обработчик событий просто выводит сообщение, но другие обработчики могут выполнять более содержательные функции. Далее в методеMain() создается объект класса событияMyEvent,aHandler() регистрируется как обработчик этого события, добавляемый в список.
MyEvent evt = new MyEvent ();
// Добавить метод Handler() в список событий, evt.SomeEvent += Handler;
Обратите внимание на то, что обработчик добавляется в список с помощью оператора +=. События поддерживают только операторы += и -=. В данном случае методHandler() является статическим, но в качестве обработчиков событий могут также служить методы экземпляра.
И наконец, событие запускается, как показано ниже.
// Запустить событие, evt.OnSomeEvent();
Вызов методаOnSomeEvent() приводит к вызову всех событий, зарегистрированных обработчиком. В данном случае зарегистрирован только один такой обработчик, но их может быть больше, как поясняется в следующем разделе.
Пример групповой адресации события
Как и делегаты, события поддерживают групповую адресацию. Это дает возможность нескольким объектам реагировать на уведомление о событии. Ниже приведен пример групповой адресации события.
// Продемонстрировать групповую адресацию события, using System;
// Объявить тип делегата для события, delegate void MyEventHandler() ;
// Объявить делегат, содержащий событие, class MyEvent {
public event MyEventHandler SomeEvent;
// Этот метод вызывается для запуска события, public void OnSomeEvent() {
if(SomeEvent != null)
SomeEvent();
}
}
class X {
public void XhandlerO {
Console.WriteLine("Событие получено объектом класса X");
}
class Y {
public void YhandlerO {
Console.WriteLine("Событие получено объектом класса Y");
}
}
class EventDemo2 {
static void Handler() {
Console.WriteLine("Событие получено объектом класса EventDemo");
}
static void Main() {
MyEvent evt = new MyEvent ();
X xOb = new X();
Y yOb = new Y();
// Добавить обработчики в список событий.
evt.SomeEvent += Handler;
evt.SomeEvent += xOb.Xhandler;
evt.SomeEvent += yOb.Yhandler;
// Запустить событие, evt.OnSomeEvent() ;
Console.WriteLine() ;
// Удалить обработчик.
evt.SomeEvent -= xOb.Xhandler;
evt.OnSomeEvent() ;
}
}
При выполнении кода этого примера получается следующий результат.
Событие получено объектом класса EventDemo Событие получено объектом класса X Событие получено объектом класса Y
Событие получено объектом класса EventDemo Событие получено объектом класса Y
В данном примере создаются два дополнительных класса, X иY,в которых также определяются обработчики событий, совместимые с делегатомMyEventHandler.Поэтому эти обработчики могут быть также включены в цепочку событий. Обратите внимание на то, что обработчики в классах X иYне являются статическими. Это означает, что сначала должны быть созданы объекты каждого из этих классов, а затем в цепочку событий должны быть введены обработчики, связанные с их экземплярами. Об отличиях между обработчиками экземпляра и статическими обработчиками речь пойдет в следующем разделе.
Методы экземпляра в сравнении со статическими методами в качестве обработчиков событий
Методы экземпляра и статические методы могут быть использованы в качестве обработчиков событий, но между ними имеется одно существенное отличие. Когда
статический метод используется в качестве обработчика, уведомление о событии распространяется на весь класс. А когда в качестве обработчика используется метод экземпляра, то события адресуются конкретным экземплярам объектов. Следовательно, каждый объект определенного класса, которому требуется получить уведомление о событии, должен быть зарегистрирован отдельно. На практике большинство обработчиков событий представляет собой методы экземпляра, хотя это, конечно, зависит от конкретного приложения. Рассмотрим применение каждой из этих двух разновидностей методов в качестве обработчиков событий на конкретных примерах.
В приведенной ниже программе создается класс X, в котором метод экземпляра определяется в качестве обработчика событий. Это означает, что каждый объект класса X должен быть зарегистрирован отдельно, чтобы получать уведомления о событиях. Для демонстрации этого факта в данной программе производится групповая адресация события трем отдельным объектам класса X.
/* Уведомления о событиях получают отдельные объекты, когда метод экземпляра используется в качестве обработчика событий. */
using System;
// Объявить тип делегата для события, delegate void MyEventHandler() ;
// Объявить класс, содержащий событие, class MyEvent {
public event MyEventHandler SomeEvent;
// Этот метод вызывается для запуска события, public void OnSomeEvent() {
if(SomeEvent != null)
SomeEvent() ;
}
}
class X { int id;
public X(int x) { id = x; }
// Этот метод экземпляра предназначен в качестве обработчика событий, public void Xhandler() {
Console.WriteLine("Событие получено объектом " + id);
}
}
class EventDemo3 { static void Main() {
MyEvent evt = new MyEvent();
X ol = new X(1);
X o2 = new X (2);
X o3 = new X(3); evt.SomeEvent += ol.Xhandler; evt.SomeEvent += o2.Xhandler; evt.SomeEvent += o3.Xhandler;
// Запустить событие, evt.OnSomeEvent() ;
}
}
Выполнение кода из этого примера приводит к следующему результату.
Событие получено объектом 1 Событие получено объектом 2 Событие получено объектом 3
Как следует из результата выполнения кода из приведенного выше примера, каждый объект должен зарегистрировать свой интерес в событии отдельно, и тогда он будет получать отдельное уведомление о событии.
С другой стороны, когда в качестве обработчика событий используется статический метод, события обрабатываются независимо от какого-либо объекта, как демонстрируется в приведенном ниже примере программы.
/* Уведомления о событии получает класс, когда статический метод используется в качестве обработчика событий. */
using System;
// Объявить тип делегата для события, delegate void MyEventHandler();
// Объявить класс, содержащий событие, class MyEvent {
public event MyEventHandler SomeEvent;
// Этот метод вызывается для запуска события, public void OnSomeEvent() {
if (SomeEvent != null)
SomeEvent() ;
}
}
class X {
/* Этот статический метод предназначен в качестве обработчика событий. */ public static void Xhandler() {
Console.WriteLine("Событие получено классом.");
}
}
class EventDemo4 { static void Main() {
MyEvent evt = new MyEvent();
evt.SomeEvent += X.Xhandler;
// Запустить событие, evt.OnSomeEvent();
При выполнение кода этого примера получается следующий результат.
Событие получено классом.
Обратите в данном примере внимание на то, что объекты класса X вообще не создаются. Но посколькуXhandler() является статическим методом классаX,то он может быть привязан к событиюSomeEventи выполнен при вызове метода
OnSomeEvent().
Применение аксессоров событий
В приведенных выше примерах события формировались в форме, допускавшей автоматическое управление списком вызовов обработчиков событий, включая добавление и удаление обработчиков событий из списка. Поэтому управление этим списком не нужно было организовывать вручную. Благодаря именно этому свойству такие события используются чаще всего. Тем не менее организовать управление списком вызовов обработчиков событий можно и вручную, чтобы, например, реализовать специальный механизм сохранения событий.
Для управления списком обработчиков событий служит расширенная форма оператораevent,позволяющая использоватьаксессоры событий.Эти аксессоры предоставляют средства для управления реализацией подобного списка в приведенной ниже форме.
eventделегат_события имя_ с о бытия{ add {
// Код добавления события в цепочку событий.
}
remove {
// Код удаления события из цепочки событий.
}
}
В эту форму входят два аксессора событий:addиremove.Аксессорaddвызывается, когда обработчик событий добавляется в цепочку событий с помощью оператора +=. В то же время аксессорremoveвызывается, когда обработчик событий удаляется из цепочки событий с помощью оператора -=.
Когда вызывается аксессорaddилиremove,он принимает в качестве параметра добавляемый или удаляемый обработчик. Как и в других разновидностях аксессоров, этот неявный параметр называетсяvalue.Реализовав аксессорыaddилиremove,можно организовать специальную схему хранения обработчиков событий. Например, обработчики событий можно хранить в массиве, стеке или очереди.
Ниже приведен пример программы, демонстрирующей аксессорную форму события. В ней для хранения обработчиков событий используется массив. Этот массив состоит всего из трех элементов, поэтому в цепочке событий можно хранить одновременно только три обработчика.
// Создать специальные средства для управления списками // вызова обработчиков событий.
using System;
// Объявить тип делегата для события.
delegate void MyEventHandler(); ,
// Объявить класс для хранения максимум трех событий, class MyEvent {
MyEventHandler[] evnt = new MyEventHandler[3];
public event MyEventHandler SomeEvent {
// Добавить событие в список, add { int i;
for(i=0; i < 3; i++) if(evnt[i] == null) { evnt[i] = value; break;
}
if (i == 3) Console.WriteLine("Список событий заполнен.");
}
// Удалить событие из списка, remove { int i;
for(i=0; i < 3; i++) if(evnt[i] == value) { evnt[i] = null; break;
}
if (i == 3) Console.WriteLine("Обработчик событий не найден.");
}
}
// Этот .метод вызывается для запуска событий, public void OnSomeEvent() {
for(int i=0; i < 3; i++)
if(evnt[i] != null) evnt[i]();
}
}
// Создать ряд классов, использующих делегат MyEventHandler. class W {
public void Whandler() {
Console.WriteLine("Событие получено объектом W");
}
}
class X {
public void Xhandler() {
Console.WriteLine("Событие получено объектом X");
}
}
class Y {
public void Yhandler() {
Console.WriteLine("Событие получено объектом Y");
class Z {
public void Zhandler() {
Console.WriteLine("Событие получено объектом Z");
}
}
class EventDemo5 { static void Main() {
MyEvent evt = new MyEvent();
// Добавить обработчики в цепочку событий.
Console.WriteLine("Добавление событий."); evt.SomeEvent += wOb.Whandler; evt.SomeEvent += xOb.Xhandler; evt.SomeEvent += yOb.Yhandler;
// Сохранить нельзя - список заполнен, evt.SomeEvent += zOb.Zhandler;
Console.WriteLine();
// Запустить события, evt.OnSomeEvent();
Console.WriteLine();
// Удалить обработчик.
Console.WriteLine("Удаление обработчика xOb.Xhandler.") ; evt.SomeEvent -= xOb.Xhandler; evt.OnSomeEvent();
Console.WriteLine();
// Попробовать удалить обработчик еще раз.
Console.WriteLine("Попытка удалить обработчик " +
"xOb.Xhandler еще раз."); evt.SomeEvent -= xOb.Xhandler; evt.OnSomeEvent();
Console.WriteLine();
//А теперь добавить обработчик Zhandler.
Console.WriteLine("Добавление обработчика zOb.Zhandler."); evt.SomeEvent += zOb.Zhandler; evt.OnSomeEvent();
}
}
Добавление событий.
Список событий заполнен.
Событие получено объектом W Событие получено объектом X Событие получено объектом Y
Удаление обработчика xOb.Xhandler.
Событие получено объектом W Событие получено объектом Y
Попытка удалить обработчик xOb.Xhandler еще раз.
Обработчик событий не найден.
Событие получено объектом W Событие получено объектом Y
Добавление обработчика zOb.Zhandler.
Событие получено объектом W Событие получено объектом X Событие получено объектом Y
Рассмотрим данную программу более подробно. Сначала в ней определяется делегат обработчиков событийMyEventHandler.Затем объявляется классMyEvent.В самом его начале определяется массив обработчиков событийevnt,состоящий из трех элементов.
MyEventHandler[] evnt = new MyEventHandler[3];
Этот массив служит для хранения обработчиков событий, добавляемых в цепочку событий. По умолчанию элементы массиваevntинициализируются пустым значением(null).
Далее объявляется событиеSomeEvent.В этом объявлении используется приведенная ниже аксессорная форма оператораevent.
public event MyEventHandler SomeEvent {
// Добавить событие в список, add { int i;
for(i=0; i < 3; i++) if(evnt[i] == null) { evnt[i] = value; break;
}
if (i == 3) Console.WriteLine("Список событий заполнен.");
}
// Удалить событие из списка, remove { int i; for(i=0; i < 3; i++) if(evnt[i] == value) { evnt[i] = null; break;
}
Когда в цепочку событий добавляется обработчик событий, вызывается аксессорadd,и в первом неиспользуемом (т.е. пустом) элементе массиваevntзапоминается ссылка на этот обработчик, содержащаяся в неявно задаваемом параметреvalue.Если в массиве отсутствуют свободные элементы, то выдается сообщение об ошибке. (Разумеется, в реальном коде при переполнении списка лучше сгенерировать соответствующее исключение.) Массивevntсостоит всего из трех элементов, поэтому в нем можно сохранить только три обработчика событий. Когда же обработчик событий удаляется из цепочки событий, то вызывается аксессорremoveи в массивеevntосуществляется поиск ссылки на этот обработчик, передаваемой в качестве параметраvalue.Если ссылка найдена, то соответствующему элементу массива присваивается пустое значение(null),а значит, обработчик удаляется из цепочки событий.
При запуске события вызывается методOnSomeEvent(). В этом методе происходит циклическое обращение к элементам массиваevntдля вызова по очереди каждого обработчика событий.
Как демонстрирует рассматриваемый здесь пример программы, механизм хранения обработчиков событий нетрудно реализовать, если в этом есть потребность. Но для большинства приложений более подходящим оказывается используемый по умолчанию механизм хранения обработчиков событий, который обеспечивает форма оператораeventбез аксессоров. Тем не менее аксессорная форма оператораeventиспользуется в особых случаях. Так, если обработчики событий необходимо выполнять в программе в порядке их приоритетности, а не в том порядке, в каком они вводятся в цепочку событий, то для их хранения можно воспользоваться очередью по приоритету.
ПРИМЕЧАНИЕ
В многопоточных приложениях обычно приходится синхронизировать доступ к аксессо-рам событий. Подробнее о многопоточном программировании речь пойдет в главе’23.
Разнообразные возможности событий
События могут быть определены и в интерфейсах. При этом события должны предоставляться классами, реализующими интерфейсы. События могут быть также определены как абстрактные(abstract).В этом случае конкретное событие должно быть реализовано в производном классе. Но аксессорные формы событий не могут быть абстрактными. Кроме того, событие может быть определено как герметичное(sealed).И наконец, событие может быть виртуальным, т.е. его можно переопределить в производном классе.
Применение анонимных методов и лямбда-выражений вместе с событиями
Анонимные методы и лямбда-выражения особенно удобны для работы с событиями, поскольку обработчик событий зачастую вызывается только в коде, реализующем механизм обработки событий. Это означает, что создавать автономный метод, как правило, нет никаких причин. А с помощью лямбда-выражений или анонимных методов можно существенно упростить код обработки событий.
Как упоминалось выше, лямбда-выражениям теперь отдается большее предпочтение по сравнению с анонимными методами, поэтому начнем именно с них. Ниже приведен пример программы, в которой лямбда-выражение используется в качестве обработчика событий.
// Использовать лямбда-выражение в качестве обработчика событий, using System;
// Объявить тип делегата для события, delegate void MyEventHandler(int n);
// Объявить класс, содержащий событие, class MyEvent {
public event MyEventHandler SomeEvent;
// Этот метод вызывается для запуска события, public void OnSomeEvent(int n) { if(SomeEvent != null)
SomeEvent(n);
}
}
class LambdaEventDemo { static void Main() {
MyEvent evt = new MyEvent();
// Использовать лямбда-выражение в качестве обработчика событий, evt.SomeEvent += (n) =>
Console.WriteLine("Событие получено. Значение равно " + п);
// Запустить событие, evt.OnSomeEvent(1); evt.OnSomeEvent(2);
}
}
Вот к какому результату приводит выполнение этой программы.
Событие получено. Значение равно 1 Событие получено. Значение равно 2
Обратите особое внимание на то, как в этой программе лямбда-выражение используется в качестве обработчика событий.
evt.SomeEvent += (n) =>
Console.WriteLine("Событие получено. Значение равно " + п);
Синтаксис для использования лямбда-выражения в качестве обработчика событий остается таким же, как для его применения вместе с любым другим типом делегата.
Несмотря на то что при создании анонимной функции предпочтение следует теперь отдавать лямбда-выражениям, в качестве обработчика событий можно по-прежнему использовать анонимный метод. Ниже приведен вариант обработчика событий из предыдущего примера, измененный с целью продемонстрировать применение анонимного метода.
11Использовать анонимный метод в качестве обработчика событий, evt.SomeEvent += delegate(int n) {
Console.WriteLine("Событие получено. Значение равно " + n);
};
Как видите, синтаксис использования анонимного метода в качестве обработчика событий остается таким же, как и для его применения вместе с любым другим типом делегата.
Рекомендации по обработке событий в среде .NET Framework
В C# разрешается формировать какие угодно разновидности событий. Но ради совместимости программных компонентов со средой .NET Framework следует придерживаться рекомендаций, установленных для этой цели корпорацией Microsoft. Эти рекомендации, по существу, сводятся к следующему требованию: у обработчиков событий должны быть два параметра. Первый из них — ссылка на объект, формирующий событие, второй — параметр типаEventArgs,содержащий любую дополнительную информацию о событии, которая требуется обработчику. Таким образом, .NET-совместимые обработчики событий должны иметь следующую общую форму.
voidобработчик(objectотправитель, EventArgs е) {
// ...
}
Как правило,отправитель —это параметр, передаваемый вызывающим кодом с помощью ключевого словаthis.А параметретипаEventArgsсодержит дополнительную информацию о событии и может быть проигнорирован, если он не нужен.
Сам классEventArgsне содержит поля, которые могут быть использованы для передачи дополнительных данных обработчику. Напротив,EventArgsслужит в качестве базового класса, от которого получается производный класс, содержащий все необходимые поля. Тем не менее в классеEventArgsимеется одно полеEmptyтипаstatic,которое представляет собой объект типаEventArgsбез данных.
Ниже приведен пример программы, в которой формируется .NET-совместимое событие.
// Пример формирования .NET-совместимого события, using System;
// Объявить класс, производный от класса EventArgs. class MyEventArgs : EventArgs { public int EventNum;
}
// Объявить тип делегата для события.
delegate void MyEventHandler(object source, MyEventArgs arg);
/’/ Объявить класс, содержащий событие, class MyEvent {
static int count = 0;
// Этот метод запускает событие SomeEvent. public void OnSomeEvent() {
MyEventArgs arg = new MyEventArgs();
if(SomeEvent != null) { arg.EventNum = count++;
SomeEvent(this, arg);
}
}
}
class X {
public void Handler(object source, MyEventArgs arg) { Console.WriteLine("Событие " + arg.EventNum +
" получено объектом класса X."); Console.WriteLine("Источник: " + source);
Console.WriteLine();
}
}
class Y {
public void Handler(object source, MyEventArgs arg) { Console.WriteLine("Событие " + arg.EventNum +
" получено объектом класса Y."); Console.WriteLine("Источник: " + source);
Console.WriteLine() ;
}
}
class EventDemo6 { static void Main() {
X obi = new X ();
Y ob2 = new Y ();
MyEvent evt - new* MyEvent ();
// Добавить обработчик Handler() в цепочку событий, evt ..SomeEvent += obi. Handler; evt.SomeEvent += ob2.Handler;
// Запустить событие, evt.OnSomeEvent(); evt.OnSomeEvent();
}
}
Ниже приведен результат выполнения этой программы.
Событие 0 получено объектом класса X Источник: MyEvent
Событие 0 получено объектом класса Y Источник: MyEvent
Событие 1 получено объектом класса X Источник: MyEvent
Событие 1 получено объектом класса Y Источник: MyEvent
В данном примере создается классMyEventArgs,производный от классаEventArgs.В классеMyEventArgsдобавляется лишь одно его собственное поле:EventNum.Затем объявляется делегатMyEventHandler,принимающий два параметра, требующиеся для среды .NET Framework. Как пояснялось выше, первый параметр содержит ссылку на объект, формирующий событие, а второй параметр — ссылку на объект классаEventArgsили производного от него класса. Обработчики событийHandler (), определяемые в классахXиY,принимают параметры тех же самых типов.
В классеMyEventобъявляется событиеSomeEventтипаMyEventHandler.Это событие запускается в методеOnSomeEvent() с помощью делегатаSomeEvent,которому в качестве первого аргумента передается ссылкаthis,а вторым аргументом служит экземпляр объекта типаMyEventArgs.Таким образом, делегату типаMyEventHandlerпередаются надлежащие аргументы в соответствии с требованиями совместимости со средой .NET.
Применение делегатов EventHandler<TEventArgs> и EventHandler
В приведенном выше примере программы объявлялся собственный делегат события. Но как правило, в этом не никакой необходимости, поскольку в среде .NET Framework предоставляется встроенный обобщенный делегат под названиемEventHandler<TEventArgs>.(Более подробно обобщенные типы рассматриваются в главе 18.) В данном случае типTEventArgsобозначает тип аргумента, передаваемого параметруEventArgsсобытия. Например, в приведенной выше программе событиеSomeEventможет быть объявлено в классеMyEventследующим образом.
public event EventHandler<MyEventArgs> SomeEvent;
В общем, рекомендуется пользоваться именно таким способом, а не определять собственный делегат.
Для обработки многих событий параметр типаEventArgsоказывается ненужным. Поэтому с целью упростить создание кода в подобных ситуациях в среду .NET Framework внедрен необобщенный делегат типаEventHandler.Он может быть использован для объявления обработчиков событий, которым не требуется дополнительная информация о событиях. Ниже приведен пример использования делегатаEventHandler.
// Использовать встроенный делегат EventHandler. using System;
// Объявить класс, содержащий событие, class MyEvent {
public event EventHandler SomeEvent; // использовать делегат EventHandler
// Этот метод вызывается для запуска события.
public void OnSomeEvent() {
if(SomeEvent != null)
SomeEvent(this, EventArgs.Empty);
}
}
class EventDemo7 {
static void handler(object source, EventArgs arg) {
Console.WriteLine("Произошло событие");
Console.WriteLine("Источник: " + source);
}
static void Main() {
MyEvent evt = new MyEvent();
// Добавить обработчик Handler() в цепочку событий, evt.SomeEvent += Handler;
// Запустить событие, evt.OnSomeEvent() ;
}
}
В данном примере параметр типаEventArgsне используется, поэтому в качестве этого параметра передается объект-заполнительEventArgs . Empty.Результат выполнения кода из данного примера следующий.
Произошло событие Источник: MyEvent
Практический пример обработки событий
События нередко применяются в таких ориентированных на обмен сообщениями средах, как Windows. В подобной среде программа просто ожидает до тех пор, пока не будет получено конкретное сообщение, а затем она предпринимает соответствующее действие. Такая архитектура вполне пригодна для обработки событий средствами С#, поскольку дает возможность создавать обработчики событий для реагирования на различные сообщения и затем просто вызывать обработчик при получении конкретного сообщения. Так, щелчок левой кнопкой мыши может быть связан с событиемLButtonClick.При получении сообщения о щелчке левой кнопкой мыши вызывается методOnLButtonClick (), и об этом событии уведомляются все зарегистрированные обработчики.
Разработка программ для Windows, демонстрирующих такой подход, выходит за рамки этой главы, тем не менее, рассмотрим пример, дающий представление о принципе, по которому действует данный подход. В приведенной ниже программе создается обработчик событий, связанных с нажатием клавиш. Всякий раз, когда на клавиатуре нажимается клавиша, запускается событиеKeyPressпри вызове методаOnKeyPress (). Следует заметить, что в этой программе формируются .NET-совместимые события и что их обработчики предоставляются в лямбда-выражениях.
// Пример обработки событий, связанных с нажатием клавиш на клавиатуре, using System;
// Создать класс, производный от класса EventArgs и // хранящий символ нажатой клавиши.
class KeyEventArgs : EventArgs { public char ch;
}
//Объявить класс события, связанного с нажатием клавиш на клавиатуре, class KeyEvent {
public event EventHandler <KeyEventArgs> KeyPress;
// Этот метод вызывается при нажатии клавиши, public void OnKeyPress(char key) {
KeyEventArgs k = new KeyEventArgs();
if(KeyPress != null) { k.ch = key;
KeyPress(this, k) ;
}
}
}
// Продемонстрировать обработку события типа KeyEvent. class KeyEventDemo { static void Main() {
KeyEvent kevt = new KeyEvent();
ConsoleKeylnfo key; int count = 0;
// Использовать лямбда-выражение для отображения факта нажатия клавиши, kevt.KeyPress += (sender, е) =>
Console.WriteLine(" Получено сообщение о нажатии клавиши: " + e.ch);
// Использовать лямбда-выражение для подсчета нажатых клавиш.
kevt.KeyPress += (sender, е) =>
count++; // count — это внешняя переменная
Console.WriteLine("Введите несколько символов. " +
"По завершении введите точку.");
do {
key = Console.ReadKey(); kevt.OnKeyPress(key.KeyChar);
} while(key.KeyChar != '.');
Console.WriteLine("Было нажато " + count + " клавиш.");
}
}
Вот, например, к какому результату приводит выполнение этой программы.
Было нажато 5 клавиш.
В самом начале этой программы объявляется классKeyEventArgs,производный от классаEventArgsи служащий для передачи сообщения о нажатии клавиши обработчику событий. Затем объявляется обобщенный делегатEventHandler,определяющий обработчик событий, связанных с нажатием клавиш. Эти события инкапсулируются в классеKeyEvent,где определяется событиеKeyPress.
В методеMain() сначала создается объектkevtклассаKeyEvent.Затем в цепочку событийkevt. KeyPressдобавляется обработчик, предоставляемый лямбда-выражением. В этом обработчике отображается факт каждого нажатия клавиши, как показано ниже.
kevt.KeyPress += (sender, е) =>
Console.WriteLine(" Получено сообщение о нажатии клавиши: " + e.ch);
Далее в цепочку событийkevt .KeyPressдобавляется еще один обработчик, предоставляемый лямбда-выражением. В этом обработчике подсчитывается количество нажатых клавиш, как показано ниже.
kevt.KeyPress += (sender, е) =>
count++; // count — это внешняя переменная
Обратите внимание на то, чтоcountявляется локальной переменной, объявленной в методеMain() и инициализированной нулевым значением.
Далее начинает выполняться цикл, в котором методkevt. OnKeyPress() вызывается при нажатии клавиши. Об этом событии уведомляются все зарегистрированные обработчики событий. По окончании цикла отображается количество нажатых клавиш. Несмотря на всю свою простоту, данный пример наглядно демонстрирует саму суть обработки событий средствами С#. Аналогичный подход может быть использован и для обработки других событий. Безусловно, в некоторых случаях анонимные обработчики событий могут оказаться непригодными, и тогда придется внедрить именованные методы.
ГЛАВА 16 Пространства имен, препроцессор и сборки
Вэтой главе речь пойдет о трех средствах С#, позволяющих улучшить организованность и доступность программы. Этими средствами являются пространства имен, препроцессор и сборки.
Пространства имен
О пространстве имен уже вкратце упоминалось в главе 2 в связи с тем, что это основополагающее понятие для С#. В действительности пространство имен в той или иной степени используется в каждой программе на С#. Потребность в подробном рассмотрении пространств имен не возникала до сих пор потому, что для каждой программы на C# автоматически предоставляется используемое по умолчанию глобальное пространство имен. Следовательно, в примерах программ, представленных в предыдущих главах, использовалось глобальное пространство имен. Но во многих реальных программах приходится создавать собственные пространства имен или же организовать взаимодействие с другими пространствами имен. Подобные пространства будут представлены далее во всех подробностях.
Пространство именопределяет область объявлений, в которой допускается хранить одно множество имен отдельно от другого. По существу, имена, объявленные в одном пространстве имен, не будут вступать в конфликт с аналогичными именами, объявленными в другой области. Так, в библиотеке классов для среды .NET Framework, которая одновременно является библиотекой классов С#, используется пространство именSystem.Именно поэтому строка кода
using System;
обычно вводится в самом начале любой программы на С#. Как пояснялось в главе 14, классы ввода-вывода определены в пространстве именSystem. 10,подчиненном пространству именSystem.Ему подчинены и многие другие пространства имен, относящиеся к разным частям библиотеки классов С#.
Пространства имен важны потому, что за последние годы в программировании "расплодились" в огромном количестве имена переменных, методов, свойств и классов, применяемых в библиотечных программах, стороннем и собственном коде. Поэтому без отдельных пространств все эти имена будут соперничать за место в глобальном пространстве имен, порождая конфликтные ситуации. Так, если в программе определен классFinder,то этот класс может вступить в конфликт с другим классомFinder,доступным в сторонней библиотеке, используемой в этой программе. К счастью, подобного конфликта можно избежать, используя отдельные пространства имен, ограничивающие область видимости объявленных в них имен.
Объявление пространства имен
Пространство имен объявляется с помощью ключевого словаnamespace.Ниже приведена общая форма объявления пространства имен:
namespaceимя {
11члены
}
гдеимяобозначает конкретное имя объявляемого пространства имен. При объявлении пространства имен определяется область его действия. Все, что объявляется непосредственно в этом пространстве, оказывается в пределах его области действия. В пространстве имен можно объявить классы, структуры, делегаты, перечисления, интерфейсы или другие пространства имен.
Ниже приведен пример объявленияnamespaceдля создания пространства именCounter.В этом пространстве локализуется имя, используемое для реализации простого класса вычитающего счетчикаCountDown.
// Объявить пространство имен для счетчиков.
namespace Counter {
// Простой вычитающий счетчик, class CountDown { int val;
public CountDown(int n) { val = n;
}
public void Reset(int n) { val = n;
}
public int Count() {
if(val > 0) return val—; else return 0;
}
} // Это конец пространства имен Counter.
Обратите внимание на то, что классCountDownобъявляется в пределах области действия пространства именCounter.Для того чтобы проработать этот пример на практике, поместите приведенный выше код в файлCounter, cs.
Ниже приведен пример программы, демонстрирующий применение пространства именCounter.
// Продемонстрировать применение пространства имен Counter.
using System;
class NSDemo {
static void Main() {
// Обратите внимание на то, как класс CountDown // определяется с помощью пространства имен Counter.
Counter.CountDown cdl = new Counter.CountDown(10); int i;
do {
i = cdl.Count();
Console.Write(i + " ");
} while (i > 0);
Console.WriteLine ();
// Еще раз обратите внимание на то, как класс CountDown // определяется с помощью пространства имен Counter.
Counter.CountDown cd2 = new Counter.CountDown(20);
do {
i = cd2.Count();
Console.Write(i + " ");
} while (i > 0);
Console.WriteLine ();
cd2.Reset (4) ; do {
i = cd2.Count ();
Console.Write(i + " ");
} while (i > 0);
Console.WriteLine () ;
}
}
При выполнении этой программы получается следующий результат.
10 9 876543210
20 19 18 17 16 15 14 13 12 11 10 9 8 7 б 5 4 3 2 1 0 4 3 2 1 0
Для того чтобы скомпилировать эту программу, вы должны включить приведенный выше код в отдельный файл и указать его вместе с упоминавшимся выше файлом, содержащим код объявления пространства именCounter.Если этот код
находится в файлеNSDemo. cs,а код объявления пространства именCounter— в файлеCounter. cs,то для компиляции программы используется следующая командная строка.
csc NSDemo.cs counter.cs
Некоторые важные аспекты данной программы заслуживают более пристального внимания. Во-первых, при создании объекта классаCountDownнеобходимо дополнительно определить его имя с помощью пространства именCounter,как показано ниже. Ведь классCountDownобъявлен в пространстве именCounter.
Counter.CountDown cdl = new Counter.CountDown(10);
Это правило можно обобщить: всякий раз, когда используется член пространства имен, его имя необходимо дополнительно определить с помощью этого пространства имен. В противном случае член пространства имен не будет обнаружен компилятором.
Во-вторых, как только объект типаCounterбудет создан, дополнительно определять его члены с помощью пространства имен уже не придется. Следовательно, методcdl. Count() может быть вызван непосредственно без дополнительного указания пространства имен, как в приведенной ниже строке кода.
i = cdl.Count();
И в-третьих, ради наглядности примера рассматриваемая здесь программа была разделена на два отдельных файла. В одном файле содержится код объявления пространства именCounter,а в другом — код самой программыNSDemo.Но оба фрагмента кода можно было бы объединить в единый файл. Более того, в одном файле исходного кода может содержаться два или более пространства имен со своими собственными областями объявлений. Когда оканчивается действие внутреннего пространства имен, возобновляется действие внешнего пространства имен — в примере сCounterэто глобальное пространство имен. Ради большей ясности в последующих примерах все пространства имен, требующиеся в программе, будут представлены в одном и том же файле. Следует, однако, иметь в виду, что их допускается распределять по отдельным файлам, что практикуется чаще в выходном коде.
Предотвращение конфликтов имен с помощью пространств имен
Главное преимущество пространств имен заключается в том, что объявленные в них имена не вступают в конфликт с именами, объявленными за их пределами. Например, в приведенной ниже программе определяются два пространства имен. Первым из них является представленное ранее пространство именCounter,а вторым —Counter2.Оба пространства имен содержат классы с одинаковым именемCountDown,но поскольку это разные пространства, то оба классаCountDownне вступают в конфликт друг с другом. Кроме того, оба пространства имен определены в одном и том же файле. Как пояснялось выше, это вполне допустимо. Безусловно, каждое из этих пространств имен можно было бы выделить в отдельный файл, если бы в этом возникла потребность.
// Пространства имен предотвращают конфликты имен.
// Объявить пространство имен Counter, namespace Counter {
// Простой вычитающий счетчик, class CountDown { int val; -
public CountDown(int n) { val = n;
}
public void Reset(int n) { val = n;
}
public int Count () {
if(val > 0) return val—; else return 0;
}
}
}
// Объявить пространство имен Counter2. namespace Counter2 {
/* Этот класс CountDown относится к пространству имен Counter2 и поэтому не вступает в конфликт с аналогичным классом из пространства имен Counter.
*/
class CountDown {
public void Count() {
Console.WriteLine("Это метод Count() из " +
"пространства имен Counter2.");
}
}
}
class NSDemo2 {
static void Main() {
// Это класс CountDown из пространства имен Counter. Counter.CountDown cdl = new Counter.CountDown(10);
// Это класс CountDown из пространства имен Counter2. Counter2.CountDown cd2 = new Counter2.CountDown(); int i;
do {
i = cdl.Count();
Console.Write(i + " ");
} while(i > 0);
Console.WriteLine();
Вот к какому результату приводит выполнение этой программы.
10 987654 3.210
Это метод Count () из пространства имен Counter2.
Как следует из приведенного выше результата, классCountDownиз пространства именCounterсуществует отдельно от класса того же названия из пространства именCounter2,и поэтому конфликт имен не возникает. Несмотря на всю простоту данного примера, он наглядно показывает, как удается избежать конфликта имен в собственном коде и коде, написанном другими разработчиками, поместив классы с одинаковыми именами в разные пространства имен.
Директива using
Если в программе присутствуют частые ссылки на члены конкретного пространства имен, то указывать это пространство всякий раз, когда требуется ссылка на него, не очень удобно. Преодолеть это затруднение помогает директиваusing.В подавляющем большинстве приводившихся ранее примеров программ с помощью этой директивы делалось видимым глобальное для C# пространство именSystem,поэтому она отчасти вам уже знакома. Как и следовало ожидать, с помощью директивыusingможно сделать видимыми вновь создаваемые пространства имен.
Существуют две формы директивыusing.Ниже приведена первая из них:
usingимя;
гдеимяобозначает имя того пространства имен, к которому требуется получить доступ. Все члены, определенные в указанном пространстве имен, становятся видимыми, и поэтому могут быть использованы без дополнительного определения их имен. Директивуusingнеобходимо вводить в самом начале каждого файла исходного кода перед любыми другими объявлениями или же в начале тела пространства имен.
Приведенная ниже программа является вариантом предыдущего примера, переработанным с целью продемонстрировать применение директивыusing,делающей видимым создаваемое пространство имён.
// Продемонстрировать применение директивы using, using System;
// Сделать видимым пространство имен Counter, using Counter; ,
// Объявить пространство имен для счетчиков, namespace Counter {
// Простой вычитающий счетчик, class CountDown { int val;
public CountDown(int n) { val = n;
}
public void Reset(int n) { val = n;
}
public int Count () {
if(val > 0) return val—; else return 0;
}
}
}
class NSDemo3 {
static void Main() {
// Теперь класс CountDown может быть использован непосредственно. CountDown cdl = new CountDown(10); int i;
do {
i = cdl.Count ();
Console.Write (i + " ") ;
} while (i > 0);
Console.WriteLine ();
CountDown cd2 = new CountDown (20);
do {
i = cd2.Count ();
Console.Write (i + " ");
} while (i > 0);
Console.WriteLine ();
cd2.Reset(4) ; do {
i = cd2.Count ();
Console.Write(i + " ");
} while (i > 0);
Console.WriteLine ();
}
}
В эту версию программы внесены два существенных изменения. Первое из них состоит в применении директивыusingв самом начале программы, как показано ниже.
using Counter;
Благодаря этому становится видимым пространство именCounter.Второе изменение состоит в том, что классCountDownбольше не нужно дополнительно определять с помощью пространства именCounter,как демонстрирует приведенная ниже строка кода из методаMain ().
CountDown cdl = new CountDown(10);
Теперь пространство именCounterстановится видимым, и поэтому классCountDownможет быть использован непосредственно. -
Рассматриваемая здесь программа иллюстрирует еще одно важное обстоятельство: применение одного пространства имен не отменяет действие другого. Когда пространство имен делается видимым, это просто дает возможность использовать его содержимое без дополнительного определения имен. Следовательно, в данном примере оба пространства имен,SystemиCounter,становятся видимыми.
Вторая форма директивы using
Вторая форма директивыusingпозволяет определить еще одно имя (так называемыйпсевдоним)типа данных или пространства имен. Эта форма приведена ниже:
usingпсевдоним = имя;
гдепсевдонимстановится еще одним именем типа (например, типа класса) или пространства имен, обозначаемого какимя.После того как псевдоним будет создан, он может быть использован вместо первоначального имени.
Ниже приведен вариант программы из предыдущего примера, измененный с целью показать создание и применение псевдонимаMyCounterвместо составного имениCounter.CountDown.
// Продемонстрировать применение псевдонима, using System;
// Создать псевдоним для составного имени Counter.CountDown. using MyCounter = Counter.CountDown;
// Объявить пространство имен для счетчиков, namespace Counter {
// Простой вычитающий счетчик, class CountDown { int val;
public CountDown(int n) { val = n;
}
public void Reset(int n) { val = n;
} '
public int Count() {
if(val > 0) return val — ; else return 0;
}
}
}
class NSDemo4 {
static void Main() {
// Здесь и далее псевдоним MyCounter используется // вместо составного имени Counter.CountDown.
MyCounter cdl = new MyCounter(10); int i;
do {
i = cdl... Count() ;
Console.Write(i + " ");
} while (i > 0);
Console.WriteLine () ;
MyCounter cd2 = new MyCounter (20);
do {
i = cd2.Count ();
Console.Write (i + " ");
} while (i > 0);
Console.WriteLine ();
cd2.Reset (4); do {
i = cd2.Count ();
Console.Write (i + " ");
} while (i > 0);
Console.WriteLine ();
}
}
ПсевдонимMyCounterсоздается с помощью следующего оператора.
using MyCounter = Counter.CountDown;
После того как псевдоним будет определен в качестве другого имени классаCounter. CountDown,его можно использовать для объявления объектов без дополнительного определения имени данного класса. Например, в следующей строке кода из рассматриваемой здесь программы создается объект классаCountDown.
MyCounter cdl = new MyCounter (10);
Аддитивный характер пространств имен
П04 одним именем можно объявить несколько пространств имен. Это дает возможность распределить пространство имен по нескольким файлам или даже разделить его в пределах одного и того же файла исходного кода. Например, в приведенной ниже программе два пространства имен определяются под одним и тем же именемCounter.Одно из них содержит классCountDown,а другое — классCountUp.Во время компиляции содержимое обоих пространств именCounterскладывается.
// Аддитивный характер пространств имен, using System;
// Сделать видимым пространство имен Counter, using Counter;
// Это одно пространство имен Counter.
namespace Counter {
// Простой вычитающий счетчик, class CountDown { int val;
public CountDown(int n) { val = n;
}
public void Reset(int n) { val = n;
}
public int Count() {
if(val > 0) return val—; else return 0;
}
}
}
//А это другое пространство имен Counter, namespace Counter {
// Простой суммирующий счетчик, class CountUp { int val; int target;
public int Target { get{
return target;
}
}
public CountUp(int n) { target = n; val = 0;
}
public void Reset(int n) { target = n; val = 0;
}
public int Count() {
if(val < target) return val++;
else return target;
}
}
}
class NSDemo5 {
static void Main() {
CountDown cd = new CountDown(10); CountUp cu = new CountUp(8);
int i; do {
i = cd.Count ();
Console^.Write (i + " ;
} -while (i > 0) ;
Console.WriteLine ();
do {
i = cu.Count ();
Console.Write(i + " ");
} while(i < cu.Target);
}
}
Вот к какому результату приводит выполнение этой программы.
10 9876543210 012345678
Обратите также внимание на то, что директива
using Counter;
делает видимым все содержимое пространства именCounter.Это дает возможность обращаться к классамCountDownиCountUpнепосредственно, т.е. без дополнительного указания пространства имен. При этом разделение пространства именCounterна две части не имеет никакого значения.
Вложенные пространства имен
Одно пространство имен может быть вложено в другое. В качестве примера рассмотрим следующую программу.
// Вложенные пространства имен.
using System;
namespace NS1 { class ClassA {
public ClassA() {
Console.WriteLine("Конструирование класса ClassA");
}
}
namespace NS2 { // вложенное пространство имен class ClassB {
public ClassB () {
Console.WriteLine("Конструирование класса ClassB");
}
}
}
}
class NestedNSDemo { static void Main() { .
NSl.ClassA a = new NS1.ClassA();
// NS2.ClassB b = new NS2.ClassB (); // Неверно!!! Пространство NS2 невидимо NS1.NS2.ClassB b = new NS1.NS2.ClassB(); // Верно!
}
}
Выполнение этой программы дает следующий результат.
Конструирование класса ClassA Конструирование класса ClassB
В этой программе пространство именNS2вложено в пространство именNS1.Поэтому для обращения к классуClassBнеобходимо дополнительно указать пространства именNS1иNS2.Указания одного лишь пространства именNS2для этого недостаточно. Как следует из приведенного выше примера, пространства имен дополнительно указываются через точку. Следовательно, для обращения к классуClassBв методеMain() необходимо указать его полное имя —NSl.NS2.ClassB.
Пространства имен могут быть вложенными больше, чем на два уровня. В этом случае член вложенного пространства имен должен быть дополнительно определен с помощью всех охватывающих пространств имен.
Вложенные пространства имен можно указать в одном оператореnamespace,разделив их точкой. Например, вложенные пространства имен
namespace OuterNS { namespace InnerNS {
// ...
}
}
могут быть указаны следующим образом.
namespace OuterNS.InnerNS {
П...
}
Глобальное пространство имен
Если впрограмме не объявлено пространство имен, то по умолчанию используется глобальное пространство имен. Именно поэтому в примерах программ, представленных в предыдущих главах книги, не нужно было обращаться для этой цели к ключевому словуnamespace.Глобальное пространство удобно для коротких программ, как в примерах из этой книги, но в большинстве случаев реальный код содержится в объявляемом пространстве имен. Главная причина инкапсуляции кода в объявляемом пространстве имен — предотвращение конфликтов имен. Пространства имен служат дополнительным средством, помогающим улучшить организацию программ и приспособить их к работе в сложной среде с современной сетевой структурой.
Применение описателя псевдонима пространства имен ::
Пространства имен помогают предотвратить конфликты имен, но не устранить их полностью. Такой конфликт может, в частности, произойти, когда одно и то же имя
объявляется в двух разных пространствах имен и затем предпринимается попытка сделать видимыми оба пространства. Допустим, что два пространства имен содержат классMyClass.Если попытаться сделать видимыми оба пространства имен с помощью директивойsing,то имяMyClassиз первого пространства вступит в конфликт с именемMyClassиз второго пространства, обусловив появление ошибки неоднозначности. В таком случае для указания предполагаемого пространства имен явным образом можно воспользоватьсяописателем псевдонима пространства имен ::.
Ниже приведена общая форма оператора : :.
псевдоним_пространства_имен: : идентификатор
Здесьпсевдоним_пространства_именобозначает конкретное имя псевдонима пространства имен, аидентификатор— имя члена этого пространства.
Для того чтобы стало понятнее назначение описателя псевдонима пространства имен, рассмотрим следующий пример программы, в которой создаются два пространства имен,CounterиAnotherCounter,и в обоих пространствах объявляется классCountDown.Затем оба пространства имен становятся видимыми с помощью директивusing.И наконец, в методеMain() предпринимается попытка получить экземпляр объекта типаCountDown.
// Продемонстрировать необходимость описателя ::. using System;
// Использовать оба пространства имен Counter и AnotherCounter.
using Counter; using AnotherCounter;
// Объявить пространство имен для счетчиков, namespace Counter {
// Простой вычитающий счетчик, class CountDown { int val;
public CountDown(int n) { val = n;
}
// ...
}
}
// Объявить еще одно пространство имен для счетчиков, namespace AnotherCounter {
// Объявить еще один класс CountDown, принадлежащий // пространству имен AnotherCounter. class CountDown { int val;
public CountDown(int n) { val = n;
}
}
}
class WhyAliasQualifier { static void Main() { int i;
// Следующая строка, по существу, неоднозначна!
// Неясно, делается ли в ней ссылка на класс CountDown // из пространства имен Counter или AnotherCounter?
CountDown cdl = new CountDown(10); // Ошибка! ! !
//...
}
}
Если попытаться скомпилировать эту программу, то будет получено сообщение об ошибке, уведомляющее о неоднозначности в следующей строке кода из методаMain ().
CountDown cdl = new CountDown(10); // Ошибка! ! !
Причина подобной неоднозначности заключается в том, что в обоих прострайствах имен,CounterиAnotherCounter,объявлен классCountDownи оба пространства сделаны видимыми. Поэтому неясно, к какому именно варианту классаCountDownследует отнести приведенное выше объявление. Для.устранения подобного рода недоразумений и предназначен описатель : :.
Для того чтобы воспользоваться описателем : :, необходимо сначала определить псевдоним для пространства имен, которое требуется описать, а затем дополнить описание неоднозначного элемента этим псевдонимом. Ниже приведен вариант предыдущего примера программы, в котором устраняется упомянутая выше неоднознач- , ность.
// Продемонстрировать применение описателя ::.
using System; using Counter; using AnotherCounter;
// Присвоить классу Counter псевдоним Ctr. using Ctr = Counter;
// Объявить пространство имен для счетчиков, namespace Counter {
// Простой вычитающий счетчик, class CountDown { int val;
public CountDown(int n) { val = n;
}
}
}
// Объявить еще одно пространство имен для счетчиков, namespace AnotherCounter {
// Объявить еще один класс CountDown, принадлежащий // пространству имен AnotherCounter. class CountDown { int val;
public CountDown(int n) { val = n;
}
//...
}
}
class AliasQualifierDemo { static void Main() {
// Здесь оператор :: разрешает конфликт, предписывая компилятору // использовать класс CountDown из пространства имен Counter.
Ctr::CountDown cdl = new Ctr::CountDown(10);
// ...
}
}
В этом варианте программы для классаCounterсначала указывается псевдонимCtrв следующей строке кода.
using Ctr = Counter;
А затем этот псевдоним используется в методеMain() для дополнительного описания классаCountDown,как показано ниже.
Ctr::CountDown cdl = new Ctr::CountDown(10);
Описатель : : устраняет неоднозначность, поскольку он явно указывает на то, что следует обратиться к классуCountDownиз пространстваCtr,а фактически —Counter.Именно это и делает теперь программу пригодной для компиляции.
Описатель : : можно также использовать вместе с предопределенным идентификаторомglobalдля ссылки на глобальное пространство имен. Например, в приведенной ниже программе классCountDownобъявляется как в пространстве именCounter,так и в глобальном пространстве имен. А для доступа к варианту классаCountDownв глобальном пространстве имен служит предопределенный псевдонимglobal.
// Использовать псевдоним глобального пространства имен, using System;
// Присвоить классу Counter псевдоним Ctr. using Ctr = Counter;
namespace Counter {
// Простой вычитающий счетчик, class CountDown { int val;
public CountDown(int n) { val = n;
}
П...
}
}
// Объявить еще один класс CountDown, принадлежащий // глобальному пространству имен, class CountDown { int val;
public CountDown(int n) { val = n;
}
// ...
}
class GlobalAliasQualifierDemo { static void Main() {
// Здесь описатель :: предписывает компилятору использовать // класс CountDown из пространства имен Counter.
Ctr::CountDown cdl = new Ctr::CountDown(10);
// Далее создать объект класса CountDown из // глобального пространства имен.
global::CountDown cd2 = new global::CountDown(10) ;
П...
}
}
Обратите внимание на то, что идентификаторglobalслужит для доступа к классуCountDownиз используемого по умолчанию пространства имен.
global::CountDown cd2 = new global::CountDown(10) ;
Этот подход можно распространить на любую ситуацию, в которой требуется указывать используемое по умолчанию пространство имен.
И последнее: описатель псевдонима пространства имен можно применять вместе с псевдонимами типаextern,как будет показано в главе 20.
Препроцессор
В C# определен ряд директив препроцессора, оказывающих влияние на интерпретацию исходного кода программы компилятором. Эти директивы определяют порядок интерпретации текста программы перед ее трансляцией в объектный код в том исходном файле, где они появляются. Терминдиректива препроцессорапоявился в связи с тем, что подобные инструкции по традиции обрабатывались на отдельной стадии компиляции, называемойпрепроцессором.Обрабатывать директивы на отдельной стадии препроцессора в современных компиляторах уже не нужно, но само ее название закрепилось.
Ниже приведены директивы препроцессора, определенные в С#.
# define
#elif
#else
#endif
#endregion
#error
#if
#line
#pragma
#region
#undef
#warning
Все директивы препроцессора начинаются со знака #. Кроме того, каждая директива препроцессора должна быть выделена в отдельную строку кода.
Принимая во внимание современную объектно-ориентированную архитектуру языка С#, потребность в директивах препроцессора в нем не столь велика, как в языках программирования предыдущих поколений. Тем не менее они могут быть иногда полезными, особенно для условной компиляции. В этом разделе все директивы препроцессора рассматриваются по очереди.
Директива #define
Директива #defineопределяет последовательность символов, называемуюидентификатором.Присутствие или отсутствие идентификатора может быть определено с помощью директивы#ifили#elifи поэтому используется для управления процессом компиляции. Ниже приведена общая форма директивы #define.
#defineидентификатор
Обратите внимание на отсутствие точки с запятой в конце этого оператора. Между директивой #defineи идентификатором может быть любое количество пробелов, но после самого идентификатора должен следовать только символ новой строки. Так, для определения идентификатораEXPERIMENTALслужит следующая директива.
#define EXPERIMENTAL
ПРИМЕЧАНИЕ
В C/C++ директива #define может использоваться для подстановки исходного текста, например для определения имени значения, а также для создания макрокоманд, похожих на функции. А в C# такое применение директивы #define не поддерживается. В этом языке директива #define служит только для определения идентификатора.
Директивы #if и #endif
Обе директивы, #if и#endif,допускают условную компиляцию последовательности кода в зависимости от истинного результата вычисления выражения, включающего в себя один или несколько идентификаторов. Идентификатор считается истинным, если он определен, а иначе — ложным. Так, если идентификатор определен директивой#define,то он будет оценен как истинный. Ниже приведена общая форма директивы#if.
#ifидентификаторное_выражение последовательность операторов#endif
Еслиидентификаторное_выражение,следующее после директивы #if, истинно, то компилируется код(последовательность операторов),указываемый между ним и директивой#endif.В противном случае этот промежуточный код пропускается. Директива#endifобозначает конец блока директивы#if.
Идентификаторное выражение может быть простым, как наименование идентификатора. В то же время в нем разрешается применение следующих операторов: !, ==, ! =, & & и | |, а также круглых скобок.
Ниже приведен пример применения упомянутых выше директив.
// Продемонстрировать применение директив // #if, #endif и #define.
#define EXPERIMENTAL
using System;
class Test {
static void Main() {
#if EXPERIMENTAL
Console.WriteLine("Компилируется для экспериментальной версии."); #endif
Console.WriteLine("Присутствует во всех версиях.");
}
}
Этот код выдает следующий результат.
Компилируется для экспериментальной версии.
Присутствует во всех версиях.
В приведенном выше коде определяется идентификаторEXPERIMENTAL.Поэтому когда в этом коде встречается директива #i f, идентификаторное выражение вычисляется как истинное и затем компилируется первый оператор, содержащий вызов методаWriteLine (). Если же удалить определение идентификатораEXPERIMENTALи перекомпилировать данный код, то первый оператор, содержащий вызов методаWriteLine (), не будет скомпилирован, поскольку идентификаторное выражение директивы #i fвычисляется как ложное. Но второй оператор, содержащий вызов методаWriteLine(), компилируется в любом случае, потому что он не входит в блок директивы#if.
Как пояснялось выше, в директиве #i fдопускается указывать идентификаторное выражение. В качестве примера рассмотрим следующую программу.
// Использовать идентификаторное выражение.
#define EXPERIMENTAL #define TRIAL
class Test {
static void Main() {
#if EXPERIMENTAL
Console.WriteLine("Компилируется для экспериментальной версии."); #endif
#if EXPERIMENTAL && TRIAL
Console.Error.WriteLine("Проверка пробной экспериментальной версии. ") ;
#endif
Console.WriteLine("Присутствует во всех версиях.");
}
}
Эта программа дает следующий результат.
Компилируется для экспериментальной версии.
Проверка пробной экспериментальной версии.
Присутствует во всех версиях.
В данном примере определены два идентификатора:EXPERIMENTALиTRIAL.Второй оператор, содержащий вызов методаWriteLine (), компилируется лишь в том случае, если определены оба идентификатора.
Для компилирования кода в том случае, если идентификатор не определен, можно воспользоваться оператором !, как в приведенном ниже примере.
#if !EXPERIMENTAL
Console.WriteLine("Этот код не экспериментальный!");
#endif
Вызов метода будет скомпилирован только в том случае, если идентификаторEXPERIMENTALнеопределен.
Директивы #else и #elif
Директива#elseдействует аналогично условному операторуelseязыка С#, определяя альтернативный ход выполнения программы, если этого не может сделать директива#if. С учетом директивы#elseпредыдущий пример программы может быть расширен следующим образом.
// Продемонстрировать применение директивы #else.
#define EXPERIMENTAL
using System;
class Test {
static void Main() {
#if EXPERIMENTAL
Console.WriteLine("Компилируется для экспериментальной версии.");
#else
Console.WriteLine("Компилируется для окончательной версии.");
#endif
#if EXPERIMENTAL && TRIAL
Console.Error.WriteLine("Проверка пробной экспериментальной версии.");
#else
Console.Error.WriteLine("Это не пробная экспериментальная версия."); #endif
Console.WriteLine("Присутствует во всех версиях.");
}
}
Вот к какому результату приводит выполнение этой программы.
Компилируется для экспериментальной версии.
Это не пробная экспериментальная версия.
Присутствует во всех версиях.
В данном примере идентификатор TRIAL не определен, и поэтому часть #else второй условной последовательности кода не компилируется.
Обратите внимание на то, что директива #else обозначает конец блока директивы #if и в то же время — начало блока самой директивы #else. Это необходимо потому, что с любой директивой #if может быть связана только одна директива #endif. Более того, с любой директивой #if может быть связана только одна директива #else.
Обозначение #elif означает "иначе если", а сама директива #elif определяет последовательность условных операций if-else-if для многовариантной компиляции. После директивы #elif указывается идентификаторное выражение. Если это выражение истинно, то компилируется следующий далее кодовый блок, а остальные выражения директивы #elif не проверяются. В противном случае проверяется следующий по порядку блок. Если же ни одну из директив #elif не удается выполнить, то при наличии директивы #else выполняется последовательность кода, связанная с этой директивой, а иначе не компилируется ни один из кодовых блоков директивы #if. Ниже приведена общая форма директивы #elif.
#ifидентификаторное_выражение последовательность операторов#elifидентификаторное_выражение последовательность операторов#elifидентификаторное_выражение последовательность операторов// . . .
#endif
В приведенном ниже примере демонстрируется применение директивы #elif.
// Продемонстрировать применение директивы #elif.
#define RELEASE
using System;
class Test {
static void Main() {
#if EXPERIMENTAL
Console.WriteLine("Компилируется для экспериментальной версии.");
#elif RELEASE
Console.WriteLine("Компилируется для окончательной версии.");
#else
Console.WriteLine("Компилируется для внутреннего тестирования."); #endif
#if TRIAL && !RELEASE
Console.WriteLine("Пробная версия. ") ;
#endif
Console.WriteLine("Присутствует во всех версиях.");
}
}
Этот код выдает следующий результат.
Компилируется для окончательной версии.
Присутствует во всех версиях.
Директива #undef
С помощью директивы #undef удаляется определенный ранее идентификатор. Это, по существу, означает, что он становится "неопределенным". Ниже приведена общая форма директивы #undef.
#undefидентификатор
Рассмотрим следующий пример кода.
#define SMALL
#if SMALL // . . .
#undef SMALL
// теперь идентификатор SMALL не определен.
После директивы #undef идентификатор SMALL уже оказывается неопределенным.
Директива #undef применяется главным образом для локализации идентификаторов только в тех фрагментах кода, в которых они действительно требуются.
Директива #еггог
Директива #error вынуждает компилятор прервать компиляцию. Она служит в основном для отладки. Ниже приведена общая форма директивы #еггог.
#еггогсообщение_об_ошибке
Когда в коде встречается директива terror, выводится сообщение об ошибке. Например, когда компилятору встречается строка кода
#еггог Это тестовая ошибка!
компиляция прерывается и выводится сообщение "Это тестовая ошибка ! ".
Директива #warning
Директива #warning действует аналогично директиве terror, за исключением того, что она выводит предупреждение, а не ошибку. Следовательно, компиляция не прерывается. Ниже приведена общая форма директивы #warning.
#warningпр едупр еж да юще е_соо бще ние
Директива #line
Директива #line задает номер строки и имя файла, содержащего эту директиву. Номер строки и имя файла используются при выводе ошибок или предупреждений во время компиляции. Ниже приведена общая форма директивы #line.
#line номер” имя_файла"
Имеются еще два варианта директивы #line. В первом из них она указывается с ключевым словом default, обозначающим возврат нумерации строк в исходное состояние, как в приведенном ниже примере.
#line default
А во втором варианте директива #line указывается с ключевым словом hidden. При пошаговой отладке программы строки кода, находящиеся между директивой
#line hidden
и следующей директивой #line без ключевого слова hidden, пропускаются отладчиком.
Директивы #region и #endregion
С помощью директив #region и #endregion определяется область, которая разворачивается или сворачивается при структурировании исходного кода в интегрированной среде разработки Visual Studio. Ниже приведена общая форма этих директив:
#regionтекст
// последовательность кода #endregionтекст
гдетекстобозначает необязательную символьную строку.
Директива #pragma
С помощью директивы #pragma инструкции задаются компилятору в виде опций. Ниже приведена общая форма этой директивы:
#pragmaопция
гдеопцияобозначает инструкцию, передаваемую компилятору.
В текущей версии C# предусмотрены две опции для директивы #pragma. Первая из них, warning, служит для разрешения или запрета отдельных предупреждений со стороны компилятора. Она принимает две формы:
#pragma warning disableпредупреждения#pragma warning restoreпредупреждения
гдепредупрежденияобозначает разделяемый запятыми список номеров предупреждений. Для отмены предупреждения используется опция disable, а для его разрешения — опция restore.
Например, в приведенной ниже директиве #pragma запрещается выдача предупреждения №168, уведомляющего о том, что переменная объявлена, но не используется.
#pragma warning disable 168
Второй для директивы #pragma является опция checksum. Она служит для формирования контрольной суммы в проектах ASP.NET. Ниже приведена ее общая форма:
#pragma checksum"имя_файла""{GUID}" "контрольная_сумма"
гдеимя_файлаобозначает конкретное имя файла;GUID— глобально уникальный идентификатор, с которым связаноимя_файла;контроль на я_сумма— шестнадцатеричное число, представляющее контрольную сумму. У этой контрольной суммы должно быть четное число цифр.
Сборки и модификатор доступа internal
Сборкаявляется неотъемлемой частью программирования на С#. Она представляет собой один или несколько файлов, содержащих все необходимые сведения о развертывании программы и ее версии. Сборки составляют основу среды .NET. Они предоставляют механизмы для надежного взаимодействия компонентов, межъязыковой возможности взаимодействия и управления версиями. Кроме того, сборки определяют область действия программного кода.
Сборка состоит из четырех разделов. Первый раздел представляет собойдекларациюсборки. Декларация содержит сведения о самой сборке. К этой информации относится, в частности, имя сборки, номер ее версии, сведения о соответствии типов и параметры культурной среды (язык и региональные стандарты). Второй раздел сборки содержит метаданные типов, т.е. сведения о типах данных, используемых в программе. Среди прочих преимуществметаданные типовспособствуют межъязыковой возможности взаимодействия. Третий раздел сборки содержитпрограммный кодв формате MSIL (Microsoft Intermediate Language — промежуточный язык корпорации Microsoft). И четвертый раздел сборки содержит ресурсы, используемые программой.
Правда, при программировании на C# сборки получаются автоматически, требуя от программирующего лишь минимальных усилий. Дело в том, что исполняемый файл, создаваемый во время компиляции программы на С#, на самом деле представляет собой сборку, содержащую исполняемый код этой программы, а также другие виды информации. Таким образом, когда компилируется программа на С#, сборка получается автоматически.
У сборок имеется много других особенностей, и с ними связано немало актуальных вопросов программирования, но, к сожалению, их обсуждение выходит за рамки этой книги. Ведь сборки являются неотъемлемой частью процесса разработки программного обеспечения в среде .NET, но формально они не относятся к средствам языка С#. Тем не менее в C# имеется одно средство, непосредственно связанное со сборкой. Это модификатор доступа internal, рассматриваемый в следующем разделе.
Модификатор доступа internal
Помимо модификаторов доступаpublic, privateиprotected,использовавшихся в представленных ранее примерах программ, в C# предусмотрен также модификатор доступаinternal.Этот модификатор определяет доступность члена во всех файлах сборки и его недоступность за пределами сборки. Проще говоря, о члене, обозначенном какinternal,известно только в самой программе, но не за ее пределами. Модификатор доступаinternalособенно полезен для создания программных компонентов.
Модификатор доступаinternalможно применять к классам и их членам, а также к структурам и членам структур. Кроме того, модификаторinternalразрешается использовать в объявлениях интерфейсов и перечислений.
Из модификаторовprotectedиinternalможно составить спаренный модификатор доступаprotected internal.Уровень доступаprotected internalможет быть задан только для членов класса. Член, объявленный какprotected internal,доступен лишь в пределах собственной сборки или для производных типов.
Ниже приведен пример применения модификатора доступаinternal.
// Использовать модификатор доступа internal.
using System;
class InternalTest { internal int x;
}
class InternalDemo { static void Main() {
InternalTest ob = new InternalTest();
ob.x = 10; // доступно, потому что находится в том же файле Console.WriteLine("Значение ob.x: " + ob.x);
}
}
В классеInternalTestполе х объявляется какinternal.Это означает, что поле х доступно в самой программе, но, как показывает код классаInternalDemo,оно недоступно за пределами программы.
ГЛАВА 17 Динамическая идентификация типов, рефлексия и атрибуты
I
В этой главе рассматриваются три эффективных средства: динамическая идентификация типов, рефлексия и атрибуты.Динамическая идентификация типовпредставляет собой механизм, позволяющий определить тип данных во время выполнения программы. Рефлексия — это средство для получения сведений о типе данных. Используя эти сведения, можно конструировать и применять объекты во время выполнения. Это довольно эффективное средство, поскольку оно дает возможность расширять функции программы динамически, т.е. в процессе ее выполнения.Атрибутописывает характеристики определенного элемента программы на С#. Атрибуты можно, в частности, указать для классов, методов и полей. Во время выполнения программы разрешается опрашивать атрибуты для получения сведений о них. Для этой цели в атрибутах используется динамическая идентификация типов и рефлексия.
Динамическая идентификация типов
Динамическая идентификация типов (RTTI) позволяет определить тип объекта во время выполнения программы. Она оказывается полезной по целому ряду причин. В частности, по ссылке на базовый класс можно довольно точно определить тип объекта, доступного по этой ссылке. Динамическая идентификация типов позволяет также проверить заранее, насколько удачным будет исход приведения типов, предотвращая исключительную ситуацию в связи с неправильным приведением типов. Кроме того, динамическая идентификация типов является главной составляющей рефлексии.
Для поддержки динамической идентификации типов в C# предусмотрены три ключевых слова: is, as и typeof. Каждое из этих ключевых слов рассматривается далее по очереди.
Проверка типа с помощью оператора is
Конкретный тип объекта можно определить с помощью оператора is. Ниже приведена его общая форма:
выражениеi sтип
гдевыражениеобозначает отдельное выражение, описывающее объект,типкоторого проверяется. Есливыражениеимеет совместимый или такой же тип, как и проверяемый тип, то результат этой операции получается истинным, в противном случае — ложным. Так, результат будет истинным, есливыражениеимеет проверяемыйтипв той или иной форме. В операторе i s оба типа определяются как совместимые, если они одного и того же типа или если предусмотрено преобразование ссылок, упаковка или распаковка.
Ниже приведен пример применения оператора is.
// Продемонстрировать применение оператора is. using System;
class A {} class В : A {}
class Usels {
static void Main() {
A a = new A();
В b = new В(); if (a is A)
Console.WriteLine("а имеет тип A"); if(b is A)
Console.WriteLine ("b совместим с А, поскольку он производный от А"); if(a is В)
Console.WriteLine("Не выводится, поскольку а не производный от В");
if(b is В)
Console.WriteLine("В имеет тип В"); if(a is object)
Console.WriteLine("а имеет тип object");
}
}
Вот к какому результату приводит выполнение этого кода.
а имеет тип А
b совместим с А, поскольку он производный от А b имеет тип В а имеет тип object
Большая часть выражений i s в приведенном выше примере не требует пояснений, но два из них необходимо все же разъяснить. Прежде всего, обратите внимание на следующую строку кода.
if (b is А)
Console.WriteLine("b совместим с А, поскольку он производный от А");
Условный оператор if выполняется, поскольку b является объектом типа В, производным от типа-А. Но обратное несправедливо. Так, если в строке кода
if(a is В)
Console.WriteLine("Не выводится, поскольку а не производный от В");
условный оператор i f не выполняется, поскольку а является объектом типа А, не производного от типа В. Поэтому анеотносится к типу В.
Применение оператора as
Иногда преобразование типов требуется произвести во время выполнения, но не генерировать исключение, если исход этого преобразования окажется неудачным, что вполне возможно при приведении типов. Для этой цели служит оператор as, имеющий следующую общую форму:
выражениеasтип
гдевыражениеобозначает отдельное выражение, преобразуемое в указанныйтип.Если исход такого преобразования оказывается удачным, то возвращается ссылка на тип, а иначе — пустая ссылка. Оператор as может использоваться только для преобразования ссылок, идентичности, упаковки, распаковки.
Внекоторых случаях оператор as может служить удобной альтернативой оператору is.Вкачестве примера рассмотрим следующую программу, в которой оператор is используется для предотвращения неправильного приведения типов.
// Использовать оператор is для предотвращения неправильного приведения типов.
using System;
class А {} class В : А {}
class CheckCast {
static void Main() {
A a = new A();
В b = new В ();
// Проверить, можно ли привести а к типу В. if(a is В) // если да, то выполнить приведение типов b = (В) а;
else // если нет, то пропустить приведение типов b = null;
if(b==null)
Console.WriteLine("Приведение типов b = (В) HE допустимо."); else
Console.WriteLine("Приведение типов b = (В) допустимо.");
Эта программа дает следующий результат.
Приведение типов b = (В) НЕ допустимо.
Как следует из результата выполнения приведенной выше программы, тип объектаане совместим с типомВ,и поэтому его приведение к типуВне допустимо и предотвращается в условном оператореif.Но такую проверку приходится выполнять в два этапа. Сначала требуется убедиться в обоснованности операции приведения типов, а затем выполнить ее. Оба этапа могут быть объединены в один с помощью оператора as, как демонстрирует приведенная ниже программа.
// Продемонстрировать применение оператора as.
using System;
class A {} class В : A {}
class CheckCast { ,
static void Main() {
A a = new A();
В b = new В();
b = a as В; // выполнить приведение типов, если это возможно if(b==null)
Console.WriteLine("Приведение типов b = (В) НЕ допустимо."); else
Console.WriteLine("Приведение типов b = (В) допустимо.");
}
}
Эта программа дает прежний результат.
Приведение типов b = (В) НЕ допустимо.
В данном варианте программы в одном и том же операторе as сначала проверяется обоснованность операции приведения типов, а затем выполняется сама операция приведения типов, если она допустима.
Применение оператора typeof
Несмотря на всю свою полезность, операторыasиisпроверяют лишь совместимость двух типов. Но зачастую требуется информация о самом типе. Для этой цели в C# предусмотрен операторtypeof.Он извлекает объект классаSystem. Туредля заданного типа. С помощью этого объекта можно определить характеристики конкретного типа данных. Ниже приведена общая форма оператораtypeof:
typeof(тип)
где тип обозначает получаемый тип. Информация, описывающая тип, инкапсулируется в возвращаемом объекте классаТуре.
Получив объект классаТуредля заданного типа, можно извлечь информацию о нем, используя различные свойства, поля и методы, определенные в классеТуре.КлассТуредовольно обширен и содержит немало членов, поэтому его рассмотрение придется отложить до следующего раздела, посвященного рефлексии. Но в качестве краткого введения в этот класс ниже приведена программа, в которой используются три его свойства:FullName, IsClassиIsAbstract.Для получения полного имени типа служит свойствоFullName.СвойствоIsClassвозвращает логическое значениеtrue,если тип относится к классу. А свойствоIsAbstractвозвращает логическое значениеtrue,если класс является абстрактным.
// Продемонстрировать применение оператора typeof.
using System; using System.10;
class UseTypeof {
static void Main() {
Type t = typeof(StreamReader);
Console.WriteLine(t.FullName);
if (t.IsClass) Console.WriteLine("Относится к классу."); if (t.IsAbstract) Console.WriteLine("Является абстрактным классом."); else Console.WriteLine("Является конкретным классом.");
}
}
Эта программа дает следующий результат.
System.10.StreamReader Относится к классу.
Является конкретным классом.
В данной программе сначала извлекается объект классаТуре,описывающий типStreamReader.Затем выводится полное имя этого типа данных и определяется его принадлежность к классу, а далее — к абстрактному или конкретному классу.
Рефлексия
Рефлексия — это средство, позволяющее получать сведения о типе данных. Терминрефлексия,или отражение, происходит от принципа действия этого средства: объект классаТуреотражает базовый тип, который он представляет. Для получения информации о типе данных объекту классаТуределаются запросы, а он возвращает (отражает) обратно информацию, связанную с определяемым типом. Рефлексия является эффективным механизмом, поскольку она позволяет выявлять и использовать возможности типов данных, известные только во время выполнения.
Многие классы, поддерживающие рефлексию, входят в состав прикладного интерфейса .NET Reflection API, относящегося к пространству именSystem. Reflection.Поэтому для применения рефлексии в код программы обычно вводится следующая строка.
using System.Reflection;
Класс System. Type - ядро подсистемы рефлексии
КлассSystem. Туресоставляет ядро подсистемы рефлексии, поскольку он инкапсулирует тип данных. Он содержит многие свойства и методы, которыми можно
пользоваться для получения информации о типе данных во время выполнения. Класс Туре является производным от абстрактного классаSystem. Re flection. Member Inf о.
В классеMember Inf оопределены приведенные ниже свойства, доступные только для чтения.
Свойство
Описание
Type DeclaringType
Тип класса или интерфейса, в котором объявляется отражаемый член
MemberTypes MemberType
Тип члена. Это значение обозначает, является ли член по
лем, методом, свойством, событием или конструктором
int MetadataToken
Значение, связанное к конкретными метаданными
Module Module
Объект типа Module, представляющий модуль (исполняемый файл), в котором находится отражаемый тип
string Name
Имя типа
Type ReflectedType
Тип отражаемого объекта
Следует иметь в виду, что свойствоMemberTypeвозвращает типMemberTypes —перечисление, в котором определяются значения, обозначающие различные типы членов. К их числу относятся следующие.
/
MemberTypes.Constructor MemberTypes.Method MemberTypes.Field MemberTypes.Event MemberTypes.Property
Следовательно, тип члена можно определить, проверив свойствоMemberType.Так, если свойствоMemberTypeимеет значениеMemberTypes .Method,то проверяемый член является методом.
В классMemberlnf овходят два абстрактных метода:GetCustomAttributes() иIs Defined(). Оба метода связаны с атрибутами. Первый из них получает список специальных атрибутов, имеющих отношение к вызывающему объекту, а второй устанавливает, определен ли атрибут для вызывающего метода. В версию .NET Framework Version 4.0 внедрен методGetCustomAttributesData(), возвращающий сведения
о специальных атрибутах. (Подробнее об атрибутах речь пойдет далее в этой главе.)
КлассТуредобавляет немало своих собственных методов и свойств к числу тех, что определены в классеMemberlnf о.В качестве примера ниже перечислен ряд наиболее часто используемых методов классаТуре.
Метод
Назначение
Constructorlnfо [ ] GetConstructors () EventInfo[] GetEvents() Fieldlnfо[] GetFields() Type [ ]
GetGenericArguments()
Получает список конструкторов для заданного типа
Получает список событий для заданного типа Получает список полей для заданного типа Получает список аргументов типа, связанных с закрыто сконструированным обобщенным типом, или же список параметров типа, если заданный тип определен как обобщенный. Для открыто сконструированного типа этот
Окончание таблицы
Метод
Назначение
список может содержать как аргументы, так и параметры типа.
•
(Более подробно обобщения рассматриваются в главе 18.)
Memberlnfo[]
Получает список членов для заданного типа
GetMembers()
Methodlnfo[]
Получает список методов для заданного типа
GetMethods()
Propertylnfo[]
Получает список свойств для заданного типа
GetProperties ()
Далее приведен ряд наиболее часто используемых свойств, доступных только для
чтения и определенных в классеТуре.
Свойство
Назначение
Assembly Assembly
Получает сборку для заданного типа
TypeAttributes Attributes Получает атрибуты для заданного типа
Type BaseType
Получает непосредственный базовый тип для заданно
го типа
string FullName
Получает полное имя заданного типа
bool IsAbstract
Истинно, если заданный тип является абстрактным
bool isArray
Истинно, если заданный тип является массивом
bool IsClass
Истинно, если заданный тип является классом
bool IsEnum
Истинно, если заданный тип является перечислением
bool IsGenericParameter Истинно, если заданный тип является параметром
обобщенного типа. (Более подробно обобщения рас
сматриваются в главе 18.)
bool IsGenericType
Истинно, если заданный тип является обобщенным. (Бо
лее подробно обобщения рассматриваются в главе 18.)
string Namespace
Получает пространство имен для заданного типа
Применение рефлексии
С помощью методов и свойств классаТуреможно получить подробные сведения о типе данных во время выполнения программы. Это довольно эффективное средство. Ведь получив сведения о типе данных, можно сразу же вызвать его конструкторы и методы или воспользоваться его свойствами. Следовательно, рефлексия позволяет использовать код, который не был доступен во время компиляции.
Прикладной интерфейс Reflection API весьма обширен и поэтому не может быть полностью рассмотрен в этой главе. Ведь для этого потребовалась бы целая книга! Но прикладной интерфейс Reflection API имеет ясную логическую структуру, а следовательно, уяснив одну его часть, нетрудно понять и все остальное. Принимая во внимание это обстоятельство, в последующих разделах демонстрируются четыре основных способа применения рефлексии: получение сведений о методах, вызов методов, конструирование объектов и загрузка типов данных из сборок.
Получение сведений о методах
Имея в своем распоряжении объект классаТуре,можно получить список методов, поддерживаемых отдельным типом данных, используя методGetMethods (). Ниже приведена одна из форм, подходящих для этой цели.
MethodInfo[] GetMethods()
Этот метод возвращает массив объектов классаMethodlnf о,которые описывают методы, поддерживаемые вызывающим типом. КлассMethodlnfoнаходится в пространстве именSystem.Reflection.
КлассMethodlnfoявляется производным от абстрактного классаMethodBase,который в свою очередь наследует от классаMember Inf о.Это дает возможность пользоваться всеми свойствами и методами, определенными в этих трех классах. Например, для получения имени метода служит свойствоName.Особый интерес вызывают два члена классаMethodlnfo: ReturnTypeиGetParameters ().
Возвращаемый тип метода находится в доступном только для чтения свойствеReturnType,которое является объектом классаТуре.
МетодGetParameters() возвращает список параметров, связанных с анализируемым методом. Ниже приведена его общая форма.
Parameterlnfо[] GetParameters();
Сведения о параметрах содержатся в объекте классаParameter Inf о.В классеParameterlnf оопределено немало свойств и методов, описывающих параметры. Особое значение имеют два свойства:Name— представляет собой строку, содержащую имя параметра, aParameterType— описывает тип параметра, который инкапсулирован в объекте классаТуре.
В качестве примера ниже приведена программа, в которой рефлексия используется для получения методов, поддерживаемых классомMyClass.В этой программе выводится возвращаемый тип и имя каждого метода, а также имена и типы любых параметров, которые может иметь каждый метод.
// Анализ методов с помощью рефлексии.
using System;
using System.Reflection;
class MyClass { int x; int y;
public MyClass(int i, int j) { x = i ;
У = j;
}
public int Sum() { return x+y;
}
public bool IsBetween(int i) { if(x < i && i < y) return true; else return false;
}
public void Set(int a, int b) { x = a;
У = b;
}
public void Set(double a, double b) { x = (int) a; у = (int) b;
}
public void Show() {
Console.WriteLine(" x: {0}, у: {1}", x, y);
}
}
class ReflectDemo { static void Main() {
Type t = typeof(MyClass); // получить объект класса Type,
// представляющий класс MyClass
Console.WriteLine("Анализ методов, определенных " +
"в классе " + t.Name);
Console.WriteLine ();
Console.WriteLine("Поддерживаемые методы: ");
MethodInfo[] mi = t.GetMethods();
// Вывести методы, поддерживаемые в классе MyClass. foreach(Methodlnfo m in mi) {
// Вывести возвращаемый тип и имя каждого метода.
Console.Write(" " + m.ReturnType.Name + " " + m.Name + "(");
// Вывести параметры.
Parameterlnfo[] pi = m.GetParameters() ; for(int i=0; i < pi.Length; i++) {
Console.Write(pi[i].ParameterType.Name + " " + pi[i].Name); if(i+l < pi.Length) Console.Write(", ");
}
Console.WriteLine(")");
Console.WriteLine();
}
}
}
Эта программа дает следующий результат.
Поддерживаемые методы:
Int32 Sum()
Boolean IsBetween (Int32 i)
Void Set(Int32 a, Int32 b)
Void Set (Double a, Double b)'
Void Show()
String ToString()
Boolean Equals(Object ob j )
Int32 GetHashCode()
Type GetType()
Как видите, помимо методов, определенных в классе MyClass, в данной программе выводятся также методы, определенные в классе object, поскольку все типы данных в C# наследуют от класса object. Кроме того, в качестве имен типов указываются имена структуры .NET. Обратите также внимание на то, что метод Set () выводится дважды, поскольку он перегружается. Один из его вариантов принимает аргументы типа int, а другой — аргументы типа double.
Рассмотрим эту программу более подробно. Прежде всего следует заметить, что в классе MyClass определен открытый конструктор и ряд открытых методов, в том числе и перегружаемый метод Set ().
Объект класса Туре, представляющий класс MyClass, создается в методе Main () в следующей строке кода.
Type t = typeof(MyClass); // получить объект класса Туре,
// представляющий класс MyClass
Напомним, что оператор typeof возвращает объект класса Туре, представляющий конкретный тип данных (в данном случае — класс MyClass).
С помощью переменной t и прикладного интерфейса Reflection API в данной программе затем выводятся сведения о методах, поддерживаемых в классе MyClass. Для этого в приведенной ниже строке кода сначала выводится список соответствующих методов.
MethodInfo[] mi = t.GetMethods();
Затем в цикле foreach организуется обращение к элементам массива mi. На каждом шаге этого цикла выводится возвращаемый тип, имя и параметры отдельного метода, как показано в приведенном ниже фрагменте кода.
foreach(Methodlnfo m in mi) {
// Вывести возвращаемый тип и имя каждого метода.
Console.Write(" " + m.ReturnType.Name + " " + m.Name + "(");
// Вывести параметры.
Parameterlnfo[] pi = m.GetParameters(); for(int i=0; i < pi.Length; i++) {
Console.Write(pi[i].ParameterType.Name + " " + pi[i].Name); if(i+1 < pi.Length) Console.Write(", ");
В этом фрагменте кода параметры, связанные с каждым методом, сначала создаются с помощью методаGetParameters() и сохраняются в массивеpi.Затем в циклеforпроисходит обращение к элементам массиваpinвыводится тип и имя каждого параметра. Самое главное, что все эти сведения создаются динамически во время выполнения программы, не опираясь на предварительную осведомленность о классеMyClass.
Вторая форма метода GetMethods ()
Существует вторая форма методаGetMethods (), позволяющая указывать различные флажки для отфильтровывания извлекаемых сведений о методах. Ниже приведена эта общая форма методаGetMethods ().
Methodlnfo[] GetMethods(BindingFlags флажки)
В этом варианте создаются только те методы, которые соответствуют указанным критериям.BindingFlagsпредставляет собой перечисление. Ниже перечислен ряд наиболее часто используемых его значений.
Значение
Описание
DeclaredOnly
Извлекаются только те методы, которые определены в заданном классе. Унаследованные методы в извлекаемые сведения не включаются
Instance
Извлекаются методы экземпляра
NonPublic
Извлекаются методы, не являющиеся открытыми
Public
Извлекаются открытые методы
Static
Извлекаются статические методы
Два или несколько флажков можно объединить с помощью логической операции ИЛИ. Но как минимум флажокInstanceилиStaticследует указывать вместе с флажкомPublicилиNon Pub lie.В противном случае не будут извлечены сведения ни об одном из методов.
ФормаBindingFlagsметодаGetMethods() чаще всего применяется для получения списка методов, определенных в классе, без дополнительного извлечения наследуемых методов. Это особенно удобно в тех случаях, когда требуется исключить получение сведений о методах, определяемых в классе конкретного объекта. В качестве примера попробуем выполнить следующую замену в вызове методаGetMethods() из предыдущей программы.
// Теперь получаются сведения только о тех методах,
// которые объявлены в классе MyClass.
Methodlnfo[] mi = t.GetMethods(BindingFlags.DeclaredOnly |
BindingFlags.Instance |
BindingFlags.Public);
После этой замены программа дает следующий результат.
Анализ методов, определенных в классе MyClass
Поддерживаемые методы:
Int32 Sum ()
Boolean IsBetween(Int32 i)
Void Set(Int32 a, Int32 b)
Void Set(Double a, Double b)
Void Show()
Как видите, теперь выводятся только те методы, которые явно определены в классеMyClass.
Вызов методов с помощью рефлексии
Как только методы, поддерживаемые определенным типом данных, становятся известны, их можно вызывать. Для этой цели служит методInvoke (), входящий в состав классаMethodlnf о.Ниже приведена одна из форм этого метода:
object Invoke(objectobj,object[]parameters)
гдеobjобозначает ссылку на объект, для которого вызывается метод. Для вызова статических методов(static)в качестве параметраobjпередается пустое значение(null).Любые аргументы, которые должны быть переданы методу, указываются в массивеparameters.Если же аргументы не нужны, то вместо массиваparametersуказывается пустое значение(null).Кроме того, количество элементов массиваparametersдолжно точно соответствовать количеству передаваемых аргументов. Так, если требуется передать два аргумента, то массивparametersдолжен состоять из двух элементов, но не из трех или четырех. Значение, возвращаемое вызываемым методом, передается методуInvoke (), который и возвращает его.
Для вызова конкретного метода достаточно вызвать методInvoke() для экземпляра объекта типаMethodlnf о,получаемого при вызове методаGetMethods (). Эта процедура демонстрируется в приведенном ниже примере программы.
// Вызвать методы с помощью рефлексии.
using System;
using System.Reflection;
class MyClass { int x; int y;
public MyClass(int i, int j) { x = i;
У = j;
}
public int Sum() { return x+y;
}
public bool IsBetween(int i) {
if((x < i) && (i < y)) return true; else return false;
public void Set (int a, int b) {
Console.Write("В методе Set (int, int). ") ; x = a;
У = b;
Show();
}
// Перегрузить метод Set.
public void Set(double a, double b) {
Console.Write("В методе Set(double, double). "); x = (int) a; у = (int) b;
Show () ;
}
public void Show() {
Console.WriteLine("Значение x: {0}, значение у: {1}", x, у);
}
}
class InvokeMethDemo { static void Main() {
Type t = typeof(MyClass);
MyClass reflectOb = new MyClass(10, 20); int val;
Console.WriteLine("Вызов методов, определенных в классе " + t.Name); Console.WriteLine();
MethodInfo[] mi = t.GetMethods();
// Вызвать каждый метод, foreach(Methodlnfo m in mi) {
// Получить параметры.
Parameterlnfo[] pi = m.GetParameters() ;
if(m.Name.CompareTo("Set")==0 &&
pi[0].ParameterType == typeof(int)) { object[] args = new object[2]; args[0] = 9; args[l] = 18;
m. Invoke(reflectOb, args);
}
else if(m.Name.CompareTo("Set") ==0 &&
pi[0].ParameterType == typeof(double)) {
object[] args = new object[2]; args[0] = 1.12; args[1] = 23.4; m. Invoke(reflectOb, args);
}
else if(m.Name.CompareTo("Sum")==0) {
val = (int) m.Invoke(reflectOb, null);
Console.WriteLine("Сумма равна " + val);
}
else if(m.Name.CompareTo("IsBetween")==0) {
object[] args = new object[1]; args[0] = 14;
if((bool) m.Invoke(reflectOb, args))
Console.WriteLine("Значение 14 находится между x и у");
}
else if(m.Name.CompareTo("Show")==0) {
m.Invoke(reflectOb, null);
}
}
}
}
Вот к какому результату приводит выполнение этой программы.
Вызов методов, определенных в классе MyClass Сумма равна 30
Значение 14 находится между х и у
В методе Set (int, int). Значение х: 9, значение у: 18 В методе Set(double, double). Значение х: 1, значение у: 23 Значение х: 1, значение у: 23
Рассмотрим подробнее порядок вызова методов. Сначала создается список методов. Затем в цикле foreach извлекаются сведения об их параметрах. Далее каждый метод вызывается с указанием соответствующего типа и числа аргументов в последовательном ряде условных операторов if/else. Обратите особое внимание на перегрузку метода Set () в приведенном ниже фрагменте кода.
if(m.Name.CompareTo("Set")==0 &&
pi(0].ParameterType == typeof(int)) {
object[] args = new object[2]; args[0] = 9; args[l] = 18;
m.Invoke(reflectOb, args);
}
else if(m.Name.CompareTo("Set")==0 &&
pi[0].ParameterType == typeof(double)) {
object[] args = new object[2]; args[0] = 1.12; args[1 ] = 23.4; m.Invoke(reflectOb, args);
}
Если имя метода — Set, то проверяется тип первого параметра, чтобы выявить конкретный вариант этого метода. Так, если это метод Set (int, int), то его аргументы загружаются в массив args. В противном случае используются аргументы типа double.
Получение конструкторов конкретного типа
В предыдущем примере при вызове методов, определенных в классе MyClass, преимущества рефлексии не использовались, поскольку объект типа MyClass создавался явным образом. В таком случае было бы намного проще вызвать для него методы обычным образом. Но сильные стороны рефлексии проявляются наиболее заметно лишь в том случае, если объект создается динамически во время выполнения. И для
этого необходимо получить сначала список конструкторов, а затем экземпляр объекта заданного типа, вызвав один из этих конструкторов. Такой механизм позволяет получать во время выполнения экземпляр объекта любого типа, даже не указывая его имя в операторе объявления.
Конструкторы конкретного типа получаются при вызове методаGetConstructors() для объекта классаТуре.Ниже приведена одна из наиболее часто используемых форм этого метода.
Constructorlnfo[] GetConstructors()
МетодGetConstructors() возвращает массив объектов классаConstructorlnfo,описывающих конструкторы.
КлассConstructorlnfoявляется производным от абстрактного классаMethodBase,который в свою очередь наследует от классаMemberlnf о.В нем также определен ряд собственных методов. К их числу относится интересующий нас методGetConstructors(), возвращающий список параметров, связанных с конструктором. Этот метод действует таким же образом, как и упоминавшийся ранее методGetParameters (), определенный в классеMethodlnf о.
Как только будет обнаружен подходящий конструктор, для создания объекта вызывается методInvoke (), определенный в классеConstructorlnfo.Ниже приведена одна из форм этого метода.
object Invoke(object[]parameters)
Любые аргументы, которые требуется передать методу, указываются в массивеparameters.Если же аргументы не нужны, то вместо массиваparametersуказывается пустое значение(null).Но в любом случае количество элементов массиваparametersдолжно совпадать с количеством передаваемых аргументов, а типы аргументов — с типами параметров. МетодInvoke() возвращает ссылку на сконструированный объект.
В приведенном ниже примере программы рефлексия используется для создания экземпляра объекта классаMyClass.
// Создать объект с помощью рефлексии.
using System;
using System.Reflection;
class MyClass { int x; int y;
l
public MyClass(int i) {
Console.WriteLine("Конструирование класса MyClass(int, int). "); x = у = i;
}
public MyClass(int i, int j) {
Console.WriteLine("Конструирование класса MyClass(int, int). "); x = i;
У = j;
Show () ;
public int Sum() {
return x+y;
}
public bool IsBetween (int i) {
if((x < i) && (i < y)) return true; else return false;
}
public void Set(int a, int b) {
Console.Write("В методе Set (int, int). ") ; x = a;
У = b;
Show () ;
}
// Перегрузить метод Set.
public void Set(double a, double b) {
Console.Write("В методе(double, double). "); ■
x = (int) a; у = (int) b;
Show();
}
public void Show() {
Console.WriteLine("Значение x: {0}, значение у: {1}", x, у);
}
}
class InvokeConsDemo { static void Main() {
Type t = typeof(MyClass); int val;
// Получить сведения о конструкторе.
Constructorlnfo[] ci = t.GetConstructors();
Console.WriteLine("Доступные конструкторы: "); foreach(Constructorlnfo с in ci) {
// Вывести возвращаемый тип и имя.
Console.Write(" " + t.Name + "(");
// Вывести параметры.
Parameterlnfo[] pi = с.GetParameters() ;
for(int i=0; i-< pi.Length; i++) {
Console.Write(pi[i].ParameterType.Name + " " + pi[i].Name); if (i + 1 < pi.Length) Console.Write(", ");
}
Console.WriteLine(")");
}
Console.WriteLine ();
// Найти подходящий конструктор, int х;
for(x=0; х < ci.Length; х++) {
Parametgrlnfo[] pi = ci[x].GetParameters(); if(pi.Length == 2) break;
}
if (x == ci.Length) {
Console.WriteLine("Подходящий конструктор не найден."); return;
}
else
Console.WriteLine("Найден конструктор с двумя параметрами.\n");
// Сконструировать объект, object[] consargs = new object[2]; consargs[0] = 10; consargs[1] = 20;
object reflectOb = ci[x].Invoke(consargs) ;
Console.WriteLine("ХпВызов методов для объекта reflectOb."); Console.WriteLine() ;
Methodlnfo[] mi = t.GetMethods();
// Вызвать каждый метод, foreach(Methodlnfo m in mi) {
// Получить параметры.
Parameterlnfo[] pi = m.GetParameters() ; if(m.Name.CompareTo("Set")==0 &&
pi[0].ParameterType == typeof(int)) {
// Это метод Set (int, int). object[] args = new object[2]; args[0] = 9; args[l] = 18;
m. Invoke(reflectOb, args);
}
else if(m.Name.CompareTo("Set")==0 &&
object[] args = new object[1]; args[.0] = 14;
if ((bool) m.Invoke(reflectOb, args))
Console.WriteLine ("Значение 14 находится между x и у");
else if(m.Name.CompareTo("Show")==0) {
m.Invoke(reflectOb, null);
}
}
}
}
Эта программа дает следующий результат.
Доступные конструкторы:
MyClass(Int32 i)
MyClass(Int32 i, Int32 j)
Найден конструктор с двумя параметрами.
Конструирование класса MyClass(int, int)
Значение х: 10, значение у: 20
Вызов методов для объекта reflectOb
Сумма равна 30
Значение 14 находится между х и у
В методе Set(int, int). Значение х: 9, значение у: 18 В методе Set(double, double). Значение х: 1, значение у: 23 Значение х: 1, значение у: 23
А теперь рассмотрим порядок применения рефлексии для конструирования объекта классаMyClass.Сначала получается перечень открытых конструкторов в следующей строке кода.
Constructorlnfo[] ci = t.GetConstructors ();
Затем для наглядности примера выводятся полученные конструкторы. После этого осуществляется поиск по списку конструктора, принимающего два аргумента, как показано в приведенном ниже фрагменте кода.
for(x=0; х < ci.Length; х++) {
Parameterlnfo[] pi = ci[x].GetParameters(); if(pi.Length == 2) break;
}
Если такой конструктор найден, как в данном примере, то в следующем фрагменте кода получается экземпляр объекта заданного типа.
// Сконструировать объект, object[] consargs = new object[2]; consargs[0] = 10; consargs[1] = 20;
object reflectOb = ci[x].Invoke(consargs);
После вызова метода Invoke () переменная экземпляра reflectOb будет ссылаться на объект типаMyClass.А далее в программе выполняются соответствующие методы для экземпляра этого объекта.
Следует, однако, иметь в виду, что ради простоты в данном примере предполагается наличие лишь одного конструктора с двумя аргументами типа int. Очевидно, что в реальном коде придется дополнительно проверять соответствие типов каждого параметра и аргумента.
Получение типов данных из сборок
В предыдущем примере все сведения о классе MyClass были получены с помощью рефлексии, за исключением одного элемента: типа самого класса MyClass. Несмотря на то что сведения о классе получались в предыдущем примере динамически, этот пример опирался на тот факт, что имя типа MyClass было известно заранее и использовалось в операторе typeof для получения объекта класса Туре, по отношению к которому осуществлялось косвенное или непосредственное обращение к методам рефлексии. В некоторых случаях такой подход может оказаться вполне пригодным, но истинные преимущества рефлексии проявляются лишь тогда, когда доступные в программе типы данных определяются динамически в результате анализа содержимого других сборок.
Как следует из главы 16, сборка несет в себе сведения о типах классов, структур и прочих элементов данных, которые в ней содержатся. Прикладной интерфейс Reflection API позволяет загрузить сборку, извлечь сведения о ней и получить экземпляры объектов любых открыто доступных в ней типов. Используя этот механизм, программа может выявлять свою среду и использовать те функциональные возможности, которые могут оказаться доступными без явного их определения во время компиляции. Это очень эффективный и привлекательный принцип. Представьте себе, например, программу, которая выполняет роль "браузера типов", отображая типы данных, доступные в системе, или же инструментальное средство разработки, позволяющее визуально составлять программы из различных типов данных, поддерживаемых в системе. А поскольку все сведения о типах могут быть извлечены и проверены, то ограничений на применение рефлексии практически не существует.
Для получения сведений о сборке сначала необходимо создать объект класса Assembly. В классе Assembly открытый конструктор не определяется. Вместо этого объект класса Assembly получается в результате вызова одного из его методов. Так, для загрузки сборки по заданному ее имени служит метод LoadFrom (). Ниже приведена его соответствующая форма:
static Assembly LoadFrom(stringфайл_сборки)
гдефайл_ сборки-обозначает конкретное имя файла сборки.
Как только будет получен объект класса Assembly, появится возможность обнаружить определенные в нем типы данных, вызвав для него метод Get Types () в приведенной ниже общей форме.
Туре [ ] GetTypesO
Этот метод возвращает массив типов, содержащихся в сборке.
Для того чтобы продемонстрировать порядок обнаружения типов в сборке, потребуются два исходных файла. Первый файл будет содержать ряд классов, обнаруживаемых в коде из второго файла. Создадим сначала файл MyClasses . cs, содержащий следующий код.
// Файл, содержащий три класса и носящий имя MyClasses.cs.
using System;
class MyClass { int x; int y;
public MyClass(int i) {
Console.WriteLine("Конструирование класса MyClass(int). "); x = у = i ;
Show () ;
}
public MyClass(int i, intj){
Console.WriteLine("Конструирование класса MyClass(int, int). ") ; x = i;
у = j;
Show();
}
public int Sum() { return x+y;
}
public bool IsBetween(int i) {
if ( (x < i) && (i < y)) return true;
else return false; \
}
public void Set(int a, int b) {
Console.Write("В методе Set(int, int). "); x = a;
У = b;
Show();
}
// Перегрузить.метод Set.
public void Set(double a, double b) {
Console.Write("В методе Set(double, double). "); x = (int) a; у = (int) b;
Show () ;
}
public void Show() {
Console.WriteLine ("Значение x: {0}, значение у: {1}", x, у);
}
}
class AnotherClass { string msg;
public AnotherClass(string str) { msg = str;
}
public void Show() {
Console.WriteLine(msg);
class Demo {
static void Main() {
Console.WriteLine("Это заполнитель.");
}
}
Этот файл содержит классMyClass,неоднократно использовавшийся в предыдущих примерах. Кроме того, в файл добавлены второй классAnotherClassи третий классDemo.Следовательно, сборка, полученная из исходного кода, находящегося в этом исходном файле, будет содержать три класса. Затем этот файл компилируется, и из него формируется исполняемый файлMyClasses . ехе.Именно эта сборка и будет опрашиваться программно.
Ниже приведена программа, в которой будут извлекаться сведения о файле сборкиMyClasses . ехе.Ее исходный текст составляет содержимое второго файла.
/* Обнаружить сборку, определить типы и создать объект с помощью рефлексии. */
using System;
using System.Reflection;
class ReflectAssemblyDemo { static void Main() { int val;
// Загрузить сборку MyClasses.exe.
Assembly asm = Assembly.LoadFrom("MyClasses.exe");
// Обнаружить типы, содержащиеся в сборке MyClasses.exe.
Туре[] alltypes = asm.GetTypes(); foreach(Type temp in alltypes)
Console.WriteLine("Найдено: " + temp.Name);
Console.WriteLine() ;
// Использовать первый тип, в данном случае — класс MyClass.
Type t = alltypes[0]; // использовать первый найденный класс Console.WriteLine("Использовано: " + t.Name);
// Получить сведения о конструкторе.
Constructorlnfo[] ci = t.GetConstructors() ;
Console.WriteLine("Доступные конструкторы: "); foreach(Constructorlnfo с in ci) {
// Вывести возвращаемый тип и имя.
Console.Write(" " + t.Name + "(");
// Вывести параметры.
Parameterlnfo[] pi = с.GetParameters(); for(int i=0; i < pi.Length; i++) {
Console.Write(pi[i].ParameterType.Name + " " + pi[i].Name);
■ if(i+1 < pi.Length) Console.Write(", ");
}
Console.WriteLine (")");
}
Console.WriteLine ();
// Найти подходящий конструктор, int x;
for(x=0; x < ci.Length; x++) {
Parameterlnfo[] pi = ci[x].GetParameters() ; if (pi.Length == 2) break;
}
if(x == ci.Length) {
Console.WriteLine("Подходящий конструктор не найден."); return;
}
else
Console.WriteLine("Найден конструктор с двумя параметрами.\n");
// Сконструировать объект, object[] consargs = new object[2]; consargs[0] = 10; consargs[1] = 20;
object reflectOb = ci[x].Invoke(consargs) ;
Console.WriteLine("\пВызов методов для объекта reflectOb."); Console.WriteLine();
MethodInfo[] mi = t.GetMethods();
// Вызвать каждый метод, foreach(Methodlnfo m in mi) {
//• Получить параметры.
Parameterlnfo[] pi = m.GetParameters();
if(m.Name.CompareTo("Set")==0 &&
pi[0].ParameterType == typeof(int)) {
// Это метод Set(int, int). object[] args = new object[2]; args[0] = 9; args[l] = 18;
m.Invoke(reflectOb, args) ;
}
else if(m.Name.CompareTo("Set")==0 &&
pi[0].ParameterType == typeof(double)) {
// Это метод Set(double, double).
object[] args = new object[2];
args[0] = 1.12;
args[l] = 23.4;
m.Invoke(reflectOb, args);
}
else if(m.Name.CompareTo("Sum")==0) {
val = (int) m.Invoke(reflectOb, null);
Console.WriteLine("Сумма равна " + val);
}
else if(m.Name.CompareTo("IsBetween")==0) {
object[] args = new object[1];
args[0] = 14;
if(<bool) m.Invoke (reflectOb, args))
Console.WriteLine("Значение 14 находится между x и у");
}
else ifjm.Name.CompareTo("Show")==0) {
• m.Invoke(reflectOb, null);
}
}
}
}
При выполнении этой программы получается следующий результат.
Найдено: MyClass Найдено: AnotherClass Найдено: Demo
Использовано: MyClass
Доступные конструкторы:
MyClass(Int32 i)
MyClass(Int32 i, Int32 j)
Найден конструктор с двумя параметрами.
Конструирование класса MyClass(int, int)
Значение х: 10, значение у: 20
Вызов методов для объекта reflectOb
Сумма равна 30
Значение 14 находится между х и у
В методе Set (int, int) . Значение х: 9, значение у: 18 В методе Set(double, double). Значение х: 1, значение у: 23 Значение х: 1, значение у: 2 3
Как следует из результата выполнения приведенной выше программы, обнаружены все три класса, содержащиеся в файле сборкиМу С lasses . ехе.Первым среди них обнаружен классMyClass,который затем был использован для получения экземпляра объекта и вызова соответствующих методов.
Отдельные типы обнаруживаются в сборкеMyClasses . ехес помощью приведенной ниже последовательности кода, находящегося в самом начале методачМал.п ().
// Загрузить сборку MyClasses.exe.
Assembly asm = Assembly.LoadFrom("MyClasses.ехе") ;
// Обнаружить типы, содержащиеся в сборке MyClasses.exe.
Туре[] alltypes = asm.GetTypes(); foreach(Type temp in alltypes)
Console.WriteLine("Найдено: " + temp.Name);
Этой последовательностью кода можно пользоваться всякий раз, когда требуется динамически загружать й опрашивать сборку.
Но сборка совсем не обязательно должна быть исполняемым файлом с расширением .ехе.Сборки могут быть также в файлах динамически компонуемых библиотек (DLL) с расширением .dll. Так, если скомпилировать исходный файл MyClasses . cs в следующей командной строке:
csc /t:library MyClasses.es
то в итоге получится файл MyClasses .dll. Преимущество размещения кода в библиотеке DLL заключается, в частности, в том, что в этом случае метод Main () в исходном коде не нужен, тогда как всем исполняемым файлам требуется определенная точка входа, с которой должно начинаться выполнение программы. Именно поэтому класс Demo содержит метод Main () в качестве такой точки входа. А для библиотеки DLL метод Main () не требуется. Если же класс MyClass нужно превратить в библиотеку DLL, то в вызов метода LoadFrom () придется внести следующее изменение.
Assembly asm = Assembly.LoadFrom("MyClasses.dll");
Полностью автоматизированное обнаружение типов
Прежде чем завершить рассмотрение рефлексии, обратимся к еще одному поучительному примеру. Несмотря на то что в программе из предыдущего примера класс MyClass был полноценно использован без явного указания на его имя в программе, этот пример все же опирается на предварительную осведомленность о содержимом класса MyClass. Так, в программе были заранее известны имена методов Set и Sum из этого класса. Но с помощью рефлексии можно воспользоваться типом данных, ничего не зная о нем заранее. С этой целью придется извлечь все сведения, необходимые для конструирования объекта и формирования вызовов соответствующих методов. Такой подход может оказаться пригодным, например, при создании инструментального средства визуального проектирования, поскольку он позволяет использовать типы данных, имеющиеся в системе.
Рассмотрим следующий пример, демонстрирующий полностью автоматизированное обнаружение типов. В этом примере сначала загружается сборка MyClasses . ехе, затем конструируется объект класса MyClass и далее вызываются все методы, объявленные в классе MyClass, причем о них ничего заранее неизвестно.
// Использовать класс MyClass, ничего не зная о нем заранее.
using System;
using System.Reflection;
class ReflectAssemblyDemo { static void Main() { int val;
Assembly asm = Assembly.LoadFrom("MyClasses.exe");
Type[] alltypes = asm.GetTypes();
Type t = alltypes[0]; // использовать первый обнаруженный класс Console.WriteLine("Использовано: " + t.Name);
Constructorlnfo [.] ci = t.GetConstructors();
// Использовать первый обнаруженный конструктор.
Parameterlnfо[] cpi = ci[0].GetParameters(); object reflectOb;
if (cpi.Length > 0) {
object[] consargs = new object[cpi.Length];
// Инициализировать аргументы, fox (int n=0; n < cpi.Length; n+ + ) consargs[n] = 10 + n * 20;
// Сконструировать объект. reflectOb = ci [0] .Invoke(consargs);
} else
reflectOb = ci [0] .Invoke(null);
Console.WriteLine("ХпВызов методов для объекта reflectOb."); Console.WriteLine();
// Игнорировать наследуемые методы.
MethodInfo[] mi = t.GetMethods(BindingFlags.DeclaredOnly |
BindingFlags.Instance | BindingFlags.Public);
// Вызвать каждый метод, foreach(Methodlnfo m in mi) {
Console.WriteLine("Вызов метода {0} ", m.Name);
// Получить параметры.
Parameterlnfо[] pi = m.GetParameters();
// Выполнить методы, switch(pi.Length) {
case 0: // аргументы отсутствуют if(m.ReturnType == typeof(int)) {
val = (int) m.Invoke (reflectOb, null);
Console.WriteLine("Результат: " + val);
}
else if(m.ReturnType == typeof(void)) {
m. Invoke(reflectOb, null);
}
break;
case 1: // один аргумент
if (pi[0] .ParameterType == typeof(int)) { object[] args = new object[1]; args[0] = 14;
if ((bool) m.Invoke(reflectOb, args))
Console.WriteLine ("Значение 14 находится между x и у"); else
Console.WriteLine ("Значение 14 не находится между х и у");
}
break;
case 2: // два аргумента
if((pi[0].ParameterType == typeof(int)) &&
(pi[1].ParameterType == typeof(int))) {
object[] args = new object[2]; args[0] = 9;
args[l] = 18;
m.Invoke(reflectOb, args);
}
else if((pi[0].ParameterType == typeof(double)) &&
(pi[1].ParameterType == typeof(double))) {
object[] args = new object [2]; args[0] = 1J12; args[l] = 23.4; m.Invoke(reflectOb, args);
}
break;
}
Console.WriteLine();
}
}
}
Эта программа дает следующий результат.
Использовано: MyClass Конструирование класса MyClass(int).
Значение х: 10, значение у: 10
Вызов методов для объекта reflectOb.
Вызов метода Sum Результат: 20
Вызов метода IsBetween
Значение 14 не находится между х и у
Вызов метода Set
В методе Set (int, int). Значение х: 9, значение у: 18 Вызов метода Set
В методе Set(double, double). Значение х: 1, значение у: 23
Вызов метода Show
Значение х: 1, значение у: 23
Эта программа работает довольно просто, но все же требует некоторых пояснений. Во-первых, получаются и используются только те методы, которые явно объявлены в классеMyClass.Для этой цели служит формаBindingFlagsметодаGetMethods (),чтобы воспрепятствовать вызову методов, наследуемых от объекта. И во-вторых, количество параметров и возвращаемый тип каждого метода получаются динамически, а затем определяются и проверяются в оператореswitch.На основании этой информации формируется вызов каждого метода.
Атрибуты
В C# разрешается вводить в программу информацию декларативного характера в формеатрибута, с помощью которого определяются дополнительные сведения (метаданные), связанные с классом, структурой, методом и т.д. Например, в программе можно указать атрибут, определяющий тип кнопки, которую должен отображать конкретный класс. Атрибуты указываются в квадратных скобках перед тем элементом, к которому они применяются. Следовательно, атрибут не является членом класса, но обозначает дополнительную информацию, присоединяемую к элементу.
Основы применения атрибутов
Атрибут поддерживается классом, наследующим от классаSystem. Attribute.Поэтому классы атрибутов должны быть подклассами классаAttribute. В классеAttributeопределены основные функциональные возможности, но далеко не все они нужны для работы с атрибутами. В именах классов атрибутов принято употреблять суффиксAttribute.Например,ErrorAttribute— это имя класса атрибута, описывающего ошибку.
При объявлении класса атрибута перед его именем указывается атрибутAttributeUsage.Этот встроенный атрибут обозначает типы элементов, к которым может применяться объявляемый атрибут. Так, применение атрибута может ограничиваться одними методами.
Создание атрибута
В классе атрибута определяются члены, поддерживающие атрибут. Классы атрибутов зачастую оказываются довольно простыми и содержат небольшое количество полей или свойств. Например, атрибут может определять примечание, описывающее элемент, к которому присоединяется атрибут. Такой атрибут может принимать следующий вид.
[AttributeUsage(AttributeTargets.All) ] public class RemarkAttribute : Attribute {
string pri_remark; // базовое поле свойства Remark
public RemarkAttribute(string comment) { pri_remark = comment;
}
public string Remark { v get {
return pri_remark;
}
}
}
Проанализируем этот класс атрибута построчно.
Объявляемый атрибут получает имяRemarkAttribute.Его объявлению предшествует встроенный атрибутAttributeUsage,указывающий на то, что атрибутRemarkAttributeможет применяться ко всем типам элементов. С помощью встроенного атрибутаAttributeUsageможно сузить перечень элементов, к которым может присоединяться объявляемый атрибут. Подробнее о его возможностях речь пойдет далее в этой главе.
Далее объявляется классRemarkAttribute,наследующий от классаAttribute.В классеRemarkAttributeопределяется единственное закрытое полеpri_remark,поддерживающее одно открытое и доступное для чтения свойствоRemark.Это свойство содержит описание, связываемое с атрибутом. (Конечно,Remarkможно было бы объявить как автоматически реализуемое свойство с закрытым аксессоромset,но ради наглядности данного примера выбрано свойство, доступное только для чтения.) В данном классе определен также один открытый конструктор, принимающий строковый аргумент и присваивающий его свойствуRemark.Этим пока что ограничиваются функциональные возможности классаRemarkAttribute,готового к применению.
Присоединение атрибута
Как только класс атрибута будет определен, атрибут можно присоединить к элементу. Атрибут указывается перед тем элементом, к которому он присоединяется, и для этого его конструктор заключается в квадратные скобки. В качестве примера ниже показано, как атрибутRemarkAttributeсвязывается с классом.
[RemarkAttribute("В этом классе используется атрибут.")] class UseAttrib {
// ...
}
В этом фрагменте кода конструируется атрибутRemarkAttribute,содержащий комментарий"В этом классе используется атрибут .11Данный атрибут затем связывается с классомUseAttrib.
Присоединяя атрибут, совсем не обязательно указывать суффиксAttribute.Например, приведенный выше класс может быть объявлен следующим образом.
[Remark("В этом классе используется атрибут.")] class UseAttrib {
// . . .
}
В этом объявлении указывается только имяRemark.Такая сокращенная форма считается вполне допустимой, но все же надежнее указывать полное имя присоединяемого атрибута, чтобы избежать возможной путаницы и неоднозначности.
Получение атрибутов объекта
Как только атрибут будет присоединен к элементу, он может быть извлечен в других частях программы. Для извлечения атрибута обычно используется один из двух методов. Первый метод,GetCustomAttributes (), определяется в классеMemberlnfои наследуется классомТуре.Он извлекает список всех атрибутов, присоединенных к элементу. Ниже приведена одна из его форм.
object[] GetCustomAttributes(boolнаследование)
Еслинаследованиеимеет логическое значениеtrue,то в список включаются атрибуты всех базовых классов, наследуемых по иерархической цепочке. В противном случае атрибуты извлекаются только из тех классов, которые определяются указанным типом.
Второй метод,GetCustomAttribute (), определяется в классеAttribute.Ниже приведена одна из его форм:
static Attribute GetCustomAttribute(Memberlnfоэлемент,Туретип_атрибута)
гдеэлементобозначает объект классаMember Inf о,описывающий тот элемент, для которого создаются атрибуты, тогда кактип_атрибута— требуемый атрибут. Данный метод используется в том случае, еслицмяполучаемого атрибута известно заранее, что зачастую и бывает. Так, если в классеUseAttribимеется атрибутRemarkAttribute,то для получения ссылки на этот атрибут можно воспользоваться следующей последовательностью кода.
// Получить экземпляр объекта класса MemberInfо, связанного // с классом, содержащим атрибут RemarkAttribute.
Type t = typeof(UseAttrib);
// Извлечь атрибут RemarkAttribute.
Type tRemAtt = typeof(RemarkAttribute);
RemarkAttribute ra = (RemarkAttribute)
Attribute.GetCustomAttribute(t, tRemAtt);
Эта последовательность кода оказывается вполне работоспособной, поскольку классMemberlnfoявляется базовым для классаТуре.Следовательно,t— это экземпляр объекта классаMemberlnfo.
Имея ссылку на атрибут, можно получить доступ к его членам. Благодаря этому информация об атрибуте становится доступной для программы, использующей элемент, к которому присоединен атрибут. Например, в следующей строке кода выводится содержимое свойстваRemark.
Console.WriteLine(га.Remark);
Ниже приведена программа, в которой все изложенные выше особенности применения атрибутов демонстрируются на примере атрибутаRemarkAttribute.
// Простой пример применения атрибута.
using System;
using System.Reflection;
[AttributeUsage(AttributeTargets.All)] public class RemarkAttribute : Attribute {
string pri_remark; // базовое поле свойства Remark
public RemarkAttribute(string comment) { pri_remark = comment;
}
public string Remark {
get { ,
return pri_remark;
}
}
}
[RemarkAttribute("В этом классе используется атрибут.")] class UseAttrib {
// ...
}
class AttribDemo { static void Main() {
Type t = typeof(UseAttrib);
Console.Write("Атрибуты в классе " + t.Name + ": ");
object[] attribs = t.GetCustomAttributes(false); foreach(object о in attribs) {
Console .WriteLine (о).;
}
Console.Write("Примечание: ");
// Извлечь атрибут RemarkAttribute.
Type tRemAtt = typeof(RemarkAttribute);
RemarkAttribute ra = (RemarkAttribute)
Attribute.GetCustomAttribute(t, tRemAtt);
Console.WriteLine(ra.Remark);
}
}
Эта программа дает следующий результат.
Атрибуты в классе UseAttrib: RemarkAttribute Примечание: В этом классе используется атрибут.
Сравнение позиционных и именованных параметров
В предыдущем примере для инициализации атрибутаRemarkAttributeего конструктору была передана символьная строка с помощью обычного синтаксиса конструктора. В этом случае параметрcommentконструктораRemarkAttribute() называетсяпозиционным.Этот термин отражает тот факт, что аргумент связан с параметром по его позиции в списке аргументов. Следовательно, первый аргумент передается первому параметру, второй аргумент — второму параметру и т.д.
Но для атрибута доступны такжеименованные параметры, которым можно присваивать первоначальные значения по их именам. В этом случае значение имеет имя, а не позиция параметра. '
ПРИМЕЧАНИЕ
Несмотря на то что именованные параметры атрибутов, по существу, подобны именованным аргументам методов, они все же отличаются в деталях.
Именованный параметр поддерживается открытым полем или свойством, которое должно быть нестатическим и доступным только для записи. Любое поле или свойство подобного рода может автоматически использоваться в качестве именованного параметра. Значение присваивается именованному параметру с помощью соответствующего оператора, расположенного в списке аргументов при вызове конструктора атрибута. Ниже приведена общая форма объявления атрибута, включая именованные параметры.
[attrib(список_позиционных_параметров,
мменованный_параметр_1=значение, именованный_параметр_2=значение, ...)]
Первыми указываются позиционные параметры, если они существуют. Далее следуют именованные параметры с присваиваемыми значениями. Порядок следования
именованных параметров особого значения не имеет. Именованным параметрам не обязательно присваивать значение, и в этом случае используется значение, устанавливаемое по умолчанию.
Применение именованного параметра лучше всего показать на конкретном примере. Ниже приведен вариант классаRemarkAttribute,в который добавлено полеSupplement,предназначенное для хранения дополнительного примечания.
[AttributeUsage(AttributeTargets.All)] public class RemarkAttribute : Attribute {
string pri_remark; // базовое поле свойства Remark
// Это поле можно использовать в качестве именованного параметра, public string Supplement;
public RemarkAttribute(string comment) { pri_remark = comment;
Supplement = "Отсутствует";
}
public string Remark { get {
return pri_remark;
}
}
}
Как видите, полеSupplementинициализируется в конструкторе символьной строкой"Отсутствует".Другого способа присвоить ему первоначальное значение в конструкторе не существует. Но поскольку полеSupplementявляется открытым в классеRemarkAttribute,его можно использовать в качестве именованного параметра, как показано ниже.
[RemarkAttribute("В этом классе используется атрибут.",
Supplement = "Это дополнительная информация.")] class UseAttrib {
// ...
}
Обратите особое внимание на вызов конструктора классаRemarkAttribute.В этом конструкторе первым, как и прежде, указывается позиционный параметр, а за ним через запятую следует именованный параметрSupplement,которому присваивается конкретное значение. И наконец, закрывающая скобка, ), завершает вызов конструкто-, ра. Таким образом, именованный параметр инициализируется в вызове конструктора. Этот синтаксис можно обобщить: позиционные параметры должны указываться в том порядке, в каком они определены в конструкторе, а именованные параметры — в произвольном порядке и вместе с присваиваемыми им значениями.
Ниже приведена программа, в которой демонстрируется применение поляSupplementв качестве именованного параметра атрибута.
// Использовать именованный параметр атрибута.
using System;
using System.Reflection;
[AttributeUsage(AttributeTargets.All)] public class RemarkAttribute : Attribute {
string pri_remark; // базовое поле свойства Remark
public string Supplement; // это именованный параметр
public RemarkAttribute(string comment) { pri_remark = comment;
Supplement = "Отсутствует";
}
public string Remark { get {
return pri_remark;
}
}
}
[RemarkAttribute("В этом классе используется атрибут.",
Supplement = "Это дополнительная информация.")] class UseAttrib {
// ...
}
class NamedParamDemo { static void Main() {
Type t = typeof(UseAttrib);
Console.Write("Атрибуты в классе " + t.Name + ": "); object[] attribs = t.GetCustomAttributes(false); foreach(object о in attribs) {
Console.WriteLine (o);
}
// Извлечь атрибут RemarkAttribute.
Type tRemAtt = typeof(RemarkAttribute);
RemarkAttribute ra = (RemarkAttribute)
Attribute.GetCustomAttribute(t, tRemAtt);
Console.Write("Примечание: ");
Console.WriteLine(ra.Remark);
Console.Write("Дополнение: ") ;
Console.WriteLine(ra.Supplement);
}
}
При выполнении этой программы получается следующий результат.
Атрибуты в классе UseAttrib: RemarkAttribute Примечание: В этом классе используется атрибут.
Дополнение: Это дополнительная информация.
Прежде чем перейти к следующему вопросу, следует особо подчеркнуть, что поле pri_remarkнельзяиспользовать в качестве именованного параметра, поскольку оно
закрыто в классеRemarkAttribute.СвойствоRemarkтакженельзяиспользовать в качестве именованного параметра, потому что оно доступно только для чтения. Напомним, что в качестве именованных параметров могут служить только открытые поля и свойства.
Открытое и доступное только для чтения свойство может использоваться в качестве именованного параметра таким же образом, как и открытое поле. В качестве примера ниже показано, как автоматически реализуемое свойствоPriorityтипаintвводится в классRemarkAttribute.
// Использовать свойство в качестве именованного параметра атрибута.
using System;
using System.Reflection;
[AttributeUsage(AttributeTargets.All)] public class RemarkAttribute : Attribute {
string pri_remark; // базовое поле свойства Remark
public string Supplement; // это именованный параметр
public RemarkAttribute(string comment) { pri_remark = comment;
Supplement = "Отсутствует";
Priority = 1;
}
public string Remark { get {
return pri_remark;
}
}
// Использовать свойство в качестве именованного параметра, public int Priority { get; set; }
}
[RemarkAttribute("В этом классе используется атрибут.",
Supplement = " Это дополнительная информация.",
Priority = 10)] class UseAttrib {
// ...
}
class NamedParamDemo { static void Main() {
Type t = typeof(UseAttrib);
Console.Write("Атрибуты в классе " + t.Name + ": ");
object[] attribs = t.GetCustomAttributes(false); foreach(object о in attribs) {
Console.WriteLine(o);
// Извлечь атрибут RemarkAttribute.
Type tRemAtt = typeof(RemarkAttribute);
RemarkAttribute ra = (RemarkAttribute)
Attribute.GetCustomAttribute(t, tRemAtt);
Console.Write("Примечание: ") ;
Console.WriteLine(ra.Remark);
Console.Write("Дополнение: ") ;
Console.WriteLine(ra.Supplement);
Console.WriteLine("Приоритет: " + ra.Priority);
}
}
Вот к какому результату приводит выполнение этого кода.
Атрибуты в классе UseAttrib: RemarkAttribute Примечание: В этом классе используется атрибут.
Дополнение: Это дополнительная информация.
Приоритет: 10
В данном примере обращает на себя внимание порядок указания атрибутов перед классом UseAttrib, как показано ниже.
[RemarkAttribute("В этом классе используется атрибут.",
Supplement = " Это дополнительная информация.",
Priority = 10)] class UseAttrib {
// ...
}
Именованные параметры атрибутов Supplement и Priorityнеобязательно указывать в каком-то определенном порядке. Порядок их указания можно свободно изменить, не меняя сами атрибуты.
И последнее замечание: тип параметра атрибута (как позиционного, так и именованного) должен быть одним из встроенных простых типов, object, Туре, перечислением или одномерным массивом одного из этих типов.
Встроенные атрибуты
В C# предусмотрено несколько встроенных атрибутов, но три из них имеют особое значение, поскольку они применяются в самых разных ситуациях. Это атрибутыAttributeUsage, ConditionalиObsolete,рассматриваемые далее по порядку.
Атрибут AttributeUsage
Как упоминалось ранее, атрибутAttributeUsageопределяет типы элементов, к которым может быть применен объявляемый атрибут.AttributeUsage— это, по существу, еще одно наименование классаSystem. AttributeUsageAttribute.У него имеется следующий конструктор:
AttributeUsage(AttributeTargetsvalidOn)
гдеvalidOnобозначает один или несколько элементов, к которым может быть применен объявляемый атрибут, тогда какAttributeTargets— перечисление, в котором определяются приведенные ниже значения.
АН
Assembly
Class
Constructor
Delegate
Enum
Event
Field
GenericParameter
Interface
Method
Module
Parameter
Property
ReturnValue
Struct
Два этих значения или более можно объединить с помощью логической операции ИЛИ. Например, для указания атрибута, применяемого только к полям и свойствам, используются следующие значения.
AttributeTargets.Field I AttributeTargets.Property
В классе атрибутаAttributeUsageподдерживаются два именованных параметра. Первым из них является параметрAllowMultiple,принимающий логическое значение. Если это значение истинно, то атрибут может быть применен к одному и тому же элементу неоднократно. Второй именованный параметр,Inherited,также принимает логическое значение. Если это значение истинно, то атрибут наследуется производными классами, а иначе он не наследуется. По умолчанию параметрAllowMultipleпринимает ложное значение(false),а параметрInherited— истинное значение(true).
В классе атрибутаAttributeUsageопределяется также доступное только для чтения свойствоValidOn.Оно возвращает значение типаAttributeTargets,определяющее типы элементов, к которым можно применять объявляемый атрибут. По умолчанию используется значениеAttributeTargets .All.
Атрибут Conditional
АтрибутConditionalпредставляет, вероятно, наибольший интерес среди всех встроенных атрибутов. Ведь он позволяет создаватьусловные методы, которые вызываются только в том случае, если с помощью директивы #defineопределен конкретный идентификатор, а иначе метод пропускается. Следовательно, условный метод служит альтернативой условной компиляции по директиве #if.
Conditional— это, по существу, еще одно наименование классаSystem. Diagnostics . ConditionalAttribute.Для применения атрибутаConditionalв исходный код программы следует включить пространство именSystem. Diagnostics.Рассмотрим применение данного атрибута на следующем примере программы.
// Продемонстрировать применение встроенного атрибута Conditional.
#define TRIAL
using System;
using System.Diagnostics;
class Test {
[Conditional("TRIAL")] void Trial() {
Console.WriteLine("Пробная версия, не " +
"предназначенная для распространения.");
[Conditional("RELEASE")] void Release () {
Console.WriteLine("Окончательная рабочая версия.");
}
static void Main() {
Test t = new Test();
t.Trial(); //вызывается только в том случае, если // определен идентификатор TRIAL t.ReleaseO; // вызывается только в том случае, если // определен идентификатор RELEASE
}
}
Эта программа дает следующий результат.
Пробная версия, не предназначенная для распространения.
Рассмотрим эту программу подробнее, чтобы стал понятнее результат ее выполнения. Прежде всего обратите внимание на то, что в этой программе определяется идентификаторTRIAL.Затем обратите внимание на определение методовTrial() иRelease (). Каждому из них предшествует атрибутConditional,общая форма которого приведена ниже:
[Conditionalидентификатор]
гдеидентификаторобозначает конкретный идентификатор, определяющий условие выполнение метода. Данный атрибут может применяться только к методам. Если идентификатор определен, то метод выполняется, когда он вызывается. Если же идентификатор не определен, то метод не выполняется.
Оба метода,Trial()nRelease(),вызываются в методеMain(). Но поскольку определен один лишь идентификаторTRIAL,то выполняется только методTrial (),тогда как методRelease() игнорируется. Если же определить идентификаторRELEASE,то методRelease() будет также выполняться. А если удалить определение идентификатораTRIAL,то методTrial() выполняться не будет.
АтрибутConditionalможно также применить в классе атрибута, т.е. в классе, наследующем от классаAttribute.Так, если идентификатор определен, то атрибут применяется, когда он встречается в ходе компиляции. В противном случае он не применяется.
На условные методы накладывается ряд ограничений. Во-первых, они должны возвращать значение типаvoid,а по существу, ничего не возвращать. Во-вторых, они должны быть членами класса или структуры, а не интерфейса. И в-третьих, они не могут предшествовать ключевому словуoverride.
Атрибут Obsolete
АтрибутObsolete(сокращенное наименование классаSystem. Obso-leteAttribute)позволяет пометить элемент программы как устаревший. Ниже приведена общая форма этого атрибута:
[Obsolete ("сообщение") ]гдесообщениевыводится при компилировании элемента программы, помеченного как устаревший. Ниже приведен краткий пример применения данного атрибута.
// Продемонстрировать применение атрибута Obsolete, using System; class Test {
[Obsolete("Лучше использовать метод MyMeth2.")] public static int MyMeth(int a, int b) { return a / b;
}
// Усовершенствованный вариант метода MyMeth. public static int MyMeth2(int a, int b) { return b == 0 ? 0 : a /b;
}
static void Main() {
// Для этого кода выводится предупреждение.
Console.WriteLine("4 / 3 равно " + Test.MyMeth(4, 3));
//А для этого кода предупреждение не выводится.
Console.WriteLine("4 / 3 равно " + Test.MyMeth2(4, 3));
}
}
Когда по ходу компиляции программы в методе Main () встречается вызов методаMyMeth (), формируется предупреждение, уведомляющее пользователя о том, что ему лучше воспользоваться методомMyMeth2 ().
Ниже приведена вторая форма атрибутаObsolete:
[Obsolete("сообщение", ошибка)]
гдеошибкаобозначает л отческое значение. Если это значение истинно(true),то при использовании устаревшего элемента формируется сообщение об ошибке компиляции вместо предупреждения. Эта форма отличается тем, что программа, содержащая подобную ошибку, не будет скомпилирована в исполняемом виде.
ГЛАВА 18 Обобщения
Эта глава посвященаобобщениям —одному из самых сложных и эффективных средств С#. Любопытно, что обобщения не вошли в первоначальную версию 1.0 и появились лишь в версии 2.0, но теперь они являются неотъемлемой частью языка С#. Не будет преувеличением сказать, что внедрение обобщений коренным образом изменило характер С#. Это нововведение не только означало появление нового элемента синтаксиса данного языка, но и открыло новые возможности для внесения многочисленных изменений и обновлений в библиотеку классов. И хотя после внедрения обобщений прошло уже несколько лет, последствия этого важного шага до сих пор сказываются на развитии C# как языка программирования.
Обобщения как языковое средство очень важны потому, что они позволяют создавать классы, структуры, интерфейсы, методы и делегаты для обработки разнотипных данных с соблюдением типовой безопасности. Как вам должно быть известно, многие алгоритмы очень похожи по своей логике независимо от типа данных, к которым они применяются. Например, механизм, поддерживающий очередь, остается одинаковым независимо от того, предназначена ли очередь для хранения элементов типа int, string, object или для класса, определяемого пользователем. До появления обобщений для обработки данных разных типов приходилось создавать различные варианты одного и того же алгоритма. А благодаря обобщениям можно сначала выработать единое решение независимо от конкретного типа данных, а затем применить его к обработке данных самых разных типов без каких-либо дополнительных усилий.
В этой главе описываются синтаксис, теория и практика применения обобщений, а также показывается, каким образом обобщения обеспечивают типовую безопасность в ряде случаев, которые раньше считались сложными. После прочтения настоящей главы у вас невольно возникнет желание ознакомиться с материалом главы 25, посвященной коллекциям, так как в ней приведено немало примеров применения обобщений в классах обобщенных коллекций.
Что такое обобщения
Терминобобщение, по существу, означаетпараметризированный тип.Особая роль параметризированных типов состоит в том, что они позволяют создавать классы, структуры, интерфейсы, методы и делегаты, в которых обрабатываемые данные указываются в виде параметра. С помощью обобщений можно, например, создать единый класс, который автоматически становится пригодным для обработки разнотипных данных. Класс, структура, интерфейс, метод или делегат, оперирующий параметри-зированным типом данных, называетсяобобщенным,как, например,обобщенный классилиобобщенный метод.
Следует особо подчеркнуть, что в C# всегда имелась возможность создавать обобщенный код, оперируя ссылками типаobject.А поскольку классobjectявляется базовым для всех остальных классов, то по ссылке типаobjectможно обращаться к объекту любого типа. Таким образом, до появления обобщений для оперирования разнотипными объектами в программах служил обобщенный код, в котором для этой цели использовались ссылки типаobject.
Но дело в том, что в таком коде трудно было соблюсти типовую безопасность, поскольку для преобразования типаob j ectв конкретный тип данных требовалось приведение типов. А это служило потенциальным источником ошибок из-за того, что приведение типов могло быть неумышленно выгколнено неверно. Это затруднение позволяют преодолеть обобщения, обеспечивая типовую безопасность, которой раньше так недоставало. Кроме того, обобщения упрощают весь процесс, поскольку исключают необходимость выполнять приведение типов для преобразования объекта или другого типа обрабатываемых данных. Таким образом, обобщения расширяют возможности повторного использования кода и позволяют делать это надежно и просто.
ПРИМЕЧАНИЕ
Программирующим на C++ и Java необходимо иметь в виду, что обобщения в C# не следует путать с шаблонами в C++ и обобщениями в Java, поскольку это разные, хотя и похожие средства. В действительности между этими тремя подходами к реализации обобщений существуют коренные различия. Если вы имеете некоторый опыт программирования на C++ или Java, то постарайтесь на основании этого опыта не делать никаких далеко идущих выводов о том, как обобщения'действуют в С#.
Простой пример обобщений
Начнем рассмотрение обобщений с простого примера обобщенного класса. В приведенной ниже программе определяются два класса. Первым из них является обобщенный классGen,вторым — классGenerics Demo,в котором используется классGen.
// Простой пример обобщенного класса, using System;
//В приведенном ниже классе Gen параметр типа Т заменяется // реальным типом данных при создании объекта типа Gen. class Gen<T> {
Т ob; // объявить переменную типа Т
// Обратите внимание на то, что у этого конструктора имеется параметр типа public Gen(T о) { ob = о;
}
// Возвратить переменную экземпляра ob, которая относится к типу Т. public Т GetObO { return ob;
}
// Показать тип Т. public void ShowTypeO {
Console.WriteLine("К типу T относится " + typeof (Т));
}
}
// Продемонстрировать применение обобщенного класса, class GenericsDemo { static void Main() {
// Создать переменную ссылки на объект Gen типа int.
Gen<int> iOb;
// Создать объект типа Gen<int> и присвоить ссылку на него переменной iOb iOb = new Gen<int> (102);
// Показать тип данных, хранящихся в переменной iOb. iOb.ShowType();
// Получить значение переменной iOb. int v = iOb.GetOb();
Console.WriteLine("Значение: " + v) ;
Console.WriteLine();
// Создать объект типа Gen для строк.
Gen<string> strOb = new Gen<string>("Обобщения повышают эффективность.");
// Показать тип данных, хранящихся в переменной strOb. strOb.ShowType();
// Получить значение переменной strOb. string str = strOb.GetOb ();
Console.WriteLine("Значение: " + str);
Эта программа дает следующий результат.
К типу Т относится' System.Int32 Значение: 102
К типу Т относится System.String
Значение: Обобщения повышают эффективность.
Внимательно проанализируем эту программу. Прежде всего обратите внимание на объявление классаGenв приведенной ниже строке кода:
class Gen<T> {
гдеТ— это имяпараметра типа.Это имя служит в качестве метки-заполнителя конкретного типа, который указывается при создании объекта классаGen.Следовательно, имяТиспользуется в классеGenвсякий раз, когда требуется параметр типа. Обратите внимание на то, что имяТзаключается в угловые скобки (< >). Этот синтаксис можно обобщить: всякий раз, когда объявляется параметр типа, он указывается в угловых скобках. А поскольку параметр типа используется в классеGen,то такой класс считаетсяобобщенным.
В объявлении классаGenможно указывать любое имя параметра типа, но по традиции выбирается имяТ.К числудругихнаиболее употребительных имен параметров типа относятсяVиЕ.Вы, конечно, вольны использовать и более описательные имена, напримерTValueилиТКеу.Но в этом случае первой в имени параметра типа принято указывать прописную буквуТ.
Далее имяТиспользуется для объявления переменнойob,как показано в следующей строке кода.
Т ob; // объявить переменную типа Т
Как пояснялось выше, имя параметра типаТслужит меткой-заполнителем конкретного типа, указываемого при создании объекта классаGen.Поэтому переменнаяobбудет иметь тип,привязываемыйкТпри получении экземпляра объекта классаGen.Так, если вместоТуказывается типstring,то в экземпляре данного объекта переменнаяobбудет иметь типstring.
А теперь рассмотрим конструктор классаGen.
public Gen(T о) { ob = о;
}
Как видите, параметроэтого конструктора относится к типуТ.Это означает, что конкретный тип параметраоопределяется типом, привязываемым кТпри создании объекта классаGen.А поскольку параметрои переменная экземпляраobотносятся к типуТ,то после создания объекта классаGenих конкретный тип окажется одним и тем же.
С помощью параметра типа Т можно также указывать тип, возвращаемый методом, как показано ниже на примере методаGetOb ().
public Т GetOb() {
return ob;
}
Переменнаяobтакже относится к типуТ,поэтому ее тип совпадает с типом, возвращаемым методомGetOb ().
МетодShowType() отображает тип параметраТ,передавая его операторуtypeof.Но поскольку реальный тип подставляется вместоТпри создании объекта классаGen,то операторtypeofполучит необходимую информацию о конкретном типе.
В классеGenerics Demoдемонстрируется применение обобщенного классаGen.Сначала в нем создается вариант классаGenдля типаint.
Gen<int> iOb;
Внимательно проанализируем это объявление. Прежде всего обратите внимание на то, что типintуказывается в угловых скобках после имени классаGen.В этом случаеintслужитаргументом типа,привязанным к параметру типаТв классеGen.В данном объявлении создается вариант классаGen,в котором типТзаменяется типомintвезде, где он встречается. Следовательно, после этого объявленияintстановится типом переменнойobи возвращаемым типом методаGetOb ().
В следующей строке кода переменнойiObприсваивается ссылка на экземпляр объекта классаGenдля варианта типаint.
iOb = new Gen<int> (102);
Обратите внимание на то, что при вызове конструктора классаGenуказывается также аргумент типаint.Это необходимо потому, что переменная (в данном случае —iOb),которой присваивается ссылка, относится к типуGen<int>.Поэтому ссылка, возвращаемая операторомnew,также должна относиться к типуGen<int>.В противном случае во время компиляции возникнет ошибка. Например, приведенное ниже присваивание станет причиной ошибки во время компиляции.
iOb = new Gen<double>(118.12); // Ошибка!
ПеременнаяiObотносится к типуGen<int>и поэтому не может использоваться для ссылки на объект типаGen<double>.Такой контроль типов относится к одним из главных преимуществ обобщений, поскольку он обеспечивает типовую безопасность.
Затем в программе отображается тип переменнойobв объектеiOb— типSystem. Int32. Это структура .NET, соответствующая типуint.Далее значение переменнойobполучается в следующей строке кода.
int v = iOb.GetOb ();
Возвращаемым для методаGetOb() является типТ,который был заменен на типintпри объявлении переменнойiOb,и поэтому методGetOb() возвращает значение того же типаint.Следовательно, данное значение может быть присвоено переменнойvтипаint.
Далее в классеGenericsDemoобъявляется объект типаGen<string>.
Gen<string> strOb = new Gen<string>("Обобщения повышают эффективность.");
В этом объявлении указывается аргумент типаstring,поэтому в объекте классаGenвместоТподставляется типstring.В итоге создается вариант классаGenдля типаstring,как демонстрируют остальные строки кода рассматриваемой здесь программы.
Прежде чем продолжить изложение, следует дать определение некоторым терминам. Когда для классаGenуказывается аргумент типа, напримерintилиstring,то создается так называемый в C#закрыто сконструированный тип.В частности,Gen<int>является закрыто сконструированным типом. Ведь, по существу, такой обобщенный тип, какGen<T>,является абстракцией. И только после того, как будет сконструирован конкретный вариант, напримерGen<int>,создается конкретный тип. А конструкция, подобнаяGen<T>,называется в C#открыто сконструированным типом, поскольку в ней указывается параметр типаТ,но не такой конкретный тип, какint.
В C# чаще определяются такие понятия, какоткрытыйизакрытый типы.Открытым типом считается такой параметр типа или любой обобщенный тип, для которого аргумент типа является параметром типа или же включает его в себя. А любой тип, не относящийся к открытому, считается закрытым.Сконструированным типомсчитается такой обобщенный тип, для которого предоставлены все аргументы типов. Если все эти аргументы относятся к закрытым типам, то такой тип считается закрыто сконструированным. А если один или несколько аргументов типа относятся к открытым типам, то такой тип считается открыто сконструированным.
Различение обобщенных типов по аргументам типа
Что касается обобщенных типов, то следует иметь в виду, что ссылка на один конкретный вариант обобщенного типа не совпадает по типу сдругимвариантом того же самого обобщенного типа. Так, если ввести в приведенную выше программу следующую строку кода, то она не будет скомпилирована.
iOb = strOb; // Неверно!
Несмотря на то что обе переменные,iObиstrOb,относятся к типуGen<T>,они ссылаются на разные типы, поскольку у них разные аргументы.
Повышение типовой безопасности с помощью обобщений
В связи с изложенным выше возникает следующий резонный вопрос: если аналогичные функциональные возможности обобщенного классаGenможно получить и без обобщений, просто указав объект как тип данных и выполнив надлежащее приведение типов, то какая польза от того, что классGenделается обобщенным? Ответ на этот вопрос заключается в том, что обобщения автоматически обеспечивают типовую безопасность всех операций, затрагивающих классGen.В ходе выполнения этих операций обобщения исключают необходимость обращаться к приведению типов и проверять соответствие типов в коде вручную.
Для того чтобы стали более понятными преимущества обобщений, рассмотрим сначала программу, в которой создается необобщенный аналог классаGen.
// Класс NonGen является полным функциональным аналогом // класса Gen, но без обобщений.
using System; ,
class NonGen {
object ob; // переменная ob теперь относится к типу object
// Передать конструктору ссылку на объект типа object, public NonGen(object о) { ob = о;
}
// Возвратить объект типа object, public object GetOb() {
return ob;
}
// Показать тип переменной ob. public void ShowTypeO {
Console.WriteLine("Тип переменной ob: " + ob.GetType());
}
}
// Продемонстрировать применение необобщенного класса, class NonGenDemo { static void Main() {
NonGen iOb;
// Создать•объект класса NonGen. iOb = new NonGen(102);
// Показать тип данных, хранящихся в переменной iOb. iOb.ShowType();
// Получить значение переменной iOb.
//На этот раз потребуется приведение типов, int v = (int) iOb.GetObO;
Console.WriteLine("Значение: " + v);
Console.WriteLine();
// Создать еще один объект класса NonGen и // сохранить строку в переменной it.
NonGen strOb = new NonGen("Тест на необобщенность");
// Показать тип данных, хранящихся в переменной strOb. strOb.ShowType();
// Получить значение переменной strOb.
//Ив этом случае требуется приведение типов.
String str = (string) strOb.GetOb();
Console.WriteLine("Значение: " + str);
// Этот код компилируется, но он принципиально неверный! iOb = strOb;
// Следующая строка кода приводит к исключительной // ситуации во время выполнения.
// v = (int) iOb.GetObO; // Ошибка при выполнении!
}
}
При выполнении этой программы получается следующий результат.
Тип переменной ob: System.Int32 Значение: 102
Тип переменной ob: System.String Значение: Тест на необобщенность
Как видите, результат выполнения этой программы такой же, как и у предыдущей программы.
В этой программе обращает на себя внимание ряд любопытных моментов. Прежде всего, типТзаменен везде, где он встречается в классеNon Gen.Благодаря этому в классеNon Genможет храниться объект любого типа, как и в обобщенном варианте этого класса. Но такой подход оказывается непригодным по двум причинам. Во-первых, для извлечения хранящихся данных требуется явное приведение типов. И во-вторых, многие ошибки несоответствия типов не могут быть обнаружены вплоть до момента выполнения программы. Рассмотрим каждую из этих причин более подробно.
Начнем со следующей строки кода.
int v = (int) iOb.GetObO;
Теперь возвращаемым типом методаGetOb() являетсяobject,а следовательно, для распаковки значения, возвращаемого методомGetOb (), и его последующего сохранения в переменнойvтребуется явное приведение к типуint.Если исключить приведение типов, программа не будет скомпилирована. В обобщенной версии этой программы приведение типов не требовалось, поскольку типintуказывался в качестве аргумента типа при создании объектаiOb.А в необобщенной версии этой программы потребовалось явное приведение типов. Но это не только неудобно, но и чревато ошибками.
А теперь рассмотрим следующую последовательность кода в конце анализируемой здесь программы.
// Этот код компилируется, но он принципиально неверный! iOb = strOb;
// Следующая строка кода приводит к исключительной // ситуации во время выполнения.
// v = (int) iOb.GetObO; // Ошибка при выполнении!
В этом коде значение переменнойstrObприсваивается переменнойiOb.Но переменнаяstrObссылается на объект, содержащий символьную строку, а не целое значение. Такое присваивание оказывается верным с точки зрения синтаксиса, поскольку все ссылки на объекты классаNonGenодинаковы, а значит, по ссылке на один объект классаNonGenможно обращаться к любому другому объекту классаNonGen.Тем не менее такое присваивание неверно с точки зрения семантики, как показывает следующая далее закомментированная строка кода. В этой строке тип, возвращаемый методомGetOb (), приводится к типуint,а затем предпринимается попытка присвоить полученное в итоге значение переменнойint.К сожалению, в отсутствие обобщений компилятор не сможет выявить подобную ошибку. Вместо этого возникнет исключительная ситуация во время выполнения, когда будет предпринята попытка приведения к типуint.Для того чтобы убедиться в этом, удалите символы комментария в начале данной строки кода, скомпилируйте, а затем выполните программу. При ее выполнении возникнет ошибка.
Упомянутая выше ситуация не могла бы возникнуть, если бы в программе использовались обобщения. Компилятор выявил бы ошибку в приведенной выше последовательности кода, если бы она была включена в обобщенную версию программы, и сообщил бы об этой ошибке, предотвратив тем самым серьезный сбой, приводящий к исключительной ситуации при выполнении программы. Возможность создавать типизированный код, в котором ошибки несоответствия типов выявляются во время
компиляции, является главным преимуществом обобщений. Несмотря на то что в C# всегда имелась возможность создавать "обобщенный" код, используя ссылки на объекты, такой код не был типизированным, т.е. не обеспечивал типовую безопасность, а его неправильное применение могло привести к исключительным ситуациям во время выполнения. Подобные ситуации исключаются благодаря обобщениям. По существу, обобщения переводят ошибки при выполнении в разряд ошибок при компиляции. В этом и заключается основная польза от обобщений.
В рассматриваемой здесь необобщенной версии программы имеется еще один любопытный момент. Обратите внимание на то, как тип переменнойobэкземпляра классаNonGenсоздается с помощью методаShowType() в следующей строке кода.
Console.WriteLine("Тип переменной ob: " + ob.GetType ());
Как пояснялось в главе 11, в классеobjectопределен ряд методов, доступных для всех типов данных. Одним из них является методGetType (), возвращающий объект классаТуре,который описывает тип вызывающего объекта во время выполнения. Следовательно, конкретный тип объекта, на который ссылается переменнаяob,становится известным во время выполнения, несмотря на то, что тип переменнойobуказан в исходном коде какobject.Именно поэтому в среде CLR будет сгенерировано исключение при попытке выполнить неверное приведение типов во время выполнения программы.
Обобщенный класс с двумя параметрами типа
В классе обобщенного типа можно указать два или более параметра типа. В этом случае параметры типа указываются списком через запятую. В качестве примера ниже приведен классTwoGen,являющийся вариантом классаGenс двумя параметрами типа.
// Простой обобщенный класс с двумя параметрами типа Т и V.
using System;
class TwoGenCT, V> {
T obi;
V ob2;
// Обратите внимание на то, что в этом конструкторе // указываются параметры типа Т и V. public TwoGen(Т ol, V о2) {
obi = ol; оЬ2 = о2;
}
// Показать типы Т и V. public void showTypes() {
Console.WriteLine("К типу T относится " + typeof(Т));
Console.WriteLine("К типу V относится " + typeof(V));
}
return obi;
}
public V Get0bj2() { return ob2;
}
}
// Продемонстрировать применение обобщенного класса с двумя параметрами типа, class SimpGen {
static void Main() {
TwoGenCint, string> tgObj =
new TwoGenCint, string>(119, "Альфа Бета Гамма");
// Показать типы. tgObj.showTypes();
// Получить и вывести значения, int v = tgObj.getobl();
Console.WriteLine("Значение: " + v); string str = tgObj.GetObj2();
Console.WriteLine("Значение: " + str);
}
}
Эта программа дает следующий результат.
К типу Т относится System.Int32 К типу V относится System.String Значение: 119
Значение: Альфа Бета Гамма
Обратите внимание на то, как объявляется классTwoGen.
class TwoGenCT, V> {
В этом объявлении указываются два параметра типаТиV,разделенные запятой. А поскольку у классаTwoGenдва параметра типа, то при создании объекта этого класса необходимо указывать два соответствующих аргумента типа, как показано ниже.
TwoGenCint, string> tgObj =
new TwoGenCint, string>(119, "Альфа Бета Гамма");
В данном случае вместоТподставляется типint,а вместоV— типstring.
В представленном выше примере указываются аргументы разного типа, но они могут быть и одного типа. Например, следующая строка кода считается вполне допустимой.
TwoGencstring, string> х =
new TwoGencstring, string>("Hello", "Goodbye");
В этом случае оба типа,ТиV,заменяются одним и тем же типом,string.Ясно, что если бы аргументы были одного и того же типа, то два параметра типа бьГли бы не нужны.
Общая форма обобщенного класса
Синтаксис обобщений, представленных в предыдущих примерах, может быть сведен к общей форме. Ниже приведена общая форма объявления обобщенного класса.
classимя_класса<список_параметров_типа> {// ...
А вот как выглядит синтаксис объявления ссылки на обобщенный класс.
имя_класса<список_аргументов_типа> имя_переменной=
newимя_класса<список_параметров_типа>(список_аргументов_конструктора);
Ограниченные типы
В предыдущих примерах параметры типа можно было заменить любым типом данных. Например, в следующей строке кода объявляется любой тип, обозначаемый какТ.
class Gen<T> {
Это означает, что вполне допустимо создавать объекты классаGen,в которых типТзаменяется типомint, double, string, FileStreamили любым другим типом данных. Во многих случаях отсутствие ограничений на указание аргументов типа считается вполне приемлемым, но иногда оказывается полезно ограничить круг типов, которые могут быть указаны в качестве аргумента типа.
Допустим, что требуется создать метод, оперирующий содержимым потока, включая объекты типаFileStreamилиMemoryStream.На первый взгляд, такая ситуация идеально подходит для применения обобщений, но при этом нужно каким-то образом гарантировать, что в качестве аргументов типа будут использованы только типы потоков, но неintили любой другой тип. Кроме того, необходимо как-то уведомить компилятор о том, что методы, определяемые в классе потока, будут доступны для применения. Так, в обобщенном коде должно быть каким-то образом известно, что в нем может быть вызван методRead ().
Дл^ выхода из подобных ситуаций в C# предусмотреныограниченные типы.Указывая параметр типа, можно наложить определенное ограничение на этот Параметр. Это делается с помощью оператораwhereпри указании параметра типа:
classимя_класса<параметр_типа>whereпараметр_типа : ограничения {// ...
гдеограниченияуказываются списком через запятую.
В C# предусмотрен ряд ограничений на типы данных.
• Ограничение на базовый класс, требующее наличия определенного базового класса в аргументе типа. Это ограничение накладывается указанием имени требуемого базового класса. Разновидностью этого ограничения являетсянеприкрытое ограничение типа,при котором на базовый класс указывает параметр типа, а не конкретный тип. Благодаря этому устанавливается взаимосвязь между двумя параметрами типа.
• Ограничение на интерфейс,требующее реализации одного или нескольких интерфейсов аргументом типа. Это ограничение накладывается указанием имени требуемого интерфейса.
• Ограничение на конструктор,требующее предоставить конструктор без параметров в аргументе типа. Это ограничение накладывается с помощью оператора new ().
• Ограничение ссылочного типа,требующее указывать аргумент ссылочного типа с помощью оператора class.
• Ограничение типа значения, требующее указывать аргумент типа значения с помощью оператора struct.
Среди всех этих ограничений чаще всего применяются ограничения на базовый класс и интерфейс, хотя все они важны в равной степени. Каждое из этих ограничений рассматривается далее по порядку.
Применение ограничения на базовый класс
Ограничение на базовый класс позволяет указывать базовый класс, который должен наследоваться аргументом типа. Ограничение на базовый класс служит двум главным целям. Во-первых, оно позволяет использовать в обобщенном классе те члены базового класса, на которые указывает данное ограничение. Это дает, например, возможность вызвать метод или обратиться к свойству базового класса. В отсутствие ограничения на базовый класс компилятору ничего не известно о типе членов, которые может иметь аргумент типа. Накладывая ограничение на базовый класс, вы тем самым даете компилятору знать, что все аргументы типа будут иметь члены, определенные в этом базовом классе.
И во-вторых, ограничение на базовый класс гарантирует использование только тех аргументов типа, которые поддерживают указанный базовый класс. Это означает, что для любого ограничения, накладываемого на базовый класс, аргумент типа должен обозначать сам базовый класс или производный от него класс. Если же попытаться использовать аргумент типа, не соответствующий указанному базовому классу или не наследующий его, то в результате возникнет ошибка во время компиляции. /
Ниже приведена общая форма наложения ограничения на базовый класс, в которой используется оператор where:
where Т : имя_базового_класса
где Г обозначает имя параметра типа, аимя_базового_класса —конкретное имя ограничиваемого базового класса. Одновременно в этой форме ограничения может быть указан только один базовый класс.
В приведенном ниже простом примере демонстрируется механизм наложения ограничения на базовый класс.
// Простой пример, демонстрирующий механизм наложения // ограничения на базовый класс.
using System;
class А {
public void Hello () {
Console.WriteLine("Hello");
// Класс В наследует класс А. class В : А { }
// Класс С не наследует класс А. class С '{ }
//В силу ограничения на базовый класс во всех аргументах типа,
// передаваемых классу Test, должен присутствовать базовый класс А. class Test<T> where Т : А {
Т obj;
public Test(Т о) { obj = о;
}
public void SayHelloO {
// Метод Hello() вызывается, поскольку он объявлен в базовом классе А. obj.Hello();
}
}
class BaseClassConstraintDemo { static void Main() {
A a = new A();
В b = new В()
С с = new С()j
// Следующий код вполне допустим, поскольку класс А указан как базовый.
Test<A> tl = new Test<A>(a);
tl.SayHello();
// Следующий код вполне допустим, поскольку класс В наследует от класса А.
Test<B> t2 = new Test<B> (b);
t2.SayHello();
// Следующий код недопустим, поскольку класс С не наследует от класса А.
// Test<C> t3 = new Test<C>(c); // Ошибка!
// t3.SayHello(); // Ошибка!
}
}
В данном примере кода классАнаследуется классомВ,но не наследуется классомС.Обратите также внимание на то, что в классеАобъявляется методHello (),а классTestобъявляется как обобщенный следующим образом.
class Test<T> where Т : А {
Операторwhereв этом объявлении накладывает следующее ограничение: любой аргумент, указываемый для типаТ,должен иметь классАв качестве базового.
А теперь обратите внимание на то, что в классеTestобъявляется методSayHello (),как показано ниже.
public void SayHelloO {
// Метод Hello () вызывается, поскольку он объявлен в базовом классе А. obj.Hello();
}
Этот метод вызывает в свою очередь методHello ()для объектаobjтипаТ.Любопытно, что единственным основанием для вызова методаHello() служит следующее требование ограничения на базовый класс: любой аргумент типа, привязанный к типуТ,должен относиться к классуАили наследовать от классаА,в котором объявлен методHello (). Следовательно, любой допустимый типТбудет также определять методHello (). Если бы данное ограничение на базовый класс не было наложено, то компилятору ничего не было бы известно о том, что методHello() может быть вызван для объекта типаТ.Убедитесь в этом сами, удалив операторwhereиз объявления обобщенного классаTest.В этом случае программа не подлежит компиляции, поскольку теперь методHello() неизвестен.
Помимо разрешения доступа к членам базового класса, ограничение на базовый класс гарантирует, что в качестве аргументов типа могут быть переданы только те типы данных, которые наследуют базовый класс. Именно поэтому приведенные ниже строки кода закомментированы.
// Test<C> t3 = new Test<C>(c); // Ошибка!
// t3.SayHello(); // Ошибка!
КлассСне наследует от классаА,и поэтому он не может использоваться в качестве аргумента типа при создании объекта типаTest.Убедитесь в этом сами, удалив символы комментария и попытавшись перекомпилировать этот код.
Прежде чем продолжить изложение дальше, рассмотрим вкратце два последствия наложения ограничения на базовый класс. Во-первых, это ограничение разрешает доступ к членам базового класса из обобщенного класса. И во-вторых, оно гарантирует допустимость только тех аргументов типа, которые удовлетворяют данному ограничению, обеспечивая тем самым типовую безопасность.
В предыдущем примере показано, как накладывается ограничение на базовый класс, но из него не совсем ясно, зачем это вообще нужно. Для того чтобы особое значение ограничения на базовый класс стало понятнее, рассмотрим еще один, более практический пример. Допустим, что требуется реализовать механизм управления списками телефонных номеров, чтобы пользоваться разными категориями таких списков, в частности отдельными списками для друзей, поставщиков, клиентов и т.д. Для этой цели можно сначала создать классPhoneNumber,в котором будут храниться имя абонента и номер его телефона. Такой класс может иметь следующий вид.
// Базовый класс, в котором хранятся имя абонента и номер его телефона, class PhoneNumber {
public PhoneNumber(string n, string num) {
Name = n;
Number = num;
}
// Автоматически реализуемые свойства, в которых // хранятся имя абонента и номер его телефона, public string Number { get; set; } public string Name { get; set; }
Далее создадим классы, наследующие классPhoneNumber: FriendиSupplier.Эти классы приведены ниже.
// Класс для телефонных номеров друзей, class Friend : -PhoneNumber {
public Friend(string n, string num, bool wk) : base(n, num)
{
IsWorkNumber = wk;
}
public bool IsWorkNumber { get; private set; }
// ...
}
// Класс для телефонных номеров поставщиков, class Supplier : PhoneNumber {
public Supplier(string n, string num) : base(n, num) { }
// ...
}
Обратите внимание на то, что в классFriendвведено свойствоIsWorkNumber,возвращающее логическое значениеtrue,если номер телефона является рабочим.
Для управления списками телефонных номеров создадим еще один класс под названиемPhoneList.Его следует сделать обобщенным, поскольку он должен служить для управления любым списком телефонных номеров. В функции такого управления должен, в частности, входить поиск телефонных номеров по заданным именам и наоборот, поэтому на данный класс необходимо наложить ограничение по типу, требующее, чтобы объекты, сохраняемые в списке, были экземплярами класса, производного от классаPhoneNumber.
// Класс PhoneList способен управлять любым видом списка телефонных // номеров, при условии, что он является производным от класса PhoneNumber.
class PhoneList<T> where T : PhoneNumber {
T[] phList; int end;
public PhoneList() {
phList = new T[10]; end = 0;
}
// Добавить элемент в список, public bool Add(T newEntry) { if(end == 10) return false; phList[end] = newEntry; end++;
return true;
// Найти и возвратить сведения о телефоне по заданному имени, public Т FindByName(string name) { for(int i=0; i<end; i++) {
// Имя может использоваться, потому что его свойство Name // относится к членам класса PhoneNumber, который является // базовым по накладываемому ограничению, if(phList[i].Name == name) return phList[i];
}
// Имя отсутствует в сгояске. throw new NotFoundException();
}
// Найти и возвратить сведения о телефоне по заданному номеру, public Т FindByNumber(string number) { for (int i=0; i<end; i++) {
// Номер телефона также может использоваться, поскольку // его свойство Number относится к членам класса PhoneNumber,
// который является базовым по накладываемому ограничению, if(phList[i].Number == number) return phList[i];
}
// Номер телефона отсутствует в списке, throw new NotFoundException();
}
// ...
}
Ограничение на базовый класс разрешает коду в классеPhoneListдоступ к свойствамNameиNumberдля управления любым видом списка телефонных номеров. Оно гарантирует также, что для построения объекта классаPhoneListбудут использоваться только доступные типы. Обратите внимание на то, что в классеPhoneListгенерируется исключениеNotFoundException,если имя или номер телефона не найдены. Это специальное исключение, объявляемое ниже.
class NotFoundException : Exception {
/* Реализовать все конструкторы класса Exception.
Эти конструкторы выполняют вызов конструктора базового класса.
Класс NotFoundException ничем не дополняет класс Exception и поэтому не требует никаких дополнительных действий. */
public NotFoundException() : base() { }
public NotFoundException(string str) : base (str) { }
public NotFoundException(
string str, Exception inner) : base(str, inner) { } protected NotFoundException(
System.Runtime.Serialization.Serializationlnfo si,
System.Runtime.Serialization.StreamingContext sc) : base(si, sc) { }
В данном примере используется только конструктор, вызываемый по умолчанию, но ради наглядности этого примера в классе исключенияNotFoundExceptionреализуются все конструкторы, определенные в классеException.Обратите внимание на то, что эти конструкторы вызывают эквивалентный конструктор базового класса, определенный в классе.Exception.А поскольку класс исключенияNotFoundExceptionничем не дополняет базовый классException,то для любых дополнительных действий нет никаких оснований.
В приведенной ниже программе все рассмотренные выше фрагменты кода объединяются вместе, а затем демонстрируется применение классаPhoneList.Кроме того, в ней создается классEmail Friend.Этот класс не наследует от классаPhoneNumber,а следовательно, оннеможет использоваться для создания объектов классаPhoneList.
// Более практический пример, демонстрирующий применение // ограничения на базовый класс.
using System;
// Специальное исключение, генерируемое в том случае,
// если имя или номер телефона не найдены, class NotFoundException : Exception {
/* Реализовать все конструкторы класса Exception.
Эти конструкторы выполняют вызов конструктора базового класса.
Класс NotFoundException ничем не дополняет класс Exception и поэтому не требует никаких дополнительных действий. */
public NotFoundException () base() { }
public NotFoundException(string str) : base(str) { }
public NotFoundException (
string str,Exception inner) : base(str, inner) { } protected NotFoundException(
System.Runtime.Serialization.Serializationlnfо si,
System.Runtime.Serialization.StreamingContext sc) : base (si, sc) { }
}
// Базовый класс, в котором хранятся имя абонента и номер его телефона, class PhoneNumber {
public PhoneNumber(string n, string num) {
Name = n;
Number = num;
}
public string Number { get; set; } public string Name { get; set; }
}
// Класс для телефонных номеров друзей, class Friend : PhoneNumber {
public Friend(string n, string num, bool wk) : base(n, num)
IsWorkNumber = wk;
}
public bool IsWorkNumber { get; private set; }
// ...
}
// Класс для телефонных номеров поставщиков, class Supplier : PhoneNumber {
public Supplier(string n, string num) : base(n, num) { }
// ...
}
// Этот класс не наследует от класса PhoneNumber. class EmailFriend {
// ...
}
// Класс PhoneList способен управлять любым видом списка телефонных номеров, // при условии, что он является производным от класса PhoneNumber. class PhoneList<T> where T : PhoneNumber {
T[] phList; int end;
public PhoneList() { phList = new T[10]; end = 0;
}
// Добавить элемент в список, public bool Add(T newEntry) { if(end == 10) return false;
phList[end] = newEntry; end++; return true;
}
// Найти и возвратить сведения о телефоне по заданному имени, public Т FindByName(string name) {
for(int i=0; i<end; i++) {
// Имя может использоваться, потому что его свойство Name // относится к членам класса PhoneNumber, который является // базовым по накладываемому ограничению, if(phList[i].Name == name) return phList[i];
}
// Имя отсутствует в списке, throw new NotFoundException() ;
// Найти и возвратить сведения о телефоне по заданному номеру, public Т FindByNumber(string number) { for(int i=0; i<end; i++) {
// Номер" телефона также может использоваться, поскольку // его свойство Number относится к членам класса PhoneNumber, // который является базовым по накладываемому ограничению, if(phList[i].Number == number) return phList[i] ;
}
// Номер телефона отсутствует в списке. • throw new NotFoundException ();
}
// ...
}
// Продемонстрировать наложение ограничений на базовый класс, class UseBaseClassConstraint { static void Main() {
// Следующий код вполне допустим, поскольку // класс Friend наследует от класса PhoneNumber.
PhoneList<Friend> plist = new PhoneList<Friend>(); plist.Add(new Friend("Том", "555-1234", true)); plist.Add(new Friend("Гари", "555-6756", true)); plist.Add(new Friend("Матт", "555-9254", false));
try {
// Найти номер телефона по заданному имени друга.
Friend frnd = plist.FindByName("Гари") ;
Console.Write(frnd.Name + ": " + frnd.Number);
if(frnd.IsWorkNumber)
Console.WriteLine(" (рабочий)"); else
Console.WriteLine ();
} catch(NotFoundException) {
Console.WriteLine("He найдено");
}
Console.WriteLine();
• // Следующий код также допустим, поскольку // класс Supplier наследует от класса PhoneNumber.
PhoneList<Supplier> plist2 = new PhoneList<Supplier>(); plist2.Add(new Supplier("Фирма Global Hardware", "555-8834")); plist2.Add(new Supplier ("Агентство Computer Warehouse", "555-9256")); plist2.Add(new Supplier("Компания NetworkCity", "555-2564")); try {
// Найти наименование поставщика по заданному номеру телефона. Supplier sp = plist2.FindByNumber("555-2564");
Console.WriteLine(sp.Name + ": " + sp.Number);
} catch(NotFoundException) {
Console.WriteLine("He найдено");
}
// Следующее объявление недопустимо, поскольку
// класс EmailFriend НЕ наследует от класса PhoneNumber.
// PhoneList<EmailFriend> plist3 =
// new PhoneList<EmailFriend>(); // Ошибка!
}
}
Ниже приведен результат выполнения этой программы.
Гари: 555-6756 (рабочий)
Компания NetworkCity: 555-2564
Поэкспериментируйте с этой программой. В частности, попробуйте составить разные виды списков телефонных номеров или воспользоваться свойствомIsWorkNumberв классеPhoneList.Вы сразу же обнаружите, что компилятор не позволит вам этого сделать, потому что свойствоIsWorkNumberопределено в классеFriend,а не в классеPhoneNumber,а следовательно, оно неизвестно в классеPhoneList.
Применение ограничения на интерфейс
Ограничение на интерфейс позволяет указывать интерфейс, который должен быть реализован аргументом типа. Это ограничение служит тем же основным целям, что и ограничение на базовый класс. Во-первых, оно позволяет использовать члены интерфейса в обобщенном классе. И во-вторых, оно гарантирует использование только тех аргументов типа, которые реализуют указанный интерфейс. Это означает, что для любого ограничения, накладываемого на интерфейс, аргумент типа должен обозначать сам интерфейс или же тип, реализующий этот интерфейс.
Ниже приведена общая форма наложения ограничения на интерфейс, в которой используется операторwhere:
whereТ : имя_интерфейса
гдеГ— это имя параметра типа, аимя_интерфейса— конкретное имя ограничиваемого интерфейса. В этой форме ограничения может быть указан список интерфейсов через запятую. Если ограничение накладывается одновременно на базовый класс и интерфейс, то первым в списке должен быть указан базовый класс.
Ниже приведена программа, демонстрирующая наложение ограничения на интерфейс и представляющая собой переработанный вариант предыдущего примера программы, управляющей списками телефонных номеров. В этом варианте классPhoneNumberпреобразован в интерфейсI PhoneNumber,который реализуется в классахFriendиSupplier.
// Применить ограничение на интерфейс, using System;
// Специальное исключение, генерируемое в том случае,
// если имя или номер телефона не найдены, class NotFoundException : Exception {
/* Реализовать все конструкторы класса Exception.
Эти конструкторы выполняют вызов конструктора базового класса. Класс NotFoundException ничем не дополняет класс Exception и поэтому не требует никаких дополнительных действий. */
public NotFoundException() : base() { }
public NotFoundException(string str) : base(str) { }
public NotFoundException(
string str,Exception inner) : base(str, inner) { }
• protected NotFoundException (
System.Runtime.Serialization.Serializationlnfo si,
System.Runtime.Serialization.StreamingContext sc) : base(si, sc) { }
}
// Интерфейс, поддерживающий имя и номер телефона, public interface IPhoneNumber {
string Number { get; set;
}
string Name { get; set;
}
}
// Класс для телефонных номеров друзей.
//В нем реализуется интерфейс IPhoneNumber. class Friend : IPhoneNumber {
public Friend(string n, string num, bool wk) {
Name = n;
Number = num;
IsWorkNumber = wk;
}
public bool IsWorkNumber { get; private set; }
// Реализовать интерфейс IPhoneNumber. public string Number { get; set; } public string Name { get; set; }
// ...
}
// Класс для телефонных номеров поставщиков, class Supplier : IPhoneNumber {
public Supplier(string n, string num) {
Name = n;
Number = num;
}
// Реализовать интерфейс IPhoneNumber. public string Number { get; set; } public string Name { get; set; }
// ...
}
// В этом классе интерфейс IPhoneNumber не реализуется, class EmailFriend {
// ...
}
// Класс PhoneList способен управлять любым видом списка телефонных // номеров, при условии, что он реализует интерфейс PhoneNumber. class PhoneList<T> where T : IPhoneNumber {
T[] phList; int end;
public PhoneList() { phList = new T[10]; end = 0;
}
public bool-Add(T newEntry) { if(end == 10) return false;
phList[end] = newEntry; end++;
return true;
}
// Найти и возвратить сведения о телефоне по заданному имени, public Т FindByName(string name) {
for(int i=0; i<end; i++) {
// Имя может использоваться, потому что его свойство Name // относится к членам интерфейса IPhoneNumber, на который // накладывается ограничение, if(phList[i].Name == name) return phList[i];
}
// Имя отсутствует в списке, throw new NotFoundException();
}
// Найти и возвратить сведения о телефоне по заданному номеру, public Т FindByNumber(string number) { for(int i=0; i<end; i++) {
// Номер телефона также может использоваться, поскольку его // свойство Number относится к членам интерфейса IPhoneNumber, // на который накладывается ограничение.
if(phList[i].Number == number) return phList[i];
}
// Номер телефона отсутствует в списке, throw new NotFoundException ();
}
// ...
}
// Продемонстрировать наложение ограничения на интерфейс, class UselnterfaceConstraint { static void Main() {
// Следующий код вполне допустим, поскольку //в классе Friend реализуется интерфейс IPhoneNumber. PhoneList<Friend> plist = new PhoneList<Friend>(); plist.Add(new Friend("Том", "555-1234", true)); plist.Add(new Friend("Гари", "555-6756", true)); plist.Add(new Friend("Матт", "555-9254", false));
try {
// Найти номер телефона по заданному имени друга.
Friend frnd = plist.FindByName("Гари") ;
Console.Write(frnd.Name + ": " + frnd.Number);
if(frnd.IsWorkNumber)
Console.WriteLine (" (рабочий)"); else
Console.WriteLine();
} catch(NotFoundException) {
Console.WriteLine("He найдено");
}
Console.WriteLine();
// Следующий код также допустим, поскольку в классе Supplier // также реализуется интерфейс IPhoneNumber.
PhoneList<Supplier> plist2 = new PhoneList<Supplier>(); plist2.Add(new Supplier("Фирма Global Hardware", "555-8834")); plist2.Add(new Supplier("Агентство Computer Warehouse", "555-9256")); plist2.Add(new Supplier("Компания NetworkCity", "555-2564"));
try {
// Найти наименование поставщика по заданному номеру телефона. Supplier sp = plist2.FindByNumber("555-2564");
Console.WriteLine(sp.Name + ": " + sp.Number);
} catch(NotFoundException) {
Console.WriteLine("He найдено");
}
// в классе EmailFriend НЕ реализуется интерфейс IPhoneNumber.
// PhoneList<EmailFriend> plist3 =
// new PhoneList<EmailFriend>(); // Ошибка!
}
}
В этой версии программы ограничение на интерфейс, указываемое в классеPhoneList,требует, чтобы аргумент типа реализовал интерфейсIPhoneList.А поскольку этот интерфейс реализуется в обоих классах,FriendиSupplier,то они относятся к допустимым типам, привязываемым к типуТ.В то же время интерфейс не реализуется в классеEmailFriend,и поэтому этот класс не может быть привязан к типуТ.Для того чтобы убедиться в этом, удалите символы комментария в двух последних строках кода в методеMain (). Вы сразу же обнаружите, что программа не компилируется.
Применение ограничения new () на конструктор
Ограничениеnew() на конструктор позволяет получать экземпляр объекта обобщенного типа. Как правило, создать экземпляр параметра обобщенного типа не удается. Но это положение изменяет ограничениеnew (), поскольку оно требует, чтобы аргумент типа предоставил конструктор без параметров. Им может быть конструктор, вызываемый по умолчанию и предоставляемый автоматически, если явно определяемый конструктор отсутствует или же конструктор без параметров явно объявлен пользователем. Накладывая ограничениеnew (), можно вызывать конструктор без параметров для создания объекта.
Ниже приведен простой пример, демонстрирующий наложение ограниченияnew().
// Продемонстрировать наложение ограничения new() на конструктор.
using System;
class MyClass {
public MyClass() {
// ...
}
//. . .
}
class Test<T> where T : new() {
T obj;
public Test() {
// Этот код работоспособен благодаря наложению ограничения new(). obj = new Т(); // создать объект типа Т
}
// ...
}
class ConsConstraintDemo {
static void Main() {
Test<MyClass> x = new Test<MyClass>();
}
}
Прежде всего обратите внимание на объявление классаTest.
class Test<T> where T : new() {
В силу накладываемого ограниченияnew() любой аргумент типа должен предоставлять конструктор без параметров.
Далее проанализируем приведенный ниже конструктор классаTest.
public Test () {
// Этот код работоспособен благодаря наложению ограничения new(). obj = new Т(); // создать объект типа Т
}
В этом фрагменте кода создается объект типа Т, и ссылка на него присваивается переменной экземпляра obj. Такой код допустим только потому, что ограничениеnew() требует наличия конструктора. Для того чтобы убедиться в этом, попробуйте сначала удалить ограничениеnew (), а затем попытайтесь перекомпилировать программу. В итоге вы получите сообщение об ошибке во время компиляции.
В методеMain() получается экземпляр объекта типаTest,как показано ниже.
Test<MyClass> х = new Test<MyClass>();
Обратите внимание на то, что аргументом типа в данном случае является классMyClassи что в этом классе определяется конструктор без параметров. Следовательно, этот класс допускается использовать в качестве аргумента типа для классаTest.Следует особо подчеркнуть, что в классеMyClassсовсем не обязательно определять конструктор без параметров явным образом. Его используемый по умолчанию конструктор вполне удовлетворяет накладываемому ограничению. Но если классу потребуются другие конструкторы, помимо конструктора без параметров, то придется объявить явным образом и вариант без параметров.
Что касается применения ограниченияnew(), то следует обратить внимание на три других важных момента. Во-первых, его можно использовать вместе с другими ограничениями, но последним по порядку. Во-вторых, ограничениеnew() позволяет конструировать объект, используя только конструктор без параметров, — даже если доступны другие конструкторы. Иными словами, передавать аргументы конструктору параметра типа не разрешается. И в-третьих, ограничениеnew() нельзя использовать одновременно с ограничением типа значения, рассматриваемым далее.
Ограничения ссылочного типа и типа значения
Два других ограничения позволяют указать на то, что аргумент, обозначающий тип, должен быть либо ссылочного типа, либо типа значения. Эти ограничения оказываются полезными в тех случаях, когда для обобщенного кода важно провести различие между ссылочным типом и типом значения. Ниже приведена общая форма ограничения ссылочного типа.
whereТ :class
В этой форме с операторомwhereключевое словоclassуказывает на то, что аргументТдолжен быть ссылочного типа. Следовательно, всякая попытка использовать тип значения, напримерintилиbool,вместоГприведет к ошибке во время компиляции.
Ниже приведена общая форма ограничения типа значения.
whereТ :struct
В этой форме ключевое словоstructуказывает на то, что аргументГдолжен быть типа значения. (Напомним, что структуры относятся к типам значений.) Следовательно, всякая попытка использовать ссылочный тип, напримерstring,вместоГприведет к ошибке во время компиляции. Но если имеются дополнительные ограничения, то в любом случаеclassилиstructдолжно быть первым по порядку накладываемым ограничением.
Ниже приведен пример, демонстрирующий наложение ограничения ссылочного типа.
// Продемонстрировать наложение ограничения ссылочного типа.
using System;
class MyClass {
//...
}
// Наложить ограничение ссылочного типа, class Test<T> where Т : class {
Т obj;
public Test () {
// Следующий оператор допустим только потому, что // аргумент Т гарантированно относится к ссылочному // типу, что позволяет присваивать пустое значение, obj = null;
}
// ...
}
class ClassConstraintDemo { static void Main() {
// Следующий код вполне допустим, поскольку MyClass является классом. Test<MyClass> х = new Test<MyClass>();
// Следующая строка кода содержит ошибку, поскольку // int относится к типу значения.
// Test<int> у = new Test<int>();
}
}
Обратите внимание на следующее объявление классаTest, class Test<T> where T : class {
Ограничение class требует, чтобы любой аргумент Т был ссылочного типа. В данном примере кода это необходимо для правильного выполнения операции присваивания в конструкторе класса Test.
public Test (){
// Следующий оператор допустим только потому, что // аргумент Т гарантированно относится к ссылочному // типу, что позволяет присваивать пустое значение, obj = null;
}
В этом фрагменте кода переменной obj типа Т присваивается пустое значение. Такое присваивание допустимо только для ссылочных типов. Как правило, пустое значение нельзя присвоить переменной типа значения. (Исключением из этого правила являетсяобнуляемый тип,который представляет собой специальный тип структуры, инкапсулирующий тип значения и допускающий пустое значение (null). Подробнее об этом — в главе 20.) Следовательно, в отсутствие ограничения такое присваивание было бы недопустимым, и код не подлежал бы компиляции. Это один из тех случаев, когда для обобщенного кода может оказаться очень важным различие между типами значений и ссылочными типами.
Ограничение типа значения является дополнением ограничения ссылочного типа. Оно просто гарантирует, что любой аргумент, обозначающий тип, должен быть типа значения, в том числе struct и enum. (В данном случае обнуляемый тип не относится к типу значения.) Ниже приведен пример наложения ограничения типа значения.
// Продемонстрировать наложение ограничения типа значения.
using System;
struct MyStruct {
//...
}
class MyClass {
// ...
}
class Test<T> where T : struct {
T obj;
public Test(T x) { obj = x;
}
// ...
}
class ValueConstraintDemo { static void Main() {
// Оба следующих объявления вполне допустимы.
Test<MyStruct> х = new Test<MyStruct>(new MyStruct ());
Test<int> у = new Test<int>(10);
//А следующее объявление недопустимо!
// Test<MyClass> z = new Test<MyClass>(new MyClass());
}
}
В этом примере кода классTestобъявляется следующим образом.
class Test<T> where Т : struct {
На параметр типаТв классеTestнакладывается ограничениеstruct,и поэтому к нему могут быть привязаны только аргументы типа значения. Это означает, что объявленияTest<MyStruct>,n Test<int>вполне допустимы, тогда как объявлениеTest<MyClass>недопустимо. Для того чтобы убедиться в этом, удалите символы комментария в начале последней строки приведенного выше кода и перекомпилируйте его. В итоге вы получите сообщение об ошибке во время компиляции.
Установление связи между двумя параметрами типа с помощью ограничения
Существует разновидность ограничения на базовый класс, позволяющая установить связь между двумя параметрами типа. В качестве примера рассмотрим следующее объявление обобщенного класса.
class Gen<T/ V> where V : T {
В этом объявлении операторwhereуведомляет компилятор о том, что аргумент типа, привязанный к параметру типа V, должен быть таким же, как и аргумент типа, привязанный к параметру типаТ,или же наследовать от него. Если подобная связь отсутствует при объявлении объекта типаGen,то во время компиляции возникнет ошибка. Такое ограничение на параметр типа называетсянеприкрытым ограничением типа.В приведенном ниже примере демонстрируется наложение этого ограничения.
// Установить связь между двумя параметрами типа.
using System;
class А {
//...
}
class В : А {
// ...
}
// Здесь параметр типа V должен наследовать от параметра типа Т. class Gen<T, V> where V : T {
// ...
}
class NakedConstraintDemo { static void Main() {
// Это объявление вполне допустимо, поскольку
// класс В наследует от класса А.
GerKA, В> х = new Gen<A, В> () ;
// А это объявление недопустимо, поскольку // класс А-.не наследует от класса В. .
// Gen<B, А> у = new Gen<B, А>();
}
}
Обратите внимание на то, что класс В наследует от класса А. Проанализируем далее оба объявления объектов класса Gen в методе Main (). Как следует из комментария к первому объявлению
Gen<A, В> х = new Gen<A, В> ();
оно вполне допустимо, поскольку класс В наследует от класса А. Но второе объявление
// Gen<B, А> у = new Gen<B, А>();
недопустимо, поскольку класс А не наследует от класса В.
Применение нескольких ограничений
С параметром типа может быть связано несколько ограничений. В этом случае ограничения указываются списком через запятую. В этом списке первым должно быть указано ограничение class либо struct, если оно присутствует, или же ограничение на базовый класс, если оно накладывается. Указывать ограничения class или struct одновременно с ограничением на базовый класс rte разрешается. Далее по списку должно следовать ограничение на интерфейс, а последним по порядку — ограничение new (). Например, следующее объявление считается вполне допустимым.
class Gen<T> where Т : MyClass, IMylnterface, new() {
// ...
В данном случае параметр типа Т должен быть заменен аргументом типа, наследующим от класса MyClass, реализующим интерфейс IMylnterface и использующим конструктор без параметра.
Если же в обобщении используются два или более параметра типа, то ограничения на каждый из них накладываются с помощью отдельного оператора where, как в приведенном ниже примере.
// Использовать несколько операторов where, using System;
// У класса Gen имеются два параметра типа, и на оба накладываются // ограничения с помощью отдельных операторов where, class Gen<T, V> where T : class
where V : struct {
T obi;
V ob2;
public Gen(T t, V v) { obi = t;
ob2 = v;
}
}
class MultipleConstraintDemo { static void Main() {
// Эта строка кода вполне допустима, поскольку // string — это ссылочный тип, a int — тип значения.
Gen<string, int> obj = new Gen<string, int>(nTecTM, 11);
//А следующая строка кода недопустима, поскольку // bool не относится к ссылочному типу.
// Gencbool, int> obj = new Gencbool, int>(true, 11);
}
}
В данном примере класс Gen принимает два аргумента с ограничениями, накладываемыми с помощью отдельных операторов where. Обратите особое внимание на объявление этого класса.
class GenCT, V> where T : class
where V : struct {
Как видите, один оператор where отделяется от другого только пробелом. Другие знаки препинания между ними не нужны и даже недопустимы.
Получение значения, присваиваемого параметру типа по умолчанию
Как упоминалось выше, при написании обобщенного кода иногда важно провести различие между типами значений и ссылочными типами. Такая потребность возникает, в частности, в том случае, если переменной параметра типа должно быть присвоено значение по умолчанию. Для ссылочных типов значением по умолчанию является null, для неструктурных типов значений — 0 или логическое значение false, если это тип bool, а для структур типа struct — объект соответствующей структуры с полями, установленными по умолчанию. В этой связи возникает вопрос: какое значение следует присваивать по умолчанию переменной параметра типа: null, 0 или нечто другое? Например, если в следующем объявлении класса Test:
class Test<T> {
Т obj;
П...
переменной obj требуется присвоить значение по умолчанию, то какой из двух вариантов
obj = null; // подходит только для ссылочных типовили
obj =0; // подходит только для числовых типов и // перечислений, но не для структур
следует выбрать? Для разрешения этой дилеммы можно воспользоваться еще одной формой оператора default, приведенной ниже.
default(тип)
Эта форма оператора default пригодна для всех аргументов типа, будь то типы значений или ссылочные типы.
Ниже приведен короткий пример, демонстрирующий данную форму оператора
default.
// Продемонстрировать форму оператора default.
using System;
class MyClass {
//...
}
// Получить значение, присваиваемое параметру типа Т по умолчанию, class Test<T> { public Т obj;
public Test() {
// Следующий оператор годится только для ссылочных типов.
// obj = null; //не годится
// Следующий оператор годится только для типов значений.
// obj = 0; // не годится
II Аэтот оператор годится как для ссылочных типов,
// так и для типов значений, obj = default(Т); // Годится!
}
II ...
}
class DefaultDemo { static void Main() {
// Сконструировать объект класса Test, используя ссылочный тип.
Test<MyClass> х = new Test<MyClass> () ; 11
if(x.obj == null)
Console.WriteLine("Переменная x.obj имеет пустое значение <null>.");
// Сконструировать объект класса Test, используя тип значения.
Test<int> у = new Test<int>();
if(у.obj == 0)
Console.WriteLine("Переменная у.obj имеет значение 0.");
}
}
Вот к какому результату приводит выполнение этого кода.
Переменная x.obj имеет пустое значение <null>.
Переменная у.obj имеет значение 0.
Обобщенные структуры
В C# разрешается создавать обобщенные структуры. Синтаксис для них такой же, как и для обобщенных классов. В качестве примера ниже приведена программа, в которой создается обобщенная структура XY для хранения координат X, Y.
// Продемонстрировать применение обобщенной структуры, using System;
// Эта структура является обобщенной, struct XY<T> {
Т х;
Т у;
public XY(Т а, Т Ь) { х = а;
У = Ь;
}
public Т X {
get { return х; } set { х = value; }
}
public T Y {
get { return y; } set { у = value; }
}
}
class StructTest { static void Main() {
XY<int> xy = new XY<int>(10, 20);
XY<double> xy2 = new XY<double>(88.0, 99.0);
Console.WriteLine(xy.X + ", " + xy.Y);
Console.WriteLine(xy2.X + ", " + xy2.Y);
}
}
При выполнении этой программы получается следующий результат.
10, 2088, 99
Как и на обобщенные классы, на обобщенные структуры могут накладываться ограничения. Например, на аргументы типа в приведенном ниже варианте структуры XY накладывается ограничение типа значения.
struct XY<T> where Т : struct {
// ...
Создание обобщенного метода
Как следует из приведенных выше примеров, в методах, объявляемых в обобщенных классах, может использоваться параметр типа из данного класса, а следовательно, такие методы автоматически становятся обобщенными по отношению к параметру типа. Но помимо этого имеется возможность объявить обобщенный метод со своими собственными параметрами типа и даже создать обобщенный метод, заключенный в необобщенном классе.
Рассмотрим для начала простой пример. В приведенной ниже программе объявляется необобщенный класс ArrayUtils, а в нем — статический обобщенный метод Copy Insert (). Этот метод копирует содержимое одного массива в другой, вводя по ходу дела новый элемент в указанном месте. Метод CopylnsertO можно использовать вместе с массивами любого типа.
// Продемонстрировать применение обобщенного метода, using System;
// Класс обработки массивов. Этот класс не является обобщенным, class ArrayUtils {
// Копировать массив, вводя по ходу дела новый элемент.
// Этот метод является обобщенным.
public static bool CopyInsert<T> (Т e, uint idx,
T[] src, T[] target) {
// Проверить, насколько велик массив, if(target.Length < src.Length+1) return false;
// Скопировать содержимое массива src в целевой массив,
// попутно введя значение е по- индексу idx. for(int i=0, j=0; i < src.Length; i++, j++) { if(i == idx) { target[j] = e; j++;
}
target[j] = src[i];
}
return true;
}
}
class GenMethDemo { static void Main() {
int[] nums = { 1, 2, 3 }; int[] nums2 = new int [4];
// Вывести содержимое массива nums.
Console.Write("Содержимое массива nums: ") ; foreach(int x in nums)
Console.Write(х + " ") ;
Console.WriteLine() ;
// Обработать массив типа int.
ArrayUtils.Copylnsert(99, 2, nums, nums2);
// Вывести содержимое массива nums2.
Console.Write("Содержимое массива nums2: "); foreach(int x in nums2)
Console.Write(x + " ") ;
Console.WriteLine();
//А теперь обработать массив строк, используя метод copylnsert. string[] strs = {"Обобщения", "весьма", "эффективны."}; string[] strs2 = new string[4];
// Вывести содержимое массива strs.
Console.Write("Содержимое массива strs: "); foreach(string s in strs)
Console.Write(s + " ");
Console.WriteLine();
// Ввести элемент в массив строк.
ArrayUtils.Copylnsert("в С#", 1, strs, strs2);
// Вывести содержимое массива strs2.
Console.Write("Содержимое массива strs2: "); foreach(string s in strs2)
Console.Write(s + " ");
Console.WriteLine();
// Этот вызов недопустим, поскольку первый аргумент // относится к типу double, а третий и четвертый // аргументы обозначают элементы массивов типа int.
// ArrayUtils.Copylnsert(0.01, 2, nums, nums2);
}
}
Вот к какому результату приводит выполнение этой программы.
Содержимое массива nums: 1 2 3 Содержимое массива nums2: 1 2 99.3
Содержимое массива strs: Обобщения весьма эффективны.
Содержимое массива strs2: Обобщения в C# весьма эффективны.
Внимательно проанализируем метод CopyInsert(). Прежде всего обратите внимание на объявление этого метода в следующей строке кода.
public static bool CopyInsert<T>(Т e, uint idx,
T[] src, T[] target) {
Параметр типа объявляетсяпослеимени метода, нопередсписком его параметров. Обратите также внимание на то, что методCopylnsert() является статическим, что позволяет вызывать его независимо от любого объекта. Следует, однако, иметь в виду, что обобщенные методы могут быть либо статическими, либо нестатическими. В этом отношении для тшх не существует никаких ограничений.
Далее обратите внимание на то, что методCopylnsert() вызывается в методеMain() с помощью обычного синтаксиса и без указания аргументов типа. Дело в том, что типы аргументов различаются автоматически, а типТсоответственно подстраивается. Этот процесс называетсявыводимостью типов.Например, в первом вызове данного метода
ArrayUtils.Copylnsert (99,2,nums, nums2);
типTстановится типомint,поскольку числовое значение99и элементы массивовnumsиnum&2относятся к типуint.А во втором вызове данного метода используются строковые типы, и поэтому типТзаменяется типомstring.
А теперь обратите внимание на приведенную ниже закомментированную строку кода.
// ArrayUtils.Copylnsert(0.01, 2, nums, nums2);
Если удалить символы комментария в начале этой строки кода и затем попытаться перекомпилировать программу, то будет получено сообщение об ошибке. Дело в том, что первый аргумент в данном вызове методаCopylnsert() относится к типуdouble,а третий и четвертый аргументы обозначают элементы массивовnumsиnums2типаint.Но все эти аргументы типа должны заменить один и тот же параметр типаТ,а это приведет к несоответствию типов и, как следствие, к ошибке во время компиляции. Подобная возможность соблюдать типовую безопасность относится к одним из самых главных преимуществ обобщенных методов.
Синтаксис объявления методаCopylnsert() может быть обобщен. Ниже приведена общая форма объявления обобщенного метода.
возвраща емый_типимя_метода<список_параметров_типа> (список_параметров) {// ...
В любом случаесписок_параметров_типаобозначает разделяемый запятой список параметров типа. Обратите внимание на то, что в объявлении обобщенного метода список параметров типа следуетпослеимени метода.
Вызов обобщенного метода с явно указанными аргументами типа
В большинстве случаев неявной выводимости типов оказывается достаточно для вызова обобщенного метода, тем не менее аргументы типа могут быть указаны явным образом. Для этого достаточно указать аргументы типа после имени метода при его вызове. В качестве примера ниже приведена строка кода, в которой методCopylnsert() вызывается с явно указываемым аргументом типаstring.
ArrayUtils.CopyInsert<string>("В С#", 1, strs, strs2);
Тип передаваемых аргументов необходимо указывать явно в том случае, если компилятор не сможет вывести тип параметраТили если требуется отменить выводимость типов.
Применение ограничений в обобщенных методах
На аргументы обобщенного метода можно наложить ограничения, указав их после списка параметров. В качестве примера ниже приведен вариант методаCopylnsert ()для обработки данных только ссылочных типов.
public static bool CopyInsert<T>(Т e, uint idx,
T[] src, T[] target) where T : class {
Если попробовать применить этот вариант в предыдущем примере программы обработки массивов, то приведенный ниже вызов методаCopylnsert( •) не будет скомпилирован, посколькуintявляется типом значения, а не ссьглочным типом.
// Теперь неправильно, поскольку параметр Т должен быть ссылочного типа! ArrayUtils.Copylnsert(99, 2, nums, nums2); // Теперь недопустимо!
Обобщенные делегаты
Как и методы, делегаты также могут быть обобщенными. Ниже приведена общая форма объявления обобщенного делегата.
delegateвозвратцаемый_тип имя_делегата<список_параметров_типа> (список_аргументов);
Обратите внимание на расположение списка параметров типа. Он следует непосредственно после имени делегата. Преимущество обобщенных делегатов заключается в том, что их допускается определять в типизированной обобщенной форме, которую можно затем согласовать с любым совместимым методом.
В приведенном ниже примере программы демонстрируется применение делегатаSomeOpс одним параметром типаТ.Этот делегат возвращает значение типаТи принимает аргумент типаТ. '
// Простой пример обобщенного делегата, using System;
// Объявить обобщенный делегат, delegate Т Some0p<T>(T v);
class GenDelegateDemo {
// Возвратить результат суммирования аргумента, static int Sum(int v) { int result = 0; for(int i=v; i>0; i—) result += i;
return result;
}
// Возвратить строку, содержащую обратное значение аргумента, static string Reflect(string str) { string result =
foreach(char ch in str)
result = ch + result; return result;
}
static- void Main() {
// Сконструировать делегат типа int.
SomeOp<int> intDel = Sum;
Console.WriteLine(intDel(3));
// Сконструировать делегат типа string.
SomeOp<string> strDel = Reflect;
Console.WriteLine(strDel("Привет")) ;
}
}
Эта программа дает следующий результат.
6
тевирП
Рассмотрим эту программу более подробно. Прежде всего обратите внимание на следующее объявление делегатаSomeOp.
delegate Т SomeOp<T>(T v);
Как видите, типТможет служить в качестве возвращаемого типа, несмотря на то, что параметр типаТуказывается после имени делегатаSomeOp.
Далее в классеGenDelegateDemoобъявляются методыSum () иReflect (), как показано ниже.
static int Sum(int v) {
static string Reflect(string str) {
МетодSum() возвращает результат суммирования целого значения, передаваемого в качестве аргумента, а методReflect () — символьную строку, которая получается обращенной по отношению к строке, передаваемой в качестве аргумента.
В методеMain() создается экземплярintDelделегата, которому присваивается ссылка на методSum ().
SomeOp<int> intDel = Sum;
МетодSum() принимает аргумент типаintи возвращает значение типаint,поэтому он совместим с целочисленным экземпляром делегатаSomeOp.
Аналогичным образом создается экземплярstrDelделегата, которому присваивается ссылка на методReflect ().
SomeOp<string> strDel = Reflect;
МетодReflect() принимает аргумент типаstringи возвращает результат типаstring,поэтому он совместим со строковым экземпляром делегатаSomeOp.
В силу присущей обобщениям типовой безопасности обобщенным делегатам нельзя присваивать несовместимые методы. Так, следующая строка кода оказалась бы ошибочной в рассматриваемой здесь программе.
Ведь метод Reflect () принимает аргумент типа string и возвращает результат типа string, а следовательно, он несовместим с целочисленным экземпляром делегата SomeOp.
Обобщенные интерфейсы
Помимо обобщенных классов и методов, в C# допускаются обобщенные интерфейсы. Такие интерфейсы указываются аналогично обобщенным классам. Ниже приведен измененный вариант примера из главы 12, демонстрирующего интерфейс ISeries. (Напомним, что ISeries является интерфейсом для класса, генерирующего последовательный ряд числовых значений.) Тип данных, которым оперирует этот интерфейс, теперь определяется параметром типа.
// Продемонстрировать применение обобщенного интерфейса, using System;
public interface ISeries<T> {
T GetNext();// возвратить следующее по порядку число
void Reset(); // генерировать ряд последовательных чисел с самого начала void SetStart(T v); // задать начальное значение
}
//Реализовать интерфейс ISeries, class ByTwos<T> : ISeries<T> {
T start;
T val;
// Этот делегат определяет форму метода, вызываемого для генерирования // очередного элемента в ряду последовательных значений, public delegate Т IncByTwo(Т v);
// Этой ссылке на делегат будет присвоен метод,
// передаваемый конструктору класса ByTwos.
IncByTwo incr;
public ByTwos(IncByTwo incrMeth) { start = default(T); val = default(T); incr = incrMeth;
}
public T GetNext() { val = incr(val); return val;
}
public void Reset() {
val = start;
}
public void SetStart(T v) { start = v; val = start;
}
}
class ThreeD {
public int x/ч у, z;
public ThreeD(int a, int b, int c) { x = a;
У = b;
z = c;
}
}
class GenlntfDemo {
// Определить метод увеличения на два каждого // последующего значения типа int. static int IntPlusTwo(int v) { return v + 2;
}
// Определить метод увеличения на два каждого // последующего значения типа double, static double DoublePlusTwo(double v) { return v + 2.0;
}
// Определить метод увеличения на два каждого // последующего значения координат объекта типа ThreeD. static ThreeD ThreeDPlusTwo(ThreeD v) { if(v==null) return new ThreeD(0, 0, 0) ; else return new ThreeD(v.x + 2, v.y +2,v.z + 2);
}
static void Main() {
// Продемонстрировать генерирование // последовательного ряда значений типа int. ByTwos<int> intBT = new ByTwos<int>(IntPlusTwo);
for(int i=0; i < 5; i++)
Console.Write(intBT.GetNext() + " ") ;
Console.WriteLine();
// Продемонстрировать генерирование // последовательного ряда значений типа double. ByTwos<double> dblBT =
new ByTwos<double>(DoublePlusTwo); dblBT.SetStart(11.4);
for(int i=0; i < 5; i++)
Console.Write(dblBT.GetNext() + " ");
Console.WriteLine();
// Продемонстрировать генерирование последовательного ряда // значений координат объекта типа ThreeD.
ByTwos<ThreeD> ThrDBT = new ByTwos<ThreeD>(ThreeDPlusTwo);
ThreeD coord;
coord.z + " ");
}
Console.WriteLine();
}
}
Этот код выдает следующий результат.
2 4 6 8 10
13.4 15.4 17.4 19.4 21.4 0, 0, 0 2,2,2 4, 4, 4 6, 6, 6 8, 8,8
В данном примере кода имеется ряд любопытных моментов. Прежде всего обратите внимание на объявление интерфейсаISeriesв следующей строке кода.
public interface ISeries<T> {
Как упоминалось выше, для объявления обобщенного интерфейса используется такой же синтаксис, что и для объявления обобщенного класса.
А теперь обратите внимание на следующее объявление классаByTwos,реализующего интерфейсI series.
class ByTwos<T> : ISeries<T> {
Параметр типаТуказывается не только при объявлении классаByTwos,но и при объявлении интерфейсаISeries.И это очень важно. Ведь класс, реализующий обобщенный вариант интерфейса, сам должен быть обобщенным. Так, приведенное ниже объявление недопустимо, поскольку параметр типаТне определен.
class ByTwos : ISeries<T> { // Неверно!
Аргумент типа, требующийся для интерфейсаISeries,должен быть передан классуByTwos.В противном случае интерфейс никак не сможет получить аргумент типа.
Далее переменные, хранящие текущее значение в последовательном ряду(val)и его начальное значение(start),объявляются как объекты обобщенного типаТ.После этого объявляется делегатIncByTwo.Этот делегат определяет форму метода, используемого для увеличения на два значения, хранящегося в объекте типаТ.Для того чтобы в классеByTwosмогли обрабатываться данные любого типа, необходимо каким-то образом определить порядок увеличения на два значения каждого типа данных. Для этого конструктору классаByTwosпередается ссылка на метод, выполняющий увеличение на два. Эта ссылка хранится в переменной экземпляра делегатаincr.Когда требуется сгенерировать следующий элемент в последовательном ряду, этот метод вызывается с помощью делегатаincr.
А теперь обратите внимание на классThreeD.В этом классе инкапсулируются координаты трехмерного пространства (X,Z,Y). Его назначение — продемонстрировать обработку данных типа класса в классеByTwos.
Далее в классеGenlntf Demoобъявляются три метода увеличения на два для объектов типаint, doubleиThreeD.Все эти методы передаются конструктору классаByTwosпри создании объектов соответствующих типов. Обратите особое внимание на приведенный ниже методThreeDPlusTwo ().
// Определить метод увеличения на два каждого // последующего значения координат объекта типа ThreeD. static ThreeD ThreeDPlusTwo(ThreeD v) { if(v==null) return new ThreeD(0, 0, 0); else return new ThreeD(v.x + 2, v.y +2,v.z + 2);
}
В этом методе сначала проверяется, содержит ли переменная экземпляраvпустое значение(null).Если она содержит это значение, то метод возвращает новый объект типаThreeDсо всеми обнуленными полями координат. Ведь дело в том, что переменнойvпо умолчанию присваивается значение типаdefault (Т)в конструкторе классаByTwos.Это значение оказывается по умолчанию нулевым для типов значений и пустым для типов ссылок на объекты. Поэтому если предварительно не был вызван методSetStart(),TOперед первым увеличением на два переменнаяvбудет содержать пустое значение вместо ссылки на объект. Это означает, что для первого увеличения на два требуется новый объект.
На параметр типа в обобщенном интерфейсе могут накладываться ограничения таким же образом, как и в обобщенном классе. В качестве примера ниже приведен вариант объявления интерфейсаISeriesс ограничением на использование только ссылочных типов.
public interface ISeries<T> where T : class {
Если реализуется именно такой вариант интерфейсаISeries,в реализующем его классе следует указать то же самое ограничение на параметр типаТ,как показано ниже.
class ByTwos<T> : ISeries<T> where T : class {
В силу ограничения ссылочного типа этот вариант интерфейсаISeriesнельзя применять к типам значений. Поэтому если реализовать его в рассматриваемом здесь примере программы, то допустимым окажется только объявлениеByTwos<ThreeD>,но не объявленияByTwos<int>иByTwos<double>.
Сравнение экземпляров параметра типа
Иногда возникает потребность сравнить два экземпляра параметра типа. Допустим, что требуется написать обобщенный методIs In (), возвращающий логическое значениеtrue,если в массиве содержится некоторое значение. Для этой цели сначала можно попробовать сделать следующее.
// Не годится!
public static bool IsIn<T>(T what, T[] obs) { foreach(T v in obs)
if(v == what) // Ошибка! return true;
return false;
}
К сожалению, эта попытка не пройдет. Ведь параметрТотносится к обобщенному типу, и поэтому компилятору не удастся выяснить, как сравнивать два объекта. Требуется ли для этого поразрядное сравнение или же только сравнение отдельных полей? А возможно, сравнение ссылок? Вряд ли компилятор сможет найти ответы на эти вопросы. Правда, из этого положения все же имеется выход.
Для сравнения двух объектов параметра обобщенного типа они должны реализовывать интерфейсIComparableилиIComparable<T>и/или интерфейсIEquatable<T>.В обоих вариантах интерфейсаIComparableдля этой цели определен методСошрагеТо (), а в интерфейсеIEquatable<T>— методEquals (). Разновидности интерфейсаIComparableпредназначены для применения в тех случаях, когда требуется определить относительный порядок следования двух объектов. А интерфейсIE qua tableслужит для определения равенства двух объектов. Все эти интерфейсы определены в пространстве именSystemи реализованы во встроенных в C# типах данных, включаяint, stringиdouble.Но их нетрудно реализовать и для собственных создаваемых классов. Итак, начнем с обобщенного интерфейсаIEquatable<T>.ИнтерфейсIEquatable<T>объявляется следующим образом.
public interface IEquatable<T>
Сравниваемый тип данных передается ему в качестве аргумента типаТ.В этом интерфейсе определяется методEquals (), как показано ниже.
bool Equals(Тother)
В этом методе сравниваются вызывающий объект и другой объект, определяемый параметромother.В итоге возвращается логическое значениеtrue,если оба объекта равны, а иначе — логическое значениеfalse.
В ходе реализации интерфейсаIEquatable<T>обычно требуется также переопределять методыGetHashCode() иEquals (Object),определенные в классеObject,чтобы они оказались совместимыми с конкретной реализацией методаEquals ().Ниже приведен пример программы, в которой демонстрируется исправленный вариант упоминавшегося ранее методаIs In ().
// Требуется обобщенный интерфейс IEquatable<T>.
public static bool IsIn<T>(T what, T[] obs) where T : IEquatable<T> { foreach(T v in obs)
if(v.Equals(what)) // Применяется метод Equals(). return true;
return false;
}
Обратите внимание в приведенном выше примере на применение следующега ограничения.
where Т : IEquatable<T>
Это ограничение гарантирует, что только те типы, в которых реализован интерфейсIEquatable,являются действительными аргументами типа для методаIs In (). Внутри этого метода применяется методEquals (), который определяет равенство одного объекта другому.
Для определения относительного порядка следования двух элементов применяется интерфейсI Comp а г able.У этого интерфейса имеются две формы: обобщенная и необобщенная. Обобщенная форма данного интерфейса обладает преимуществом обеспечения типовой безопасности, и поэтому мы рассмотрим здесь именно ее. Обобщенный интерфейсIComparable<T>объявляется следующим образом.
public interface IComparable<T>
Сравниваемый тип данных передается ему в качестве аргумента типаТ.В этом интерфейсе определяется методCompareTo (), как показано ниже.
int CompareTo(Тother)
В этом методе сравниваются вызывающий объект и другой объект, определяемый параметромother.В итоге возвращается нуль, если вызывающий объект оказывается больше, чем объектother; и отрицательное значение, если вызывающий объект оказывается меньше, чем объектother.
Для того чтобы воспользоваться методомCompareTo (), необходимо указать ограничение, которое требуется наложить на аргумент типа для реализации обобщенного интерфейсаIComparable<T>.А затем достаточно вызвать методCompareTo(), чтобы сравнить два экземпляра параметра типа.
Ниже приведен пример применения обобщенного интерфейсаIComparable<T>.В этом примере вызывается методInRange (), возвращающий логическое значениеtrue,если объект оказывается среди элементов отсортированного массива.
// Требуется обобщенный интерфейс IComparable<T>. В данном методе // предполагается, что массив отсортирован. Он возвращает логическое // значение true, если значение параметра what оказывается среди элементов // массива, передаваемых параметру obs.
public static bool InRange<T>(T what, T[] obs) where T : IComparable<T> { if(what.CompareTo(obs[0]) < 0 ||
what.CompareTo(obs[obs.Length-1]) > 0) return false; return true;
}
В приведенном ниже примере программы демонстрируется применение обоих методовIs In() иInRange() на практике.
// Продемонстрировать применение обобщенных // интерфейсов IComparable<T> и IEquatable<T>.
using System;
// Теперь в классе MyClass реализуются обобщенные // интерфейсы IComparable<T> и IEquatable<T>. class MyClass : IComparable<MyClass>, IEquatable<MyClass> { public int Val;
public MyClass(int x) { Val = x; }
// Реализовать обобщенный интерфейс IComparable<T>. public int CompareTo(MyClass other) {
return Val - other.Val; // Now, no cast is needed.
> \
// Реализовать обобщенный интерфейс IEquatable<T>.
public bool Equals(MyClass other) { return Val == other.Val;
}
// Переопределить метод Equals(Object).
public override bool Equals(Object obj) { if(obj is MyClass)
return Equals((MyClass) obj); return false;
}
// Переопределить метод GetHashCode().
public override int GetHashCode() { return Val.GetHashCode() ;
}
}
class CompareDemo {
// Требуется обобщенный интерфейс IEquatable<T>.
public static bool IsIn<T>(T what, T[] obs) where T : IEquatable<T> { foreach(T v in obs)
if(v.Equals(what)) // Применяется метод Equals() return true;
return false;
}
// Требуется обобщенный интерфейс IComparable<T>. В данном методе // предполагается, что массив отсортирован. Он возвращает логическое // значение true, если значение параметра what оказывается среди элементов // массива, передаваемых параметру obs.
public static bool InRange<T>(T what, T[] obs) where T : IComparable<T> { if(what.CompareTo(obs[0]) < 0 ||
what.CompareTo(ob§[obs.Length-1]) > 0) return false; return true;
}
// Продемонстрировать операции сравнения, static void Main() {
// Применить метод Isln() к данным типа int. int[] nums = { 1, 2, 3, 4, 5 };
if(IsIn(2, nums))
Console.WriteLine("Найдено значение 2.");
if(Isln(99, nums))
Console.WriteLine("He подлежит выводу.");
// Применить метод Isln() к объектам класса MyClass.
MyClass[] mcs = { new MyClass(1), new MyClass(2),
new MyClass(3), new MyClass(4) };
if(lsln(new MyClass(3), mcs))
Console.WriteLine("Найден объект MyClass(3).");
if(Isln(new MyClass(99), mcs))
Console.WriteLine("He подлежит выводу.");
// Применить метод InRange() к данным типа int. if(InRange(2, nums))
Console.WriteLine("Значение 2 находится в границах массива nums."); if(InRange(1, nums))
Console.WriteLine("Значение 1 находится в границах массива nums."); if(InRange(5, nums))
Console.WriteLine("Значение 5 находится в границах массива nums."); if(!InRange(0, nums))
Console.WriteLine("Значение О HE находится в границах массива nums."); if(!InRange(6, nums))
Console.WriteLine("Значение 6 HE находится в границах массива nums.");
// Применить метод InRange() к объектам класса MyClass.. if(InRange(new MyClass(2), mcs))
Console.WriteLine("Объект MyClass(2) находится в границах массива nums."); if(InRange(new MyClass(1), mcs))
Console.WriteLine("Объект MyClass(1) находится " +
"в границах массива nums."); if(InRange(new MyClass(4), mcs))
Console.WriteLine("Объект MyClass(4) находится " +
"в границах массива nums."); if(!InRange(new MyClass(0), mcs))
Console.WriteLine("Объект MyClass(0) HE " +
"находится в границах массива nums."); if(!InRange(new MyClass(5), mcs))
Console.WriteLine("Объект MyClass(5) HE " +
"находится в границах массива nums.");
}
}
Выполнение этой программы приводит к следующему результату.
Найдено значение 2.
Найден объект MyClass (3) .
Значение 2 находится в границах массива nums.
Значение 1 находится в границах массива nums.
Значение 5 находится в границах массива nums.
Значение 0 НЕ находится в границах массива nums
Значение 6 НЕ находится в границах массива nums
Объект MyClass(2) находится в границах массива nums.
Объект MyClass(1) находится в границах массива nums.
Объект MyClass(4) находится в границах массива nums.
Объект MyClass(0) НЕ находится в границах массива nums.
Объект MyClass(5) НЕ находится в границах массива nums.
ПРИМЕЧАНИЕ
Если параметр типа обозначает ссылку или ограничение на базовый класс, то к экземплярам объектов, определяемых таким параметром типа, можно применять операторы == и ! =, хотя они проверяют на равенство только ссылки. А для сравнения значений придется реализовать интерфейс IComparable или же обобщенные интерфейсы IComparable<T> и IEquatable<T>.
Иерархии обобщенных классов
Обобщенные классы могут входить в иерархию классов аналогично необобщенным классам. Следовательно, обобщенный класс может действовать как базовый или производный класс. Главное отличие между иерархиями обобщенных и необобщенных классов заключается в том, что в первом случае аргументы типа, необходимые обобщенному базовому классу, должны передаваться всеми производными классами вверх по иерархии аналогично передаче аргументов конструктора.
Применение обобщенного базового класса
Ниже приведен простой пример иерархии, в которой используется обобщенный базовый класс.
// Простая иерархия обобщенных классов, using System;
// Обобщенный базовый класс, class Gen<T> {
Т ob;
public Gen(Т о) { ob = о;
}
// Возвратить значение переменной ob. public Т GetOb() { return ob;
}
}
// Класс, производный от класса Gen. class Gen2<T> : Gen<T> {
public Gen2(T o) : base(o) {
II ...
}
1
class GenHierDemo { static void Main() {
Gen2<string> g2 = new Gen2<string>("Привет") ;
Console.WriteLine(g2.GetOb());
В этой иерархии классGen2 наследует от обобщенного классаGen.Обратите внимание на объявление классаGen2 в следующей строке кода.
class Gen2<T> : Gen<T> {
Параметр типаТуказывается в объявлении классаGen2и в то же время передается классуGen.Это означает, что любой тип, передаваемый классуGen2,будет передаваться также классуGen.Например, в следующем объявлении:
Gen2<string> g2 = new Gen2<string>("Привет");
параметр типаstringпередается классуGen.Поэтому переменнаяobв той части классаGen2,которая относится к классуGen,будет иметь типstring.
Обратите также внимание на то, что в классеGen2параметр типаТне используется, а только передается вверх по иерархии базовому классуGen.Это означает, что в производном классе следует непременно указывать параметры типа, требующиеся его обобщенному базовому классу, даже если этот производный класс не обязательно должен быть обобщенным.
Разумеется, в производный класс можно свободно добавлять его собственные параметры типа, если в этом есть потребность. В качестве примера ниже приведен вариант предыдущей иерархии классов, где в классGen2добавлен собственный параметр типа.
// Пример добавления собственных параметров типа в производный класс, using System;
// Обобщенный базовый класс, class Gen<T> {
Т ob; // объявить переменную типа Т
// Передать конструктору ссылку типа Т. public Gen(T о) { ob = о;
}
// Возвратить значение переменной ob. public Т GetOb () {
return ob;
}
}
// Класс, производный от класса Gen. В этом классе // определяется второй параметр типа V. class Gen2<T, V> : Gen<T> {
V ob2;
public Gen2(T о, V o2) : base (o) {
ob2 = o2;
}
public V Get0bj2() { return ob2;
' }
}
11Создать объект класса Gen2. class GenHierDemo2 { static void Main() {
// Создать объект класса Gen2 с параметрами // типа string и int.
Gen2<string, in.t> x =
new Gen2<string, int>("Значение равно: ", 99);
Console.Write(x.GetOb());
Console.WriteLine(x.GetObj2());
}
}
Обратите внимание на приведенное ниже объявление классаGen2в данном варианте иерархии классов.
class Gen2<T, V> : Gen<T> {
В этом объявленииТ— это тип, передаваемый базовому классуGen;аV— тип, характерный только для производного классаGen2.Он служит для объявления объектаоЬ2и в качестве типа, возвращаемого методомGetOb j 2(). В методеMain() создается объект классаGen2с параметромТтипаstringи параметромVтипаint.Поэтому код из приведенного выше примера дает следующий результат.
Значение равно: 99
Обобщенный производный класс
Необобщенный класс может быть вполне.законно базовым для обобщенного производного класса. В качестве примера рассмотрим следующую программу.
// Пример необобщенного класса в качестве базового для // обобщенного производного класса.
using System;
// Необобщенный базовый класс, class NonGen { int num;
public NonGen(int i) { num = i;
}
public int GetNum() { return num;
}
}
// Обобщенный производный класс, class Gen<T> : NonGen {
T ob;
public Gen(T о, int i) : base (i) {
ob = o;
}
// Возвратить значение переменной ob. public T GetOb() { return ob;
}
}
// Создать объект класса Gen. class HierDemo3 {
static void Main() {
// Создать объект класса Gen с параметром типа string.
Gen<String> w = new Gen<String>("Привет", 47);
Console.Write(w.GetOb() + " ");
Console.WriteLine(w.GetNum());
}
}
Эта программа дает следующий результат.
Привет 47
В данной программе обратите внимание на то, как классGenнаследует от классаNonGenв следующем объявлении.
class Gen<T> : NonGen {
КлассNonGenне является обобщенным, и поэтому аргумент типа для него не указывается. Это означает, что параметрТ,указываемый в объявлении обобщенного производного классаGen,не требуется для указания базового классаNonGenи даже не может в нем использоваться. Следовательно, классGenнаследует от классаNonGenобычным образом, т.е. без выполнения каких-то особых условий.
Переопределение виртуальных методов в обобщенном классе
В обобщенном классе виртуальный метод может быть переопределен таким же образом, как и любой другой метод. В качестве примера рассмотрим следующую программу, в которой переопределяется виртуальный методGetOb ().
// Пример переопределения виртуального метода в обобщенном классе, using System;
// Обобщенный базовый класс, class 'Gen<T> { protected Т ob;
public Gen(T о) { ob = о;
}
// Возвратить значение переменной ob. Этот метод является виртуальным.
public virtual T GetOb () {
Console.Write("Метод GetOb () из класса Gen" + " возвращает результат: "); return ob;
}
}
// Класс, производный от класса Gen. В этом классе // переопределяется метод GetOb (). class Gen2<T> : Gen<T> {
public Gen2 (T o) : base(o) { }
// Переопределить метод GetOb(). public override T GetOb() {
Console.Write("Метод GetOb() из класса Gen2" + " возвращает результат: ") ; return ob;
}
}
// Продемонстрировать переопределение метода в обобщенном классе, class OverrideDemo { static void Main() {
// Создать объект класса Gen с параметром типа int.
Gen<int> iOb = new Gen<int>(88);
// Здесь вызывается вариант метода GetOb() из класса Gen.
Console.WriteLine(iOb.GetOb());
//А теперь создать объект класса Gen2 и присвоить // ссылку на него переменной iOb типа Gen<int>. iOb = new Gen2<int> (99);
// Здесь вызывается вариант метода GetOb() из класса Gen2.
Console.WriteLine(iOb.GetOb());
}
}
Ниже приведен результат выполнения этой программы.
Метод GetOb() из класса Gen возвращает результат: 88 Метод GetOb() из класса Gen2 возвращает результат: 99
Как следует из результата выполнения приведенной выше программы, переопределяемый вариант метода GetOb () вызывается для объекта типа Gen2, а его вариант из базового класса вызывается для объекта типа Gen.
Обратите внимание на следующую строку кода.
iOb = new Gen2<int>(99);
Такое присваивание вполне допустимо, поскольку iOb является переменной типа Gen<int>. Следовательно, она может ссылаться на любой объект типа Gen<int> или же объект класса, производного от Gen<int>, включая и Gen2<int>. Разумеется, переменную iOb нельзя использовать, например, для ссылки на объект типа Gen2<int>, поскольку это может привести к несоответствию типов.
Перегрузка методов с несколькими параметрами типа
Методы, параметры которых объявляются с помощью параметров типа, могут быть перегружены. Но правила их перегрузки упрощаются по сравнению с методами без параметров типа. Как правило, метод, в котором параметр типа служит для указания типа данных параметра этого метода, может быть перегружен при условии, что сигнатуры обоих его вариантов отличаются. Это означает, что оба варианта перегружаемого метода должны отличаться по типу или количеству их параметров. Но типовые различия должны определяться не по параметру обобщенного типа, а исходя из аргумента типа, подставляемого вместо параметра типа при конструировании объекта этого типа. Следовательно, метод с параметрами типа может быть перегружен таким образом, что он окажется пригодным не для всех возможных случаев, хотя и будет выглядеть верно.
В качестве примера рассмотрим следующий обобщенный класс.
// Пример неоднозначности, к которой может привести // перегрузка методов с параметрами типа.
//
// Этот код не подлежит компиляции, using System;
// Обобщенный класс, содержащий метод Set(), перегрузка // которого может привести к неоднозначности, class Gen<T, V> {
Т obi;
V ob2 ;
// ...
// В некоторых случаях эти два метода не будут // отличаться своими параметрами типа, public void Set(T о) { obi = о;
}
public void Set(V о) {fob2 = o;
}
}
class AmbiguityDemo { static void Main() {
Gen<int, double> ok = new Gencint, double>();
Gen<int, int> notOK = new Gencint, int>();
ok.Set(10); // верно, поскольку аргументы типа отличаются
notOK.Set(10); // неоднозначно, поскольку аргументы ничем не отличаются!
}
}
Рассмотрим приведенный выше код более подробно. Прежде всего обратите внимание на то, что классGenобъявляется с двумя параметрами типа:ТиV.В классеGenметодSet() перегружается по параметрам типаТиV,как показано ниже.
public void Set (T о) { obi = о;
}
public void Set(V o) { ob2 = o;
}
Такой подход кажется вполне обоснованным, поскольку типыТиVничем внешне не отличаются. Но подобная перегрузка таит в себе потенциальную неоднозначность.
При таком объявлении классаGenне соблюдается никаких требований к различению типовТиV.Например, нет ничего принципиально неправильного в том, что объект классаGenбудет сконструирован так, как показано ниже.
Gencint, int> notOK = new Gencint, int>();
В данном случае оба типа,ТиV,заменяются типомint.В итоге оба варианта методаSet() оказываются совершенно одинаковыми, что, разумеется, приводит к ошибке. Следовательно, при последующей попытке вызвать методSet() для объектаnotOKв методеMain() появится сообщение об ошибке вследствие неоднозначности во время компиляции.
Как правило, методы с параметрами типа перегружаются при условии, что объект конструируемого типа не приводит к конфликту. Следует, однако, иметь в виду, что ограничения на типы не учитываются при разрешении конфликтов, возникающих при перегрузке методов. Поэтому ограничения на типы нельзя использовать для исключения неоднозначности. Конструкторы, операторы и индексаторы с параметрами типа могут быть перегружены аналогично конструкторам по тем же самым правилам.
Ковариантность и контравариантность в параметрах обобщенного типа
В главе 15 ковариантность и контравариантность были рассмотрены в связи с необобщенными делегатами. Эта форма ковариантности и контравариантности по-прежнему поддерживается в С#, поскольку она очень полезна. Но в версии C# 4.0 возможности ковариантности и контравариантности были расширены до параметров обобщенного типа, применяемых в обобщенных интерфейсах и делегатах. Ковариантность и контравариантность применяется, главным образом, для рационального разрешения особых ситуаций, возникающих в связи с применением обобщенных интерфейсов и делегатов, определенных в среде .NET Framework. И поэтому некоторые интерфейсы и делегаты, определенные в библиотеке, были обновлены, чтобы использовать ковариантность и контравариантность параметров типа. Разумеется, преимуществами ковариантности и контравариантности можно также воспользоваться в интерфейсах и делегатах, создаваемых собственными силами. В этом разделе механизмы ковариантности и контравариантности параметров типа поясняются на конкретных примерах.
Применение ковариантности в обобщенном интерфейсе
Применительно к обобщенному интерфейсу ковариантность служит средством, разрешающим методу возвращать тип, производный от класса, указанного в параметре типа. В прошлом возвращаемый тип должен был в точности соответствовать параметру типа в силу строгой проверки обобщений на соответствие типов. Ковариантность смягчает это строгое правило таким образом, чтобы обеспечить типовую безопасность. Параметр ковариантного типа объявляется с помощью ключевого словаout,которое предваряет имя этого параметра.
Для того чтобы стали понятнее последствия применения ковариантности, обратимся к конкретному примеру. Ниже приведен очень простой интерфейсIMyCoVarGenlF,в котором применяется ковариантность.
//В этом обобщенном интерфейсе поддерживается ковариантность, public interface IMyCoVarGenlFCout Т> {
Т GetObject();
}
Обратите особое внимание на то, как объявляется параметр обобщенного типаТ.Его имени предшествует ключевое словоout.В данном контексте ключевое словоoutобозначает, что обобщенный типТявляется ковариантным. А раз он ковариантный, то методGetOb j ect() может возвращать ссылку на обобщенный типТили же ссылку на любой класс, производный от типаТ.
Несмотря на свою ковариантность по отношению к обобщенному типуТ,интерфейсIMyCoVarGenlFреализуется аналогично любому другому обобщенному интерфейсу. Ниже приведен пример реализации этого интерфейса в классеMyClass.
// Реализовать интерфейс IMyCoVarGenlF. class MyClass<T> : IMyCoVarGenIF<T> {
T obj;
public MyClass(T v) { obj = v; } public T GetObject() { return obj; }
}
Обратите внимание на то, что ключевое словоoutне указывается еще раз в выражении, объявляющем реализацию данного интерфейса в классеMyClass.Это не только не нужно, но и вредно, поскольку всякая попытка еще раз указать ключевое словоoutбудет расцениваться компилятором как ошибка.
А теперь рассмотрим следующую простую реализацию иерархии классов.
// Создать простую иерархию классов, class Alpha { string name;
public Alpha(string n) { name = n; }
public string GetNameO { return name; }
// ...
}
class Beta : Alpha {
public Beta(string n) : base (n) { }
// ...
}
Как видите, классBetaявляется производным от классаAlpha.
С учетом всего изложенного выше, следующая последовательность операций будет считаться вполне допустимой.
// Создать ссылку из интерфейса IMyCoVarGenlF на объект типа MyClass<Alpha>.
// Это вполне допустимо как при наличии ковариантности, так и без нее. IMyCoVarGenIF<Alpha> AlphaRef =
new MyClass<Alpha>(new Alpha("Alpha #1"));
Console.WriteLine("Имя объекта, на который ссылается переменная AlphaRef: " + AlphaRef.GetObj ect() .GetName());
//А теперь создать объект MyClass<Beta> и присвоить его переменной AlphaRef.
// *** Эта строка кода вполне допустима благодаря ковариантности. ***
AlphaRef = new MyClass<Beta>(new Beta("Beta #1"));
Console.WriteLine("Имя объекта, на который теперь ссылается " +
"переменная AlphaRef: " + AlphaRef.GetObject().GetName());
Прежде всего, переменной-AlphaRef типа IMyCoVarGenIF<Alpha> в этом фрагменте кода присваивается ссылка на объект типа MyClass<Alpha>. Это вполне допустимая операция, поскольку в классе MyClass реализуется интерфейс IMyCoVarGenlF, причем и в том, и в другом в качестве аргумента типа указывается Alpha. Далее имя объекта выводится на экран при вызове метода GetName () для объекта, возвращаемого методом GetOb j ect (). И эта операция вполне допустима, поскольку Alpha — это и тип, возвращаемый методом GetName (), и обобщенный тип Т. После этого переменной AlphaRef присваивается ссылка на экземпляр объекта типа MyClass<Beta>, что также допустимо, потому что класс Beta является производным от класса Alpha, а обобщенный тип Т — ковариантным в интерфейсе IMyCoVarGenlF. Если бы любое из этих условий не выполнялось, данная операция оказалась бы недопустимой.
Ради большей наглядности примера вся рассмотренная выше последовательность операций собрана ниже в единую программу.
// Продемонстрировать ковариантность в обобщенном интерфейсе, using System;
// Этот обобщенный интерфейс поддерживает ковариантность. public interface IMyCoVarGenIF<out Т> {
Т GetObjectO;
}
// Реализовать интерфейс IMyCoVarGenlF. class MyClass<T> : IMyCoVarGenIF<T> {
T obj;
public MyClass(T v) { obj = v; } public T GetObjectO { return obj; }
}
// Создать простую иерархию классов, class Alpha { string name;
public string GetNameO { return name; }
// ...
}
class Beta : Alpha {
public Beta(string n) : base(n) { }
// ...
}
class VarianceDemo { static void Main() {
// Создать ссылку из интерфейса IMyCoVarGenlF на объект типа MyClass<Alpha>.
// Это вполне допустимо как при наличии ковариантности, так и без нее. IMyCoVarGenIF<Alpha> AlphaRef = new MyClass<Alpha>(new Alpha("Alpha #1"));
Console.WriteLine("Имя объекта, на который ссылается переменная " +
"AlphaRef: " + AlphaRef.GetObj ect().GetName());
//А теперь создать объект MyClass<Beta> и присвоить его // переменной AlphaRef.
// *** Эта строка кода вполне допустима благодаря ковариантности. *** AlphaRef = new MyClass<Beta>(new Beta("Beta #1"));
Console.WriteLine("Имя объекта, на который теперь ссылается переменная " + "AlphaRef: " + AlphaRef.GetObj ect() .GetName());
}
}
Результат выполнения этой программы выглядит следующим образом.
Имя объекта, на который ссылается переменная AlphaRef: Alpha #1 Имя объекта, на который теперь ссылается переменная AlphaRef: Beta #1
Следует особо подчеркнуть, что переменнойAlphaRefможно присвоить ссылку на объект типаMyClass<Beta>благодаря только тому, что обобщенный типТуказан как ковариантный в интерфейсеIMyCoVarGenlF.Для того чтобы убедиться в этом, удалите ключевое словоoutиз объявления параметра обобщенного типаТв интерфейсеIMyCoVarGenlFи попытайтесь скомпилировать данную программу еще раз. Компиляция завершится неудачно, поскольку строгая проверка на соответствие типов не разрешит теперь подобное присваивание.
Один обобщенный интерфейс может вполне наследовать от другого. Иными словами, обобщенный интерфейс с параметром ковариантного типа можно расширить, как показано ниже.
public interface IMyCoVarGenIF2<out Т> : IMyCoVarGenIF<T> {
// ...
}
Обратите внимание на то, что ключевое словоoutуказано только в объявлении расширенного интерфейса. Указывать его в объявлении базового интерфейса не только не нужно, но и не допустимо. И последнее замечание: обобщенный типТдопускается не указывать как ковариантный в объявлении интерфейсаIMyCoVarGenIF2.Но при этом исключается ковариантность, которую может обеспечить расширенный интерфейс
IMyCoVarGetlF.Разумеется, возможность сделать интерфейсIMyCoVarGenIF2инвариантным может потребоваться в некоторых случаях его применения.
На применение ковариантности накладываются некоторые ограничения. Ковариантность параметра типа может распространяться только на тип, возвращаемый методом. Следовательно, ключевое словоoutнельзя применять в параметре типа, служащем для объявления параметра метода. Ковариантность оказывается пригодной только для ссылочных типов. Ковариантный тип нельзя использовать в качестве ограничения в интерфейсном методе. Так, следующий интерфейс считается недопустимым.
public interface IMyCoVarGenIF2<out Т> {
void M<V>() where V:T; // Ошибка, ковариантный тип T нельзя
// использовать как ограничение
}
Применение контравариантности в обобщенном интерфейсе
Применительно к обобщенному интерфейсу контравариантность служит сред- . ством, разрешающим методу использовать аргумент, тип которого относится к базовому классу, указанному в соответствующем параметре типа. В прошлом тип аргумента метода должен был в точности соответствовать параметру типа в силу строгой проверки обобщений на соответствие типов. Контравариантность смягчает это строгое правило таким образом, чтобы обеспечить типовую безопасность. Параметр контрава-риантного типа объявляется с помощью ключевого словаin,которое предваряет имя этого параметра.
Для того чтобы стали понятнее последствия применения ковариантности, вновь обратимся к конкретному примеру. Ниже приведен обобщенный интерфейсIMyContraVarGenlFконтравариантного типа. В нем указывается контравариантный параметр обобщенного типаТ,который используется в объявлении методаShow ().
// Это обобщенный интерфейс, поддерживающий контравариантность. public interface IMyContraVarGenIF<in Т> { void Show(T obj);
}
Как видите, обобщенный типТуказывается в данном интерфейсе как контравариантный с помощью ключевого словаin,предшествующего имени его параметра. Обратите также внимание на то, чтоТявляется параметром типа для аргументаobjв методеShow ().
• Далее интерфейсIMyContraVarGenlFреализуется в классеMyClass,как показано ниже.
// Реализовать интерфейс IMyContraVarGenlF. class MyClass<T> IMyContraVarGenIF<T> {
public void Show(T x) { Console.WriteLine(x); }
}
В данном случае методShow() просто выводит на экран строковое представление переменнойх,получаемое в результате неявного обращения к методуToString() из методаWriteLine ().
После этого объявляется иерархия классов, как показано ниже.
// Создать простую иерархию классов, class Alpha {
public override string ToString() { return "Это объект класса Alpha.";
}
// ...
}
class Beta : Alpha {
public override string ToString() {
return "Это объект класса Beta.";
}
// ...
}
Ради большей наглядности классыAlphaиBetaнесколько отличаются от аналогичных классов из предыдущего примера применения ковариантности. Обратите также внимание на то, что методToString() переопределяется таким образом, чтобы возвращать тип объекта.
С учетом всего изложенного выше, следующая последовательность операций будет считаться вполне допустимой.}
// Создать ссылку из интерфейса IMyContraVarGenIF<Alpha>
//на объект типа MyClass<Alpha>.
// Это вполне допустимо как при наличии контравариантности, так и без нее. IMyContraVarGenIF<Alpha> AlphaRef = new MyClass<Alpha>();
// Создать ссылку из интерфейса IMyContraVarGenIF<beta>
// на объект типа MyClass<Beta>.
//И это вполне допустимо как при наличии контравариантности, так и без нее. IMyContraVarGenIF<Beta> BetaRef = new MyClass<Beta>();
// Создать ссылку из интерфейса IMyContraVarGenIF<beta>
7/ на объект типа MyClass<Alpha>.
// *** Это вполне допустимо благодаря контравариантности. *** IMyContraVarGenIF<Beta> BetaRef2 = new MyClass<Alpha>();
// Этот вызов допустим как при наличии контравариантности, так и без нее.
BetaRef.Show(new Beta());
// Присвоить переменную AlphaRef переменной BetaRef.
// *** Это вполне допустимо благодаря контравариантности. ***
BetaRef = AlphaRef;
BetaRef.Show(new Beta ());
Прежде всего, обратите внимание на создание двух переменных ссылочного типаIMyContraVarGenlF,которым присваиваются ссылки на объекты классаMyClass,где параметры типа совпадают с аналогичными параметрами в интерфейсных ссылках. В первом случае используется параметр типаAlpha,а во втором — параметр типаBeta.Эти объявления не требуют контравариантности и допустимы в любом случае.
Далее создается переменная ссылочного типаIMyContraVarGenIF<Beta>,но на этот раз ей присваивается ссылка на объект классаMyClass<Alpha>.Эта операция вполне допустима, поскольку обобщенный типТобъявлен как контравариантный.
Как и следовало ожидать, следующая строка, в которой вызывается методBetaRef. Show() с аргументомBeta,является вполне допустимой. ВедьBeta— это обобщенный типТв классеMyClass<Beta>и в то же время аргумент в методеShow ().
В следующей строке переменнаяAlphaRefприсваивается переменнойBetaRef.Эта операция вполне допустима лишь в силу контравариантности. В данном случае переменная относится к типуMyClass<Beta>,а переменнаяAlphaRef— к типуMyClass<Alpha>.Но посколькуAlphaявляется базовым классом для классаBeta,то такое преобразование типов оказывается допустимым благодаря контравариантности. Для того чтобы убедиться в необходимости контравариантности в рассматриваемом здесь примере, попробуйте удалить ключевое словоinиз объявления обобщенного типаТв интерфейсеIMyContraVarGenlF,а затем попытайтесь скомпилировать приведенный выше код еще раз. В результате появятся ошибки компиляции.
Ради большей наглядности примера вся рассмотренная выше последовательность операций собрана ниже в единую программу.
// Продемонстрировать контравариантность в обобщенном интерфейсе, using System;
// Это обобщенный интерфейс, поддерживающий контравариантность. public interface IMyContraVarGenIF<in Т> { void Show(T obj);
}
// Реализовать интерфейс IMyContraVarGenlF. class MyClass<T> : IMyContraVarGenIF<T> {
public void Show(T x) { Console.WriteLine(x); }
}
// Создать простую иерархию классов, class Alpha {
public override string ToStringO { return "Это объект класса Alpha.";
}
// ...
}
class Beta : Alpha {
public override string ToStringO { return "Это объект класса Beta.";
}
// ...
}
class VarianceDemo { static void Main() {
// Создать ссылку из интерфейса IMyContraVarGenIF<Alpha>
//на объект типа MyClass<Alpha>.
// Это вполне допустимо как при наличии контравариантности, так и без нее. IMyContraVarGenIF<Alpha> AlphaRef = new MyClass<Alpha>();
// Создать ссылку из интерфейса IMyContraVarGenIF<beta>
// на объект типа MyClass<Beta>.
//И это вполне допустимо как при наличии контравариантности,
// так и без нее.
IMyContraVarGenIF<Beta> BetaRef = new MyClass<Beta>();
// Создать ссылку из интерфейса IMyContraVarGenIF<beta>
//на объект типа MyClass<Alpha>.
// *** Это вполне допустимо благодаря контравариантности. *** IMyContraVarGenIF<Beta> BetaRef2 = new MyClass<Alpha>();
// Этот вызов допустим как при наличии контравариантности, так и без нее. BetaRef.Show(new Beta());
// Присвоить переменную AlphaRef переменной BetaRef.
// *** Это вполне допустимо благодаря контравариантности. ***
BetaRef = AlphaRef;
BetaRef.Show(new Beta ());
}
}
Выполнение этой программы дает следующий результат.
Это объект класса Beta.
Это объект класса Beta.
Контравариантный интерфейс может быть расширен аналогично описанному выше расширению ковариантного интерфейса. Для достижения контравариантного характера расширенного интерфейса в его объявлении должен быть указан такой же параметр обобщенного типа, как и у базового интерфейса, но с ключевым словом in, как показано ниже.
public interface IMyContraVarGenIF2<in Т> : IMyContraVarGenIF<T> {
// ...
}
Следует иметь в виду, что указывать ключевое словоinв объявлении базового интерфейса не только не нужно, но и не допустимо. Более того, сам расширенный интерфейсIMyContraVarGenIF2не обязательно должен быть контравариантным. Иными словами, обобщенный типТв интерфейсеIMyContraVarGenIF2не требуется модифицировать ключевым словомin.Разумеется, все преимущества, которые сулит контравариантность в интерфейсеIMyContraVarGen,при этом будут утрачены в интерфейсеIMyContraVarGenIF2.
Контравариантность оказывается пригодной только для ссылочных типов, а параметр контравариантного типа можно применять только к аргументам методов. Следовательно, ключевое слово in нельзя указывать в параметре типа, используемом в качестве возвращаемого типа.
Вариантные делегаты
Как пояснялось в главе 15, ковариантность и контравариантность поддерживается в необобщенных делегатах в отношении типов, возвращаемых методами, и типов, указываемых при объявлении параметров. Начиная с версии C# 4.0, возможности ковариантности и контравариантности были распространены и на обобщенные делегаты. Подобные возможности действуют таким же образом, как было описано выше в отношении обобщенных интерфейсов.
Ниже приведен пример контравариантного делегата.
// Объявить делегат, контравариантный по отношению к обобщенному типу Т. delegate bool SomeOpcin Т>(Т obj);
Этому делегату можно присвоить метод с параметром обобщенного типаТили же класс, производный от типаТ.
А вот пример ковариантного делегата.
// Объявить делегат, ковариантный по отношению к обобщенному типу Т. delegate Т AnotherOp<out Т, V>(V obj);
Этому делегату можно присвоить метод, возвращающий обобщенный типТ,или же класс, производный от типаТ.В данном случаеVоказывается просто параметром инвариантного типа.
В следующем примере программы демонстрируется применение обоих разновидностей вариантных делегатов на практике.
// Продемонстрировать конвариантность и контравариантность // в обобщенных делегатах.
using System;
// Объявить делегат, контравариантный по отношению к обобщенному типу Т. delegate bool SomeOpcin Т>(Т obj);
// Объявить делегат, ковариантный по отношению к обобщенному типу Т. delegate Т AnotherOpCout Т, V>(V obj);
class Alpha {
public int Val { get; set; }
public Alpha(int v) { Val = v; }
}
class Beta : Alpha {
public Beta (int v) : base (v) {• }
}
class GenDelegateVarianceDemo {
// Возвратить логическое значение true, если значение // переменной obj.Val окажется четным, static bool IsEven(Alpha obj) {
if((obj.Val % 2) == 0) return true; return false;
}
static Beta Changelt(Alpha obj) { return new Beta(obj.Val +2);
}
static void Main() {
Alpha objA = new Alpha(4);
Beta objB = new Beta(9);
// Продемонстрировать сначала контравариантность.
// Объявить делегат SomeOp<Alpha> и задать для него метод IsEven. SomeOp<Alpha> checklt = IsEven;
// Объявить делегат SomeOp<Beta>.
SomeOp<Beta> checklt2;
//А теперь- присвоить делегат SomeOp<Alpha> делегату SomeOp<Beta>.
// *** Это допустимо только благодаря контравариантности. *** checklt2 = checklt;
// Вызвать метод через делегат.
Console.WriteLine(checklt2(objB));
// Далее, продемонстрировать контравариантность.
// Объявить сначала два делегата типа AnotherOp.
// Здесь возвращаемым типом является класс Beta,
//а параметром типа — класс Alpha.
// Обратите внимание на то, что для делегата modifylt // задается метод Changelt.
AnotherOp<Beta, Alpha> modifylt = Changelt;
// Здесь возвращаемым типом является класс Alpha,
// а параметром типа — тот же класс Alpha.
AnotherOp<Alpha, Alpha> modifyIt2;
// А теперь присвоить делегат modifylt делегату modifyIt2.
// *** Это допустимо только благодаря ковариантности. *** modifyIt2 = modifylt;
// Вызвать метод и вывести результаты на экран. objA = modifyIt2(objA);
Console.WriteLine(objA.Val);
}
}
Выполнение этой программы приводит к следующему результату.
False
6
Каждая операция достаточно подробно поясняется в комментариях к данной программе. Следует особо подчеркнуть, для успешной компиляции программы в объявлении обоих типов делегатовSomeOpandAnotherOpдолжны быть непременно указаны ключевые словаinиoutсоответственно. Без этих модификаторов компиляция программы будет выполнена с ошибками из-за отсутствия неявных преобразований типов в означенных строках кода.
Создание экземпляров объектов обобщенных типов
Когда приходится иметь дело с обобщениями, то нередко возникает вопрос: не приведет ли применение обобщенного класса к неоправданному раздуванию кода? Ответ на этот вопрос прост: не приведет. Дело в том, что в C# обобщения реализованы весьма эффективным образом: новые объекты конструируемого типа создаются лишь по мере надобности. Этот процесс описывается ниже.
Когда обобщенный класс компилируется в псевдокод MSIL, он сохраняет все свои параметры типа в их обобщенной форме. А когда конкретный экземпляр класса потребуется во время выполнения программы, то JIT-компилятор сконструирует конкретный вариант этого класса в исполняемом коде, в котором параметры типа заменяются аргументами типа. В каждом экземпляре с теми же самыми аргументами типа будет использоваться один и тот же вариант данного класса в исполняемом коде.
Так, если имеется некоторый обобщенный классGen<T>,то во всех объектах типаGen<T>будет использоваться один и тот же исполняемый код данного класса. Следовательно, раздувание кода исключается благодаря тому, что в программе создаются только те варианты класса, которые действительно требуются. Когда же возникает потребность сконструировать объект другого типа, то компилируется новый вариант класса в исполняемом коде.
Как правило, новый исполняемый вариант обобщенного класса создается для каждого объекта конструируемого типа, в котором аргумент имеет тип значения, напримерintилиdouble.Следовательно, в каждом объекте типаGen<int>будет использоваться один исполняемый вариант классаGen,а в каждом объекте типаGen<double> —другой вариант классаGen,причем каждый вариант приспосабливается к конкретному типу значения. Но во всех случаях, когда аргумент оказывается ссылочного типа, используетсятолько один вариантобобщенного класса, поскольку все ссылки имеют одинаковую длину (в байтах). Такая оптимизация также исключает раздувание кода.
Некоторые ограничения, присущие обобщениям
Ниже перечислен ряд ограничений, которые следует иметь в виду при использовании обобщений.
• Свойства, операторы, индексаторы и события не могут быть обобщенными. Но эти элементы могут использоваться в обобщенном классе, причем с параметрами обобщенного типа этого класса.
• К обобщенному методу нельзя применять модификатор extern.
• Типы указателей нельзя использовать в аргументах типа.
• Если обобщенный класс содержит поле типаstatic,то в объектекаждогоконструируемого типа должна бытьсвоякопия этого поля. Это означает, что во всех экземплярах объектоводногоконструируемого типа совместно используется одно и то же поле типаstatic.Но в экземплярах объектовдругогоконструируемого типа совместно используетсядругаякопия этого поля. Следовательно, поле типаstaticне может совместно использоваться объектамивсехконструируемых типов.
Заключительные соображения относительно обобщений
Обобщения являются весьма эффективным дополнением С#, поскольку они упрощают создание типизированного, повторно используемого кода. Несмотря на несколько усложненный, на первый взгляд, синтаксис обобщений, их применение быстро входит в привычку. Айалогично, умение применять ограничения к месту требует некоторой практики и со временем не вызывает особых затруднений. Обобщения теперь стали неотъемлемой частью программирования на С#. Поэтому освоение этого важного языкового средства стоит затраченных усилий.
ГЛАВА 19
LINQ
Без сомнения, LINQ относится к одним из самых интересных средств языка С#. Эти средства были внедрены в версии C# 3.0 и явились едва ли не самым главным его дополнением, которое состояло не только во внесении совершенно нового элемента в синтаксис С#, добавлении нескольких ключевых слов и предоставлении больших возможностей, но и в значительном расширении рамок данного языка программирования и круга задач, которые он позволяет решать. Проще говоря, внедрение LINQ стало поворотным моментом в истории развития С#.
Аббревиатура LINQ означает Language-Integrated Query, т.е.язык интегрированных запросов.Это понятие охватывает ряд средств, позволяющих извлекать информацию из источника данных. Как вам должно быть известно, извлечение данных составляет важную часть многих программ. Например, программа может получать информацию из списка заказчиков, искать информацию в каталоге продукции или получать доступ к учетному документу, заведенному на работника. Как правило, такая информация хранится в базе данных, существующей отдельно от приложения. Так, каталог продукции может храниться в реляционной базе данных. В прошлом для взаимодействия с такой базой данных приходилось формировать запросы на языке структурированных запросов (SQL). А для доступа к другим источникам данных, например в формате XML, требовался отдельный подход. Следовательно, до версии 3.0 поддержка подобных запросов в C# отсутствовала. Но это положение изменилось после внедрения LINQ.
LINQ дополняет C# средствами, позволяющими формировать запросы для любого LINQ-совместимого источника
данных. При этом синтаксис, используемый для формирования запросов, остается неизменным, независимо от типа источника данных. Это, в частности, означает, что синтаксис, требующийся для формирования запроса к реляционной базе данных, практически ничем не отличается от синтаксиса запроса данных, хранящихся в массиве.Дляэтой цели теперь не нужно прибегать к средствам SQL или другого внешнего по отношению к C# механизма извлечения данных из источника. Возможности формировать запросы отныне полностью интегрированы в язык С#.
Помимо SQL, LINQ можно использовать вместе с XML-файлами и наборами данных ADO.NET Dataset. Не менее важным является применение LINQ вместе с массивами и коллекциями в C# (подробнее рассматриваемыми в главе 25). Таким образом, средства LINQ предоставляют, в целом, единообразный доступ к данным. И хотя такой принцип уже сам по себе является весьма эффективным и новаторским, преимущества LINQ этим не ограничиваются. LINQ предлагает осмыслить иначе и подойти по-другому к решению многих видов задач программирования, помимо традиционной организации доступа к базам данных. И в конечном итоге многие решения могут быть выработаны на основе LINQ.
LINQ поддерживается целым рядом взаимосвязанных средств, включая внедренный в C# синтаксис запросов, лямбда-выражения, анонимные типы и методы расширения. О лямбда-выражениях речь уже шла в главе 15, а остальные средства рассматриваются в этой главе.
ПРИМЕЧАНИЕ
LINQ в C# — это, по сути, язык в языке. Поэтому предмет рассмотрения LINQ довольно обширен и включает в себя многие средства, возможности и альтернативы. Несмотря на то что в этой главе дается подробное описание средств LINQ, рассмотреть здесь все их возможности, особенности и области применения просто невозможно. Для этого потребовалась бы отдельная книга. В связи с этим в настоящей главе основное внимание уделяется главным элементам LINQ, применение которых демонстрируется на многочисленных примерах. А в долгосрочной перспективе LINQ представляет собой подсистему, которую придется изучать самостоятельно и достаточно подробно.
Основы LINQ
В основу LINQ положено понятиезапроса,в котором определяется информация, получаемая из источника данных. Например, запрос списка рассылки почтовых сообщений заказчикам может потребовать предоставления адресов всех заказчиков, проживающих в конкретном городе; запрос базы данных товарных запасов — список товаров, запасы которых исчерпались на складе; а запрос журнала, регистрирующего интенсивность использования Интерента, — список наиболее часто посещаемых вебсайтов. И хотя все эти запросы отличаются в деталях, их можно выразить, используя одни и те же синтаксические элементы LINQ.
Как только запрос будет сформирован, его можно выполнить. Это делается, в частности, в цикле foreach. В результате выполнения запроса выводятся его результаты. Поэтому использование запроса может быть разделено на две главные стадии. На первой стадии запрос формируется, а на второй — выполняется. Таким образом, при формировании запроса определяется, чтоименноследует извлечь из источника данных. А при выполнении запроса выводятся конкретныерезультаты.
Для обращения к источнику данных по запросу, сформированному средствами LINQ, в этом источнике должен быть реализован интерфейсI Enumerable.Он имеет две формы: обобщенную и необобщенную. Как правило, работать с источником данных легче, если в нем реализуется обобщенная формаIEnumerable<T>,гдеТобозначает обобщенный тип перечисляемых данных. Здесь и далее предполагается, что в источнике данных реализуется форма интерфейсаIEnumerable<T>.Этот интерфейс объявляется в пространстве именSystem. Collections. Generic.Класс, в котором реализуется форма интерфейсаIEnumerable<T>,поддерживает перечисление, а это означает, что его содержимое может быть получено по очереди или в определенном порядке. Форма интерфейсаIEnumerable<T>поддерживается всеми массивами в С#. Поэтому на примере массивов можно наглядно продемонстрировать основные принципы работы LINQ. Следует, однако, иметь в виду, что применение LINQ не ограничивается одними массивами.
Простой запрос
А теперь самое время обратиться к простому примеру использования LINQ. В приведенной ниже программе используется запрос для получения положительных значений, содержащихся в массиве целых значений.
// Сформировать простой запрос LINQ.
using System; using System.Linq;
class SimpQuery {
static void Main() {
int[] nums = { .1,-2,3, 0, -4, 5 };
// Сформировать простой запрос на получение только положительных значений, var posNums = from n in nums where n > 0 select n;
Console.Write("Положительные значения из массива nums: ");
// Выполнить запрос и отобразить его результаты, foreach(int i in posNums) Console.Write(i + " ");
Console.WriteLine();
}
}
Эта программа дает следующий результат.
Положительные значения из массива nums: 1 3 5
Как видите, в конечном итоге отображаются только положительные значения, хранящиеся в массивеnums.Несмотря на всю свою простоту, этот пример наглядно демонстрирует основные возможности LINQ. Поэтому рассмотрим его более подробно.
Прежде всего обратите внимание на применение в данном примере программы следующего оператора.
Для применения средств LINQ в исходный текст программы следует включить пространство именSystem.Linq.
Затем в программе объявляется массивnumsтипаint.Все массивы в C# неявным образом преобразуются в форму интерфейсаIEnumerable<T>.Благодаря этому любой массив в C# может служить в качестве источника данных, извлекаемых по запросу LINQ.
Далее объявляется запрос, по которому из массиваnumsизвлекаются элементы только с положительными значениями.
var posNums = from n in nums where n > 0 select попеременнаяposNumsназываетсяпеременной запроса.В ней хранится ссылка на ряд правил, определяемых в запросе. Обратите внимание на применение ключевого словаvarдля объявления переменнойposNumsнеявным образом. Как вам должно быть уже известно, благодаря этому переменнаяposNumsстановится неявно типизированной. Такими переменными удобно пользоваться в запросах, хотя их тип можно объявить и явным образом (это должна быть одна из форм интерфейсаIEnumerable<T>).Объявляемой переменнойposNumsв итоге присваивается выражение запроса.
Все запросы начинаются с оператораfrom,определяющего два элемента. Первым из них являетсяпеременная диапазона, принимающая элементы из источника данных. В рассматриваемом здесь примере эту роль выполняет переменнаяп.Вторым элементом является источник данных (в данном случае — массивnums).Тип переменной диапазона выводится из источника данных. Поэтому переменнаяпотносится к типуint.Ниже приведена общая форма оператораfrom.
fromпеременная_диапазонаinисточник_данных
Далее следует операторwhere,обозначающий условие, которому должен удовлетворять элемент в источнике данных, чтобы его можно было получить по запросу. Ниже приведена общая форма синтаксиса оператораwhere.
whereбулево_выражение
В этой формебулево_выражениедолжно давать результат типаbool.Такое выражение иначе называетсяпредикатом.В запросе можно указывать несколько операторовwhere.В данном примере программы используется следующий операторwhere.
where n > О
Этот оператор будет давать истинный результат только для тех элементов массива, значения которых оказываются больше нуля. Выражениеп> 0будет вычисляться для каждого изпэлементов массиваппри выполнении запроса. В итоге будут получены только те значения, которые удовлетворяют этому условию. Иными словами, операторwhereвыполняет роль своеобразного фильтра, отбирая лишь определенные элементы.
Все запросы оканчиваются операторомselectилиgroup.В данном примере используется операторselect,точно определяющий, что именно должно быть получено по запросу. В таких простых примерах запросов, как рассматриваемый здесь, выбирается конкретное значение диапазона. Поэтому по данному запросу возвращаются только те целые значения, которые удовлетворяют условию, указанному в оператореwhere.В более сложных запросах можно дополнительно уточнять, что именно
следует выбирать. Например, по запросу списка рассылки может быть получена лишь фамилия адресата вместо его полного адреса. Обратите внимание на то, что операторselectзавершается точкой с запятой, поскольку это последний оператор в запросе. А другие его операторы не оканчиваются точкой с запятой.
Итак, переменная запросаposNumsсоздана, но результаты запроса пока еще не получены. Дело в том, что сам запрос определяет лишь ряд конкретных правил, а результаты будут только после выполнения запроса. Кроме того, один и тот же запрос может быть выполнен два раза или больше, причем с разными результатами, если в промежутке между последовательно производимыми попытками выполнить один и тот же запрос изменяется базовый источник данных. Поэтому одного лишь объявления переменной запросаposNumsсовершенно недостаточно для того, чтобы она содержала результаты запроса.
Для выполнения запроса в данном примере программы организуется следующий цикл.
foreach(int i in posNums) Console.WriteLine(i + " ");
В этом цикле переменнаяposNumsуказывается в качестве коллекции, к которой происходит обращение на каждом шаге цикла. В циклеforeachсоблюдаются правила, определенные в запросе и доступные по ссылке из переменнойposNums.На каждом шаге цикла возвращается очередной элемент, полученный из массива. Этот процесс завершается, когда запрашиваемых элементов в массиве больше не обнаружено. В данном примере типintпеременной шага циклаiуказывается явно, поскольку по запросу извлекаются элементы именно этого типа. Явное указание типа переменной шага цикла вполне допустимо в тех случаях, когда заранее известен тип значения, выбираемого по запросу. Но в более сложных случаях оказывается проще, а иногда даже нужно, указывать тип переменной шага цикла неявным образом с помощью ключевого словаvar.
Неоднократное выполнение запросов
Итак, в запросе определяются правила, по которым извлекаются данные, но этого явно недостаточно для получения результатов, поскольку запрос должен быть выполнен, причем это может быть сделано несколько раз. Если же в промежутке между последовательно производимыми попытками выполнить один и тот же запрос источник данных изменяется, то получаемые результаты могут отличаться. Поэтому как только запрос определен, его выполнение будет всегда давать только самые последние результаты. Обратимся к конкретному примеру. Ниже приведен другой вариант рассматриваемой здесь программы, где содержимое массиваnumsизменяется в промежутке между двумя последовательно производимыми попытками выполнить один и тот же запрос, хранящийся в переменнойposNums.
// Сформировать простой запрос.
using System;
using System.Linq;
using System.Collections.Generic;
class SimpQuery {
static void Main() {
int[] nums = { 1,-2,3, 0, -4, 5 };
// Сформировать простой запрос на получение только положительных значений, var posNums = from n in nums where n > 0 select n;
Console.Write("Положительные значения из массива nums: ");
// Выполнить запрос и отобразить его результаты, foreach(int i in posNums) Console.Write(i + " ");
Console.WriteLine ();
// Внести изменения в массив nums.
Console.WriteLine("ХпЗадать значение 99 для элемента массива nums[l]."); nums[l] = 99;
Console.Write("Положительные значения из массива nums\n" +
"после изменений в нем: ");
// Выполнить запрос второй раз.
foreach(int i in posNums) Console.Write(i + " ");
Console.WriteLine();
}
}
Вот к какому результату приводит выполнение этой программы.
Положительные значения из массива nums: 1 3 5
Задать значение 99 для элемента массива nums[l].
Положительные значения из массива nums после изменений в нем: 1 99 3 5
Как следует из результата выполнения приведенной выше программы, значение элемента массива nums [ 1 ] изменилось с -2 на 99, что и отражают результаты повторного выполнения запроса. Этот важный момент следует подчеркнуть особо. Каждая попытка выполнить запрос приносит свои результаты, получаемые при перечислении текущего содержимого источника данных. Поэтому если источник данных претерпевает изменения, то могут измениться и результаты выполнения запроса. Преимущества такого подхода к обработке'запросов весьма значительны. Так, если по запросу получается список необработанных заказов в Интернет-магазине, то при каждой попытке выполнить запрос желательно получить сведения обо всех заказах, включая и только что введенные.
Связь между типами данных в запросе
Как показывает предыдущий пример, запрос включает в себя переменные, типы которых связаны друг с другом. К их числу относятся переменная запроса, переменная диапазона и источник данных. Соблюсти соответствие этих типов данных очень важно, но в то же время нелегко — по крайней мере, так кажется на первый взгляд, поэтому данный вопрос заслуживает более пристального внимания.
Тип переменной диапазона должен соответствовать типу элементов, хранящихся в источнике данных. Следовательно, тип переменной диапазона зависит от типа источника данных. Как правило, тип переменной диапазона может быть выведен средствами С#. Но выводимость типов может быть осуществлена при условии, что в источнике данных реализована форма интерфейсаIEnumerable<T>,гдеТобозначает тип элементов в источнике данных. (Как упоминалось выше, форма интерфейсаIEnumerable<T>реализуется во всех массивах, как, впрочем, и во многих других источниках данных.) Но если в источнике данных реализован необобщенный вариант интерфейсаI Enumerable,то тип переменной диапазона придется указывать явно. И это делается в оператореfrom.Ниже приведен пример явного объявления типаintпеременной диапазонап.
var posNums = from int n in nums // ...
Очевидно, что явное указание типа здесь не требуется, поскольку все массивы неявно преобразуются в форму интерфейсаIEnumerable<T>,которая позволяет вывести тип переменной диапазона.
Тип объекта, возвращаемого по запросу, представляет собой экземпляр интерфейсаIEnumerable<T>,гдеТ— тип получаемых элементов. Следовательно, тип переменной запроса должен быть экземпляром интерфейсаIEnumerable<T>,а значениеТдолжно определяться типом значения, указываемым в оператореselect.В предыдущих примерах значениюТсоответствовал типint,поскольку переменнаяпимела типint.(Как пояснялось выше, переменнаяпотносится к типуint,потому что элементы именно этого типа хранятся в массивеnums.)С учетом явного указания типаIEnumerable<int>упомянутый выше запрос можно было бы составить следующим образом.
IEnumerable<int> posNums = from n in nums
where n > 0 select п^-
Следует иметь в виду, что тип элемента, выбираемого операторомselect,должен соответствовать типу аргумента, передаваемого форме интерфейсаIEnumerable<T>,используемой для объявления переменной запроса. Зачастую при объявлении переменных запроса используется ключевое словоvarвместо явного указания ее типа, поскольку это дает компилятору возможность самому вывести соответствующий тип данной переменной из оператораselect.Как будет показано далее в этой главе, такой подход оказывается особенно удобным в тех случаях, когда операторselectвозвращает из источника данных нечто более существенное, чем отдельный элемент.
Когда запрос выполняется в циклеforeach,тип переменной шага цикла должен быть таким же, как и тип переменной диапазона. В предыдущих примерах тип этой переменной указывался явно какint.Но имеется и другая возможность: предоставить компилятору самому вывести тип данной переменной, и для этого достаточно указать ее тип какvar.Как будет показано далее в этой главе, ключевое словоvarприходится использовать и в тех случаях, когда тип данных просто неизвестен.
Общая форма запроса
У всех запросов имеется общая форма, основывающаяся на ряде приведенных ниже контекстно-зависимых ключевых слов.
Ascending
by
descending equals
from
group
in into
join
let
on orderby
select
where
Среди них лишь торов запроса.
приведенные ниже ключевые слова используются в начале опера-
from
group
join let
orderby
select
where
Запрос должен начинаться с ключевого слова from и оканчиваться ключевым словом select или group. Оператор select определяет тип значения, перечисляемого по запросу, а оператор group возвращает данные группами, причем каждая группа может перечисляться по отдельности. Как следует из приведенных выше примеров, в операторе where указываются критерии, которым должен удовлетворять искомый элемент, чтобы быть полученным по запросу. А остальные операторы позволяют уточнить запрос. Все они рассматриваются далее по порядку.
Отбор запрашиваемых значений с помощью оператора where
Как пояснялось выше, оператор where служит для отбора данных, возвращаемых по запросу. В предыдущих примерах этот оператор был продемонстрирован в своей простейшей форме, в которой для отбора данных используется единственное условие. Однако для более тщательного отбора данных можно задать несколько условий и, в частности, в нескольких операторах where. В качестве примера рассмотрим следующую программу, в которой из массива выводятся только те значения, которые положительны и меньше 10.
// Использовать несколько операторов where.
using System; using System.Linq;
class TwoWheres {
static void Main() {
int [ ] nums = { 1, -2, 3, -3, 0, -8, 12, 19, 6, 9, 10 };
// Сформировать запрос на получение положительных значений меньше 10. var posNums = from n in nums where n > 0 where n < 10 select n;
Console.Write("Положительные значения меньше 10: ");
// Выполнить запрос и вывести его результаты. foreach(int i in posNums) Console.Write (i + " ");
Console.WriteLine();
Эта программа дает следующий результат.
Положительные значения меньше 10: 13 6 9
Как видите, по данному запросу извлекаются только положительные значения меньше. 10. Этот результат достигается благодаря двум следующим операторам
where.
where n > 0 where n < 10
Условие в первом оператореwhereтребует, чтобы элемент массива был больше нуля. А условие во втором оператореwhereтребует, чтобы элемент массива был меньше 10. Следовательно, запрашиваемый элемент массива должен находиться в пределах от 1 до 9 (включительно), чтобы удовлетворять обоим условиям.
В таком применении двух операторовwhereдля отбора данных нет ничего дурного, но аналогичного эффекта можно добиться с помощью более компактно составленного условия в единственном оператореwhere.Ниже приведен тот же самый запрос, переформированный по этому принципу.
var posNums = from n in nums
where n>0&&n<10 select n;
Как правило, в условии оператораwhereразрешается использовать любое допустимое в C# выражение, дающее булев результат. Например, в приведенной ниже программе определяется массив символьных строк. В ряде этих строк содержатся адреса Интернета. По запросу в переменойnetAddrsизвлекаются только те строки, которые содержат более четырех символов и оканчиваются на ".net". Следовательно, по данному запросу обнаруживаются строки, содержащие адреса Интернета с именем .netдомена самого верхнего уровня.
// Продемонстрировать применение еще одного оператора where.
using System; using System.Linq;
class WhereDemo2 { static void Main() {
string[] strs = { ".com", ".net", "hsNameA.com",
"hsNameB.net", "test", ".network",
"hsNameC.net", "hsNameD.com" };
// Сформировать запрос на получение адресов // Интернета, оканчивающихся на .net. var netAddrs = from addr in strs
where addr.Length > 4 && addr.EndsWith(".net", StringComparison.Ordinal) sel-ect addr;
// Выполнить запрос и вывести его результаты. foreach(var str in netAddrs) Console.WriteLine(str);
Вот к какому результату приводит выполнение этой программы.
hsNameB.net » hsNameC.net
Обратите внимание на то, что в оператореwhereданной программы используется один из методов обработки символьных строк под названиемEnds With (). Он возвращает логическое значениеtrue,если вызывающая его строка оканчивается последовательностью символов, указываемой в качестве аргумента этого метода.
Сортировка результатов запроса с помощью оператора orderby
Зачастую результаты запроса требуют сортировки. Допустим, что требуется получить список просроченных счетов по порядку остатка на счету: от самого большого до самого малого или же список имен заказчиков в алфавитном порядке. Независимо от преследуемой цели, результаты запроса можно очень легко отсортировать, используя такое средство LINQ, как оператор orderby.
Оператор orderby можно использовать для сортировки результатов запроса по одному или нескольким критериям. Рассмотрим для начала самый простой случай сортировки по одному элементу. Ниже приведена общая форма оператора orderby для сортировки результатов запроса по одному критерию:
orderbyэлемент порядок
гдеэлементобозначает конкретный элемент, по которому проводится сортировка. Это может быть весь элемент, хранящийся в источнике данных, или только часть одного поля в данном элементе. Апорядокобозначает порядок сортировки по нарастающей или убывающей с обязательным добавлением ключевого слова ascending или descending соответственно. По умолчанию сортировка проводится по нарастающей, и поэтому ключевое слово ascending, как правило, не указывается.
Ниже приведен пример программы, в которой оператор orderby используется для извлечения значений из массива типаintпо нарастающей.
// Продемонстрировать применение оператора orderby.
using System; using System.Linq;
class OrderbyDemo { static void Main() {
int[] nums = { 10, -19, 4, 7, 2, -5, 0 };
// Сформировать запрос на получение значений в отсортированном порядке, var posNums = from n in nums orderby n select n;
Console.Write("Значения по нарастающей: ");
// Выполнить запрос и вывести его результаты. foreach(int i in posNums) Console.Write(i + " ");
При выполнении этой программы получается следующий результат.
Значения по нарастающей: -19 -5 0 2 4 7 10
Для того чтобы изменить порядок сортировки по нарастающей на сортировку по убывающей, достаточно указать ключевое слово descending, как показано ниже.
var posNums = from n in nums
orderby n descending select n;
Попробовав выполнить этот запрос, вы получите значения в обратном порядке. Зачастую сортировка результатов запроса проводится по единственному критерию. Тем не менее для сортировки по нескольким критериям служит приведенная ниже форма оператора orderby.
orderbyэлемент_А направление,элемент_В направление,элемент_С направление,...
В данной формеэлемент_Аобозначает конкретный элемент, по которому проводится основная сортировка;элемент_В— элемент, по которому производится сортировка каждой группы эквивалентных элементов;элемент_С— элемент, по которому производится сортировка всех этих групп, и т.д. Таким образом, каждый последующийэлементобозначает дополнительный критерий сортировки. Во всех этих критериях указыватьнаправлениесортировки необязательно, но по умолчанию сортировка проводится по нарастающей. Ниже приведен пример программы, в которой сортировка информации о банковских счетах осуществляется по трем критериям: фамилии, имени и остатку на счете.
// Сортировать результаты запроса по нескольким // критериям, используя оператор orderby.
using System; using System.Linq;
class Account {
public string FirstName { get; private set; } public string LastName { get; private set; } public double Balance { get; private set; } public string AccountNumber { get; private set; }
public Account(string fn, string In, string accnum, double b) {
FirstName = fn;
LastName = In;
AccountNumber = accnum;
Balance = b;
}
}
class OrderbyDemo { static void Main() {
// Сформировать исходные данные.
Account[] accounts =
{ new Account("Том","Смит","132CK", 100.23), new Account("Том","Смит","132CD", 10000.00),
new Account("Ральф", "Джонс", "436CD", 1923.85),
new Account ("Ральф", "Джонс", "454MM", 987.132),
new Account("Тед", "Краммер", "897CD", 3223.19),
new Account("Ральф", "Джонс", "434CK", -123.32),
new Account("Capa", "Смит", "543MM", 5017.40),
new Account("Capa", "Смит", "547CD", 34955.79),
new Account("Capa", "Смит", "843CK", 345.00),
new Account("Альберт", "Смит", "445CK", -213.67), new Account("Бетти", "Краммер","968MM",5146.67), new Account("Карл", "Смит", "078CD", 15345.99),
new Account("Дженни", "Джонс", "108CK", 10.98)
};
// Сформировать запрос на получение сведений о // банковских счетах в отсортированном порядке.
// Отсортировать эти сведения сначала по имени, затем //по фамилии и, наконец, по остатку на счете, var acclnfo = from асс in accounts
orderby acc.LastName, acc.FirstName, acc.Balance select acc;
Console.WriteLine("Счета в отсортированном порядке: ") ; string str = "";
// Выполнить запрос и вывести его результаты, foreach(Account acc in acclnfo) { if(str != acc.FirstName) {
Console.WriteLine(); str = acc.FirstName;
}
Console.WriteLine("{0}, {l}\tHoMep счета: {2}, {3,10:C}", acc.LastName, acc.FirstName, acc. AccountNumber, acc.Balance);
}
Console.WriteLine ();
}
}
Ниже приведен результат выполнения этой программы.
Счета в отсортированном порядке:
Джонс, Дженни Номер счета: 108СК, $10.98
Джонс, Ральф Номер счета: 434СК, ($123.32)
Джонс, Ральф Номер счета: 454ММ, $987.13
Джонс, Ральф Номер счета: 436CD, $1,923.85
Краммер, Бетти Номер счета: 968ММ, $5,146.67
Краммер, Тед Номер счета: 897CD, $3,223.19
Смит, Альберт Номер счета: 445СК, ($213.67)
Смит, Карл Номер счета: 078CD, $15,345.99
Смит, Сара Номер счета: 843СК, $345.00
Смит, Сара Номер счета: 543ММ, $5,017.40
Смит, Сара Номер счета: 547CD, $34,955.79
Смит, Том Номер счета: 132СК, $100.23
Смит, Том Номер счета: 132CD, $10,000.00
Внимательно проанализируем оператор orderby в следующем запросе из приведенной выше программы.
var acclnfo = from асс in accounts
orderby acc.LastName, acc.FirstName, acc.Balance select acc;
Сортировка результатов этого запроса осуществляется следующим образом. Сначала результаты сортируются по фамилии, затем элементы с одинаковыми фамилиями сортируются по имени. И наконец, группы элементов с одинаковыми фамилиями и именами сортируются по остатку на счете. Именно поэтому список счетов вкладчиков по фамилии Джонс выглядит так.
Джонс, Дженни Номер счета: 108СК, $10.98
Джонс, Ральф Номер счета: 434СК, ($123.32)
Джонс, Ральф Номер счета: 454ММ, $987.13
Джонс, Ральф Номер счета: 436CD, $1,923.85
Как показывает результат выполнения данного запроса, список счетов отсортирован сначала по фамилии, затем по имени и, наконец, по остатку на счете.
Используя несколько критериев, можно изменить на обратный порядок любой сортировки с помощью ключевого слова descending. Например, результаты следующего запроса будут выведены по убывающей остатков на счетах.
var acclnfo = from acc in accounts
orderby x.LastName, x.FirstName, x.Balance descending select acc;
В этом случае список счетов вкладчиков по фамилии Джонс будет выглядеть так, как показано ниже.
Джонс, Дженни Номер счета: 108СК, $10.98
Джонс, Ральф Номер счета: 436CD, $1,923.85
Джонс, Ральф Номер счета: 454ММ, $987.13
Джонс, Ральф Номер счета: 434СК, ($123.32)
Как видите, теперь счета вкладчика по фамилии Ральф Джонс выводятся по убывающей: от наибольшей до наименьшей суммы остатка на счете.
Подробное рассмотрение оператора select
Оператор seleqt определяет конкретный тип элементов, получаемых по запросу. Ниже приведена его общая форма.
selectвыражение
В предыдущих примерах операторselectиспользовался для возврата переменной диапазона. Поэтомувыражениев нем просто обозначало имя переменной диапазона. Но применение оператораselectне ограничивается только этой простой функцией. Он может также возвращать отдельную часть значения переменной диапазона, результат выполнения некоторой операции или преобразования переменной диапазона и даже новый тип объекта, конструируемого из отдельных фрагментов информации, извлекаемой из переменной диапазона. Такое преобразование исходных данных называетсяпроецированием.
Начнем рассмотрение других возможностей оператораselectс приведенной ниже программы.*В этой программе выводятся квадратные корни положительных значений, содержащихся в массиве типаdouble.
// Использовать оператор select для возврата квадратных корней всех // положительных значений, содержащихся в массиве типа double.
using System; using System.Linq;
class SelectDemo { static void Main() {
double[] nums =
{ -10.0, 16.4, 12.125, 100.85, -2.2, 25.25, -3.5 };
// Сформировать запрос на получение квадратных корней всех // положительных значений, содержащихся в массиве nums. var sqrRoots = from n in nums where n > 0 select Math.Sqrt(n);
Console.WriteLine("Квадратные корни положительных значений,\n" + "округленные до двух десятичных цифр:");
// Выполнить запрос и вывести его результаты, foreach(double г in sqrRoots)
Console.WriteLine("{0:#.##}", r);
}
}
Эта программа дает следующий результат.
Квадратные корни положительных значений, округленные до двух десятичных цифр:
4.05
3.48
10.04
5.02
Обратите особое внимание в данном примере запроса на следующий операторselect.
select Math.Sqrt(n);
Он возвращает квадратный корень значения переменной диапазона. Для этого значение переменной диапазона передается методуMath.Sqrt (), который возвращает квадратный корень своего аргумента. Это означает, что последовательность результатов, получаемых при выполнении запроса, будет содержать квадратные корни положительных значений, хранящихся в массиве nums. Если обобщить этот принцип, то его эффективность станет вполне очевидной. Так, с помощью оператора select можно сформировать любой требующийся тип последовательности результатов, исходя из значений, получаемых из источника данных.
Ниже приведена программа, демонстрирующая другое применение оператора select. В этой программе сначала создается класс EmailAddress, содержащий два свойства. В первом из них хранится имя адресата, а во втором — адрес его электронной почты. Затем в этой программе создается массив, содержащий несколько элементов данных типа EmailAddress. И наконец, в данной программе создается список, состоящий только из адресов электронной почты, извлекаемых по запросу.
// Возвратить часть значения переменной диапазона.
using System; using System.Linq;
class EmailAddress {
public string Name { get; set; } public string Address { get; set; }
public EmailAddress(string n, string a) {
Name = n;
Address = a;
}
}
class SelectDemo2 { static void Main() {
EmailAddress[] addrs = { new EmailAddress("Герберт", "Herb@HerbSchildt.com"), new EmailAddress("Tom", "Tom@HerbSchildt.com"), new EmailAddress("Capa", "Sara@HerbSchildt.com")
};
// Сформировать запрос на получение адресов
// электронной почты.
var eAddrs = from entry in addrs
select entry.Address;
Console.WriteLine("Адреса электронной почты:");
// Выполнить запрос и вывести его результаты, foreach(string s in eAddrs)
Console.WriteLine(" " + s);
}
}
Вот к какому результату приводит выполнение этой программы.
Адреса электронной почты:
Herb@HerbSchildt.com
Tom@HerbSchildt.comSara@HerbSchildt.com
Обратите особое внимание на следующий операторselect, select entry.Address;
Вместо полного значения переменной диапазона этот оператор возвращает лишь его адресную часть(Address).Это означает, что по данному запросу возвращается последовательность символьных строк, а не объектов типаEmail Address.Именно поэтому переменнаяsуказывается в циклеforeachкакstring.Ведь как пояснялось ранее, тип последовательности результатов, возвращаемых по запросу, определяется типом значения, возвращаемым операторомselect.
Одной из самых эффективных для оператораselectявляется возможность возвращать последовательность результатов, содержащую элементы данных, формируемые во время выполнения запроса. В качестве примера рассмотрим еще одну программу. В ней определяется классContactlnfo,в котором хранится имя, адрес электронной почты и номер телефона адресата. Кроме того, в этой программе определяется классEmail Addr ess,использовавшийся в предыдущем примере. В методеMain() создается массив объектов типаContactlnfo,а затем объявляется запрос, в котором источником данных служит этот массив, но возвращаемая последовательность результатов содержит объекты типаEmailAddress.Таким образом, типом последовательности результатов, возвращаемой операторомselect,является классEmailAddress,а не классContactlnfo,причем его объекты создаются во время выполнения запроса.
// Использовать запрос для получения последовательности объектов // типа EmailAddresses из списка объектов типа Contactlnfo.
using System; using System.Linq;
class Contactlnfo {
public string Name { get; set; } public string Email { get; set; }
public string Phone { get; set; }
public Contactlnfo(string n, string a, string p) {
Name = n;
Email = a;
Phone = p;
}
}
class EmailAddress {
public string Name { get; set; } public string Address { get; set; } public EmailAddress(string n, string a) {
Name = n;
Address = a;
} -
}
class SelectDemo3 { static void Main() {
Contactlnfo[] contacts = {
new Contactlnfo("Герберт", "Herb@HerbSchildt.com", "555-1010"), new Contactlnfo("Том", "Tom@HerbSchildt.com", "555-1101"), new Contactlnfo("Capa", "Sara@HerbSchildt.com", "555-0110")
}; •
// Сформировать запрос на получение списка объектов типа EmailAddress. var emailList = from entry in contacts
select new EmailAddress(entry.Name, entry.Email);
Console.WriteLine("Список адресов электронной почты:");
// Выполнить запрос и вывести его результаты, foreach(EmailAddress е in emailList)
Console.WriteLine(" {0}: {1}", e.Name, e.Address );
}
}
Ниже приведен результат выполнения этой программы.
• Список адресов электронной почты:
Герберт:Herb@HerbSchildt.comТом:Tom@HerbSchildt.comСара:Sara@HerbSchildt.com
Обратите особое внимание в данном запросе на следующий операторselect.
select new EmailAddress(entry.Name, entry.Email);
В этом операторе создается новый объект типаEmailAddress,содержащий имя и адрес электронной почты, получаемые из объекта типаContactlnfo,хранящегося в массивеcontacts.Но самое главное, что новые объекты типаEmailAddressсоздаются в оператореselectво время выполнения запроса.
Применение вложенных операторов from
Запрос может состоять из нескольких операторов-f г от,которые оказываются в этом случае вложенными. Такие операторыfromнаходят применение в тех случаях, когда по запросу требуется получить данные из двух разных источников. Рассмотрим простой пример, в котором два вложенных оператораfromиспользуются в запросе для циклического обращения к элементам двух разных массивов символов. В итоге по такому запросу формируется последовательность результатов, содержащая все возможные комбинации двух наборов символов.
// Использовать два вложенных оператора from для составления списка // всех возможных сочетаний букв А, В и С с буквами X, Y и Z.
using System; using System.Linq;
// Этот класс содержит результат запроса, class ChrPair {
public char First;
public char Second;
public ChrPair(char c, char c2) {
First = c;
Second = c2;
}
}
class MultipleFroms { static void Main() {
char[] chrs = { 'A', 1В', 'C' };
char[] chrs2 = { 'X', 1Y', 'Z' };
// В первом операторе from организуется циклическое обращение //к массиву символов chrs, а во втором операторе from —
// циклическое обращение к массиву символов chrs2. var pairs = from chi in chrs
from ch2 in chrs2
select new ChrPair(chl, ch2);
Console.WriteLine("Все сочетания букв ABC и XYZ: "); foreach(var p in pairs)
Console.WriteLine("{0} {1}", p.First, p.Second);
}
}
Выполнение этого кода приводит к следующему результату.
Все сочетания букв ABC и XYZ:
А X A Y A Z В X В Y В Z С X С Y С Z
Этот пример кода начинается с создания классаChrPair,в котором содержатся результаты запроса. Затем в нем создаются два массива,chrsиchrs2, и, наконец, формируется следующий запрос для получения всех возможных комбинаций двух последовательностей результатов.
var pairs = from chi in chrs
from ch2 in chrs2
select new ChrPair(chi, ch2);
Во вложенных операторахfromорганизуется циклическое обращение к обоим массивам символов,chrsиchrs2.Сначала из массиваchrsполучается символ, сохраняемый в переменнойchi.Затем перечисляется содержимое массиваchrs2.На каждом шаге циклического обращения во внутреннем оператореfromсимвол из массиваchrs2сохраняется в переменнойch2и далее выполняется операторselect.В результате выполнения оператораselectсоздается новый объект типаChrPair,
содержащий пару символов, которые сохраняются в переменныхchiиch2на каждом шаге циклического обращения к массиву во внутреннем оператореfrom.А в конечном итоге получается объект типаChrPair,содержащий все возможные сочетания извлекаемых символов.
Вложенные'операторыfromприменяются также для циклического обращения к источнику данных, который содержится в другом источнике данных. Соответствующий пример приведен в разделе "Применение оператораletдля создания временной переменной в запросе" далее в этой главе.
Группирование результатов с помощью оператора group
Одним из самых эффективных средств формирования запроса является операторgroup,поскольку он позволяет группировать полученные результаты по ключам. Используя последовательность сгруппированных результатов, можно без особого труда получить доступ ко всем данным, связанным с ключом. Благодаря этому свойству оператораgroupдоступ к данным, организованным в последовательности связанных элементов, осуществляется просто и эффективно. Операторgroupявляется одним из двух операторов, которыми может оканчиваться запрос. (Вторым оператором, завершающим запрос, являетсяselect.)Ниже приведена общая форма оператораgroup.
groupпеременная_диапазонаbyключ
Этот оператор возвращает данные, сгруппированные в последовательности, причем каждую последовательность обозначает общийключ.
Результатом выполнения оператораgroupявляется последовательность, состоящая из элементов типаIGrouping<TKey, TElement>,т.е. обобщенного интерфейса, объявляемого в пространстве именSystem. Linq.В этом интерфейсе определена коллекция объектов с общим ключом. Типом переменной запроса, возвращающего группу, являетсяIEnumerable<IGrouping<TKey, TElement>>.В интерфейсеIGroupingопределено также доступное только для чтения свойствоKey,возвращающее ключ, связанный с каждой коллекцией.
Ниже приведен пример, демонстрирующий применение оператораgroup.В коде этого примера сначала объявляется массив, содержащий список веб-сайтов, а затем формируется запрос, в котором этот список группируется по имени домена самого верхнего уровня, например .orgили . сот.
// Продемонстрировать применение оператора group.
using System; using System.Linq;
class GroupDemo {
static void Main() {
string[] websites = { "hsNameA.com", "hsNameB.net", "hsNameC.net", "hsNameD.com", "hsNameE.org", "hsNameF.org",
"hsNameG.tv", "hsNameH.net", "hsNamel.tv"
};
// Сформировать запрос на получение списка веб-сайтов,
// группируемых по имени домена самого верхнего уровня.
var webAddrs = from addr in websites
where addr.LastlndexOf('.') != -1
group addr by addr.Substring(addr.LastlndexOf('.'));
// Выполнить запрос и вывести его результаты, foreach(var sites in webAddrs) {
Console.WriteLine("Веб-сайты, сгруппированные " +
"по имени домена" + sites.Key); foreach(var site in sites)
Console.WriteLine (" " + site);
Console.WriteLine();
}
}
}
Вот к какому результату приводит выполнение этого кода.
Веб-сайты, сгруппированные по имени домена .сот
hsNameA.сот hsNameD.сот
Веб-сайты, сгруппированные по имени домена .net hsNameB.net hsNameC.net
hsNameH.net *
Веб-сайты, сгруппированные по имени домена .org hsNameE.org hsNameF.org
Веб-сайты, сгруппированные по имени домена .tv hsNameG.tv hsNamel.tv
Как следует из приведенного выше результата, данные, получаемые по запросу, группируются по имени домена самого верхнего уровня в адресе веб-сайта. Обратите внимание на то, как это делается в оператореgroupиз следующего запроса.
var webAddrs = from addr in websites
where addr.LastlndexOf('.') != -1
group addr by addr.Substring(addr.LastlndexOf('.'));
Ключ в этом операторе создается с помощью методовLastlndexOf() иSubstring (), определенных для данных типаstring.(Эти методы упоминаются в главе 7, посвященной массивам и строкам. Вариант методаSubstring (), используемый в данном примере, возвращает подстроку, начинающуюся с места, обозначаемого индексом, и продолжающуюся до конца вызывающей строки.) Индекс последней точки в адресе веб-сайта определяется с помощью методаLastlndexOf().По этому индексу в методеSubstring() создается оставшаяся часть строки, в которой содержится имя домена самого верхнего уровня. Обратите внимание на то, что в оператореwhereотсеиваются все строки, которые не содержат точку. МетодLastlndexOf() возвращает -1, если указанная подстрока не содержится в вызывающей строке.
Последовательность результатов, получаемых при выполнении запроса, хранящегося в переменнойwebAddrs,представляет собой список групп, поэтому для доступа к каждому члену группы требуются два циклаforeach.Доступ к каждой группе осуществляется во внешнем цикле, а члены внутри группы перечисляются во внутреннем цикле. Переменная шага внешнего циклаforeachдолжна быть экземпляром интерфейсаI Grouping,совместимым с ключом и типом элемента данных. В рассматриваемом здесь примере ключи и элементы данных относятся к типуstring.Поэтому переменнаяsitesшага внешнего цикла имеет типIGrouping<string, string>,а переменнаяsiteшага внутреннего цикла — типstring.Ради краткости данного примера обе переменные объявляются неявно, хотя их можно объявить и явным образом, как показано ниже.
foreach(IGrouping<string, string> sites in webAddrs) {
Console.WriteLine("Веб-сайты, сгруппированные " +
"по имени домена" + sites.Key); foreach(string site in sites)
Console.WriteLine(" " + site);
Console.WriteLine ();
}
Продолжение запроса с помощью оператора into
При использовании в запросе оператораselectилиgroupиногда требуется сформировать временный результат, который будет служитьпродолжением запросадля получения окончательного результата. Такое продолжение осуществляется с помощью оператораintoв комбинации с операторомselectилиgroup.Ниже приведена общая форма оператораinto:
intoимя тело_запроса
гдеимяобозначает конкретное имя переменной диапазона, используемой для циклического обращения к временному результату в продолжении запроса, на которое указываеттело_запроса.Когда операторintoиспользуется вместе с операторомselectилиgroup,то его называют продолжением запроса, поскольку он продолжает запрос. По существу, продолжение запроса воплощает в себе принцип построения нового запроса по результатам предыдущего.
ПРИМЕЧАНИЕ
Существует также форма оператора into, предназначенная для использования вместе с оператором join, создающим групповое объединение, о котором речь пойдет далее в этой главе.
Ниже приведен пример программы, в которой операторintoиспользуется вместе с операторомgroup.Эта программа является переработанным вариантом предыдущего примера, в котором список веб-сайтов формируется по имени домена самого верхнего уровня. А в данном примере первоначальные результаты запроса сохраняются в переменной диапазонаwsи затем отбираются для исключения всех групп, состоящих менее чем из трех элементов.
// Использовать оператор into вместе с оператором group.
using System; using System.Linq;
class IntoDemo {
static void Main() {
string[] websites = { "hsNameA.com", "hsNameB.net", "hsNameC.net", "hsNameD.com", "hsNameE.org", "hsNameF.org",
"hsNameG.tv", "hsNameH.net", "hsNamel.tv"
};
// Сформировать запрос на получение списка веб-сайтов, группируемых //по имени домена самого верхнего уровня, но выбрать только те // группы, которые состоят более чем из двух членов.
// Здесь ws — это переменная диапазона для ряда групп,
// возвращаемых при выполнении первой половины запроса, var webAddrs = from addr in websites
where addr.LastlndexOf(1.1) != -1
group addr by addr.Substring(addr.LastlndexOf('.'))
into ws where ws.Count() > 2 select ws;
// Выполнить запрос и вывести его результаты.
Console.WriteLine("Домены самого верхнего уровня " +
"с более чем двумя членами.\п");
foreach(var sites in webAddrs) {
Console.WriteLine("Содержимое домена: " + sites.Key); foreach(var site in sites)
Console.WriteLine (" " + site);
Console.WriteLine();
}
}
}
Эта программа дает следующий результат:
Домены самого верхнего уровня с более чем двумя членами.
Содержимое домена: .net hsNameB.net hsNameC.net hsNameH.net
Как следует из результата выполнения приведенной выше программы, по запросу возвращается только группа .net, поскольку это единственная группа, содержащая больше двух элементов.
Обратите особое внимание в данном примере программы на следующую последовательность операторов в формируемом запросе.
group addr by addr.Substring(addr.LastlndexOf('.'))
into ws where ws.Count() > 2 select ws;
Сначала результаты выполнения оператораgroupсохраняются как временные для последующей обработки операторомwhere.В качестве переменной диапазона в данный момент служит переменнаяws.Она охватывает все группы, возвращаемые операторомgroup.Затем результаты запроса отбираются в оператореwhereс таким расчетом, чтобы в конечном итоге остались только те группы, которые содержат больше двух членов. Для этой цели вызывается методCount (), который являетсяметодом расширенияи реализуется для всех объектов типаI Enumerable.Он возвращает количество элементов в последовательности. (Подробнее о методах расширения речь пойдет далее в этой главе.) А получающаяся в итоге последовательность групп возвращается операторомselect.
Применение оператора let для создания временной переменной в запросе
Иногда возникает потребность временно сохранить некоторое значение в самом запросе. Допустим, что требуется создать переменную перечислимого типа, которую можно будет затем запросить, или же сохранить некоторое значение, чтобы в дальнейшем использовать его в оператореwhere.Независимо от преследуемой цели, эти виды функций могут быть осуществлены с помощью оператораlet.Ниже приведена общая форма оператораlet:
letимя = выражение
гдеимяобозначает идентификатор, получающий значение, которое даетвыражение.Тип имени выводится из типа выражения.
В приведенном ниже примере программы демонстрируется применение оператораletдля создания еще одного перечислимого источника данных. В качестве входных данных в запрос вводится массив символьных строк, которые затем преобразуются в массивы типаchar.Для этой цели служит еще один метод обработки строк, называемыйToCharArray() и возвращающий массив, содержащий символы в строке. Полученный результат присваивается переменнойchrArray,которая затем используется во вложенном оператореfromдля извлечения отдельных символов из массива. И наконец, полученные символы сортируются в запросе, и из них формируется результирующая последовательность.
// Использовать оператор let в месте с вложенным оператором from.
using System; using System.Linq;
class LetDemo {
static void Main() {
string[] strs = { "alpha", "beta", "gamma" };
// Сформировать запрос на получение символов, возвращаемых из // строк в отсортированной последовательности. Обратите внимание // на применение вложенного оператора from, var chrs = from str in strs
let chrArray = str.ToCharArray()
from ch in chrArray orderby ch select ch;
Console.WriteLine("Отдельные символы, отсортированные по порядку:");
// Выполнить запрос и вывести его результаты, foreach(char с in chrs) Console.Write(с + " ");
Console.WriteLine();
}
}
Вот к какому результату приводит выполнение этой программы.
Отдельные символы, отсортированные по порядку: aaaaabeghlmmpt
Обратите внимание в данном примере программы на то, что в оператореletпеременнойchrArrayприсваивается ссылка на массив, возвращаемый методомstr.
ToCharArray().
let chrArray = str.ToCharArray()
После оператораletпеременнаяchrArrayможет использоваться в остальных операторах, составляющих запрос. А поскольку все массивы в C# преобразуются в типIEnumerable<T>,то переменнуюchrArrayможно использовать в качестве источника данных для запроса во втором, вложенном оператореfrom.Именно это и происходит в рассматриваемом здесь примере, где вложенный операторfromслужит для перечисления в массиве отдельных символов, которые затем сортируются по нарастающей и возвращаются в виде конечного результата.
Операторletможет также использоваться для хранения неперечислимого значения. В качестве примера ниже приведен более эффективный вариант формирования запроса в программеIntoDemoиз предыдущего раздела.
var webAddrs = from addr in websites
let idx = addr.LastlndexOf('.') where idx != -1
group addr by addr.Substring(idx) into ws where ws.Count() > 2 select ws;
В этом варианте индекс последнего вхождения символа точки в строку присваивается переменнойidx.Данное значение затем используется в методеSubstring ().Благодаря этому исключается необходимость дважды искать символ точки в строке.
Объединение двух последовательностей с помощью оператора join
Когда приходится иметь дело с базами данных, то зачастую требуется формировать последовательность, увязывающую данные из разных источников. Например, в Интернет-магазине может быть организована одна база данных, связывающая наименование товара с его порядковым номером, и другая база данных, связывающая порядковый номер товара с состоянием его запасов на складе. В подобной ситуации может возникнуть потребность составить список, в котором состояние запасов товаров на складе отображается по их наименованию, а не порядковому номеру. Для этой цели придется каким-то образом "увязать" данные из двух разных источников (баз данных). И это нетрудно сделать с помощью такого средства LINQ, как оператор join.
Ниже приведена общая форма оператораjoin(совместно с операторомfrom).
fromпеременная_диапазона_Аinисточник_данных_Аjoinпеременная_диапазона_Вinисточник_данных_В
onпеременная_диапазона_А. свойствоequalsпеременная_диапазона_В.свойство
Применяя оператор join, следует иметь в виду, что каждый источник должен содержать общие данные, которые можно сравнивать. Поэтому в приведенной выше форме этого оператораисточник_данных_Аиисточник_данных_Вдолжны иметь нечто общее, что подлежит сравнению. Сравниваемые элементы данных указываются в части on данного оператора. Поэтому еслипеременная_диапазона_А. свойствоипеременная_диапазона_А.свойстворавны, то эти элементы данных "увязываются" успешно. По существу, оператор join выполняет роль своеобразного фильтра, отбирая только те элементы данных, которые имеют общее значение.
Как правило, оператор join возвращает последовательность, состоящую из данных, полученных из двух источников. Следовательно, с помощью оператора j о in можно сформировать новый список, состоящий из элементов, полученных из двух разных источников данных. Это дает возможность организовать данные по-новому.
Ниже приведена программа, в которой создается классItem,инкапсулирующий наименование товара и его порядковый номер. Затем в этой программе создается еще один классInStockStatus,связывающий порядковый номер товара с булевым свойством, которое указывает на наличие или отсутствие товара на складе. И наконец, в данной программе создается классTempс двумя полями: строковым(string)и булевым(bool).В объектах этого класса будут храниться результаты запроса. В этом запросе операторjoinиспользуется для получения списка, в котором наименование товара связывается с состоянием его запасов на складе.
// Продемонстрировать применение оператора join.
using System; using System.Linq;
// Класс, связывающий наименование товара с его порядковым номером, class Item {
public string Name { get; set; } public int ItemNumber { get; set; }
public Item(string n, int inum) {
Name = n;
ItemNumber = inum;
}
}
// Класс, связывающий наименование товара с состоянием его запасов на складе, class InStockStatus {
public int ItemNumber { get; set; } public bool InStock { get; set; }
public InStockStatus (int n, bool b) {
ItemNumber = n;
InStock = b;
}
}
// Класс, инкапсулирующий наименование товара и // состояние его запасов на складе, class Temp {
public string Name { get; set; } public bool InStock { get; set; }
public Temp(string n, bool b) {
Name = n;
InStock = b;
}
}
class JoinDemo {
static void Main() {
Item[] items = {
new Item("Кусачки", 1424), new Item("Тиски", 7892), new Item("Молоток", 8534), new Item("Пила", 6411)
};
InStockStatus[] statusList = {
new InStockStatus(1424, true), new InStockStatus(7892, false), new InStockStatus(8534, true),
new InStockStatus(6411, true) 4
};
// Сформировать запрос, объединяющий объекты классов Item //и InStockStatus для составления списка наименований товаров // и их наличия на складе. Обратите внимание на формирование // последовательности объектов класса Temp, var inStockList = from item in items
join entry in statusList
on item.ItemNumber equals entry.ItemNumber select new Temp(item.Name, entry.InStock);
9
Console.WriteLine("Товар\^Наличие\п");
// Выполнить запрос и вывести его результаты. foreach(Temp t in inStockList)
Console.WriteLine("{0}\t{l}", t.Name, t.InStock);
}‘
Товар Наличие
Кусачки True Тиски - False Молоток True Пила True
Для того чтобы стал понятнее принцип действия оператораjoin,рассмотрим каждую строку запроса из приведенной выше программы по порядку. Этот запрос начинается, как обычно, со следующего оператораfrom.
var inStockList = from item in items
В этом операторе указывается переменная диапазонаitemдля источника данныхitems,который представляет собой массив объектов классаItem.В классеItemинкапсулируются наименование товара и порядковый номер товара, хранящегося на складе.
Далее следует приведенный ниже операторjoin.
join entry in statusList
on item.ItemNumber equals entry.ItemNumber
В этом операторе указывается переменная диапазонаentryдля источника данныхstatusList,который представляет собой массив объектов классаInStockStatus,связывающего порядковый номер товара с состоянием его запасов на складе. Следовательно, у массивовitemsиstatusListимеется общее свойство: порядковый номер товара. Именно это свойство используется в частиon/equalsоператораjoinдля описания связи, по которой из двух разных источников данных выбираются наименования товаров, когда их порядковые номера совпадают.
И наконец, операторselectвозвращает объект классаTemp,содержащий наименование товара и состояние его запасов на складе.
select new Temp(item.Name, entry.InStock);
Таким образом, последовательность результатов, получаемая по данному запросу, состоит из объектов типаTemp.
Рассмотренный здесь пример применения оператораjoinдовольно прост. Тем не менее этот оператор поддерживает и более сложные операции с источниками данных. Например, используя совместно операторыintoиjoin,можно создатьгрупповое объединение,чтобы получить результат, состоящий из первой последовательности и группы всех совпадающих элементов из второй последовательности. (Соответствующий пример будет приведен далее в этой главе.) Как правило, время и усилия, затраченные на полное освоение оператораjoin,окупаются сторицей, поскольку он дает возможность распознавать данные во время выполнения программы. Это очень ценная возможность. Но она становится еще ценнее, если используются анонимные типы, о которых речь пойдет в следующем разделе.
Анонимные типы
В C# предоставляется средство, называемоеанонимным типоми связанное непосредственно с LINQ. Как подразумевает само название, анонимный тип представляет собой класс, не имеющий имени. Его основное назначение состоит в создании объекта, возвращаемого оператором select. Результатом запроса нередко оказывается последовательность объектов, которые составляются из членов, полученных из двух или более источников данных (как, например, в операторе join), или же включают в себя подмножество членов из одного источника данных. Но в любом случае тип возвращаемого объекта зачастую требуется только в самом запросе и не используется в остальной части программы. Благодаря анонимному типу в подобных случаях отпадает необходимость объявлять класс, который предназначается только для хранения результата запроса.
Анонимный тип объявляется с помощью следующей общей формы:
new {имя_А = значение_А, имя_В = значение_В,... }
где имена обозначают идентификаторы, которые преобразуются в свойства, доступные только для чтения и инициализируемые значениями, как в приведенном ниже примере.
new { Count = 10, Max = 100, Min = 0 }
В данном примере создается класс с тремя открытыми только для чтения свойствами: Count, Мах и Min, которым присваиваются значения 10, 100 и 0 соответственно. К этим свойствам можно обращаться по имени из другого кода. Следует заметить, что в анонимном типе используются инициализаторы объектов для установки их полей и свойств в исходное состояние. Как пояснялось в главе 8, инициализаторы объектов обеспечивают инициализацию объекта без явного вызова конструктора. Именно это и требуется для анонимных типов, поскольку явный вызов конструктора для них невозможен. (Напомним, что у конструкторов такое же имя, как и у их класса. Но у анонимного класса нет имени, а значит, и нет возможности вызвать его конструктор.)
Итак, у анонимного типа нет имени, и поэтому для обращения к нему приходится использовать неявно типизированную переменную. Это дает компилятору возможность вывести надлежащий тип. В приведенном ниже примере объявляется переменная шуОЬ, которой присваивается ссылка на объект, создаваемый в выражении анонимного типа.
var myOb = new { Count = 10, Max = 100, Min = 0 }
Это означает, что следующие операторы считаются вполне допустимыми.
Console.WriteLine("Счет равен " + myOb.Count);
if(i <= myOb.Max && i >= myOb.Min) // ...
Напомним, что при создании объекта анонимного типа указываемые идентификаторы становятся свойствами, открытыми только для чтения. Поэтому их можно использовать вдругихчастях кода.
Терминанонимный типне совсем оправдывает свое название. Ведь тип оказывается анонимным только для программирующего, но не для компилятора, который присваивает ему внутреннее имя. Следовательно, анонимные типы не нарушают принятые в C# правила строгого контроля типов.
Для того чтобы стало более понятным особое назначение анонимных типов, рассмотрим переделанную версию прорраммы из предыдущего раздела, посвященного оператору join. Напомним, что в этой программе класс Temp требовался для инкапсуляции результата, возвращаемого оператором join. Благодаря применению
анонимного типа необходимость в этом классе-заполнителе отпадает, а исходный код программы становится менее громоздким. Результат выполнения программы при этом не меняется.
// Использовать анонимный тип для усовершенствования // программы, демонстрирующей применение оператора join-.
using System; using System.Linq;
// Класс, связывающий наименование товара с его порядковым номером, class Item {
public string Name { get; set; } public int ItemNumber { get; set; }
public Item(string nv int inum) {
Name = n;
ItemNumber = inum;
}
}
// Класс, связывающий наименование товара с состоянием его запасов на складе, class InStockStatus {
public int ItemNumber { get; set; } public bool InStock { get; set; }
public InStockStatus(int n, bool b) {
ItemNumber = n;
InStock = b;
}
}
class AnonTypeDemo { static void Main() {
Item[] items = {
new Item("Кусачки", 1424), new Item("Тиски", 7892), new Item("Молоток", 8534), new Item("nnna", 6411)
};
InStockStatus[] statusList = {
new InStockStatus(1424, true), new InStockStatus(7892, false), new InStockStatus(8534, true), new InStockStatus (6411, true)
};
// Сформировать запрос, объединяющий объекты классов Item и // InStockStatus для составления списка наименований товаров и их // наличия на складе. Теперь для этой цели используется анонимный тип. var inStockList = from item in items
join entry in statusList
on item.ItemNumber equals entry.ItemNumber select new { Name = item.Name,
InStock = entry.InStock };
Console .WriteLine ("Товар\Маличие\п") ;
// Выполнить запрос и вывести его результаты, foreach(var t in inStockList)
Console.WriteLine("{0}\t{1}", t.Name, t.InStock);
}
}
Обратите особое внимание на следующий операторselect.
select new { Name = item.Name,
InStock = entry.InStock };
Он возвращает объект анонимного типа с двумя доступными только для чтения свойствами:NameиInStock.Этим свойствам присваиваются наименование товара и состояние его наличия на складе. Благодаря применению анонимного типа необходимость в упоминавшемся выше классеTempотпадает.
Обратите также внимание на циклforeach,в котором выполняется запрос. Теперь переменная шага этого цикла объявляется с помощью ключевого словаvar.Это необходимо потому, что у типа объекта, хранящегося в переменнойinStockList,нет имени. Данная ситуация послужила одной из причин, по которым в C# были внедрены неявно типизированные переменные, поскольку они нужны для поддержки анонимных типов.
Прежде чем продолжить изложение, следует отметить еще один заслуживающий внимания аспект анонимных типов. В некоторых случаях, включая и рассмотренный выше, синтаксис анонимного типа упрощается благодаря применениюинициализатора проекции.В данном случае просто указывается имя самого инициализатора. Это имя автоматически становится именем свойства. В качестве примера ниже приведен другой вариант оператораselectиз предыдущей программы.
select new { item.Name, entry.InStock };
В данном примере имена свойств остаются такими же, как и прежде, а компилятор автоматически "проецирует" идентификаторыNameиInStock,превращая их в свойства анонимного типа. Этим свойствам присваиваются прежние значения, обозначаемыеitem.Nameиentry. InStockсоответственно.
Создание группового объединения
Как пояснялось ранее, операторintoможно использовать вместе с операторомjoinдля созданиягруппового объединения, образующего последовательность, в которой каждый результат состоит из элементов данных из первой последовательности и группы всех совпадающих элементов из второй последовательности. Примеры группового объединения не приводились выше потому, что в этом объединении нередко применяется анонимный тип. Но теперь, когда представлены анонимные типы, можно обратиться к простому примеру группового объединения.
В приведенном ниже примере программы групповое объединение используется для составления списка, в котором различные транспортные средства (автомашины, суда и самолеты) организованы по общим для них категориям транспорта: наземного, морского, воздушного и речного. В этой программе сначала создается классTransport,связывающий вид транспорта с его классификацией. Затем в методеMain() формируются две входные последовательности. Первая из них представляет собой массив символьных строк, содержащих названия общих категорий транспорта: наземного, морского, воздушного и речного, а вторая — массив объектов типаTransport,инкапсулирующих различные транспортные средства. Полученное в итоге групповое объединение используется для составления списка транспортных средств, организованных по соответствующим категориям.
// Продемонстрировать применение простого группового объединения.
using System; using System.Linq;
*
// Этот класс связывает наименование вида транспорта,
// например поезда, с общей классификацией транспорта:
// наземного, морского, воздушного или речного, class Transport {
public string Name { get; set; } public string How { get; set; }
public Transport(string n, string h) {
Name = n;
How = h;
}
}
class GroupJoinDemo { static void Main() {
// Массив классификации видов транспорта, string[] travelTypes = {
"Воздушный",
"Морской",
"Наземный",
"Речной",
};
// Массив видов транспорта.
Transport[] transports = { 1
new Transport("велосипед", "Наземный"), new Transport ("аэростат", "Воздушный"), new Transport("лодка", "Речной"), new Transport("самолет", "Воздушный"), new Transport("каноэ", "Речной"), new Transport("биплан", "Воздушный"), new Transport("автомашина", "Наземный"), new Transport("судно", "Морской"), new Transport("поезд", "Наземный")
};
// Сформировать запрос, в котором групповое // объединение используется для составления списка
// видов транспорта по соответствующим категориям, var byHow = from how in travelTypes
join trans in transports on how equals trans.How into 1st
select new { How = how, Tlist = 1st };
// Выполнить запрос и вывести его результаты, foreach(var t in byHow) {
Console.WriteLine("К категории <{0} транспорт> относится:", t.How);
foreach(var m in t.Tlist)
Console.WriteLine(" " + m.Name);
Console.WriteLine();
}
}
}
Ниже приведен результат выполнения этой программы.
К категории <Воздушный транспорт> относится: аэростат самолет биплан
К категории <Морской транспорт> относится: судно
К категории <Наземный транспорт> относится: велосипед автомашина поезд
К категории <Речной транспорт> относится: лодка каноэ
Главной частью данной программы, безусловно, является следующий запрос.
var byHow = from how in travelTypes
join trans in transports on how equals trans.How into 1st
select new { How = how, Tlist = 1st };
Этот запрос формируется следующим образом. В операторе from используется переменная диапазона how для охвата всего массива travelTypes. Напомним, что массив travelTypes содержит названия общих категорий транспорта: воздушного, наземного, морского и речного. Каждый вид транспорта объединяется в операторе join со своей категорией. Например, велосипед, автомашина и поез^объедйняются с наземным транспортом. Но благодаря оператору into для каждой категории транспорта в операторе join составляется список видов транспорта, относящихся к данной категории. Этот список сохраняется в переменной 1st. И наконец, оператор select возвращает объект анонимного типа, инкапсулирующий каждое значение переменнойhow(категории транспорта) вместе со списком видов транспорта. Именно поэтому для вывода результатов запроса требуются два циклаforeach.
foreach(var t in byHow) {
Console.WriteLine("К категории <{0} транспорт> относится:", t.How);
foreach(var m in t.Tlist)
Console.WriteLine(" " + m.Name);
Console.WriteLine();
}
Во внешнем цикле получается объект, содержащий наименование общей категории транспорта, и список видов транспорта, относящихся к этой категории. А во внутреннем цикле выводятся отдельные виды транспорта.
Методы запроса
Синтаксис запроса, описанный в предыдущих разделах, применяется при формировании большинства запросов в С#. Он удобен, эффективен и компактен, хотя и не является единственным способом формирования запросов. Другой способ состоит в использованииметодов запроса, которые могут вызываться для любого перечислимого объекта, например массива.
Основные методы запроса
Методы запроса определяются в классеSystem. Linq. Enumerableи реализуются ввиде методов расширенияфункций обобщенной формы интерфейсаIEnumerable<T>.(Методы запроса определяются также в классеSystem. Linq. Queryable,расширяющем функции обобщенной формы интерфейсаIQueryable<T>,но этот интерфейс в настоящей главе не рассматривается.) Метод расширения дополняет функциидругогокласса, но без наследования. Поддержка методов расширения была внедрена в версию C# 3.0 и более подробно рассматривается далее в этой главе. А до тех пор достаточно сказать, что методы запроса могут вызываться только для тех объектов, которые реализуют интерфейсIEnumerable<T>.
В классеEnumerableпредоставляется немало методов запроса, но основными считаются те методы, которые соответствуют описанным ранее операторам запроса. Эти методы перечислены ниже вместе с соответствующими операторами запроса. Следует, однако, иметь в виду, что эти методы имеют также перегружаемые формы, а здесь они представлены лишь в самой простой своей форме. Но именно эта их форма используется чаще всего. v
Оператор запроса
Эквивалентный метод запроса
select
Select(selector)
where
Where(predicate)
orderby
OrderBy(keySelector)или OrderByDescending(keySelector)
join
Join(inner, outerKeySelector, innerKeySelector, resultSelector)
group
GroupBy(keySelector)
За исключением методаJoin(), остальные методы запроса принимают единственный аргумент, который представляет собой объект некоторой разновидности обобщенного типаFunc<T, TResultxЭто тип встроенного делегата, объявляемый следующим образом:
delegate TResult Funccin Т, out TResult>(Т arg)
гдеTResultобозначает тип результата, который дает делегат, аТ— тип элемента. В методах запроса аргументыselector,predicateилиkeySelectorопределяют действие, которое предпринимает метод запроса. Например, в методеWhere() аргументpredicateопределяет порядок отбора данных в запросе. Каждый метод запроса возвращает перечислимый объект. Поэтому результат выполнения одного метода запроса можно использовать для вызова другого, соединяя эти методы в цепочку.
МетодJoin() принимает четыре аргумента. Первый аргумент(inner)представляет собой ссылку на вторую объединяемую последовательность, а первой является последовательность, для которой вызывается методJoin (). Селектор ключа для первой последовательности передается в качестве аргументаouterKeySelector,аселектор ключа для второй последовательности — в качестве аргументаinnerKeySelector.Результат объединения обозначается как аргументresultSelector.АргументouterKeySelectorимеет типFunc<T0uter,ТКеу>,аргументinnerKeySelector— типFunc<TInner,ТКеу>,тог^а как аргументresultSelector— типFunc<T0uter, Tinner, TResult>,гдеTOuter— тип элемента из вызывающей последовательности;Tinner— тип элемента из передаваемой последовательности;TResult— тип элемента из объединяемой в итоге последовательности, возвращаемой в виде перечислимого объекта.
Аргумент метода запроса представляет собой метод, совместимый с указываемой формой делегатаFun с,но он не обязательно должен быть явно объявляемым методом. На самом деле вместо него чаще всего используется лямбда-выражение. Как пояснялось в главе 15, лямбда-выражение обеспечивает более простой, но эффективный способ определения того, что, по существу, является анонимным методом, а компилятор C# автоматически преобразует лямбда-выражение в форму, которая может быть передана в качестве параметра делегатуFun с.Благодаря тому что лямбда-выражения обеспечивают более простой и рациональный способ программирования, они используются во всех примерах, представленных далее в этом разделе.
Формирование запросов с помощью методов запроса
Используя методы запроса одновременно с лямбда-выражениями, можно формировать запросы, вообще не пользуясь синтаксисом, предусмотренным в C# для запросов. Вместо этого достаточно вызвать соответствующие методы запроса. Обратимся сначала к простому примеру. Он представляет собой вариант первого примера программы из этой главы, переделанный с целью продемонстрировать применение методов запросаWhere() иSelect() вместо соответствующих операторов.
// Использовать методы запроса для формирования простого запроса.
// Это переделанный вариант первого примера программы из настоящей главы.
using System; using System.Linq;
class SimpQuery {
static void Main() {
int[] nums = { 1, -2, 3, О, -4, 5 };
// Использовать методы Where() и Select () для // формирования простого запроса.
var posNums = nums.Where(n => n > 0).Select(r => r);
Console.Write("Положительные значения из массива nums: ");
// Выполнить запрос и вывести его результаты, foreach(int i in posNums) Console.Write(i + " ") ;
Console.WriteLine ();
}
}
Эта версия программы дает такой же результат, как и исходная.
Положительные значения из массива nums: 13 5
Обратите особое внимание в данной программе на следующую строку кода.
var posNums = nums.Where(n => n > 0).Select(r => r);
В этой строке кода формируется запрос, сохраняемый в переменнойposNums.По этому запросу, в свою очередь, формируется последовательность положительных значений, извлекаемых из массиваnums.Для этой цели служит методWhere (), отбирающий запрашиваемые значения, а также методSelect (), избирательно формирующий из этих значений окончательный результат. МетодWhere() может быть вызван для массиваnums,поскольку во всех массивах реализуется интерфейсIEnumerable<T>,поддерживающий методы расширения запроса.
Формально методSelect() в рассматриваемом здесь примере не нужен, поскольку это простой запрос. Ведь последовательность, возвращаемая методомWhere (), уже содержит конечный результат. Но окончательный выбор можно сделать и по более сложному критерию, как это было показано ранее на примерах использования синтаксиса запросов. Так, по приведенному ниже запросу из массиваnumsвозвращаются положительные значения, увеличенные на порядок величины.
var posNums = nums.Where(n => n > 0) .Select (r => r * 10);
Как и следовало ожидать, в цепочку можно объединять и другие операции над данными, получаемыми по запросу. Например, по следующему запросу выбираются положительные значения, которые затем сортируются по убывающей и возвращаются в виде результирующей последовательности:
var posNums = nums.Where(n => n > 0).OrderByDescending(j => j);
где выражениеj=>jобозначает, что упорядочение зависит от входного параметра, который является элементом данных из последовательности, получаемой из методаWhere().
В приведенном ниже примере демонстрируется применение метода запросаGroupBy (). Это измененный вариант представленного ранее примера.
// Продемонстрировать применение метода запроса GroupBy().
// Это переработанный вариант примера, представленного ранее // для демонстрации синтаксиса запросов.
using System; using System.Linq;
class GroupByDemo { static void Main() {
string[] websites = {
"hsNameA.com", "hsNameB.net", "hsNameC.net",
’"hsNameD.com", "hsNameE.org", "hsNameF.org",
"hsNameG.tv", "hsNameH.net", "hsNamel.tv"
};
// Использовать методы запроса для группирования
// веб-сайтов по имени домена самого верхнего уровня.
var webAddrs = websites.Where(w => w.LastlndexOf) != 1).
GroupBy(x => x.Substring(x.LastlndexOf(".", x.Length)));
// Выполнить запрос и вывести его результаты, foreach(var sites in webAddrs) {
Console.WriteLine("Веб-сайты, сгруппированные " +
"по имени домена " + sites.Key); foreach(var site in sites)
Console.WriteLine (" " + site);
Console.WriteLine();
}
}
}
Эта версия программы дает такой же результат, как и предыдущая. Единственное отличие между ними заключается в том, как формируется запрос. В данной версии для этой цели используются методы запроса.
Рассмотрим другой пример. Но сначала приведем еще раз запрос из представленного ранее примера применения оператораjoin.
var inStockList = from item in items
join entry in statusList
on item.ItemNumber equals entry.ItemNumber select new Temp(item.Name, entry.InStock);
По этому запросу формируется последовательность, состоящая из объектов, инкапсулирующих наименование товара и состояние его запасов на складе. Вся эта информация получается путем объединения двух источников данных:itemsиstatusList.Ниже приведен переделанный вариант данного запроса, в котором вместо синтаксиса, предусмотренного в C# для запросов, используется метод запросаJoin ().
// Использовать метод запроса Join() для составления списка // наименований товаров и состояния их запасов на складе, var inStockList = items.Join(statusList, kl => kl.ItemNumber, k2 => k2.ItemNumber,
(kl, k2) => new Temp(kl.Name, k2.InStock) );
В данном варианте именованный классTempиспользуется для хранения результирующего объекта, но вместо него можно воспользоваться анонимным типом. Такой вариант запроса приведен ниже.
var inStockList = items.Join(statusList, kl => kl.ItemNumber,
к2 => к2.ItemNumber,
(kl, к2) => new { kl.Name, k2.InStock} );
Синтаксис запросов и методы запроса
Как пояснялось в предыдущем разделе, запросы в C# можно формировать двумя способами, используя синтаксис запросов или методы запроса. Любопытно, что оба способа связаны друг с другом более тесно, чем кажется, глядя на исходный код программы. Дело в том, что синтаксис запросов компилируется в вызовы методов запроса. Поэтому код
where х < 10
будет преобразован компилятором в следующий вызов.
Where(х => х < 10)
Таким образом, оба способа формирования запросов в конечном итоге сходятся на одном и том же.
Но если оба способа оказываются в конечном счете равноценными, то какой из них лучше для программирования на С#? В целом, рекомендуется чаще пользоваться синтаксисом запросов, поскольку он полностью интегрирован в язык С#, поддерживается соответствующими ключевыми словами и синтаксическим конструкциями.
Дополнительные методы расширения, связанные с запросами
Помимо методов, соответствующих операторам запроса, поддерживаемым в С#, име-ется ряд других методов расширения, связанных с запросами и зачастую оказывающих помощь в формировании запросов. Эти методы предоставляются в среде .NET Framework и определены для интерфейса IEnumerable<T> в классе Enumerable. Ниже приведены наиболее часто используемые методы расширения, связанные с запросами. Многие из них могут перегружаться, поэтому они представлены лишь в самой общей форме.
Метод
Описание
All(predicate)
Возвращает логическое значение true, если все элементы в последовательности удовлетворяют условию, задаваемому параметром
.predicate
Any(predicate)
Возвращает логическое значение true, если любой элемент в последовательности удовлетворяет условию, задаваемому параметром
predicate
Average()
Возвращает среднее всех значений в числовой последовательности
Contains(value)
Возвращает логическое значение true, если в последовательности содержится указанный объект
Count()
Возвращает длину последовательности, т.е. количество составляющих ее элементов
First()
Возвращает первый элемент в последовательности
Last()
Возвращает последний элемент в последовательности
Max ()
Возвращает максимальное значение в последовательности
Min ()
Возвращает минимальное значение в последовательности
Sum ()
Возвращает сумму значений в числовой последовательности
МетодCount() уже демонстрировался ранее в этой главе. А в следующей программе демонстрируются остальные методы расширения, связанные с запросами.
// Использовать ряд методов расширения, определенных в классе Enumerable.
using System; using System.Linq;
class ExtMethods { static void Main() {
int[] nums = { 3, 1, 2, 5, 4 };
Console .WriteLine ("Минимальное значение равно " + nums.MinO);
Console.WriteLine("Максимальное значение равно " + nums.Max());
Console.WriteLine("Первое значение равно " + nums.First());
Console.WriteLine("Последнее значение равно " + nums.Last());
Console.WriteLine("Суммарное значение равно " + nums.SumO);
Console.WriteLine("Среднее значение равно " + nums.Average());
if(nums.All(n => n > 0))
Console.WriteLine("Все значения больше нуля.");
if(nums.Any(n => (n % 2) == 0))
Console.WriteLine("По крайней мере одно значение является четным.");
if(nums.Contains(3))
Console.WriteLine("Массив содержит значение 3.");
}
}
Вот к какому результату приводит выполнение этой программы.
Минимальное значение равно 1 Максимальное значение равно 5 Первое значение равно 3 Последнее значение равно 4 Суммарное значение равно 15 Среднее значение равно 3 Все значения больше нуля.
По крайней мере одно значение является четным Массив содержит значение 3.
Методы расширения, связанные с запросами, можно также использовать в самом запросе, основываясь на синтаксисе запросов, предусмотренном в С#. И в действительности это делается очень часто. Например, методAverage() используется в приведенной ниже программе для получения последовательности, состоящей только из тех значений, которые оказываются меньше среднего всех значений в массиве.
// Использовать метод Average() вместе с синтаксисом запросов.
using System; using System.Linq;
class ExtMethods2 { static void Main() {
int[] nums = { 1, 2, 4, 8, 6, 9, 10, 3, 6, 7 };
var ItAvg = from n in nums
let x = nums.Average() where n < x select n;
Console.WriteLine("Среднее значение равно " + nums.Average());
Console.Write("Значения меньше среднего: ");
// Выполнить запрос и вывести его результаты,foreach(int i in ItAvg) Console.Write(i + " ");
Console.WriteLine ();
}
}
При выполнении этой программы получается следующий результат.
Среднее значение равно 5.6 Значения меньше среднего: 12 4 3
Обратите особое внимание в этой программе на следующий код запроса.
var ItAvg = from n in nums
let x = nums.Average() where n < x select n;
Как видите, переменнойxв оператореletприсваивается среднее всех значений в массивеnums.Это значение получается в результате вызова методаAverage() для массиваnums.
Режимы выполнения запросов: отложенный и немедленный
В LINQ запросы выполняются в двух разных режимах: немедленном и отложенном. Как пояснялось ранее в этой главе, при формировании запроса определяется ряд правил, которые не выполняются вплоть до оператора циклаforeach.Это так называемоеотложенное выполнение.
Но если используются методы расширения, дающие результат, отличающийся от последовательности, то запрос должен быть выполнен для получения этого результата. Рассмотрим, например, метод расширенияCount (). Для того чтобы этот метод возвратил количество элементов в последовательности, необходимо выполнить запрос, и это делается автоматически при вызове методаCount (). В этом случае имеет местонемедленное выполнение, когда запрос выполняется автоматически для получения требуемого результата. Таким образом, запрос все равно выполняется, даже если он не1используется явно в циклеforeach.
Ниже приведен простой пример программы для получения количества положительных элементов, содержащихся в последовательности.
using System.Linq;
class ImmediateExec { static void Main() {
int[] nums = { 1,-2,3, 0,-4,5 };
// Сформировать запрос на получение количества // положительных значений в массиве nums. int len = (from n in nums where n > 0 select n).Count();
Console.WriteLine("Количество положительных значений в массиве nums: " + len) ;
}
}
Эта программа дает следующий результат.
Количество положительных значений в массиве nums: 3
Обратите внимание на то, что циклforeachне указан в данной программе явным образом. Вместо этого запрос выполняется автоматически благодаря вызову метода расширенияCount ().
Любопытно, что запрос из приведенной выше программы можно было бы сформировать и следующим образом.
var posNums = from n in nums where n > 0 select n;
int len = posNums.Count(); // запрос выполняется здесь
В данном случае методCount() вызывается для переменной запроса. И в этот момент запрос выполняется для получения подсчитанного количества.
К числу других методов расширения, вызывающих немедленное выполнение запроса, относятся методыТо Array() иToList (). Оба этих метода расширения определены в классеEnumerable.МетодToAtray() возвращает результаты запроса в массиве, а методToList () — результаты запроса в форме коллекцииList.(Подробнее о коллекциях речь пойдет в главе 25.) В обоих случаях для получения результатов выполняется запрос. Например, в следующем фрагменте кода сначала получается массив результатов, сформированных по приведенному выше запросу в переменнойposNums,а затем эти результаты выводятся на экран.
int[] pnums = posNum.ToArray(); // запрос выполняется здесь
foreach(int i in pnums)
Console.Write(i + " ");
}
Деревья выражений
Еще одним средством, связанным с LINQ, являетсядерево выражений,которое представляет лямбда-выражение в виде данных. Это означает, что само лямбда-выражение
нельзя выполнить, но можно преобразовать в исполняемую форму. Деревья выражений инкапсулируются в классеSystem. Linq. Expressions . Expression<TDelegate>.Они оказываются пригодными в тех случаях, когда запрос выполняется вне программы, например средствами SQL в базе данных. Если запрос представлен в виде данных, то его можно преобразовать в формат, понятный для базы данных. Этот процесс выполняется, например, средствами LINQ to SQL в интегрированной среде разработки Visual Studio. Таким образом, деревья выражений способствуют поддержке в C# различных баз данных.
Для получения исполняемой формы дерева выражений достаточно вызвать методCompile (), определенный в классеExpression.Этот метод возвращает ссылку, которая может быть присвоена делегату для последующего выполнения. А тип делегата может быть объявлен собственным или же одним из предопределенных типов делегатаFuncв пространстве именSystem.Две формы делегатаFuncуже упоминались ранее при рассмотрении методов запроса, но существует и другие его формы.
Деревьям выражений присуще следующее существенное ограничение: они могут представлять только одиночные лямбда-выражения. С их помощью нельзя представить блочные лямбда-выражения.
Ниже приведен пример программы, демонстрирующий конкретное применение дерева выражений. В этой программе сначала создается дерево выражений, данные которого представляют метод, определяющий, является ли одно целое число множителем другого. Затем это дерево выражений компилируется в исполняемый код. И наконец, в этой программе демонстрируется выполнение скомпилированного кода.
// Пример простого дерева выражений.
using System;
using System.Linq;
using System.Linq.Expressions;
class SimpleExpTree { static void Main() {
// Представить лямбда-выражение в виде данных.
Expression<Func<int, int, bool»
IsFactorExp = (n, d) => (d != 0) ? (n % d) ==0 : false;
// Скомпилировать данные выражения в исполняемый код.
Func<int, int, bool> IsFactor = IsFactorExp.Compile ();
// Выполнить выражение, if(IsFactor(10, 5))
Console.WriteLine("Число 5 является множителем 10.");
if(!IsFactor(10, 7))
Console.WriteLine("Число 7 не является множителем 10.");
Console.WriteLine ();
}
}
Вот к какому результату приводит выполнение этой программы.
Число 5 является множителем 10.
Число 7 не является множителем 10.
Данный пример программы наглядно показывает два основных этапа применения дерева выражений. Сначала в ней создается дерево выражений с помощью следующего оператора.
Expression<Func<int, int, bool»
IsFactorExp = (n, d) => (d != 0) ? (n % d) ==0 : false;
В этом операторе конструируется представление лямбда-выражения в оперативной памяти. Как пояснялось выше, это представление доступно по ссылке, присваиваемой делегатуIsFactorExp.А в следующем операторе данные выражения преобразуются в исполняемый код.
Func<int, int, bool> IsFactor = IsFactorExp.Compile();
После выполнения этого оператора делегатIsFactorExpможет быть вызван, чтобы определить, является ли одно целое число множителем другого.
Обратите также внимание на то, что<Func<int, int, boo 1>обозначает тип делегата. В этой форме делегатаFun суказываются два параметра типаintи возвращаемый типbool.В рассматриваемой здесь программе использована именно эта форма делегатаFun с,совместимая с лямбда-выражениями, поскольку для выражения требуются два параметра. Для других лямбда-выражений могут подойти иные формы делегатаFun св зависимости от количества требуемых параметров. Вообще говоря, конкретная форма делегатаFun сдолжна удовлетворять требованиям лямбда-выражения.
Методы расширения
Как упоминалось выше, методы расширения предоставляют средства для расширения функций класса, не прибегая к обычному механизму наследования. Методы расширения создаются нечасто, поскольку механизм наследования, как правило, предлагает лучшее решение. Тем не менее знать, как они действуют, никогда не помешает. Ведь они имеют существенное значение для LINQ.
Метод расширения является статическим и поэтому должен быть включен в состав статического, необобщенного класса. Тип первого параметра метода расширения определяет тип объектов, для которых этот метод может быть вызван. Кроме того, первый параметр может быть указан с модификаторомthis.Объект, для которого вызывается метод расширения, автоматически передается его первому параметру. Он не передается явным образом в списке аргументов. Следует, однако, иметь в виду, что метод расширения может по-прежнему вызываться для объекта аналогично методу экземпляра, несмотря на то, что он объявляется как статический.
Ниже приведена общая форма метода расширения.
staticвозращаемый_тип имя (this тип_вызывающего_объекта ob, список_параметров)
Очевидно, чтосписок_параметровокажется пустым в отсутствие аргументов, за исключением аргумента, неявно передаваемого вызывающим объектом оЬ. Не следует, однако, забывать, что первым параметром метода расширения является автоматически передаваемый объект, для которого вызывается этот метод. Как правило, метод расширения становится открытым членом своего класса.
В приведенном ниже примере программы создаются три простых метода расширения.
// Создать и использовать ряд методов расширения, using System;
using System.Globalization; static class MyExtMeths {
// Возвратить обратную величину числового значения типа double, public static double Reciprocal(this double v) { return 1.0 / v;
}
// Изменить на обратный регистр букв в символьной // строке и возвратить результат, public static string RevCase(this string str) { string temp =
foreach(char ch in str) {
if(Char.IsLower(ch)) temp += Char.ToUpper (ch, Culturelnfo.
CurrentCulture);
else temp += Char.ToLower(ch, Culturelnfo.CurrentCulture);
}
return temp;
}
// Возвратить абсолютное значение выражения n / d. public static double AbsDivideBy(this double n, double d) { return Math.Abs(n / d);
}
}
class ExtDemo {
static void Main() {
double val = 8.0;
string str = "Alpha Beta Gamma";
// Вызвать метод расширения Reciprocal()..
Console.WriteLine("Обратная величина {0} равна {1}", val, val.Reciprocal());
// Вызвать метод расширения RevCaseO .
Console.WriteLine(str + " после смены регистра: " + str.RevCase ());
// Использовать метод расширения AbsDivideBy() .
Console.WriteLine("Результат вызова метода val.AbsDivideBy(-2) : " + val.AbsDivideBy(-2));
}
}
Обратная величина 8 равна 0.125
Alpha Beta Gamma после смены регистра: aLPHA ЬЕТА дАММА Результат вызова метода val.AbsDivideBy(-2): 4
В данном примере программы каждый метод расширения содержится в статическом классеMyExtMeths.Как пояснялось выше, метод расширения должен быть объявлен в статическом классе. Более того, этот класс должен находиться в области действия своих методов расширения, чтобы ими можно было пользоваться. (Именно поэтому в исходный текст программы следует включить пространство именSystem. Linq,так как это дает возможность пользоваться методами расширения, связанными с LINQ.)
Объявленные методы расширения вызываются для объекта таким же образом, как и методы экземпляра. Главное отличие заключается в том, что вызывающий объект передается первому параметру метода расширения. Поэтому при выполнении выражения
val.AbsDivideBy(-2)
объектvalпередается параметрупметода расширенияAbsDivideBy (), а значение-2— параметруd.
Любопытно, что методы расширенияReciprocal() иAbsDivideBy() могут вполне законно вызываться и для литерала типаdouble,как показало ниже, поскольку они определены для этого типа данных.
8.0.Reciprocal()
8.0.AbsDivideBy(-1)
Кроме того, метод расширенияRevCase() может быть вызван следующим образом."AbCDe".RevCase()
В данном случае возвращается строковый литерал с измененным на обратный регистром букв.
PLINQ
В версии .NET Framework 4.0 внедрено новое дополнение LINQ под названием PLINQ. Это средство предназначено для поддержки параллельного программирования. Оно позволяет автоматически задействовать в запросе несколько доступных процессоров. Подробнее о PLINQ и других средствах, связанных с параллельным програмт-мированием, речь пойдет в главе 24.
ГЛАВА 20 Небезопасный код, указатели, обнуляемые типы и разные ключевые слова
В этой главе рассматривается средство языка С#, которое обычно захватывает программистов врасплох. Это небезопасный код. В таком коде зачастую используются указатели. Совместно с небезопасным кодом указатели позволяют разрабатывать на C# приложения, которые обычно связываются с языком C++, высокой производительностью и системным кодом. Более того, благодаря включению небезопасного кода и указателей в состав C# в этом языке появились возможности, которые отсутствуют в Java.
В этой главе рассматриваются также обнуляемые типы, определения частичных классов и методов, буферы фиксированного размера. И в заключение этой главы представлен ряд ключевых слов, не упоминавшихся в предыдущих главах.
Небезопасный код
В C# разрешается писать так называемый "небезопасный" код. В этом странном на первый взгляд утверждении нет на самом деле ничего необычного. Небезопасным считается не плохо написанный код, а такой код, который не может быть выполнен под полным управлением в общеязыковой исполняющей среде (CLR). Как пояснялось в главе 1, результатом программирования на C# обычно является управляемый код. Тем не менее этот язык программирования допускает написание кода, который не выполняется под полным управлением в среде CLR. Такой неуправляемый код не подчиняется тем же самым средствам
управления и ограничениям, что и управляемый код, и называется он небезопасным потому, что нельзя никак проверить, не выполняет ли он какое-нибудь опасное действие. Следовательно, терминнебезопасныйсовсем не означает, что коду присущи какие-то изъяны. Это просто означает, что код может выполнять действия, которые не подлежат контролю в управляемой среде.
Если небезопасный код может вызвать осложнения, то зачем вообще создавать такой код? Дело в том, что управляемый код не позволяет использовать указатели. Если у вас имеется некоторый опыт программирования на С или C++, то вам должно быть известно, чтоуказателипредставляют собой переменные, предназначенные для хранения адресов других объектов, т.е. они в какой-то степени похожи на ссылки в С#. Главное отличие указателя заключается в том, что он мпжет указывать на любую область памяти, тогда как ссылка всегда указывает на объект своего типа. Но поскольку указатель способен указывать практически на любую область памяти, то существует большая вероятность его неправильного использования. Кроме того, используя указатели, легко допустить программные ошибки. Именно поэтому указатели не поддерживаются при создании управляемого кода в С#. А поскольку указатели все-таки полезны и необходимы для некоторых видов программирования (например, утилит системного уровня), в C# разрешается создавать и использовать их. Но при этом все операции с указателями должны быть помечены как небезопасные, потому что они выполняются вне управляемой среды.
В языке C# указатели объявляются и используются таким же образом, как и в С/С-н-. Если вы знаете, как пользоваться ими в С/С-н-, то вам нетрудно будет сделать это и в С#. Но не забывайте, что главное назначение C# — создание управляемого кода. А способность этого языка программирования поддерживать неуправляемый код следует использовать для решения лишь особого рода задач. Это, скорее, исключение, чем правило для программирования на С#. По существу, для компилирования неуправляемого кода следует использовать параметр компилятора /unsafe.
Указатели составляют основу небезопасного кода, поэтому мы начнем его рассмотрение именно с них.
Основы применения указателей
Указатель представляет собой переменную, хранящую адрес какого-нибудь другого объекта, например другой переменной. Так, если в переменной х хранится адрес переменной у, то говорят, что переменная х указывает на переменную у. Когда указатель указывает на переменную, то значение этой переменной может быть получено или изменено по указателю. Такие операции с указателями называютнепрямой адресацией.
Объявление указателя
Переменные-указатели должны быть объявлены как таковые. Ниже приведена общая форма объявления переменной-указателя:
тип*имя_переменной;
гдетипобозначаетсоотносимый тип,который не должен быть ссылочным. Это означает, что в C# нельзя объявить указатель на объект определенного класса. Соотносимый тип указателя иногда еще называютбазовым.Обратите внимание на положение знака * в объявлении указателя. Он должен следовать после наименования типа. Аимя_переменнойобозначает конкретное имя указателя-переменной.
Обратимся к конкретному примеру. Для того чтобы сделать переменную ip указателем на значение типа int, необходимо объявить ее следующим образом.
int* ip;
А указательлтша float объявляется так, как показано ниже.
float* fp;
Вообще говоря, если в операторе объявления после имени типа следует знак *, то это означает, что создается переменная типа указателя.
Тип данных, на которые будет указывать сам указатель, зависит от его соотносимого типа. Поэтому в приведенных выше примерах переменная ip может служить для указания на значение типа int, а переменная fp — для указания на значение типа float. Следует, однако, иметь в виду, что указателю ничто не мешает указыватьна что угодно. Именно поэтому указатели потенциально небезопасны.
Если у вас есть опыт программирования на C/C++, то вы должны ясно понимать главное отличие в объявлении указателей в C# и C/C++. При объявлении указателя в C/C++ знак *неразделяет список переменных в объявлении. Поэтому в следующей строке кода:
int* р, q;
объявляется указатель р типа int и переменная q типа int. Это равнозначно двум следующим объявлениям.
int* р; int q;
А в C# знак *являетсяразделительным, и поэтому в объявлении
int* р, q;
создаются две переменные-указателя. Это равнозначно двум следующим объявлениям.
int* р; int* q;
Это главное отличие следует иметь в виду при переносе кода C/C++ на С#.
Операторы * и & в указателях
В указателях применяются два оператора: * и &. Оператор & является унарным и возвращает адрес памяти своего операнда. (Напомним, что для унарного оператора требуется единственный операнд.) Например, в следующем фрагменте кода:
int* ip; int num = 10; ip = #
в переменной ip сохраняется адрес памяти переменной num. Это адрес расположения переменной num в оперативной памяти компьютера. Он не имеетникакогоотношения кзначениюпеременной num. Поэтому в переменной ip содержится не значение 10, являющееся исходным для переменной num, а конкретный адрес, по которому эта переменная хранится в оперативной памяти. Операцию & можно рассматривать как возврат адреса той переменной, перед которой она указывается. Таким образом, приведенное выше присваивание словами можно описать так: "Переменная ip получает адрес переменной num."
Второй оператор, *, является дополнением оператора &. Этот унарный оператор находит значение переменной, расположенной по адресу, на который указывает его операнд. Следовательно, этот оператор обращается к значению переменной, на которую указывает соответствующий указатель. Так, если переменнаяipсодержит адрес памяти переменнойnum,как показано в предыдущем примере, то в следующей строке кода:
int val = *ip;
в переменнойvalсохраняется значение10переменнойnum,на которую указывает переменнаяip.Операцию * можно рассматривать как получение значения по адресу. Поэтому приведенный выше оператор присваивания описывается словами следующим образом: "Переменнаяvalполучает значение по адресу, хранящемуся в переменнойip."
Оператор * можно использовать также в левой части оператора присваивания. В этом случае он задает значение, на которое указывает соответствующий указатель, как в приведенном ниже примере.
*ip = 100;
В данном примере значение 100 присваивается переменной, на которую указывает переменнаяip,т.е. переменнойnum.Поэтому приведенный выше оператор присваивания описывается словами следующим образом: "Разместить значение 100 по адресу, хранящемуся в переменнойip."
Применение ключевого слова unsafe
Любой код, в котором используются указатели, должен быть помечен как небезопасный с помощью специального ключевого словаunsafe.Подобным образом можно пометить конкретные типы данных (например, классы и структуры), члены класса (в том числе методы и операторы) или отдельные кодовые блоки как небезопасные. В качестве примера ниже приведена программа, где указатели используются в методеMain (), помеченном как небезопасный.
// Продемонстрировать применение указателей и ключевого слова unsafe.
using System;
class UnsafeCode {
// Пометить метод Main() как небезопасный, unsafe static void Main() { int count = 99;
int* p; // создать указатель типа int
p = &count; // поместить адрес переменной count в переменной р
Console.WriteLine("Исходное значение переменной count: " + *р) ;
*р = 10; // присвоить значение 10 переменной count,
// на которую указывает переменная р
Console.WriteLine("Новое значение переменной count: " + *р);
Эта программа дает следующий результат.
Исходное значение переменной count: 99 Новое значение переменной count: 10
Применение модификатора fixed
В работе с указателями нередко используется модификатор fixed, который препятствует удалению управляемой переменной средствами "сборки мусора'7. Потребность в этом возникает, например, в том случае, если указатель обращается к полю в объекте определенного класса. А поскольку указателю ничего не известно о действиях системы "сборки мусора", то он будет указывать не на тот объект, если удалить нужный объект. Ниже приведена общая форма модификатора fixed:
fixed(тип* р = &фиксированный_объект){
// использовать фиксированный объект
}
гдеробозначает указатель, которому присваивается адрес объекта. Этот объект будет оставаться на своем текущем месте в памяти до конца выполнения кодового блока. В качестве адресата оператора fixed может быть также указано единственное выражение, а не целый кодовый блок. Модификатор fixed допускается использовать только в коде, помеченном как небезопасный. Кроме того, несколько указателей с модификатором fixed могут быть объявлены списком через запятую.
Ниже приведен пример применения модификатора fixed.
// Продемонстрировать применение оператора fixed.
using System;
class Test {
public int num;
public Test (int i) { num = i; }
}
class FixedCode {
// Пометить метод Main() как небезопасный, unsafe static void Main() {
Test о = new Test(19);
fixed (int* p = &o.num) { // использовать модификатор fixed для размещения
// адреса переменной экземпляр о.num в переменной р
Console.WriteLine("Исходное значение переменной о.num: " + *р);
*р = 10; // присвоить значение 10 переменной count,
// на которую указывает переменная р
Console.WriteLine("Новое значение переменной о.num: " + *р);
}
}
}
Вот к какому результату приводит выполнение этой программы.
Исходное значение переменной о.num: 19 Новое значение переменной о.num: 10
В данном .примере модификатор fixed препятствует удалению объекта о. А поскольку переменная р указывает на переменную экземпляра о . num, то она будет указывать на недостоверную область памяти, если удалить объект о.
Доступ к членам структуры с помощью указателя
Указатель может указывать на объект типа структуры при условии, что структура не содержит ссылочные типы данных. Для доступа к члену структуры с помощью указателя следует использовать оператор-стрелку (->), а не оператор-точку (.). Например, доступ к членам структуры
struct MyStruct { public int a; public int b;
public int Sum() { return a + b; }
}
осуществляется следующим образом.
MyStruct о = new MyStruct();
MyStruct* p; // объявить указатель
p = &o;
p->a = 10; // использовать оператор -> p->b =20; // использовать оператор ->
Console.WriteLine("Сумма равна " + p->Sum());
Арифметические операции над указателями
Над указателями можно выполнять только четыре арифметические операции: ++, —, + и -. Для того чтобы стало понятнее, что именно происходит в арифметических операциях над указателями, рассмотрим сначала простой пример. Допустим, что переменная pi является указателем с текущим значением 2000, т.е. она содержит адрес 2000. После выполнения выражения
pl++;
переменная pi будет содержать значение 2004, а не 2001! Дело в том, что после каждого инкрементирования переменная pi указывает на следующее значение типа int. А поскольку тип int представлен в C# 4 байтами, то в результате инкрементирования значение переменной pi увеличивается на 4. Справедливо и обратное: при каждом декрементировании переменной pi ее значение уменьшается на 4. Например выражение
pl—;
приводит к тому, что значение переменной pl становится равным 1996, если раньше оно было равно 2000!
Все сказанное выше можно обобщить: после каждого инкрементирования указатель будет указывать на область памяти, где хранится следующий элемент его соотносимого типа, а после каждого декрементирования указатель будет указывать на область памяти, где хранится предыдущий элемент его соотносимого типа.
Арифметические операции над указателями не ограничиваются только инкрементированием и декрементированием. К указателям можно добавлять и вычитать из них целые значения. Так, после вычисления следующего выражения:
pi = pi + 9; ,
переменная pi будет указывать на девятый элемент ее соотносимого типа по отношению к элементу, на который она указывает в настоящий момент.
Если складывать указатели нельзя, то разрешается вычитать один указатель из другого, при условии, что оба указателя имеют один и тот же соотносимый тип. Результатом такой операции окажется количество элементов соотносимого типа, которые разделяют оба указателя.
Кроме сложения и вычитания целого числа из указателя, а также вычитания двух указателей, другие арифметические операции над указателями не разрешаются. В частности, к указателям нельзя добавлять или вычитать из них значения типа float или double. Не допускаются также арифметические операции над указателями типа void*.
Для того чтобы проверить на практике результаты арифметических операций над указателями, выполните приведенную ниже короткую программу, где выводятся физические адреса, на которые указывает целочисленный указатель (ip) и указатель с плавающей точкой одинарной точности (fp). Понаблюдайте за изменениями каждого из этих указателей по отношению к их соотносимым типам на каждом шаге цикла.
// Продемонстрировать результаты арифметических операций над указателями.
using System;
class PtrArithDemo {
unsafe static void Main() { int x; int i; double d;
int* ip = &i; double* fp = &d;
Console.WriteLine("int double\n");
for(x=0; x < 10; x++) {
Console.WriteLine((uint) (ip) + " " + (uint) (fp)); ip++; fp++;
}
}
}
Ниже приведен примерный результат выполнения данной программы. У вас он может оказаться иным, хотя промежутки между выводимыми значения должны быть такими же самыми.
int double
1243464 1243468 1243468 1243476 1243472 1243484 1243476 1243492 1243480 1243500
1243484 1243508 1243488 1243516 1243492 1243524 1243496 1243532 1243500 1243540
Как следует из приведенного выше результата, арифметические операции выполняются над указателями относительно их соотносимого типа. Так, значения типа int занимают в памяти 4 байта, а значения типа double — 8 байтов, и поэтому их адреса изменяются с приращением именно на эти величины.
Сравнение указателей
Указатели можно сравнивать с помощью таких операторов отношения, как ==, < и >. Но для того чтобы результат сравнения указателей оказался содержательным, оба указателя должны быть каким-то образом связаны друг с другом. Так, если переменные pi и р2 являются указателями на две разные и не связанные вместе переменные, то любое их сравнение, как правило, не имеет никакого смысла. Но если переменные pi и р2 указывают на связанные вместе переменные, например на элементы одного массива, то их сравнение может иметь определенный смысл.
Указатели и массивы
В C# указатели и массивы связаны друг с другом. Например, при указании имени массива без индекса в операторе с модификатором fixed формируется указатель на начало массива. В качестве примера рассмотрим следующую программу.
/* Указание имени массива без индекса приводит к формированию указателя на начало массива. */
using System;
class PtrArray {
unsafe static void Main() { int[] nums = new int[10];
fixed(int* p = &nums[0], p2 = nums) { if(p == p2)
Console.WriteLine("Указатели p и p2 содержат " +
"один и тот же адрес.");
}
}
}
Ниже приведен результат выполнения этой программы.
Указатели р и р2 содержат один и тот же адрес
Как следует из приведенного выше результата, выражения&nums[0]
И
nums
оказываются одинаковыми. Но поскольку вторая форма более лаконична, то она чаще используется в программировании, когда требуется указатель на начало массива.
Индексирование указателей
Когда указатель обращается к массиву, его можно индексировать как сам массив. Такой синтаксис служит более удобной в некоторых случаях альтернативой арифметическим операциям над указателями. Рассмотрим следующий пример программы.
// Проиндексировать указатель как массив.
using System;
class PtrlndexDemo {
unsafe static void Main() { int[] nums = new int [10];
// Проиндексировать указатель.
Console.WriteLine("Индексирование указателя как массива."); fixed (int* p = nums) { for(int i=0; i < 10; i++)
p[i] = i; // индексировать указатель как массив
forjint i=0; i < 10; i++)
Console.WriteLine("p[{0}]: {1} ", i, p[i]);
}
// Использовать арифметические операции над указателями.
Console.WriteLine("ХпПрименение арифметических " +
"операций над указателями."); fixed (int* р = nums) { for(int i=0; i < 10; i++)
* (p+i) = i; // использовать арифметическую операцию над указателем
for(int i=0; i < 10; i++)
Console.WriteLine("*(p+{0}): {1} ", i, *(p+i));
}
}
}
Ниже приведен результат выполнения этой программы.
Индексирование указателя как массива.
Р[9] : 9
Применение арифметических операций над указателями.
*(р+0) : 0 *(P+1) : 1 *(р+2) : 2 *(р+3) : 3 *(р+4) : 4
* (р+5) : 5
* (р+6) : 6
* (р+7) : 7 *(р+8): 8
* (р+9) : 9
Как следует из результата выполнения приведенной выше программы, общая форма выражения с указателем
* (ptr + i)
может быть заменена следующим синтаксисом индексирования массива.
ptr[i]
Что касается индексирования указателей, то необходимо иметь в виду следующее. Во-первых, при таком индексировании контроль границ массива не осуществляется. Поэтому указатель может обращаться к элементу вне границ массива. И во-вторых, для указателя не предусмотрено свойство Length, определяющее длину массива. Поэтому, если используется указатель, длина массива зарайее неизвестна.
Указатели и строки
Символьные строки реализованы в C# в вид^объектов. Тем не менее отдельные символы в строке могут быть доступны по указателю. Для этого указателю типа char* присваивается адрес начала символьной строки в следующем операторе с модификатором fixed.
fixed(char*р = str){ // ...
После выполнения оператора с модификатором fixed переменнаярбудет указывать на начало массива символов, составляющих строку. Этот массив оканчивается символом конца строки, т.е. нулевым символом. Поэтому данное обстоятельство можно использовать для проверки конца массива. В C/C++ строки реализуются в виде массивов, оканчивающихся символом конца строки, а следовательно, получив указатель типа char* на строку, ею можно манипулировать таким же образом, как и в C/C++.
Ниже приведена программа, демонстрирующая доступ к символьной строке по указателю типа char*.
// Использовать модификатор fixed для получения // указателя на начало строки.
using System;
class FixedString {
unsafe static void Main() { string str = "это тест";
// Получить указатель р на начало строки str. fixed(char* р = str) {
// Вывести содержимое строки str по указателю р. for(int i=0; p[i] != 0; i++)
Console.Write(p[i]);
Console.WriteLine();
}
}
Эта программа дает следующий результат.
это тест
Многоуровневая непрямая адресация
Один указатель может указывать на другой, а тот, свою очередь, — на целевое значение. Это так называемаямногоуровневая непрямая адресация, или применениеуказателей на указатели.Такое применение указателей может показаться, на первый взгляд, запутанным. Для прояснения принципа многоуровневой непрямой адресации обратимся за помощью к рис. 20.1. Как видите, значением обычного указателя является адрес переменной, содержащей требуемое значение. Если же применяется указатель на указатель, то первый из них содержит адрес второго, указывающего на переменную, содержащую требуемое значение.
Указатель Переменная
адрес -► значение
Одноуровневая непрямая адресация
Указатель Указатель Переменная
адрес -► адрес -► значение
Многоуровневая непрямая адресация Рис. 20.1. Одно- и многоуровневая непрямая адресация
Многоуровневая непрямая адресация может быть продолжена до любого предела, но потребность более чем в двух уровнях адресации по указателям возникает крайне редко. На самом деле чрезмерная непрямая адресация очень трудно прослеживается и чревата ошибками.
Переменная, являющаяся указателем на указатель, должна быть объявлена как таковая. Для этого достаточно указать дополнительный знак * после имени типа переменной. Например, в следующем объявлении компилятор уведомляется о том, что переменная q является указателем на указатель и относится к типу int.
int** q;
Следует, однако, иметь в виду, что переменная q является указателем не на целое значение, а на указатель типа int.
Для доступа к целевому значению, косвенно адресуемому по указателю на указатель, следует дважды применить оператор *, как в приведенном ниже примере.
using System;
class Multiplelndirect {
unsafe static void Main() {
int x; // содержит значение типа int
int* p; // содержит указатель типа int
int** q; // содержит указатель на указатель типа int
х = 10;
р = &х; // поместить адрес переменной х в переменной р q = &р; // поместить адрес переменной р в переменной q
Console.WriteLine(**q); // вывести значение переменной х
}
}
Результатом выполнения этой программы будет выведенное на экран значение 10 переменной х. В данной программе переменная р объявляется как указатель на значение типа int, а переменная q — как указатель на указатель типа int.
И последнее замечание: не путайте многоуровневую непрямую адресацию со структурами данных высокого уровня, в том числе связными списками, так как это совершенно разные понятия.
Массивы указателей
Указатели могут быть организованы в массивы, как и любой другой тип данных. Ниже приведен пример объявления массива указателей типа int длиной в три элемента.
int * [] ptrs = new int * [3];
Для того чтобы присвоить адрес переменной var типа int третьему элементу массива указателей, достаточно написать следующую строку кода.
ptrs[2] = &var;
А для того чтобы обнаружить значение переменной var, достаточно написать приведенную ниже строку кода.
*ptrs[2]
Оператор sizeof
Во время работы с небезопасным кодом иногда полезно знать размер в байтах одного из встроенных в C# типов значений. Для получения этой информации служит оператор sizeof. Ниже приведена его общая форма:
sizeof(тип)
гдетипобозначает тот тип, размер которого требуется получить. Вообще говоря, оператор sizeof предназначен главным образом для особых случаев и, в частности, для работы со смешанным кодом: управляемым и неуправляемым.
Оператор stackalloc
Для распределения памяти, выделенной под стек, служит оператор stackalloc. Им можно пользоваться лишь при инициализации локальных переменных. Ниже приведена общая форма этого оператора:тип*р = stackallocтип [размер]
гдеробозначает указатель, получающий адрес области памяти, достаточной для хранения объектов, имеющих указанныйтип,в количестве, которое обозначаетразмер.Если же в стеке недостаточно места для распределения памяти, то генерируется исключениеSystem. StackOverflowException.И наконец, операторstackallocможно использовать только в небезопасном коде.
Как правило, память для объектов выделяется из кучи — динамически распределяемой свободной области памяти. А выделение памяти из стека является исключением. Ведь переменные, располагаемые в стеке, не удаляются средствами "сборки мусора", а существуют только в течение времени выполнения метода, в котором они объявляются. После возврата из метода выделенная память освобождается. Преимущество применения оператораstackallocзаключается, в частности, в том, что в этом случае не нужно беспокоиться об очистке памяти средствами "сборки мусора".
Ниже приведен пример применения оператораstackalloc*
// Продемонстрировать применение оператора stackalloc.
using System;
class UseStackAlloc {
unsafe static void Main() {
int* ptrs = stackalloc int[3];
ptrs[0] = 1; ptrs[1] = 2; ptrs[2] = 3;
for(int i=0; i < 3; i++)
Console.WriteLine(ptrs[i]);
}
}
Вот к какому результату приводит выполнение кода из данного примера.
1
2
3
Создание буферов фиксированного размера
Ключевое словоfixedнаходит еще одно применение при создании одномерных массивов фиксированного размера. В документации на C# такие массивы называютсябуферами фиксированного размера.Такие буферы всегда являются членами структуры. Они предназначены для создания структуры, в которой содержатся элементы массива, образующйе буфер. Когда элемент массива включается в состав структуры, в ней, как правило, хранится лишь ссылка на этот массив. Используя буфер фиксированного размера, в структуре можно разместить весь массив. В итоге получается структура, пригодная в тех случаях, когда'важен ее размер, как, например, в многоязыковом программировании, при согласовании данных, созданных вне программы на С#, или же когда требуется неуправляемая структура, содержащая массив. Но буферы фиксированного размера можно использовать только в небезопасном коде.
Для создания буфера фиксированного размера служит следующая общая форма:
fixedтип имя_буфера[размер];
гдетипобозначает тип данных массива;имя_буфера— конкретное имя буфера фиксированного размера;размер— количество элементов, образующих буфер. Буферы фиксированного размера могут указываться только в структуре.
Для того чтобы стала очевиднее польза от буферов фиксированного размера, рассмотрим ситуацию, в которой программе ведения счетов, написанной на C++, требуется передать информацию о банковском счете. Допустим также, что учетная запись каждого счета организована так, как показано ниже.
Name
Строка длиной 80 байтов, состоящая из 8-разрядных символов в коде ASCII
Balance
Числовое значение типа double длиной 8 байтов
ID
Числовое значение типа long длиной 8 байтов
В программе на C++ каждая структура содержит массивName,тогда как в программе на C# в такой структуре хранится лишь ссылка на массив. Поэтому для правильного представления данных из этой структуры в C# требуется буфер фиксированного размера, как показано ниже.
// Использовать буфер фиксированного-размера. unsafe struct FixedBankRecord {
public fixed byte Name[80]; // создать буфер фиксированного размера public double Balance; public long ID;
}
Когда буфер фиксированного размера используется вместо массиваName,каждый экземпляр структурыFixedBankRecordбудет содержать все 80 байтов массиваName.Именно таким образом структура и организована в программе на C++. Следовательно, общий размер структурыFixedBankRecordокажется равным 96, т.е. сумме ее членов. Ниже приведена программа, демонстрирующая этот факт.
// Продемонстрировать применение буфера фиксированного размера, using System;
// Создать буфер фиксированного размера, unsafe struct FixedBankRecord { public fixed byte Name[80]; public double Balance; public long ID;
}
class FixedSizeBuffer {
// Пометить метод Main() как небезопасный, unsafe static void Main() {
Console.WriteLine("Размер структуры FixedBankRecord: " + sizeof(FixedBankRecord));
}
}
Эта программа дает следующий результат.
Размер структуры FixedBankRecord: 96
Размер структурыFixedBankRecordоказывается в точности равным сумме ее членов, но так бывает далеко не всегда со структурами, содержащими буферы фиксированного размера. Ради повышения эффективности кода общая длина структуры может быть увеличена для выравнивания по четной границе, например по границе слова. Поэтому общая длина структуры может оказаться на несколько байтов больше, чем сумма ее членов, даже если в ней содержатся буферы фиксированного размера. Как правило, аналогичное выравнивание длины структуры происходит и в C++. Следует, однако, иметь в виду возможные отличия в этом отношении.
И наконец, обратите внимание на то, как в данной программе создается буфер фиксированного размера вместо массиваName.
public fixed byte Name[80]; // создать буфер фиксированного размера
Как видите, размер массива указывается после его имени. Такое обозначение обычно принято в C++ и отличается в объявлениях массивов в С#. В данном операторе распределяется по 80 байтов памяти в пределах каждого объекта типаFixedBankRecord.
Обнуляемые типы
Начиная с версии 2.0, в C# внедрено средство, обеспечивающее изящное решение типичной и не очень приятной задачи распознавания и обработки полей, не содержащих значения, т.е. неинициализированных полей. Это средство называетсяобнуляемым типом. Длятогочтобы стала более понятной суть данной задачи, рассмотрим пример простой базы данных заказчиков, в которой хранится запись с именем, адресом, идентификационным номером заказчика, номером счета-фактуры и текущим остатком на счету. В подобной ситуации может быть вполне создан элемент данных заказчика, вко-тором одно или несколько полей не инициализированы. Например, заказчик может просто запросить каталог продукции, и в этом случае номер счета-фактуры не потребуется, а значит, его поле окажется неиспользованным.
Раньше для обработки неиспользуемых полей приходилось применять заполняющие значения или дополнительные поля, которые просто указывали, используется поле или нет. Безусловно, заполняющие значения пригодны лишь в том случае, если они подставляются вместо значения, которое в противном случае окажется недействительным, но так бывает далеко не всегда. А дополнительные поля, указывающие, используется поле или нет, пригодны во всех случаях, но их ввод и обработка вручную доставляют немало хлопот. Оба эти затруднения позволяет преодолеть обнуляемый тип.
Основы применения обнуляемых типов
Обнуляемый тип — это особый вариант типа значения, представленный структурой. Помимо значений, определяемых базовым типом, обнуляемый тип позволяет хранить пустые значения(null).Следовательно, обнуляемый тип имеет такой же диапазон представления чисел и характеристики, как и его базовый тип. Он предоставляет дополнительную возможность обозначить значение, указывающее на то, что переменная данного типа не инициализирована. Обнуляемые типы являются объектами типаSystem. Nullable<T>,гдеТ— тип значения, которое не должно быть обнуляемым.
ПРИМЕЧАНИЕ
Обнуляемые эквиваленты могут быть только у типов значений.
Обнуляемый тип может быть указан двумя способами. Во-первых, объекты типаNullable<T>,определенного в пространстве именSystem,могут быть объявлены явным образом. Так, в приведенном ниже примере создаются обнуляемые переменные типаintиbool.
System.Nullable<int> count;
System.Nullable<bool> done;
И во-вторых, обнуляемый тип объявляется более кратким и поэтому чаще используемым способом с указанием знака ? после имени базового типа. В приведенном ниже примере демонстрируется более распространенный способ объявления обнуляемых переменных типаintиbool.
int? count; bool? done;
Когда в коде применяются обнуляемые типы, создаваемый обнуляемый объект обычно выглядит следующим образом.
int? count = null;
В данной строке кода переменнаяcountявно инициализируется пустым значением(null).Это вполне соответствует принятому правилу: прежде чем использовать переменную, ей нужно присвоить значение. В данном случае присваиваемое значение означает, что переменная не определена.
Значение может быть присвоено обнуляемой переменной обычным образом, поскольку преобразование базового типа в обнуляемый определено заранее. Например, в следующей строке кода переменнойcountприсваивается значение 100.
count = 100;
Определить, имеет переменная обнуляемого типа пустое или конкретное значение, можно двумя способами. Во-первых, можно проверить переменную на пустое значение. Так, если переменнаяcountобъявлена так, как показано выше, то в следующей строке определяется, имеет ли эта переменная конкретное значение.
if (count != null) // переменная имеет значение
Если переменнаяcountне является пустой, то она содержит конкретное значение. И во-вторых, можно воспользоваться доступным только для чтения свойствомHasValueтипаNullable<T>,чтобы определить, содержит ли переменная обнуляемого типа конкретное значение. Это свойство показано ниже.
bool HasValue
СвойствоHasValueвозвращает логическое значениеtrue,если экземпляр объекта, для которого оно вызывается, содержит конкретное значение, а иначе оно возвращает логическое значениеfalse.Ниже приведен пример, в котором конкретное значение обнуляемого объектаcountопределяется вторым способом с помощью свойстваHasValue.
if(count.HasValue) // переменная имеет значение
Если обнуляемый объект содержит конкретное значение, то получить это значение можно с помощью доступного только для чтения свойстваValueтипаNullable<T>.
Т Value
СвойствоValueвозвращает экземпляр обнуляемого объекта, для которого оно вызывается. Если же попытаться получить с помощью этого свойства значение пустой переменной, то в итоге будет сгенерировано исключениеSystem. InvalidOperationException.Кроме того, значение экземпляра обнуляемого объекта можно получить путем приведения к его базовому типу.
В следующей программе демонстрируется основной механизм обращения с обнуляемым типом.
// Продемонстрировать применение обнуляемого типа.
using System;
class NullableDemo { static void Main() { int? count = null;
if (count.HasValue)
Console.WriteLine("Переменная count имеет следующее значение: " + count.Value);
else
Console.WriteLine("У переменной count отсутствует значение");
count = 100; if(count.HasValue)
Console.WriteLine("Переменная count имеет следующее значение: " + count. Va^lue) ;
else
Console.WriteLine("У переменной count отсутствует значение");
}
}
Вот к какому результату приводит выполнение этой программы.
У переменной count отсутствует значение Переменная count имеет следующее значение: 100
Применение обнуляемых объектов в выражениях
Обнуляемый объект может использоваться в тех выражениях, которые являются действительными для его базового типа. Более того, обнуляемые объекты могут сочетаться с необнуляемыми объектами в одном выражении. И это вполне допустимо благодаря предопределенному преобразованию базового типа в обнуляемый. Когда обнуляемые и необнуляемые типы сочетаются в одной операции, ее результатом становится значение обнуляемого типа.
В приведенной ниже программе демонстрируется применение обнуляемых типов в выражениях.
// Использовать обнуляемые объекты в выражениях.
using System;
class NullableDemo { static void Main() {
int? count = null; int? result = null;
int incr = 10; // переменная incr не является обнуляемой
// переменная result содержит пустое значение,
// переменная оказывается count пустой, result = count + incr; if(result.HasValue)
Console.WriteLine("Переменная result имеет следующее значение: " + result.Value);
else
Console.WriteLine("У переменной result отсутствует значение");
// Теперь переменная count получает свое‘значение, и поэтому
// переменная result будет содержать конкретное значение.
count = 100;
result = count + incr;
if(result.HasValue)
Console.WriteLine("Переменная result имеет следующее значение: " + result.Value);
else
Console.WriteLine("У переменной result отсутствует значение");
}
}
При выполнении этой программы получается следующий результат.
У переменной result отсутствует значение Переменная result имеет следующее значение: 110
Оператор ??
Попытка преобразовать обнуляемый объект в его базовый тип путем приведения типов обычно приводит к генерированию исключенияSystem. InvalidOperationException,если обнуляемый объект содержит пустое значение. Это может произойти, например, в том случае, если значение обнуляемого объекта присваивается переменной его базового типа с помощью приведения типов. Появления данного исключения можно избежать, если воспользоваться оператором ? ?, называемымнулеобъединяющим оператором.Этот оператор позволяет указать значение, которое будет использоваться по умолчанию, если обнуляемый объект содержит пустое значение. Он также исключает потребность в приведении типов.
Ниже приведена общая форма оператора ??.
обнуляемый_объект??значение_по_умолчанию
Еслиобнуляемый_объектсодержит конкретное значение, то результатом операции ? ? будет именно это значение. В противном случае результатом операции ? ? окажетсязначение_по_умолчанию.
Например, в приведенном ниже фрагменте кода переменнаяbalanceсодержит пустое значение. Вследствие этого переменнойcurrentBalanceприсваивается значение 0 . 0, используемое по умолчанию, и тем самым устраняется причина для генерирования исключения.
double? balance = null; double currentBalance;
currentBalance = balance ?? 0.0;
В следующем фрагменте кода переменнойbalanceприсваивается значение123.75.
double? balance = 123.75; double currentBalance;
currentBalance = balance ?? 0.0;
Теперь переменнаяcurrentBalanceсодержит значение123.75переменнойbalance.
И еще одно замечание: выражение в правой части оператора ? ? вычисляется только в том случае, если выражение в левой его части не содержит значение. Этот факт демонстрируется в приведенной ниже программе.
// Применение оператора ??
using System;
class NullableDemo2 {
// Возвратить нулевой остаток, static double GetZeroBalO {
Console. WriteLine ("В методе GetZeroBalO."); return 0.0;
}
static void Main() {
double? balance = 123.75; double currentBalance;
// Здесь метод GetZeroBalO не вызывается, поскольку // переменная balance содержит конкретное значение. currentBalance = balance ?? GetZeroBalO;
Console.WriteLine(currentBalance);
}
}
В этой программе методGetZeroBal() не вызывается, поскольку переменнаяbalanceсодержит конкретное значение. Как пояснялось выше, если выражение в левой части оператора ? ? содержит конкретное значение, то выражение в правой его части не вычисляется.
Обнуляемые объекты, операторы отношения и логические операторы
Обнуляемые объекты могут использоваться в выражениях отношения таким же образом, как и соответствующие объекты необнуляемого типа. Но они должны подчиняться следующему дополнительному правилу: когда два обнуляемых объекта сравниваются в операциях сравнения <, >, <= или >=, то их результат будет ложным, если любой из обнуляемых объектов оказывается пустым, т.е. содержит значениеnull.В качестве примера рассмотрим следующий фрагмент кода.
byte? lower = 16; byte? upper = null;
// Здесь переменная lower определена, а переменная upper не определена, if(lower < upper) // ложно%
В данном случае проверка того, что значение одной переменой меньше значения другой, дает ложный результат. Хотя это и не совсем очевидно, как, впрочем, и следующая проверка противоположного характера.
if(lower > upper) // .. также ложно!
Следовательно, если один или оба сравниваемых обнуляемых объекта оказываются пустыми, то результат их сравнения всегда будет ложным. Это фактически означает, что пустое значение (null) не участвует в отношении порядка.
Тем не менее с помощью операторов == и ! = можно проверить, содержит ли обнуляемый объект пустое значение. Например, следующая проверка вполне допустима и дает истинный результат.
if(upper == null) // ...
Если в логическом выражении участвуют два объекта типа bool?, то его результат может иметь одно из трех следующих значений: true (истинное), false (ложное) или null (неопределенное). Ниже приведены результаты применения логических операторов & и | к объектам типа bool?.
р
Q
P 1 Q
P&Q
true
null
true
null
false
null
null
false
null
true
true
null
null
false
null
false
null
null
null
null
И наконец, если логический оператор ! применяется к значению типа bool?, которое является пустым (null), то результат этой операции будет неопределенным (null).
Частичные типы
Начиная с версии 2.0, в C# появилась возможность разделять определение класса, структуры или интерфейса на две или более части с сохранением каждой из них в отдельном файле. Это делается с помощью контекстного ключевого слова partial. Все эти части объединяются вместе во время компиляции программы.
Если модификатор partial используется для создания частичного типа, то он принимает следующую общую форму:
partialтип имя_типа{//...
гдеимя_типаобозначает имя класса, структуры или интерфейса, разделяемого на части. Каждая часть получающегося частичного типа должна указываться вместе с модификатором partial.
Рассмотрим пример разделения простого класса, содержащего координаты ХУ, на три отдельных файла. Ниже приведено содержимое первого файла.
partial class XY {
public XY(int a, int b) {
X =• a;
Y = b;
}
}
Далее следует содержимое второго файла.
partial class XY {
public int X { get; set; }
}
И наконец, содержимое третьего файла.
partial class XY {
public int Y { get; set; }
}
В приведенном ниже файле исходного текста программы демонстрируется применение классаXY.
// Продемонстрировать определения частичного класса.
using System;
class Test {
static void Main() {
XY xy = new XY (1, 2);
Console.WriteLine(xy.X + + xy.Y);
}
}
Для того чтобы воспользоваться классомXY,необходимо включить в компиляцию все его файлы. Так, если файлы классаXYназываютсяxyl. cs, ху2 . csихуЗ . cs,а классTestсодержится в файлеtest. cs,то для его компиляции достаточно ввести в командной строке следующее.
csc test.cs xyl.cs xy2.cs xy3.cs
И последнее замечание: в C# допускаются частичные обобщенные классы. Но параметры типа в объявлении каждого такого класса должны совпадать с теми, что указываются в остальных его частях.
Частичные методы
Как пояснялось в предыдущем разделе, с помощью модификатораpartialможно создать класс частичного типа. Начиная с версии 3.0, в C# появилась возможность использовать этот модификатор и для созданиячастичного методав элементе данных частичного типа. Частичный метод объявляется в одной его части, а реализуется в другой. Следовательно, с помощью модификатораpartialможно отделить объявление метода от его реализации в частичном классе или структуре.
Главная особенность частйчного метода заключается в том, что его реализация не требуется! Если частичный метод не реализуется в другой части класса или структуры, то все его вызовы молча игнорируются. Это дает возможность определить, но не востребовать дополнительные, хотя и не обязательные функции класса. Если эти функции не реализованы, то они просто игнорируются.
Ниже приведена расширенная версия предыдущей программы, в которой создается частичный методShow (). Этот метод вызывается другим методом,ShowXY (). Ради удобства все части классаXYпредставлены в одном файле, но они могут быть распределены по отдельным фацлам, как было показано в предыдущем разделе.
// Продемонстрировать применение частичного метода.
using System;
partial class XY {
public XY(int a, int b) {
X = a;
Y = b;
}
// Объявить частичный метод, partial void Show();
}
partial class XY {
public int X { get; set; }
// Реализовать частичный метод, partial void Show() {
Console.WriteLine("{0}, {1}", X, Y);
}
}
partial class XY {
public int Y { get; set; }
// Вызвать частичный метод, public void ShowXY() {
Show();
}
}
class Test {
static void Main() {
XY xy = new XY(1, 2); xy.ShowXY ();
}
}
Обратите внимание на то, что методShow() объявляется в одной части классаXY,а реализуется в другой его части. В реализации этого метода выводятся значения координат X и Y. Это означает, что когда методShow () вызывается из методаShowXY (),то данный вызов действительно имеет конкретные последствия: вывод значений
координат X и Y. Но если закомментировать реализацию методаShow (), то его вызов из методаShowXY() ни к чему не приведет.
Частичным методам присущ ряд следующих ограничений. Они должны возвращать значение типа void. У них не может быть модификаторов доступа и они не могут быть виртуальными. В них нельзя также использовать параметры out.
Создание объектов динамического типа
Как уже упоминалось не раз, начиная с главы 3, C# является строго типизированным языком программирования. Вообще говоря, это означает, что все операции проверяются во время компиляции на соответствие типов, и поэтому действия, не поддерживаемые конкретным типом, не подлежат компиляции. И хотя строгий контроль типов дает немало преимуществ программирующему, помогая создавать устойчивые и надежные программы, он может вызвать определенные осложнения в тех случаях, когда тип объекта остается неизвестным вплоть до времени выполнения. Нечто подобное может произойти при использовании рефлексии, доступе к COM-объекту или же в том случае, если требуется возможность взаимодействия с таким динамическим языком, как, например, IronPython. До появления версии C# 4.0 подобные ситуации были трудноразрешимы. Поэтому для выхода из столь затруднительного положения в версии C# 4.0 был внедрен новый тип данных под названием dynamic.
За одним важным исключением, тип dynamic очень похож на тип object, поскольку его можно использовать для ссылки на объект любого типа. А отличается он от типа object тем, что вся проверка объектов типа dynamic на соответствие типов откладывает до времени выполнения, тогда как объекты типа object подлежат этой проверке во время компиляции. Преимущество откладывания подобной проверки до времени выполнения состоит в том, что во время компиляции предполагается, что объект типа dynamic поддерживает любые операции, включая применение операторов, вызовы методов, доступ к полям и т.д. Это дает возможность скомпилировать код без ошибок. Конечно, если во время выполнения фактический тип, присваиваемый объекту, не поддерживает ту или иную операцию, то возникнет исключительная ситуация во время выполнения.
В приведенном ниже примере программы применение типа dynamic демонстрируется на практике.
// Продемонстрировать применение типа dynamic, using System;
using System.Globalization;
class DynDemo {
static void Main() {
// Объявить две динамические переменные, dynamic str; dynamic val;
// Поддерживается неявное преобразование в динамические типы.
// Поэтому следующие присваивания вполне допустимы, str = "Это строка"; val = 10;
Console.WriteLine("Переменная str содержит: " + str);
Console.WriteLine("Переменная val содержит: " + val + '\n');
str = str.ToUpper (Culturelnfo.CurrentCulture);
Console.WriteLine("Переменная str теперь содержит: " + str);
val = val + 2;
Console.WriteLine("Переменная val теперь содержит: " + val + '\n');
string str2 = str.ToLower(Culturelnfo.CurrentCulture);
Console.WriteLine("Переменная str2 содержит: " + str2);
// Поддерживаются неявные преобразования из динамических типов.
int х = val * 2;
Console.WriteLine("Переменная x содержит: " + x);
}
}
Выполнение этой программы дает следующий результат.
Переменная str содержит: Это строка Переменная val содержит: 10
Переменная str теперь содержит: ЭТО СТРОКА Переменная val теперь содержит: 12
Переменная str2 содержит: это строка Переменная х содержит: 24
Обратите внимание в этой программе на две переменныеstrиval,объявляемые с помощью типаdynamic.Это означает, что проверка на соответствие типов операций с участием обеих переменных не будет произведена во время компиляции. В итоге для них оказывается пригодной любая операция. В данном случае для переменнойstrвызываются методыToUpper() иToLower() классаString,а переменная участвует в операциях сложения и умножения. И хотя все перечисленные выше действия совместимы с типами объектов, присваиваемых обеим переменным в рассматриваемом здесь примере, компилятору об этом ничего не известно — он просто принимает. И это, конечно, упрощает программирование динамических процедур, хотя и допускает возможность Появления ошибок в подобных действиях во время выполнения.
В разбираемом здесь примере программа ведет себя "правильно" во время выполнения, поскольку объекты, присваиваемые упомянутым выше переменным, поддерживают действия, выполняемые в программе. В частности, переменнойvalприсваивается целое значение, и поэтому она поддерживает такие целочисленные операции, как сложение. А переменнойstrприсваивается символьная строка, и поэтому она поддерживает строковые операции. Следует, однако, иметь в виду, что ответственность за фактическую поддержку типом объекта, на который делается ссылка, всех операций над данными типаdynamicвозлагается на самого программирующего. В противном случае выполнение программы завершится аварийным сбоем.
В приведенном выше примере обращает на себя внимание еще одно обстоятельство: переменной типаdynamicможет быть присвоен любой тип ссылки на объект благодаря неявному преобразованию любого типа в типdynamic.Кроме того, типdynamicавтоматически преобразуется в любой другой тип. Разумеется, если во время выполнения такое преобразование окажется неправильным, то произойдет ошибка
при выполнении. Так, если добавить в конце рассматриваемой здесь программы следующую строку кода:
bool b = val;
то возникнет ошибка при выполнении из-за отсутствия неявного преобразования типа int (который оказывается типом переменной val во время выполнения) в тип bool. Поэтому данная строка кода приведет к ошибке при выполнении, хотя она и будет скомпилирована безошибочно.
Прежде чем оставить данный пример программы, попробуйте поэкспериментировать с ней. В частности, измените тип переменных str и val на object, а затем попытайтесь скомпилировать программу еще раз. В итоге появятся ошибки при компиляции, поскольку тип object не поддерживает действия, выполняемые над обеими переменными, что и будет обнаружено во время компиляции. В этом, собственно, и заключается основное отличие типов object и dynamic. Несмотря на то что оба типа могут использоваться для ссылки на объект любого другого типа, над переменной типа object можно производить только те действия, которые поддерживаются типом object. Если же вы используете тип dynamic, то можете указать какое угодно действие, при условии что это действие поддерживается конкретным объектом, на который делается ссылка во время выполнения.
Для того чтобы стало понятно, насколько тип dynamic способен упростить решение некоторых задач, рассмотрим простой пример его применения вместе с рефлексией. Как пояснялось в главе 17, чтобы вызвать метод для объекта класса, получаемого во время выполнения с помощью рефлексии, можно, в частности, обратиться к методу Invoke (). И хотя такой способ оказывается вполне работоспособным, нужный метод намного удобнее вызвать по имени в тех случаях, когда его имя известно. Например, вполне возможна такая ситуация, когда в некоторой сборке содержится конкретный класс, поддерживающий методы, имена и действия которых заранее известны. Но поскольку эта сборка подвержена изменениям, то приходится постоянно убеждаться в том, что используется последняя ее версия. Для проверки текущей версии сборки можно, например, воспользоваться рефлексией, сконструировать объект искомого класса, а затем вызвать методы, определенные в этом классе. Теперь эти методы можно вызвать по имени с помощью типа dynamic, а не метода Invoke (), поскольку их имена известны.
Разместите сначала приведенный ниже код в файле с именем MyClass . cs. Этот код будет динамически загружаться посредством рефлексии.
public class DivBy {
public bool IsDivBy(int a, int b) { if ( (a % b) == 0) return true; return false;
}
public bool IsEven(int a) { if ( (a % 2) == 0) return true; return false;
}
}
Затем скомпилируйте этот файл в библиотеку DLL под именем MyClass .dll. Если вы пользуетесь компилятором командной строки, введите в командной строке следующее.
Далее составьте программу, в которой применяется библиотекаMyClass . dll,как показано ниже.
// Использовать тип dynamic вместе с рефлексией.
using System;
using System.Reflection;
class DynRefDemo { static void Main() {
Assembly asm = Assembly.LoadFrom("MyClass.dll");
Type[] all = asm.GetTypes();
// Найти класс DivBy. int i;
for(i =0; i < all.Length; i++) if(all[i].Name == "DivBy") break;
if(i == all.Length) {
Console.WriteLine("Класс DivBy не найден в сборке."); return;
}
Type t = all[i];
//А теперь найти используемый по умолчанию конструктор.
Constructorlnfo[] ci = t.GetConstructors();
int j ;
for(j =0; j < ci.Length; j++)
if(ci[j].GetParameters().Length == 0) break;
if(j == ci.Length) {
Console.WriteLine("Используемый по умолчанию конструктор не найден."); return;
}
I
// Создать объект класса DivBy динамически, dynamic obj = ci[j].Invoke(null);
// Далее вызвать по имени методы для переменной obj. Это вполне допустимо,
// поскольку переменная obj относится к типу dynamic, а вызовы методов // проверяются на соответствие типов во время выполнения, а не компиляции, if(obj.IsDivBy(15, 3))
Console.WriteLine("15 делится нацело на 3."); else
Console.WriteLine("15 HE делится нацело на 3.");
if(obj.IsEven(9))
Console.WriteLine("9 четное число."); else
Как видите, в данной программе сначала динамически загружается библиотека MyClass . dll, а затем используется рефлексия для построения объекта класса DivBy. Построенный объект присваивается далее переменной obj типа dynamic. А раз так, то методы Is DivBy () и IsEven () могут быть вызваны для переменной obj по имени, а не с помощью метода Invoke (). В данном примере это вполне допустимо, поскольку переменная obj на самом деле ссылается на объект класса DivBy. В противном случае выполнение программы завершилось бы неудачно.
Приведенный выше пример сильно упрощен и несколько надуман. Тем не менее он наглядно показывает главное преимущество, которое дает тип dynamic в тех случаях, когда типы получаются во время выполнения. Когда характеристики искомого типа, в том числе методы, операторы, поля и свойства, заранее известны, эти характеристики могут быть получены по имени с помощью типа dynamic, как следует из приведенного выше примера. Благодаря этому код становится проще, короче и понятнее.
Применяя тип dynamic, следует также иметь в виду, что при компиляции программы тип dynamic фактически заменяется объектом, а для описания его применения во время выполнения предоставляется соответствующая информация. И поскольку тип dynamic компилируется в тип object для целей перегрузки, то оба типа dynamic и object расцениваются как одно и то же. Поэтому при компиляции двух следующих перегружаемых методов возникнет ошибка.
static void f(object v) { // ... }
static void f(dynamic v) {//...}// Ошибка!
И последнее замечание: тип dynamic поддерживается компонентом DLR (Dynamic Language Runtime — Средство создания динамических языков во время выполнения), внедренным в .NET 4.0.
Возможность взаимодействия с моделью СОМ
В версии C# 4.0 внедрены средства, упрощающие возможность взаимодействия с неуправляемым кодом, определяемым моделью компонентных объектов (СОМ) и применяемым, в частности, в COM-объекте Office Automation. Некоторые из этих средств, в том числе тип dynamic, именованные и необязательные свойства, пригодны для применения помимо возможности взаимодействия с моделью СОМ. Тема модели СОМ вообще и COM-объекта Office Automation в частности весьма обширна, а порой и довольно сложна, чтобы обсуждать ее в этой книге. Поэтому возможность взаимодействия с моделью СОМ выходит за рамки данной книги.
Тем не менее две особенности, имеющие отношение к возможности взаимодействия с моделью СОМ, заслуживают краткого рассмотрения в этом разделе. Первая из них состоит в применении индексированных свойств, а вторая — в возможности передавать аргументы значения тем COM-методам, которым требуется ссылка.
Как вам должно быть уже известно, в C# свойство обычно связывается только с одним значением с помощью одного из аксессоровgetилиset.Но совсем иначе дело обстоит со свойствами модели СОМ. Поэтому, начиная с версии C# 4.0, в качестве выхода из этого затруднительного положения во время работы с COM-объектом появилась возможность пользоватьсяиндексированным свойствомдля доступа к COM-свойству, имеющему несколько параметров. С этой целью имя свойства индексируется, почти так же, как это делается с помощью индексатора. Допустим, что имеется объектmyXLApp,который относится к типуMicrosoft.Office. Inter op.Execl . Application.
В прошлом для установки строкового значения "ОК" в ячейках С1-СЗ электронной таблицы Excel можно было бы воспользоваться оператором, аналогичным следующему.
myXLapp.get_Range("Cl", "СЗ").set_Value(Type.Missing, "OK");
В этой строке кода интервал ячеек электронной таблицы получается при вызове методаget Range (), для чего достаточно указать начало и конец интервала. А значения задаются при вызове методаset_Value (), для чего достаточно указать тип (что не обязательно) и конкретное значение. В этих методах используются свойстваRangeиValue,поскольку у обоих свойств имеются два параметра. Поэтому в прошлом к ним нельзя было обращаться как к свойствам, но приходилось пользоваться упомянутыми выше методами. Кроме того, аргументType .Missingслужил в качестве обычного заполнителя, который передавался для указания на тип, используемый по умолчанию. Но, начиная с версии C# 4.0, появилась возможно переписать приведенный выше оператор, приведя его к следующей более удобной форме.
myXLapp.Range["Cl", "СЗ"].Value = "OK";
В этом случае значения интервала ячеек электронной таблицы передаются с использованием синтаксиса индексаторов, а заполнительType .Missingуже не нужен, поскольку данный параметр теперь задается по умолчанию.
Как правило, при определении в методе параметраrefприходится передавать ссылку на этот параметр. Но, работая с моделью СОМ, можно передавать параметруrefзначение, не заключая его предварительно в оболочку объекта. Дело в том, что компилятор будет автоматически создавать временный аргумент, который уже заключен в оболочку объекта, и поэтому указывать параметрrefв списке аргументов уже не нужно.
Дружественные сборки
Одну сборку можно сделатьдружественнойпо отношению к другой. Такой сборке доступны закрытые члены дружественной ей сборки. Благодаря этому средству становится возможным коллективное использование членов выбранных сборок, причем эти члены не нужно делать открытыми. Для того чтобы объявить дружественную сборку, необходимо воспользоваться атрибутомInternals Vi sibleTo.
Разные ключевые слова
В заключение этой главы в частности и всей части I вообще будут вкратце представлены ключевые слова, определенные в C# и не упоминавшиеся в предыдущих главах данной книги.
Ключевое слов lock
Ключевое словоlockиспользуется при создании многопоточных программ. Подробнее оно рассматривается в главе 23, где речь пойдет о многопоточном программировании. Но ради полноты изложения ниже приведено краткое описание этого ключевого слова.
Программа на C# может состоять из несколькихпотоков исполнения.В этом случае программа считаетсямногопоточной,и отдельные ее части выполняются параллельно, т.е. одновременно и независимо друг от друга. В связи с такой организацией программы возникает особого рода затруднение, когда два потока пытаются воспользоваться ресурсом, которым можно пользоваться только по очереди. Для разрешения этого затруднения можно создатькритический раздел кода,который будет одновременно выполняться одним и только одним потоком. И это делается с помощью ключевого слова lock. Ниже приведена общая форма этого ключевого слова:
lock(obj) {
// критический раздел кода
}
где obj обозначает объект, для которого согласуется блокировка кода. Если один поток уже вошел в критический раздел кода, то второму потоку придется ждать до тех пор, пока первый поток не выйдет из данного критического раздела кода. Когда же первый поток покидает критический раздел кода, блокировка снимается и предоставляется второму потоку. С этого момента второй поток может выполнять критический раздел кода.
ПРИМЕЧАНИЕ
Более подробно ключевое слово lock рассматривается в главе 23.
Ключевое слово readonly
Отдельное поле можно сделать доступным в классе только для чтения, объявив его как readonly. Значение такого поля можно установить только с помощью инициализатора, когда оно объявляется или же когда ему присваивается значение в конструкторе. После того как значение доступного только для чтения поля будет установлено, оно не подлежит изменению за пределами конструктора. Следовательно, поле типа readonly удобно для установки фиксированного значения с помощью конструктора. Такое поле можно, например, использовать для обозначения размера массива, который часто используется в программе. Допускаются как статические, так и нестатические поля типа readonly.
ПРИМЕЧАНИЕ
Несмотря на кажущееся сходство, поля типа readonly не следует путать с полями типа const, которые рассматриваются далее в этой главе.
Ниже приведен пример применения поля с ключевым словомreadonly. // Продемонстрировать применение поля с ключевым словом readonly.
using System;
class MyClass {
public static readonly int SIZE = 10;
class DemoReadOnly {
■ static void Main() {
int[] source = new int[MyClass.SIZE]; int[] target = new int[MyClass.SIZE];
// Присвоить ряд значений элементам массива source, for(int i=0; i < MyClass.SIZE; i++) source[i] = i;
foreach(int i in source)
Console.Write(i + " ");
Console.WriteLine();
// Перенести обращенную копию массива source в массив target. for(int i = MyClass.SIZE-1, j = 0; i > 0; i—, j++) target[j] = source[i];
foreach(int i in target)
Console.Write(i + " ");
Console.WriteLine();
// MyClass.SIZE = 100; // Ошибка!!! He подлежит изменению!
}
}
В данном примере полеMyClass .SIZEинициализируется значением 10. После этого его можно использовать, но не изменять. Для того чтобы убедиться в этом, удалите символы комментария в начале последней строки приведенного выше кода и попробуйте скомпилировать его. В итоге вы получите сообщение об ошибке.
Ключевые слова const и volatile
Ключевое слово, или модификатор,constслужит для объявления полей и локальных переменных, которые нельзя изменять. Исходные значения таких полей и переменных должны устанавливаться при их объявлении. Следовательно, переменная с модификаторомconst,по существу, является константой. Например, в следующей строке кода:
const int i = 10;
создается переменнаяiтипаconstи устанавливается ее значение 10. Поле типаconstочень похоже на поле типаreadonly,но все же между ними есть отличие. Если поле типаreadonlyможно устанавливать в конструкторе, то поле типаconst— нельзя.
Ключевое слово, или модификатор,volatileуведомляет компилятор о том, что значение поля может быть изменено двумя или более параллельно выполняющимися потоками. В этой ситуации одному потоку может быть неизвестно, когда поле было изменено другим потоком. И это очень важно, поскольку компилятор C# будет автоматически выполнять определенную оптимизацию, которая будет иметь результат лишь в том случае, если поле доступно только одному потоку. Для того чтобы подобной оптимизации не подвергалось общедоступное поле, оно объявляется какvolatile.
Этим компилятор уведомляется о том, что значение поля типаvolatileследует получать всякий раз, когда к нему осуществляется доступ.
Оператор using
Помимо рассматривавшейся ранеедирективыusing, имеется вторая форма ключевого слова using в видеоператора.Ниже приведены две общие формы этого оператора:
using (obj) {
// использовать объектobj
}
using (типobj = инициализатор){
// использовать объектobj
}
гдеobjявляется выражением, в результате вычисления которого должен быть получен объект, реализующий интерфейсSystem. IDisposable.Этот объект определяет переменную, которая будет использоваться в блоке оператораusing.В первой форме объект объявляется вне оператораusing,а во второй форме — в этом операторе. По завершении блока оператораusingдля объектаobjвызывается методDispose (),определенный в интерфейсеSystem. IDisposable.Таким образом, операторusingпредоставляет средства, необходимые для автоматической утилизации объектов, когда они больше не нужны. Не следует, однако, забывать, что операторusingприменяется только к объектам, реализующим йнтерфейсSystem. IDisposable.
В приведенном ниже примере демонстрируются обе формы оператора using.
// Продемонстрировать применение оператора using.
using System; using System.10;
class UsingDemo {
static void Main() { try {
StreamReader sr = new StreamReader("test.txt") ;
// Использовать объект в операторе using, using(sr) {
// ...
}
} catch(IOException exc) {
// ...
}
try {
// Создать объект класса StreamReader в операторе using, using(StreamReader sr2 = new StreamReader("test.txt")) {
// ...
}
} catch(IOException exc) {
// ...
В данном примере интерфейсI Disposableреализуется в классеStreamReader(посредством его базового классаTextReader).Поэтому он может использоваться в оператореusing.По завершении этого оператора автоматически вызывается методDispose() для переменной потока, закрывая тем самым поток.
Как следует из приведенного выше примера, операторusingособенно полезен для работы с файлами, поскольку файл автоматически закрывается по завершении блока этого оператора, даже если он и завершается исключением. Таким образом, закрытие файла с помощью оператораusingзачастую упрощает код обработки файлов. Разумеется, применение оператораusingне ограничивается только работой с файлами. В среде .NET Framework имеется немало других ресурсов, реализующих интерфейсI Disposable.И всеми этими ресурсами можно управлять с помощью оператораusing.
Ключевое слово extern
Ключевое словоexternнаходит два основных применения. Каждое из них рассматривается далее по порядку.
Объявление внешних методов
В первом своем применении ключевое словоexternбыло доступно с момента создания С#. Оно обозначает, что метод предоставляется в неуправляемом коде, который не является составной частью программы. Иными словами, метод предоставляется внешним кодом.
Для того чтобы объявить метод как внешний, достаточно указать в самом начале его объявления модификаторextern.Таким образом, общая форма объявления внешнего метода выглядит следующим образом.
externвозвращаемый_тип имя_метода (список_аргументов);
Обратите внимание на отсутствие фигурных скобок.
В данном варианте ключевое словоexternнередко применяется вместе с атрибутомDll Import,обозначающим библиотеку DLL, в которой содержится внешний метод. АтрибутDll Importпринадлежит пространству именSystem. Runtime . Inter op Services.Он допускает несколько вариантов, но, как правило, достаточно указать лишь имя библиотеки DLL, в которой содержится внешний метод. Вообще говоря, внешние методы следует программировать на С. (Если же это делается на C++, то имя внешнего метода может быть изменено в библиотеке DLL путем дополнительного оформления типов.)
Для того чтобы стало понятнее, как пользоваться внешними методами, обратимся к примеру конкретной программы, состоящей из двух файлов. Ниже приведен исходный код С из первого файлаExtMeth. с,где определяется методAbsMax ().
#include <stdlib.h>
int _declspec(dllexport) AbsMax(int a, int b) {
return abs(a) < abs(b) ? abs(b) : abs(a);
}
В методеAbsMax() сравниваются абсолютные значения двух его параметров и возвращается самое большое из них. Обратите внимание на обозначение _declspec (dllexport). Это специальное расширение языка С для программных средств корпорации Microsoft. Оно уведомляет компилятор о необходимости экспортировать метод AbsMax () из библиотеки DLL, в которой он содержится. Для компилирования файла ExtMeth. с в командной строке указывается следующее.
CL /LD /MD ExtMeth.с ,
В итоге создается библиотечный файл DLL — ExtMeth .dll.
Далее следует программа на С#, в которой применяется внешний метод AbsMax().
using System;
using System.Runtime.InteropServices;
class ExternMeth {
// Здесь объявляется внешний метод.
[Dlllmport("ExtMeth.dll")]
public extern static int AbsMax(int a, int b);
static void Main() {
// Использовать внешний метод, int max = AbsMax(-10, -20);
Console.WriteLine(max);
}
}
Обратите внимание на использование атрибута Dlllmport в приведенной выше программе. Он уведомляет компилятор о наличии библиотеки DLL, содержащей внешний метод AbsMax (). В данном случае это файл ExtMeth. dll, созданный во время компиляции файла с исходным текстом метода AbsMax () на С. В результате выполнения данной программы на экран, как и ожидалось, выводится значение 20.
Объявление псевдонима внешней сборки
Во втором применении ключевое слово extern предоставляет псевдоним для внешней сборки, что полезно в тех случаях, когда в состав программы включаются две отдельные сборки с одним и тем же именем элемента. Так, если в сборке testl содержится класс MyClass, а в сборке test2 класс с таким же именем, то при обращении к классу по этому имени в одной и той же программе может возникнуть конфликт.
Для разрешения подобного конфликта необходимо создать псевдоним каждой сборки. Это делается в два этапа. На первом этапе нужно указать псевдонимы, используя параметр компилятора /г, как в приведенном ниже примере.
/г:Asml=testl /г:Asm2=test2
А на втором этапе необходимо ввести операторы с ключевым словом extern, в которых делается ссылка на указанные выше псевдонимы. Ниже приведена форма такого оператора для создания псевдонима сборки.
extern aliasимя_сборки;
Если продолжить приведенный выше пример, то в программе должны появиться следующие строки кода.
extern alias Asml; extern alias Asm2;
Теперь оба варианта классаMyClassбудут доступны в программе по соответствующему псевдониму.
Рассмотрим полноценный пример программы, в которой демонстрируется применение внешних псевдонимов. Эта программа состоит из трех файлов. Ниже приведен исходный текст, который следует поместить в первый файл —testl.cs.
using System;
namespace MyNS {
public class MyClass { public MyClassO {
Console.WriteLine("Конструирование из файла MyClassl.dll.");
}
}
}
Далее следует исходный текст из файлаtest2.cs.
using System;
namespace MyNS {
public class MyClass { public MyClassO {
Console.WriteLine("Конструирование из файла MyClass2.dll.");
}
}
}
Обратите внимание на то, что в обоих файлах,testl. csиtest2 . cs,объявляется пространство именMyNSи что именно в этом пространстве в обоих файлах определяется классMyClass.Следовательно, без псевдонима оба варианта классаMyClassбудут недоступными ни одной из программ.
И наконец, ниже приведен исходный текст из третьего файлаtest3.cs,где используются оба варианта классаMyClassиз файловtestl. csиtest2 . cs.Это становится возможным благодаря операторам с внешними псевдонимами.
// Операторы с внешними псевдонимами должны быть указаны в самом начале файла, extern alias Asml; extern alias Asm2;
using System;
class Demo {
static void Main() {
Asml::MyNS.MyClass t = new Asml::MyNS.MyClass() ;
Asm2::MyNS.MyClass t2 = new Asm2::MyNS.MyClass();
}
}
Сначала следует скомпилировать файлыtestl. csиtest2 . csв их библиотечные эквиваленты DLL. Для этого достаточно ввести в командной строке следующее.
csc /t:library testl.cs csc /t:library test2.cs
Затем необходимо скомпилировать файлtest3.cs,указав в командной строке
csc /г:Asml=testl.dll /г:Asm2=test2.dll test3.cs
Обратите внимание на применение параметра / г, уведомляющего компилятор
о том, что ссылка на метаданные находится в соответствующем файле. В данном случае псевдонимAsmlсвязывается с файломtestl. dll,а псевдонимAsm2— с файломtest2.dll.
В самой программе псевдонимы указываются в приведенных ниже операторах с модификаторомextern,которые располагаются в самом начале файла.
extern alias Asml; extern alias Asm2;
А в методеMain() псевдонимы используются для разрешения неоднозначности ссылок на классMyClass.Обратите внимание на следующее применение псевдонима для обращения к классуMyClass.
Asml::MyNS.MyClass
В этой строке кода первым указывается псевдоним, затем оператор разрешения пространства имен, далее имя пространства имен, в котором находится класс с неоднозначным именем, и, наконец, имя самого класса, следующее после оператора-точки. Та же самая общая форма пригодна и для других внешних псевдонимов.
Ниже приведен результат выполнения данной программы.
Конструирование из файла MyClassl.dll.
Конструирование из файла MyClass2.dll.
ЧАСТЬ 2 Библиотека C#
В части II рассматривается библиотека С#. Как пояснялось в части I, используемая в C# библиотека на самом деле является библиотекой классов для среды .NET Framework. Поэтому материал этой части книги имеет отношение не только к языку С#, но и ко всей среде .NET Framework в целом.
Библиотека классов для среды .NET Framework организована по пространствам имен. Для использования отдельной части этой библиотеки, как правило, достаточно импортировать ее пространство имен, указав его с помощью директивы using в исходном тексте программы. Конечно, ничто не мешает определить имя отдельного элемента библиотеки полностью вместе с его пространством имен, но ведь намного проще импортировать сразу все пространство имен.
Библиотека среды .NET Framework довольно обширна, и поэтому ее полное описание выходит за рамки этой книги. (На самом деле для этого потребовалась бы отдельная и довольно объемистая книга!) Поэтому в части II рассматриваются лишь самые основные элементы данной библиотеки, многие из которых находятся в пространстве имен System. Кроме того, в этой части описываются классы коллекций, а также вопросы организации многопоточной обработки и сетей.
ГЛАВА 21 Пространство имен System
ГЛАВА 22 Строки и форматирование
ГЛАВА 23 Многопоточное программирование. Часть первая: основы
ГЛАВА 24 Много] юточное программирование. Часть вторая: библиогека TPL
ГЛАВА 25 Коллекции, перечислители и итераторы
ГЛАВА 26 Сетевые средства подключения к Интернету
ПРИМЕЧАНИЕ
Классы ввода-вывода подробно рассматривались в главе 14.
ГЛАВА 21 Пространство имен System
В этой главе речь пойдет о пространстве именSystem.Это пространство имен самого верхнего уровня в библиотеке классов для среды .NET Framework. В нем непосредственно находятся те классы, структуры, интерфейсы, делегаты и перечисления, которые чаще всего применяются в программах на C# или же считаются неотъемлемой частью среды .NET Framework. Таким образом, пространство именSystemсоставляет ядро рассматриваемой здесь библиотеки классов.
Кроме того, в пространство именSystemвходит много вложенных пространств имен, поддерживающих отдельные подсистемы, напримерSystem.Net.Некоторые из этих пространств имен рассматриваются далее в этой книге. А в этой главе речь пойдет только о членах самого пространства именSystem.
Члены пространства имен System
Помимо большого количества классов исключений, в пространстве имен содержатся приведенные ниже классы.
ActivationContext
Activator
AppDomain
AppDomainManager
AppDomainSetup
Applicationld
Applicationldentity
Array
Asse m b ly Loa d Eve ntA rgs
Attribute
AttributeUsageAttribute
BitConverter
Buffer
CharEnumerator
CLSCompliantAttribute
Console
ConsoleCancelEventArgs
Co ntext Bo u n d 0 bj ect
Co ntextStat i cAttri b ute
Convert
DBNull
Delegate
Enum
Environment
EventArgs
Exception
FileStyleUriParser
FlagsAttribute
FtpStylellri Parser
GC
GenericUriParser
GopherStylellri Parser
HttpStyleUri Parser
Lazy<T>
Lazy<T, TMetadata>
LdapStyleUriParser
LoaderOptimizationAttribute
Local DataStoreSlot
Ma rsha 1 By RefO bject
Math
MTATh read Attribute
MulticastDelegate
NetPipeStylellriParser
NetTcpStylellriParser
NewsStyleUriParser
NonSerializedAttribute
Nullable
Object
ObsoleteAttribute
Ope rati ngSystem
Pa ra m Ar rayAtt r i b ute
Random
ResolveEventArgs
SerializableAttribute
STAThreadAttribute
String
StringCom parer
ThreadStaticAttribute
TimeZone
TimeZonelnfo
TimeZonelnfo.AdjustmentRule
Tuple
Tuple<...> (различные формы)
Type
Unhandled Exception EventArgs
Uri
UriBuilder
Uri Parser
UriTemplate
UriTemplateEquivalenceComparer
UriTemplateMatch
UriTemplateTable
UriTypeConverter
ValueType
Version
WeakReference
Ниже приведены структуры, определенные в пространстве имен System.
Arglterator
ArraySegment<T>
Boolean
Byte
Char '
ConsoleKeylnfo
DateTime
DateTimeOffset
Decimal
Double
Guid
Int 16
Int32
Int64
IntPtr
ModuleHandle
Nullable<T>
RuntimeArgumentHandle
RuntimeFieldHandle
RuntimeMethodHandle
RuntimeTypeHandle
Sbyte
Single
TimeSpan
TimeZonelnfo.TransitionTime
Typed Reference
Uint16
Ulnt32
Ulnt64
U IntPtr
Void
_AppDomain
lappDomainSetup
lAsyncResult
ICIoneable
IComparable
IComparable<T>
IConvertible
1 Custom Formatter
IDisposable
IEquatable<T>
IFormatProvider
IFormattable
IObservable<T>
IObserver<T>
IServiceProvider
Ниже приведены делегаты, определенные в пространстве именSystem.
Action
Action<...> (различные формы)
AppDomainlnitializer
AssemblyLoadEventHandler
AsyncCallback
Comparison<T>
ConsoleCancelEventHandler
Converter<Tlnput, VOutput>
CrossAppDoma in Delegate
EventHandler
EventHandler<TEventArgs>
Func<...> (различные формы)
Predicate<T>
ResolveEventHandler
Unhandled ExceptionEventHandler
В пространстве именSystemопределены приведенные ниже перечисления.
ActivationContext.contextForrr
i AppDomainManagerlnitializationOptions AttributeTargets
Base64Formatting0ptions
ConsoleColor
ConsoleKey
ConsoleModifiers
ConsoleSpecialKey
DateTimeKind
DayOfWeek
Environment.SpecialFolder
Environment.SpecialFolderOption
EnvironmentVariableTarget
GCCol lection Mode
GCNotificationStatus
GenericUriParserOptions
LoaderOptimization
MidpointRounding
PlatformID
StringComparison
StringSplitOptions
TypeCode
UriComponents
UriFormat
UriHostNameType
UrildnScope
UriKind
Uri Partial
Как следует из приведенных выше таблиц, пространство именSystemдовольно обширно, поэтому в одной главе невозможно рассмотреть подробно все его составляющие. К тому же, некоторые члены пространства именSystem,в том числеNullable<T>, Type, ExceptionиAttribute,уже рассматривались в части I или будут представлены в последующих главах части II. И наконец, классSystem. String,в котором определяется типstringдля символьных строк в С#, обсуждается вместе с вопросами форматирования в главе 22. В силу этих причин в настоящей главе рассматриваются только те члены данного пространства имен, которые чаще всего применяются в программировании на C# и не поясняются полностью в остальных главах книги.
Класс Math
В классеMathопределен ряд стандартных математических операций, в том числе извлечение квадратного корня, вычисление синуса, косинуса и логарифмов. КлассMathявляется статическим, а это означает, что все методы, определенные в нем, относятся к типуstatic,объекты типаMathне конструируются, а сам классMathнеявно герметичен и не может наследоваться. Методы, определенные в классеMath,перечислены в табл. 21.1, где все углы указаны в радианах.
В классеMathопределены также два следующих поля:
public const double Е public const double PI
гдеE— значение основания натурального логарифма числа, которое обычно обозначается каке) aPI— значение числа пи.
Метод
Описание
public static double
Возвращает абсолютную величину value
Abs(doublevalue)
public static float
Возвращает абсолютную величину value
Abs(floatvalue)
public static decimal
Возвращает абсолютную величину value
Abs(decimalvalue)
public static int Abs(int
' Возвращает абсолютную величину value
value)
public static short
Возвращает абсолютную величину value
Abs(shortvalue)
public static long Abs(long
Возвращает абсолютную величину value
value)
public static sbyte
Возвращает абсолютную величину value
Abs(sbytevalue)
public static double
Возвращает арккосинус d. Значение d должно на
Acos(double d)
ходиться в пределах от -1 до 1
public static double
Возвращает арксинус d. Значение d должно нахо
Asin(double d)
диться в пределах от -1 до 1
public static double
Возвращает арктангенс d
Atan(double d)
public static double
Возвращает арктангенс частного от деления у/х
Atan2(double y, doublex)
public static long
Возвращает произведение а*Ь в виде значения
BigMul(inta,intb)
типа long, исключая переполнение
public static double
Возвращает наименьшее целое, которое пред
Ceiling(doublea)
ставлено в виде значения с плавающей точкой и не меньше а. Так, если а равно 1,02, метод Ceiling () возвращает значение 2,0. А если а равно -1,02, то метод Ceiling () возвращает значение -1
public static double
Возвращает наименьшее целое, которое представ
Ceiling(decimal d)
лено в виде значения десятичного типа и не меньше d. Так, если d равно 1,02, метод Ceiling () возвращает значение 2,0. А если d равно -1,02, то метод Ceiling () возвращает значение -1
public static double
Возвращает косинус d
Cos(double d)
public static double
Возвращает гиперболический косинус d
Cosh(double d)
public static int DivRem(inta,
Возвращает частное от деления а / Ь, а остаток —
intb,out intresult)
в виде параметра resul t типа out -
public static
Возвращает частное от деления а / Ь, а остаток —
long DivRem(longa,
в виде параметра result типа out
longb,out longresult)
_Продолжение табл. 21.1
Метод
Описание
public static double
Возвращает основание натурального логарифма е,
Exp (double"d)
возведенное в степень d
public static decimal
Возвращает наибольшее целое, которое представ
Floor(decimal d)
лено в виде значения десятичного типа и не больше d. Так, если d равно 1,02, метод Floor () возвращает значение 1,0. А если d равно -1,02, метод Floor () возвращает значение -2
public static double
Возвращает наибольшее целое, которое представ
Floor(double d)
лено в виде значения с плавающей точкой и не больше d. Так, если d равно 1,02, метод Floor () возвращает значение 1,0. А если d равно -1,02, метод Floor () возвращает значение -2
public static double
Возвращает остаток от деления х/у
IEEERemainder(doublex,
double y)
public static double
Возвращает натуральный логарифм значения d
Log(double d)
public static double'
Возвращает натуральный логарифм по основанию
Log(doubled,double
newBase значения d
newBase)
public static double
Возвращает логарифм по основанию 10 значения d
LoglO(double d)
public static double
Возвращает большее из значений vail и val2
Max(doublevail,doubleval2)
public static float
Возвращает большее из значений vail и val2
Max(floatvail,floatval2)
public static decimal
Возвращает большее из значений vail и val2
Max(decimalvail,decimal
val2)
public static int Max(int
Возвращает большее из значений vail и val2
vail,intval2)
public static short Max(short
Возвращает большее из значений vail и val2
vail,shortval2)
public static long Max(long
Возвращает большее из значений vail и val2
vail,longval2)
public static uint Max(uint
Возвращает большее из значений vail и val2
vail,uintval2)
public static ushort
Возвращает большее из значений vail и val2
Max(ushortvail,ushortval2)
public static ulong
Возвращает большее из значений vail и val2
Max(ulongvail,ulongval2)
public static byte Max(byte
Возвращает большее из значений vail и val2
vail,byteval2)
public static sbyte Max(sbyte
Возвращает большее из значений vail и val2
vail,sbyteval2)
Метод
Описание
public static double
Возвращает меньшее из значений vail и val2
Min(doublevail,doubleval2)
public static float
Возвращает меньшее из значений vail и val2
Min(floatvail,floatval2)
public static decimal
Возвращает меньшее из значений vail и val2
Min(decimalvail,decimal
‘val2)
public static int Min(int
Возвращает меньшее из значений vail и val2
vail,intval2)
public static short Min(short
Возвращает меньшее из значений vail и val2
vail,shortval2)
public static long Min(long
Возвращает меньшее из значений vail и val2
vail,longval2)
public static uint Min(uint
Возвращает меньшее из значений vail и val2
vail,uintval2)
public static ushort
Возвращает меньшее из значений vail и val2
Min(ushortvail,ushortval2)
public static ulong Min(ulong
Возвращает меньшее из значений vail и val2
vail,ulongval2)
public static byte Min(byte
Возвращает меньшее из значений vail и val2
vail,byteval2)
public static sbyte Min(sbyte
Возвращает меньшее из значений vail и val2
vail,sbyteval2)
public static double
Возвращает значение х, возведенное в степень
Pow(doublex,double y)
у(хУ)
public static double
Возвращает значение а, округленное до ближайше
Round(doublea)
го целого числа
public static decimal
Возвращает значение d, округленное до ближайше
Round(decimal d)
го целого числа
public static double
Возвращает значение value, округленное до чис
Round(doublevalue,
ла, количество цифр в дробной части которого рав
intdigits)
но значению параметра digits
public static decimal
Возвращает значение d, округленное до числа, ко
Round(decimald,intdigits)
личество цифр в дробной части которого равно значению digi ts
public static double
Возвращает значение value, округленное до бли
Round(doublevalue,
жайшего целого числа в режиме, определяемом
MidpointRoundingmode)
параметром mode
public static decimal
Возвращает значение d, округленное до ближайше
Round(decimald,
го целого числа в режиме, определяемом параме
MidpointRoundingmode)
тром mode
public static double
Возвращает значение value, округленное до чис
Round(doublevalue,int
ла, количество цифр в дробной части которого рав
digits,MidpointRounding
но значению digi ts, а параметр mode определяет
mode)
режим округления
Окончание табл. 21.1
Метод
Описание
public
static decimal
Возвращает значение d, округленное до числа,
Round(decimald,intdigits,
количество цифр в дробной части которого равно
MidpointRoundingmode)
значению digits, а параметр mode определяет
режим округления
public
static int
Возвращает -1, если значение value меньше нуля;
Sign(doublevalue)
0, если значение value равно нулю; и 1, если зна
чение value больше нуля
public
static int Sign(float
Возвращает -1, если значение value меньше нуля;
value)
0, если значение value равно нулю; и 1, если значение value больше нуля
public
static int
Возвращает -1, если значение value меньше нуля;
Sign(decimalvalue)
0, если значение value равно нулю; и 1, если зна
чение value больше нуля
public
static int Sign(int
Возвращает -1, если значение value меньше нуля;
value)
0, если значение value равно нулю; и 1, если значение value больше нуля
public
static int Sign(short
Возвращает -1, если значение value меньше нуля;
value)
0, если значение value равно нулю; и 1, если значение value больше нуля
public
static int Sign(long
Возвращает -1, если значение value меньше нуля;
value)
0, если значение value равно нулю; и 1, если значение value больше нуля
public
static int Sign(sbyte
Возвращает -1, если значение value меньше нуля;
value)
0, если значение value равно нулю; и 1, если значение value больше нуля
public
static double
Возвращает синус числа а
Sin(doublea)
public
static double
Возвращает гиперболический синус числа value
Sinh(doublevalue)
public
static double
Возвращает квадратный корень числа d
Sqrt(double d)
public
static double
Возвращает тангенс числа а
Tan(doublea)
public
static double
Возвращает гиперболический тангенс числа
Tanh(doublevalue)
value
public
static double
Возвращает целую часть числа d
Truncate(double d)
public
static decimal
Возвращает целую часть числа d
Truncate(decimal d)
В приведенном ниже примере программы метод Sqrt () служит для расчета гипотенузы по длине противоположных сторон прямоугольного треугольника согласно теореме Пифагора.
// Расчет гипотенузы по теореме Пифагора.
using System;
class Pythagorean { static void Main() { double si; double s2; double hypot; string str;
Console.WriteLine("Введите длину первой стороны треугольника: "); str = Console.ReadLine(); si = Double.Parse(str);
Console.WriteLine("Введите длину второй стороны треугольника: "); str = Console.ReadLine(); s2 = Double .'Parse (str) ;
hypot = Math.Sqrt(sl*sl + s2*s2);
Console.WriteLine("Длина гипотенузы равна " + hypot);
}
}
Ниже приведен один из возможных результатов выполнения этой программы.
Введите длину первой стороны треугольника: 3 Введите длину второй стороны треугольника: 4 Длина гипотенузы равна: 5
Далее следует пример программы, в которой метод Pow () служит для расчета первоначальных капиталовложений, требующихся для получения предполагаемой будущей стоимости, исходя из годовой нормы прибыли и количества лет. Ниже приведена формула для расчета первоначальных капиталовложений.
первоначальные капиталовложения =
будущая стоимость/ (1 +норма прибыли)количество лет
В вызове метода Pow () необходимо указывать аргументы типа double, поэтому норма прибыли и количество лет задаются в виде значений типа double. А первоначальные капиталовложения и будущая стоимость задаются в виде значений типа decimal.
/* Рассчитать первоначальные капиталовложения, необходимые для получения заданной будущей стоимости, исходя из годовой нормы прибыли и количества лет. */
using System;
class Initiallnvestment { static void Main() {
decimal initInvest; // первоначальные капиталовложения
decimal futVal; // будущая стоимость
double numYears; // количество лет
double intRate; // годовая норма прибыли
string str;
Console.Write("Введите будущую стоимость: "); str = Console.ReadLine(); try {
ftitVal = Decimal. Parse (str) ;
} catch(FormatException exc) {
Console.WriteLine(exc.Message); return;
}
Console.Write("Введите норму прибыли (например, 0.085): ") ; str = Console.ReadLine(); try {
intRate = Double.Parse (str);
} catch(FormatException exc) {
Console.WriteLine(exc.Message); return;
}
Console.Write("Введите количество лет: "); str = Console.ReadLine(); try {
numYears = Double.Parse(str);
} catch(FormatException exc) {
Console.WriteLine(exc.Message); return;
}
initlnvest =
futVal / (decimal) Math.Pow(intRate+1.0, numYears);
Console.WriteLine("Необходимые первоначальные капиталовложения: {0:C}", initlnvest);
}
}
Ниже приведен один из возможных результатов выполнения этой программы.
Введите будущую стоимость: 10000
Введите норму прибыли (например, 0.085): 0.07
Введите количество лет: 10
Необходимые первоначальные капиталовложения: $5,083.49
Структуры .NET, соответствующие встроенным типам значений
Структуры, соответствующие встроенным в C# типам значений, были представлены в главе 14, где они упоминались в связи с преобразованием строк, содержащих числовые значения в удобочитаемой форме, в эквивалентные двоичные значения. В этом разделе структуры .NET рассматриваются более подобно.
Имена структур .NET и соответствующие им ключевые слова, обозначающие типы значений в С#, перечислены в приведенной ниже таблице.
Имя структуры в .NET
Имя типа значения в C#
System.Boolean
bool
System.Char
char
System.Decimal
decimal
System.Double
double
System.Single
float
System.Intl6
short
System.Int32
int
System.Int64
long
System.Ulntl6
ushort
System.UInt32
uint
System.UInt64
ulong
System.Byte
byte
System.Sbyte
sbyte
Используя члены, определенные в этих структурах, можно выполнять операции над значениями простых типов данных. Все перечисленные выше структуры рассматриваются далее по порядку.
ПРИМЕЧАНИЕ
Некоторые методы, определенные в структурах, соответствующих встроенным в C# типам значений, принимают параметры типа iFormatProvider или NumberStyles. Тип
I Format Provide г вкратце описывается далее в этой главе, а тип NumberStyles представляет собой перечисление из пространства имен System.Globalization. Вопросы форматирования подробнее рассматриваются в главе 22.
Структуры целочисленных типов данных
Ниже перечислены структуры целочисленных типов данных.
Byte
SByte
I n 116
Uintl6
Int32
UInt32
Int64
Uint64
Каждая из этих структур содержит одинаковое количество членов. В табл. 21.2 для примера перечислены члены структурыInt32.Аналогичные члены в виде методов имеются и у других структур, за исключением целочисленного типа, который они представляют.
Помимо перечисленных выше методов, в структурах целочисленных типов данных определены следующие поля типаconst.
MaxValue
MinValue
В каждой структуре эти поля содержат наибольшее и наименьшее значения, допустимые для данных соответствующего целочисленного типа.
Во всех структурах целочисленных типов данных реализуются следующие интерфейсы:IComparable, IComparable<T>, IConvertible, IFormattable
иIEquatable<T>,где параметр обобщенного типаТзаменяется соответствующим типом данных. Например, в структуреInt32вместоТподставляется типint.
Метод
Назначение
public int
Сравнивает числовое значение вызывающего объекта со
CompareTo(objectvalue)
значением value. Возвращает нуль, если сравниваемые значения равны; отрицательное значение, если вызывающий объект имеет меньшее значение; и, наконец, положительное значение, если вызывающий объект имеет большее значение
public int CompareTo(int
Сравнивает числовое значение вызывающего объекта со
value)
значением value. Возвращает нуль, если сравниваемые значения равны; отрицательное значение, если вызывающий объект имеет меньшее значение; и, наконец, положительное значение, если вызывающий объект имеет большее значение
public override bool
Возвращает логическое значение true, если значе
Equals(objectobj)
ние вызывающего объекта равно значению параметра
obj
public bool Equals(int
Возвращает логическое значение true, если значе
obj)
ние вызывающего объекта равно значению параметра
obj
public override int
Возвращает хеш-код для вызывающего объекта
GetHashCode()
public TypeCode
Возвращает значение перечисления TypeCode для экви
GetTypeCode()
валентного типа. Например, для структуры Int32 возвращается значение TypeCode. Int32
public static int
Возвращает двоичный эквивалент числа, заданного
Parse(strings)
в виде символьной строки s. Если числовое значение не представлено в строке так, как определено в структуре данного типа, то генерируется исключение
public static int
Возвращает двоичный эквивалент числа, заданного
Parse(strings,
в виде символьной строки s, с использованием фор
IformatProvider
матов данных, характерных для конкретной культурной
provi der)
среды и определяемых параметром provider. Если числовое значение не представлено в строке так, как определено в структуре данного типа, то генерируется исключение
public static int
Возвращает двоичный эквивалент числа, заданного
Parse(strings,
в виде символьной строки s, с использованием дан
NumberStylesstyles)
ных о стилях, определяемых параметром styles. Если числовое значение не представлено в строке так, как определено в структуре данного типа, то генерируется исключение
Метод
Назначение
public static int
Возвращает двоичный эквивалент числа, заданного в виде
Parse(strings,
строки символьной s, с использованием данных о стилях,
NumberStylesstyles,
определяемых параметром styles, а также форматов
IformatProvider
данных, характерных для конкретной культурной среды
provi der)
и определяемых параметром provider. Если числовое значение не представлено в строке так, как определено в структуре данного типа, то генерируется исключение
public override
string
Возвращает строковое представление значения вызы
ToString()
вающего объекта
public string
Возвращает строковое представление значения вызы
ToString(string
format)
вающего объекта, как указано в форматирующей строке, определяемой параметром format
public string
Возвращает строковое представление значения вызы
ToString(IformatProvider
вающего объекта с использованием форматов данных,
provi der)
характерных для конкретной культурной среды и определяемых параметром provider
public string
Возвращает строковое представление значения вызы
ToString(string
format,
вающего объекта, как указано в форматирующей строке,
IformatProvider
определяемой параметром format, но с использова
provider)
нием форматов данных, характерных для конкретной культурной среды и определяемых параметром provider
public static bool
Предпринимает попытку преобразовать числовое значе
TryParse(string
s,out
ние, заданное в виде символьной строки s, в двоичное
intresult)
значение. При успешной попытке это значение сохраняется в параметре result и возвращается логическое значение true, а иначе возвращается логическое значение false, в отличие от метода Parse (), который генерирует исключение при неудачном исходе преобразования
public static bool
Предпринимает попытку преобразовать числовое зна
TryParse(string
s,
чение, заданное в виде символьной строки s, в двоич
NumberStylesstyles,
ное значение с использованием информации о стилях,
IformatProvider
обозначаемых параметром styles, а также форматов
provider,out int
данных, характерных для конкретной культурной среды
result)
и определяемых параметром provider. При успешной попытке это значение сохраняется в параметре result и возвращается логическое значение true, а иначе возвращается логическое значение false, в отличие от метода Parse (), который генерирует исключение при неудачном исходе преобразования
Структуры типов данных с плавающей точкой
Типам данных с плавающей точкой соответствуют только две структуры:DoubleиSingle.СтруктураSingleпредставляет типfloat.Ее методы перечислены в табл. 21.3, а поля — в табл. 21.4. СтруктураDoubleпредставляет типdouble.
Ее методы перечислены в табл. 21.5, а поля — в табл. 21.6. Как и в структурах целочисленных типов данных, при вызове методаParse ()илиToStringOиз структур типов данных с плавающей точкой можно указывать информацию, характерную для конкретной культурной среды, а также данные форматирования.
Таблица 21.3. Методы, поддерживаемые структурой Single
Метод
Назначение
public int
Сравнивает числовое значение вызывающего объекта
CompareTo(objectvalue)
со значением value. Возвращает нуль, если сравниваемые значения равны; отрицательное число, если вызывающий объект имеет меньшее значение, и, наконец, положительное значение, если вызывающий объект имеет большее значение
public int
Сравнивает числовое значение вызывающего объекта
CompareTo(floatvalue)
со значением value. Возвращает нуль, если сравниваемые значения равны; отрицательное число, если вызывающий объект имеет меньшее значение, и, наконец, положительное значение, если вызывающий объект имеет большее значение
public override bool
Возвращает логическое значение true, если значение
Equals(objectobj)
вызывающего объекта равно значению obj
public bool Equals(float
Возвращает логическое значение true, если значение
obj)
вызывающего объекта равно значению obj
public override int
Возвращает хеш-код для вызывающего объекта
GetHashCode()
public TypeCode
Возвращает значение из перечисления TypeCode для
GetTypeCode()
структуры Single, т.е. TypeCode . Single
public static bool
Возвращает логическое значение true, если значение
Islnfinity(floatf)
f представляет плюс или минус бесконечность. В противном случае возвращает логическое значение false
public static bool
Возвращает логическое значение true, если значение f
IsNaN(floatf)
не является числовым. В противном случае возвращает логическое значение false
public static bool
Возвращает логическое значение true, если значение
IsPositivelnfinity(float
f представляет плюс бесконечность. В противном случае
f)
возвращает логическое значение false
public static bool
Возвращает логическое значение true, если значение f
IsNegativelnfinity(float
представляет минус бесконечность. В противном случае
f)
возвращает логическое значение false
public static float
Возвращает двоичный эквивалент числа, заданного в виде
Parse(strings)
символьной строки s. Если в строке не представлено числовое значение типа float, то генерируется исключение
public static float
Возвращает двоичный эквивалент числа, заданного
Parse(strings,
в виде символьной строки s, с использованием фор
IformatProvider
матов данных, характерных для конкретной культурной
provider)
среды и определяемых параметром provider. Если в строке не представлено числовое значение типа float, то генерируется исключение
Метод
Назначение
public static float
Возвращает двоичный эквивалент числа, заданного в
Parse(strings,
виде символьной строки s, с использованием данных о
NumberStylesstyles)
стилях, определяемых параметром styles. Если в строке не представлено числовое значение типа float, то генерируется исключение
public static float
Возвращает двоичный эквивалент числа, заданного в
Parse(strings,
виде символьной строки s, с использованием форматов
NumberStylesstyles,
данных, характерных для конкретной культурной среды
IformatProvider
и определяемых параметром provider, а также сведе
provi der)
ний о стилях, обозначаемых параметром styles. Если в строке не представлено числовое значение типа float, то генерируется исключение
public override string
Возвращает строковое представление значения вызы
ToString ()
вающего объекта
public string
Возвращает строковое представление значения вызы
ToString(stringformat)
вающего объекта, как указано в форматирующей строке, 'определяемой параметром format
public string
Возвращает строковое представление значения вызы
ToString(IformatProvider
вающего объекта с использованием форматов данных,
provi der)
характерных для конкретной культурной среды и определяемых параметром provider
public string
Возвращает строковое представление значения вызы
ToString(stringformat,
вающего объекта, как указано в форматирующей строке,
IformatProvider
определяемой параметром format, но с использовани
provider)
ем форматов данных, характерных для конкретной культурной среды и определяемых параметром provider
public static bool
Предпринимает попытку преобразовать число, заданное
TryParse(strings,out
в виде символьной строки s, в значение типа float.
floatresult)
При успешной попытке это значение сохраняется в параметре result и возвращается логическое значение true, а иначе возвращается логическое значение false, в отличие от метода Parse (), который генерирует исключение при неудачном исходе преобразования
public static bool
Предпринимает попытку преобразовать числовое зна
TryParse(strings,
чение, заданное в виде символьной строки s, в значе
NumberStylesstyles,
ние типа float, как указано в форматирующей строке,
IformatProvider
определяемой параметром format, но с использовани
provider,out float
ем форматов данных, характерных для конкретной куль
result)
турной среды и определяемых параметром provider, a, также сведений о стилях, обозначаемых параметром styles. При успешной попытке это значение сохраняет
ся в параметре result и возвращается логическое значение true, а иначе возвращается логическое значение false, в отличие от метода Parse (), который генерирует исключение при неудачном исходе преобразования
Поле
Назначение
public const float Epsilon
Наименьшее ненулевое положительное значение
public const float
Наибольшее значение, допустимое для данных типа
MaxValue
float
public const float
Наименьшее значение, допустимое для данных типа
MinValue
float
public const float NaN
Значение, не являющееся числом
public const float
Значение, представляющее минус бесконечность
NegativeInfinity
public const float
Значение, представляющее плюс бесконечность
PositiveInfinity
Таблица 21.5. Методы, поддерживаемые структурой Double
Метод
Назначение
public
int CompareTo(object
Сравнивает числовое значение вызывающего объек
value)
та со значением value. Возвращает нуль, если сравниваемые значения равны; отрицательное число, если вызывающий объект имеет меньшее значение, и, наконец, положительное значение, если вызывающий объект имеет большее значение
public
int CompareTo(double
Сравнивает числовое значение вызывающего объек
value)
та со значением value. Возвращает нуль, если сравниваемые значения равны; отрицательное число, если вызывающий объект имеет меньшее значение, и, наконец, положительное значение, если вызывающий объект имеет большее значение
public
override bool
Возвращает логическое значение true, если значе
Equals(objectobj)
ние вызывающего объекта равно значению obj
public
bool Equals(double
Возвращает логическое значение true, если значе
obj)
ние вызывающего объекта равно значению obj
public
override int
Возвращает хеш-код для вызывающего объекта
GetHashCode()
public
TypeCode
Возвращает значение из перечисления TypeCode
GetTypeCode()
для структуры Double, т.е. TypeCode . Double
public
static bool
Возвращает логическое значение true, если значе
Islnfinity(double d)
ние d представляет плюс или минус бесконечность.
В противном случае возвращает логическое значе
ние false
public
static bool
Возвращает логическое значение true, если зна
IsNaN(double d)
чение d не является числовым. В противном случае
возвращает логическое значение false
public
static bool
Возвращает логическое значение true, если значе
IsPositivelnfinity(double
ние d представляет плюс бесконечность. В противном
d)
случае возвращает логическое значение false
Метод
Назначение
public static bool
Возвращает логическое значение true, если значе
IsNegativelnfinity(double
ние d представляет минус бесконечность. В против
d)
ном случае возвращает логическое значение false
public static double
Возвращает двоичный эквивалент числа, заданного
Parse(strings)
в виде символьной строки s. Если в строке не представлено числовое значение типа double, то генерируется исключение
public static double
Возвращает двоичный эквивалент числа, заданного в
Parse(strings,
виде символьной строки s, с использованием форма
IFormatProviderprovider)
тов данных, характерных для конкретной культурной среды и определяемых параметром provider. Если в строке не представлено числовое значение типа double, то генерируется исключение
public static double
Возвращает двоичный эквивалент числа, заданного в
Parse (strings,
виде символьной строки s, с использованием данных
NumberStylesstyles)
о стилях, определяемых параметром styles. Если в строке не представлено числовое значение типа double, то генерируется исключение
public static double
Возвращает двоичный эквивалент числа, заданного
Parse(strings,
в виде символьной строки s, с использованием фор
NumberStylesstyles,
матов данных, характерных для конкретной культур
IFormatProviderprovider)
ной среды и определяемых параметром provider, а также данных о стилях, обозначаемых параметром styles. Если в строке не представлено числовое
1
значение типа double, то генерируется исключение
public override string
Возвращает строковое представление значения вы
ToString()
зывающего объекта
public string
Возвращает строковое представление значения вы
ToString(stringformat)
зывающего объекта, как указано в форматирующей строке, определяемой параметром format
public string
Возвращает строковое представление значения вы
ToString(IformatProvider
зывающего объекта с использованием форматов
provi der)
данных, характерных для конкретной культурной среды и определяемых параметром provider
public string
Возвращает строковое представление значения вызы
ToString(stringformat,
вающего объекта, как указано в форматирующей строке,
IformatProviderprovider)
определяемой параметром format, но с использованием форматов данных, характерных для конкретной культурной среды и определяемых параметром provider
public static bool
Предпринимает попытку преобразовать число, заданное
TryParse(strings,out
в виде символьной строки s, в значение типа double.
doubleresult)
При успешной попытке это значение сохраняется в параметре result и возвращается логическое значение true, а иначе возвращается логическое значение false, в отличие от метода Parse (), который генерирует исключение при неудачном исходе преобразования
Окончание табл. 21.5
Метод
Назначение
public static bool TryParse(strings,NumberStylesstyles,IFormatProviderprovider,out doubleresult)
Предпринимает попытку преобразовать числовое значение, заданное в виде символьной строки s, в значение типа double, как указано в форматирующей строке, определяемой параметром format, но с использованием форматов данных, характерных для конкретной культурной среды и определяемых параметром provider, а также сведений о стилях, обозначаемых параметром styles. При успешной попытке это значение сохраняется в параметре result и возвращается логическое значение true, а иначе возвращается логическое значение false, в отличие от метода Parse (), который генерирует исключение при неудачном исходе преобразования
Таблица 21.6. Поля, поддерживаемые структурой Double
Поле
Назначение
public const double public const double MaxValue
public const double MinValue
public const double public const double NegativeInfinity public const double PositiveInfinity
Epsilon
NaN
Наименьшее ненулевое положительное значение Наибольшее значение, допустимое для данных типа
double
Наименьшее значение, допустимое для данных типа
double
Значение, не являющееся числом
Значение, представляющее минус бесконечность
Значение, представляющее плюс бесконечность
Структура Decimal
СтруктураDecimalнемного сложнее, чем ее аналоги для целочисленных типов данных, а также типов данных с плавающей точкой. Она содержит немало конструкторов, полей, методов и операторов, способствующих использованию типаdecimalвместе с другими числовыми типами, поддерживаемыми в С#. Так, целый ряд методов из этой структуры обеспечивает преобразование типаdecimalв другие числовые типы.
В структуреDecimalопределено восемь открытых конструкторов. Ниже приведены шесть наиболее часто используемых из них.
public Decimal(intзначение)public Decimal(uintзначение)public Decimal(longзначение)public Decimal(ulongзначение)public Decimal(floatзначение)public Decimal(doubleзначение)
Каждый из этих конструкторов создает объект типаDecimalиз значения указанного типа.
Кроме того, объект типаDecimalможет быть создан из отдельно указываемых составляющих с помощью следующего конструктора.
public Decimal(intlo,intmid,inthi,boolIsNegative,bytescale)
Десятичное значение состоит из трех частей. Первую часть составляет 96-разрядное целое значение, вторую — флаг знака, третью — масштабный коэффициент. В частности, 96-разрядное целое значение передается конструктору тремя 32-разрядными фрагментами с помощью параметровlo, mid и hi;знак флага — с помощью параметраIsNegative,причем логическое значениеfalseэтого параметра обозначает положительное число, тогда как логическое значениеtrueобозначает отрицательное число; а масштабный коэффициент — с помощью параметраscale, принимающего значения от 0 до 28. Этот коэффициент обозначает степень числа 10 (т.е. 10scaJe), на которую делится число для получения его дробной части.
Вместо того чтобы передавать каждую составляющую объекта типаDecimalотдельно, все его составляющие можно указать в массиве, используя следующий конструктор.
public Decimal(int[]bits)
Три первых элемента типаintв массивеbitsсодержат 96-разрядное целое значение; 31-й разряд содержимого элементаbits [3]обозначает флаг знака (0 — положительное число, 1 — отрицательное число); а в разрядах 16-23 содержится масштабный коэффициент.
В структуреDecimalреализуются следующие интерфейсы:IComparable, IComparable<decimal>, IConvertible, IFormattable, IEquatable<decimal>,а такжеIDeserializationCallback.
В приведенном ниже примере программы значение типаdecimalформируется вручную.
// Сформировать десятичное число вручную.
using System;
class CreateDec {
static void Main() {
decimal d = new decimal(12345, 0, 0, false, 2);
Console.WriteLine(d);
}
}
Эта программа дает следующий результат.
123.45
В данном примере значение 96-разрядного целого числа равно 12345. У него положительный знак и два десятичных разряда в дробной части.
Методы, определенные в структуреDecimal,приведены в табл._21.7, а поля — в табл. 21.8. Кроме того, в структуреDecimalопределяется обширный ряд операторов и преобразований, позволяющих использовать десятичные значения вместе со значениямидругихтипов в выражениях. Правила, устанавливающие порядок присваивания десятичных значений и их применения в выражениях, представлены в главе 3.
Глава 21. Пространство имен System 737 Таблица 21.7. Методы, определенные в структуре Decimal
Метод
Назначение
public static decimal Add(decimal"dl,decimald2)
Возвращает значение dl + d2
public static decimal
Возвращает наименьшее целое, которое представ
Ceiling(d)
лено в виде значения типа decimal и не меньше d. Так, если d равно 1,02, метод Ceiling () возвращает значение 2,0. А если d равно -1,02, то метод Ceiling () возвращает значение -1
public static int
Сравнивает числовое значение dl со значением
Compare(decimaldl,decimald2)
d2. Возвращает нуль, если сравниваемые значения равны; отрицательное значение, если dl меньше d2; и, наконец, положительное значение, если dl больше d2
public int CompareTo(object
Сравнивает числовое значение вызывающего
value)
объекта со значением value. Возвращает нуль, если сравниваемые значения равны; отрицательное значение, если вызывающий объект имеет меньшее значение; и, наконец, положительное значение, если вызывающий объект имеет большее значение
public int CompareTo(decimal
Сравнивает числовое значение вызывающего объ
value)
екта со значением value. Возвращает нуль, если сравниваемые значения равны; отрицательное значение, если вызывающий объект имеет меньшее значение; и, наконец, положительное значение, если вызывающий объект имеет большее значение
public static decimal Divide(decimaldl,
Возвращает частное отделения dl / d2
decimald2)
public bool Equals(decimal
Возвращает логическое значение true, если
value)
значение вызывающего объекта равно значению value
public override bool
Возвращает логическое значение true, если
Equals(objectvalue)
значение вызывающего объекта равно значению value
public static bool
Возвращает логическое значение true, если
Equals(decimaldl,decimald2)
если dl равно d2
public static decimal
Возвращает наибольшее целое, которое пред
Floor(decimal d)
ставлено в виде значения типа decimal и не больше d. Так, если d равно 1,02, метод Floor () возвращает значение 1,0. А если d равно -1,02, метод Floor 0 возвращает значение -2
public static decimal
Преобразует значение су из формата денеж
FromOACurrency(longcy)
ной единицы, применяемого в компоненте OLE Automation, в его десятичный эквивалент и воз-
Метод
Назначение
public static int[]
Возвращает двоичное представление значения
GetBits(decimal d)
d в виде массива типа int. Организация этого массива описана в тексте настоящего раздела
public override int
Возвращает хеш-код для вызывающего объекта
GetHashCode()
public TypeCode GetTypeCode()
Возвращает значение из перечисления TypeCode для структуры Decimal, т.е. TypeCode.Decimal
public static decimal
Возвращает произведение dl * d2
Multiply(decimaldl,decimald2)
public static decimal
Возвращает значение -d
Negate(decimal d)
public static decimal
Возвращает двоичный эквивалент числа, за
Parse(strings)
данного в виде символьной строки s. Если в строке не представлено числовое значение типа decimal, то генерируется исключение •
public static decimal
Возвращает двоичный эквивалент числа, за
Parse(strings,
данного в виде символьной строки s, с исполь
IFormatProviderprovider)
зованием форматов данных, характерных для конкретной культурной среды и определяемых параметром provider. Если в строке не представлено числовое значение типа decimal, то генерируется исключение
public static decimal
Возвращает двоичный эквивалент числа, за
Parse(strings,NumberStyles
данного в виде символьной строки s, с исполь
styles)
зованием данных о стилях, определяемых параметром styles. Если в строке не представлено числовое значение типа decimal, то генерируется исключение
public static decimal
Возвращает двоичный эквивалент числа, за
Parse (strings,NumberStyles
данного в виде символьной строки s, с исполь
styles,IformatProvider
зованием форматов данных, характерных для
provi der)
конкретной культурной среды и определяемых параметром provider, а также данных о стилях, обозначаемых параметром styles. Если в строке не представлено числовое значение типа decimal, то генерируется исключение
public static decimal
Возвращает остаток от целочисленного деления
Remainder(decimaldl,decimald2)
dl/ d2
public static decimal
Возвращает значение d, округленное до ближай
Round(decimal d)
шего целого числа
public static decimal
Возвращает значение d, округлеяное до числа с
Round(decimald,intdecimals)
количеством цифр в дробной части, равным значению параметра decimals, которое должно находиться в пределах от 0 до 28
_Продолжение табл. 21.7
Метод
Назначение
public static decimal
Возвращает значение d, округленное до бли
Round(decimal d,
жайшего целого числа в режиме, определяе
MidPoiritRoundingmode)
мом параметром mode. Режим округления применяется лишь в том случае, если значение d оказывается посредине между двумя целыми числами
public static decimal
Возвращает значение d, округленное до числа с
Round(decimald,intdecimals,
количеством цифр в дробной части, равным зна
MidPointRoundingmode)
чению параметра decimals, которое должно находиться в пределах от 0 до 28, а параметр mode определяет режим округления. Режим округления применяется лишь в том случае, если значение d оказывается посредине между двумя округляемыми числами
public static decimal
Возвращает разность dl - d2
Subtract(decimaldl,decimald2)
public static byte
Возвращает эквивалент значения value типа
ToByte(decimalvalue)
byte. Дробная часть отбрасывается. Если значение value оказывается вне диапазона представления чисел для типа byte, то генерируется исключение OverflowException
public static double
Возвращает эквивалент значения dTnna double.
ToDouble(decimald)
При этом возможна потеря точности, поскольку у значения типа double меньше значащих цифр, чем у значения типа decimal
public static short
Возвращает эквивалент значения dTnna short.
Tolntl6(decimal d)
Дробная часть отбрасывается. Если значение d оказывается вне диапазона представления чисел для типа short, то генерируется исключение
OverflowException
public static int
Возвращает эквивалент значения d типа int.
ToInt32(decimal d)
Дробная часть отбрасывается. Если значение d оказывается вне диапазона представления чисел для типа int, то генерируется исключение
OverflowException
public static long
Возвращает эквивалент значения d типа long.
ToInt64(decimal d)
Дробная часть отбрасывается. Если значение d оказывается вне диапазона представления чисел для типа long, то генерируется исключение
OverflowException
public static long
Преобразует значение value в его эквивалент
ToOACurrency(decimalvalue)
формата денежной единицы, применяемого в компоненте OLE Automation, и возвращает полученный результат
Метод
Назначение
public static sbyte
Возвращает эквивалент значения value типа
ToSByte(decimalvalue)
sbyte. Дробная часть отбрасывается. Если значение value оказывается вне диапазона представления чисел для типа sbyte, то генерируется исключение Overf lowException
public static float
Возвращает эквивалент значения dTnna float.
ToSingle(decimal d)
Дробная часть отбрасывается. Если значение d оказывается вне диапазона представления чисел для типа float, то генерируется исключение
OverflowException
public override string
Возвращает строковое представление значения
ToString()
вызывающего объекта в используемом по умолчанию формате
public string ToString(string
Возвращает строковое представление значения
format)
вызывающего объекта, как указано в форматирующей строке, определяемой параметром
format
public string
Возвращает строковое представление значе
ToString(IFormatProvider
ния вызывающего объекта с использованием
provider)
форматов данных, характерных для конкретной культурной среды и определяемых параметром
provider
public string ToString (string
Возвращает строковое представление значения
format,IFormatProvider
вызывающего объекта, как указано в форма
provider)
тирующей строке, определяемой параметром format, но с использованием форматов данных, характерных для конкретной культурной среды и определяемых параметром provider
public static ushort
Возвращает эквивалент значения value типа
ToUIntl6(decimalvalue)
ushort. Дробная часть отбрасывается. Если значение value оказывается вне диапазона представления чисел для типа ushort, то генерируется исключение Overf lowException
public static uint
Возвращает эквивалент значения dTnna uint.
ToUInt32(decimal d)
Дробная часть отбрасывается. Если значение d оказывается вне диапазона представления чисел для типа uint, то генерируется исключение
OverflowException
public static ulong
Возвращает эквивалент значения dTnna ulong.
ToUInt64(decimal d)
Дробная часть отбрасывается. Если значение d
оказывается вне диапазона представления чисел для типа ulong, то генерируется исключение
OverflowException
public static decimal
Возвращает целую часть числа d. Дробная часть
Truncate(decimal d)
отбрасывается
Таблица 21.8. Поля, поддерживаемые структурой Decimal
Структура Char
СтруктураCharсоответствует типуcharи применяется довольно часто, поскольку предоставляет немало методов, позволяющих обрабатывать символы и распределять их по отдельным категориям. Например, символ строчной буквы можно преобразовать в символ прописной буквы, вызвав методToUpper (), а с помощью методаIs Digit () можно определить, обозначает ли символ цифру.
Методы, определенные в структуреChar,приведены в табл. 21.9. Следует, однако, иметь в виду, что некоторые методы, напримерConvertFromUtf32 () иConvertToUtf32 (), позволяют обрабатывать символы уникода в форматах UTF-16 и UTF-32. Раньше все символы уникода могли быть представлены 16 разрядами, что соответствует величине значения типаchar.Но несколько лет назад набор символов уникода был расширен, для чего потребовалось более 16 разрядов. Каждый символ уникода представленкодовой точкой,а способ кодирования кодовой точки зависит от используемого формата преобразования уникода (UTF). Так, в формате UTF-16 для кодирования большинства кодовых точек требуется одно 16-разрядное значение, а для кодирования остальных кодовых точек — два 16-разрядных значения. Если для этой цели требуются два 16-разрядных значения, то для их представления служат два значения типаchar.Первое символьное значение называетсястаршим суррогатом,а второе —младшим суррогатом.В формате UTF-32 каждая кодовая точка кодируется с помощью одного 32-разрядного значения. В структуреCharпредоставляются все необходимые средства для преобразования из формата UTF-16 в формат UTF-32 и обратно.
В отношении методов структурыCharнеобходимо также отметить следующее: в используемых по умолчанию формах методовToUpper() иToLower() применяются текущие настройки культурной среды (языки и региональные стандарты), чтобы указать способ представления символов верхнего и нижнего регистра. На момент написания этой книги рекомендовалось явно указывать текущие настройки культурной среды, используя для этой цели параметр типаCulturelnfoво второй форме обоих упоминаемых методов. КлассCulturelnfoотносится к пространству именSystem. Globalization,а для указания текущей культурной среды следует передать свойствоCulturelnfo . CurrentCultureсоответствующему методу.
В структуреCharопределены также следующие поля.
public const char MaxValue public const char MinValue
Кроме того, в структуреCharреализуются следующие интерфейсы:I Comparable, IComparable<char>, IConvertible иIEquatable<char>.
Таблица 21.9. Методы, определенные в структуре Char
Метод
Назначение
public int CompareTo(charvalue)
public int CompareTo(objectvalue)
public static string ConvertFromUtf32(intutf32)
Сравнивает символ в вызывающем объекте с символом value. Возвращает нуль, если сравниваемые символы равны; отрицательное значение, если вызывающий объект имеет меньшее значение; и, наконец, положительное значение, если вызывающий объект имеет большее значение Сравнивает символ в вызывающем объекте с символом value. Возвращает нуль, если сравниваемые символы равны; отрицательное значение, если вызывающий объект имеет меньшее значение; и, наконец, положительное значение, если вызывающий объект имеет больщее значение Преобразует кодовую точку уникода, представленную параметром utf32 в формате UTF-32, в символьную строку формата UTF-16 и возвращает полученный результат
_Продолжение табл. 21.9
Метод
Назначение
pubic static int
Преобразует старший и младший суррогаты,
ConvertToUtf*32 (char
представленные параметрами highSurrogate
highSurrogate,char
и lowSurrogate в формате UTF-16, в кодовую
lowSurrogate)
точку формата UTF-32 и возвращает полученный результат
pubic static int
Преобразует пару суррогатов формата UTF-16,
ConvertToUtf32(strings,int
доступных из символьной строки по индексу
index)
s [index], в кодовую точку формата UTF-32 и возвращает полученный результат
public bool Equals(charobj)
Возвращает логическое значение true, если значение вызывающего объекта равно значению obj
public override bool
Возвращает логическое значение true, если зна
Equals(objectobj)
чение вызывающего объекта равно значению obj
public override int
Возвращает хеш-код для вызывающего объекта
GetHashCode()
public static double
Возвращает числовое значение символа с, если
GetNumericValue(char c)
он обозначает цифру. В противном случае возвращает -1
public static double
Возвращает числовое значение символа, доступ
GetNumericValue(strings,int
ного из строки по индексу s [index], если он
index)
обозначает цифру. В противном случае возвращает -1
public TypeCode GetTypeCode()
Возвращает значение из перечисления TypeCode для структуры Char, т.е. TypeCode . Char
public static UnicodeCategory
Возвращает значение из перечисления
GetUnicodeCategory(char c)
UnicodeCategory для символа с. Перечисление UnicodeCategory определено в пространстве имен System.Globalization и распределяет символы уникода по категориям
public static UnicodeCategory
Возвращает значение из перечисления
GetUnicodeCategory(strings,
UnicodeCategory для символа, доступного
intindex)
из строки по индексу s [index]. Перечисление UnicodeCategory определено в пространстве имен System.Globalization и распределяет символы уникода по категориям
public static bool
Возвращает логическое значение true, если сим
IsControl(char c)
вол с является управляющим, иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если сим
IsControl(strings,int
вол, доступный из строки по индексу s [ index],
index)
является управляющим, иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если
IsDigit(char c)
символ с обозначает цифру, а иначе возвращает логическое значение false
Метод
Назначение
public static bool
Возвращает логическое значение true, если сим
IsDigit(strings,intindex)
вол, доступный из строки по индексу s [index],
обозначает цифру, а иначе возвращает логиче
ское значение false
public static bool
Возвращает логическое значение true, если
IsHighSurrogate(char c)
символьное значение с является действительным старшим суррогатом формата UTF-32, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если сим
IsHighSurrogate(string
srint
вольное значение, доступное из строки по индек
index)
су s [ index], является действительным старшим суррогатом формата UTF-32, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если
IsLetter(char c)
символ с обозначает букву алфавита, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если сим
IsLetter(strings,int
index)
вол, доступный из строки по индексу s [index], обозначает букву алфавита, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если
IsLetterOrDigit(char c)
символ с обозначает букву алфавита или цифру, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если сим
IsLetterOrDigit(string
s,int
вол, доступный из строки по индексу s [ index],
index)
обозначает букву алфавита или цифру, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если
IsLower(char c)
символ с обозначает строчную букву алфавита, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если сим
IsLower(strings,intindex)
вол, доступный из строки по индексу s [index],
обозначает строчную букву алфавита, а иначе
возвращает логическое значение false
public static bool
Возвращает логическое значение true, если
IsLowSurrogate(char c)
символьное значение с является действительным младшим суррогатом формата UTF-32, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если сим
IsLowSurrogate(strings
, int
вольное значение, доступное из строки по индек
index)
су s [ index], является действительным младшим суррогатом формата UTF-32, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если
IsNumber(char c)
символ с обозначает число (десятичное или шестнадцатеричное), а иначе возвращает логическое значение false
Метод
Назначение
public static bool
Возвращает логическое значение true, если сим
IsNumber(strings,int
index)
вол, доступный из строки по индексу s [ index], обозначает число (десятичное или шестнадцатеричное), а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если
IsPunctuation(char c)
символ с обозначает знак препинания, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если сим
IsPunctuation(strings
, int
вол, доступный из строки по индексу s [index],
index)
обозначает знак препинания, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если
IsSeparator(char c)
символ с обозначает разделительный знак, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если сим
IsSeparator(strings,
int
вол, доступный из строки по индексу s [ index],
index)
обозначает разделительный знак, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если
IsSurrogate(char c)
символьное значение с является суррогатным символом уникода, а иначе возвращает логическое значение false
public static bool
Вбзвращает логическое значение true, если сим
IsSurrogate(strings,intindex)
вольное значение, доступное из строки по индексу
s [ index], является суррогатным символом унико
да, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если
IsSurrogatePair(char
символьные значения highSurrogate и
highSurrogate, char
lowSurrogate образуют суррогатную пару
lowSurrogate)
public static bool
Возвращает логическое значение true, если
IsSymbol(char c)
символ с обозначает символический знак, например денежной единицы, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если сим
IsSymbol(strings,int
index)
вол, доступный из строки по индексу s [ index], обозначает символический знак, например денежной единицы, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если
IsUpper(char c)
символ с обозначает прописную букву алфавита, а иначе возвращает логическое значение false
public static bool
Возвращает логическое значение true, если сим
IsUpper(strings,int
index)
вол, доступный из строки по индексу s [ index], обозначает прописную букву алфавита, а иначе возвращает логическое значение false
Метод
Назначение
public static
bool
Возвращает логическое значение true, если
IsWhiteSpace(char c)
символ с обозначает пробел, табуляцию или пу
стую строку, а иначе возвращает логическое зна
чение false
public static
bool
Возвращает логическое значение true,
IsWhiteSpace(strings,int
если символ, доступный из строки по индексу
index)
s [ index], обозначает пробел, табуляцию или пустую строку, а иначе возвращает логическое значение false
public static
char
Возвращает эквивалент типа char символа
Parse (string ,
s)
из строки s. Если строка s состоит из нескольких символов, то генерируется исключение
FormatException
public static
char
Возвращает строчный эквивалент символа с,
ToLower(char
c)
если он обозначает прописную букву. В противном случае значение символа с не изменяется
public static
char
Возвращает строчный эквивалент символа с,
ToLower(char <
c, Culturelnfo
если он обозначает прописную букву. В против
culture)
ном случае значение символа с не изменяется. Преобразование выполняется в соответствии с информацией о культурной среде, указываемой в параметре culture, где Culturelnfo — это класс, определенный в пространстве имен System.Globalization
public static
char
Возвращает строчный эквивалент символа с не
ToLowerlnvariant(char c)
зависимо от настроек культурной среды
public override string
Возвращает строковое представление значения
ToString()
вызывающего объекта типа Char
public static
string
Возвращает строковое представление символь
ToString(char
c)
ного значения с
public string
Возвращает строковое представление значения
ToString(IFormatProvider
вызывающего объекта типа Char с учетом ин
provi der)
формации о культурной среде, указываемой в параметре provider
public static
char
Возвращает прописной эквивалент символа с,
ToUpper(char <
c)
если он обозначает строчную букву. В противном случае значение символа с не изменяется
public static
char
Возвращает прописной эквивалент символа с,
ToUpper(char i
c,Culturelnfo
если он обозначает строчную букву. В противном
culture)
случае значение символа с не изменяется. Преобразование выполняется в соответствии о информацией о культурной среде, указываемой в параметре culture, где Culturelnfo — это класс, определенный в пространстве имен System. Globalization
Окончание табл. 21.9
Метод
Назначение
public static char ■ ToUpperlnvariant(char c) public' static bool TryParse(strings,out charresult)
Возвращает прописной эквивалент символа с независимо от настроек культурной среды Предпринимает попытку преобразовать символ из строки s в его эквивалентное значение типа char. При успешной попытке это значение сохраняется в параметре result и возвращается логическое значение true. Если же строка s состоит из нескольких символов, то возвращается логическое значение false, в отличие от метода Parse (), который генерирует исключение при неудачном исходе преобразования
Ниже приведен пример программы, в которой демонстрируется применение нескольких методов, определенных в структуреChar.
// Продемонстрировать применение нескольких методов,
// определенных в структуре Char.
using System;
using System.Globalization;
class CharDemo {
static void Main() {
string str = "Это простой тест. $23"; int i;
for(i=0; i < str.Length; i++) {
Console.Write(str[i] + " является"); if(Char.IsDigit(str[i]))
Console.Write(" цифрой"); if(Char.IsLetter(str[i]))
Console.Write(" буквой"); if(Char.IsLower(str [i]))
Console.Write(" строчной"); if(Char.IsUpper(str[i]))
Console.Write(" прописной"); if(Char.IsSymbol(str[i]))
Console.Write(" символическим знаком"); if(Char.IsSeparator (str[i]))
Console.Write(" разделительным"); if(Char.IsWhiteSpace (str [i]))
Console.Write(" пробелом"); if(Char.IsPunctuation(str [i]))
Console.Write(" знаком препинания");
Console.WriteLine("Исходная строка: " + str);
// Преобразовать в прописные буквы.
string newstr = "";
for(i=0; i < str.Length; i++)
newstr += Char.ToUpper (str[i], Culturelnfo.CurrentCulture);
Console.WriteLine("После преобразования: " + newstr);
}
}
Эта программа дает следующий результат.
Э является буквой прописной т является буквой строчной о является буквой строчной
является разделительным пробелом п является буквой строчной р является буквой строчной
о является буквой строчной с является буквой строчной т является буквой строчной
о является буквой строчной й является буквой строчной
является разделительным пробелом т является буквой строчной е является буквой строчной с является буквой строчной т является буквой строчной . является знаком препинания
является разделительным пробелом $ является символическим знаком
2 является цифрой
3 является цифрой
Исходная строка: Это простой тест. $23 После преобразования: ЭТО ПРОСТОЙ ТЕСТ. $23
Структура Boolean
В структуреBooleanподдерживаются данные типаbool.Методы, определенные в этой структуре, перечислены в табл. 21.10. Кроме того, в ней определены следующие поля.
public static readonly string FalseString public static readonly string TrueString
В этих полях логические значенияtrueиfalseсодержатся в удобочитаемой форме. Так, если вывести содержимое поляFalseStringс помощью-методаWriteLine (), то на экране появится строка"False".
В структуреBooleanреализованы следующие интерфейсы:I Comp а г able, IComparable<bool>, IConvertibleиIEquatable<bool>.
Метод
Назначение
public int CompareTo(bool
Сравнивает логическое значение вызывающего объек
value).
та со значением параметра value. Возвращает нуль, если сравниваемые значения равны; отрицательное значение, если вызывающий объект имеет логическое значение false, а параметр value — логическое значение true; и, наконец, положительное значение, если вызывающий объект имеет логическое значение true, а параметр value —логическое значение false
public int
Сравнивает логическое значение вызывающего объек
CompareTo(objectobj)
та со значением параметра obj. Возвращает нуль, если сравниваемые значения равны; отрицательное значение, если вызывающий объект имеет логическое значение false, а параметр obj — логическое значение true; и, наконец, положительное значение, если вызывающий объект имеет логическое значение true, а параметр obj — логическое значение false
public bool Equals(boolobj)
Возвращает логическое значение true, если значение вызывающего объекта равно значению параметра obj
public override bool
Возвращает логическое значение true, если значение
Equals(objectobj)
вызывающего объекта равно значению параметра obj
public override int
Возвращает хеш-код для вызывающего объекта
GetHashCode()
public TypeCode
Возвращает значение перечисления TypeCode для
GetTypeCode()
структуры Boolean, т.е. TypeCode . Boolean
public static bool
Возвращает эквивалент типа bool символьной стро
Parse(strings)
ки s. Если строка s не содержит ни поле Boolean. TrueString, ни поле Boolean. FalseString, то генерируется исключение FormatException, независимо оттого, какими буквами набрано содержимое строки: прописными или строчными
public override string
Возвращает строковое, представление значения вызы
ToString()
вающего объекта, которое должно быть либо значением поля TrueString, либо значением поля FalseString
public string
Возвращает строковое представление значения вызы
ToString(IFormatProvider
вающего объекта, которое должно быть либо значением
provider)
поля TrueString, либо значением поля FalseString. При этом параметр provider игнорируется
public static bool
Предпринимает попытку преобразовать символ из стро
TryParse(strings,out
ки s в его эквивалентное значение типа bool. При
boolresult)
успешной попытке это значение сохраняется в параметре result и возвращается логическое значение true. Если же строка s не содержит ни поле Boolean. TrueString, ни поле Boolean. FalseString, то возвращается логическое значение false, независимо от того, какими буквами набрано содержимое строки: прописными или строчными, в отличие от метода Parse (), который генеоиоует исключение в аналогичной ситуации
Класс Array
КлассArrayотносится к числу наиболее часто используемых в пространстве именSystem.Он является базовым классом для всех массивов в С#. Следовательно, его методы можно применять к массивам любого встроенного в C# типа или же к массивам определяемого пользователем типа. Свойства, определенные в классеArray,перечислены в табл. 21.11, а методы — в табл. 21.12.
В классеArrayреализуются следующие интерфейсы:ICloneable, ICollection, IEnumerable, IStructuralComparable, IStructuralEquatable,а такжеIList.Все интерфейсы, кромеICloneable,определены в пространстве именSystem. Collections,подробнее рассматриваемом в главе 25.
В ряде методов данного класса используется параметр типаI ComparerилиIComparer<T>.ИнтерфейсIComparerнаходится в пространстве именSystem. Collections.В нем определяется методCompare() для сравнения значений двух объектов, как показано ниже.
int Compare(object х, object у)
Этот метод возвращает значение больше нуля, если х больше у; значение меньше нуля, если х меньше у; и, наконец, нулевое значение, если оба значения равны.
ИнтерфейсIComparer<T>находится в пространстве именSystem. Collections . Generic.В нем определяется методCompare(), общая форма которого приведена ниже.
int Compare(Т х, Т у)
Он действует таким же образом, как и его необобщенный аналог, возвращая значение больше нуля, еслихбольшеузначение меньше нуля, если х меньшеуи, наконец, нулевое значение, если оба значения равны. Преимущество интерфейсаIComparer<T>заключается в том, что он обеспечивает типовую безопасность. Ведь в этом случае тип обрабатываемых данных указывается явным образрм, а следовательно, никакого приведения типов не требуется.
В последующих разделах демонстрируется ряд наиболее распространенных операций с массивами.
Таблица 21.11. Свойства, определенные в классе Array
Свойство
Назначение
public bool IsFixedSize { get; }
public bool IsReadOnly { get; }
public bool
IsSynchronized { get; }
Доступно только для чтения. Принимает логическое значение true, если массив имеет фиксированный размер, и логическое значение false, если массив может изменять его динамически
Доступно только для чтения. Принимает логическое значение true, если объект класса Array предназначен только для чтения, а иначе — логическое значение false. Для массивов это свойство всегда имеет логическое значение true Доступно только для чтения. Принимает логическое значение true, если массив можно безопасно использовать в многопоточной среде, а иначе — логическое значение false. Для массивов это свойство всегда имеет логическое значение true
Свойство
Назначение '
public int Length {
Доступно только для чтения. Имеет тип int и содержит ко
get; }
личество элементов в массиве
public long LongLength
Доступно только для чтения. Имеет тип long и содержит
{ get; }
количество элементов в массиве
public int Rank { get; }
Доступно только для чтения. Содержит размерность массива
public object SyncRoot
Доступно только для чтения. Содержит объект, предназна
{ get; }
ченный для синхронизации доступа к массиву
Таблица 21.12. Методы, определенные в классе Array
Метод
Назначение
public static
Возвращает доступную только для чтения коллек
ReadOnlyCollection<T>
цию, которая включает в себя массив, определяе
AsReadOnly<T>(Т[] array)
мый параметром array
public static int
Осуществляет поиск значения value в массиве
BinarySearch(Array
array,
array. Возвращает индекс первого вхождения
objectvalue)
искомого значения. Если оно не найдено, возвращает отрицательное значение. Массив array должен быть отсортированным и одномерным
public static int
Осуществляет поиск значения value в массиве
BinarySearch<T>(T[]
array,
array. Возвращает индекс первого вхождения
Tvalue)
искомого значения. Если оно не найдено, возвращает отрицательное значение. Массив array должен быть отсортированным и одномерным
public static int
Осуществляет поиск значения value в масси
BinarySearch(Array
array,
ве, определяемом параметром array, исполь
objectvalue,IComparer
зуя способ сравнения, задаваемый параметром
comparer)
comparer. Возвращает индекс первого вхождения искомого значения. Если оно не найдено, возвращает отрицательное значение. Массив array должен быть отсортированным и одномерным
public static int
Осуществляет поиск значения value в массиве
BinarySearch<T> (T [ ]
array,
array, используя способ сравнения, задаваемый
Tvalue,IComparer<T>
параметром comparer. Возвращает индекс перво
comparer)
го вхождения искомого значения. Если оно не найдено, возвращает отрицательное значение. Массив array должен быть отсортированным и одномерным
public static int
Осуществляет поиск значения value в части мас
BinarySearch(Array
array,
сива array. Поиск начинается с индекса, зада
intindex,intlength,
ваемого параметром index, и охватывает число
objectvalue)
элементов, определяемых параметром length. Возвращает индекс первого вхождения искомого значения. Если оно не найдено, возвращает отрицательное значение. Массив array должен быть отсортированным и одномерным
Метод
Назначение
public static int
Осуществляет поиск значения value в части мас
BinarySearch<T>(T[] array,
сива array. Поиск начинается с индекса, зада
int index, int length, T
ваемого параметром index, и охватывает число
value)
элементов, определяемых параметром length. Возвращает индекс первого вхождения искомого значения. Если оно не найдено, возвращает отрицательное значение. Массив array должен быть отсортированным и одномерным
public static int
Осуществляет поиск значения value в части мас
BinarySearch(Array array,
сива array, используя способ сравнения, опреде
int index, int length,
ляемый параметром comparer. Поиск начинается
object value, IComparer
с индекса, задаваемого параметром index, и охва
comparer)
тывает число элементов, определяемых параметром length. Возвращает индекс первого вхождения искомого значения. Если оно не найдено, возвращает отрицательное значение. Массив array должен быть отсортированным и одномерным
public static int
Осуществляет поиск значения value в части мас
BinarySearch<T>(T [] array,
сива array, используя способ сравнения, опреде
int index, int length,
ляемый параметром comparer. Поиск начинается
T value, Icomparer<T>
с индекса, задаваемого параметром index, и охва
comparer)
тывает число элементов, определяемых параметром length. Возвращает индекс первого вхождения искомого значения. Если оно не найдено, возвращает отрицательное значение. Массив array должен быть отсортированным и одномерным
public static void
Устанавливает заданные элементы массива array
Clear(Array array, int
равными нулю, пустому значению null или логи
index, int length)
ческому значению false в зависимости оттипэ элемента: значения, ссылочного или логического. Подмножество элементов, подлежащих обнулению, начинается с индекса, задаваемого параметром index, и включает в себя число элементов, определяемых параметром length
public object Clone ()
Возвращает копию вызывающего массива. Эта копия ссылается на те же элементы, что и оригинал, поэтому она называется “неполной". Таким образом, изменения, вносимые в элементы, влияют на оба массива, поскольку и в том и в другом используются одни и те же элементы
public static void
Копирует число элементов, задаваемых па
ConstrainedCopy(Array
раметром length, из исходного массива
sourceArray, int sourcelndex,
sourceArray, начиная с элемента, указывае
Array destinationArray, int
мого по индексу sourcelndex, в целевой мас
destinationlndex, int length)
сив destinationArray, начиная с элемента,
_Продолжение табл. 21.12
Метод
Назначение
указываемого по индексу destinationlndex. Если
оба массива имеют одинаковый ссылочный тип, то метод ConstrainedCopy () создает “неполную копию", в результате чего оба массива будут ссылаться на одни и те же элементы. Если же во время копирования возникает ошибка, то содержимое целевого массива destinationAr ray остается прежним
public static TTo [ ]
Преобразует массив array из типа Tlnput в тип
ConvertА11<ТInput,
TOutput и возвращает получающийся в итоге
TTo>(TFrom[] array,
массив. Исходный массив остается прежним. Пре
Converter<TOutput, TTo>
образование выполняется преобразователем, за
converter)
даваемым параметром converter
public static void
Копирует число элементов, задаваемых параметром
Copy(Array sourceArray,
length, из исходного массива sourceArray в це
Array destinationArray,
int
левой массив destinationArray, начиная с пер
length)
вого элемента массива. Если оба массива имеют одинаковый ссылочный тип, то метод Сору () создает “неполную копию", в результате чего оба массива будут ссылаться на одни и те же элементы. Если же во время копирования возникает ошибка, то содержимое целевого массива destinationArray оказывается неопределенным
public static void
Копирует число элементов, задаваемых параметром
Copy(Array sourceArray,
length, из исходного массива sourceArray в це
Array destinationArray,
long
левой массив destinationArray, начиная с пер
length)
вого элемента массива. Если оба массива имеют одинаковый ссылочный тип, то метод Сору () создает “неполную копию”, в результате чего оба массива будут ссылаться на одни и те же элементы. Если же во время копирования возникает ошибка, то содержимое целевого массива destinationArray оказывается неопределенным
public static void
Копирует число элементов, задаваемых параме
Copy(Array sourceArray,
тром length, из исходного массива sourceArray,
int sourcelndex, Array
начиная с элемента, указываемого по индексу
destinationArray, int
sourceArray [ sourcelndex], в целевой массив
destinationlndex, int
destinationArray, начиная с элемента, указы
length)
ваемого по индексу destinationAr ray [destinationlndex] . Если оба массива имеют одинаковый ссылочный тип, то метод Сору () создает “неполную копию”, в результате чего оба массива будут ссылаться на одни и те же элементы. Если же во время копирования возникает ошибка, то содержимое целевого массива destinationArray оказывается неопределенным
Метод
Назначение
public static void
Копирует число элементов, задаваемых параме
Copy(Array sourceArray,
тром length, из исходного массива sourceArray,
long sourcelndex, Array
начиная с элемента, указываемого по индексу
destinationArray, long
sourceArray [source Index], в целевой массив
destinationlndex, long
destinationArray, начиная с элемента, указы
length)
ваемого по индексу destinationArray [destinationlndex] . Если оба массива имеют одинаковый ссылочный тип, то метод Сору () создает “неполную копию”, в результате чего оба массива будут ссылаться на одни и те же элементы. Если же во время копирования возникает ошибка, то содержимое целевого массива destinationArray оказывается неопределенным
public void CopyTo(Array
Копирует элементы вызывающего массива в це
array, int index)
левой массив array, начиная с элемента, указываемого по индексу array [index]. Если же во время копирования возникает ошибка, то содержимое целевого массива array оказывается неопределенным
public void CopyTo(Array
Копирует элементы вызывающего массива в це
array, long index)
левой массив array, начиная с элемента, указываемого по индексу array [index]. Если же во время копирования возникает ошибка, то содержимое целевого массива array оказывается неопределенным
public static Array
Возвращает ссылку на одномерный массив, кото
Createlnstance(Type
рый содержит число элементов типа elementType,
elementType, int length)
определяемое параметром length
public static Array
Возвращает ссылку на двумерный массив разме
Createlnstance(Type
ром lengthl*length2. Каждый элемент этого
elementType, int lengthl,
массива имеет тип elementType
int length2)
public static Array
^ Возвращает ссылку на трехмерный массив разме
Createlnstance(Type
ром lengthl* length2* length3. Каждый эле
elementType, int lengthl,
мент этого массива имеет тип elementType
int length2, int length3)
public static Array
Возвращает ссылку на многомерный массив, раз
Createlnstance(Type
мерность которого задается в массиве lengths.
elementType, params int[]
Каждый элемент этого массива имеет тип
lengths)
elementType
public static Array
Возвращает ссылку на многомерный массив, раз
Createlnstance(Type
мерность которого задается в массиве lengths.
elementType, params long[]
Каждый элемент этого массива имеет тип
lengths)
elementType
_Продолжение табл. 21.12
Метод
Назначение
public static Array
Возвращает ссылку на многомерный массив, раз
Createlnstance(Type
мерность которого задается в массиве lengths.
elementType,int[]lengths,
Каждый элемент этого массива имеет тип
int[]lowerBounds)
elementType. Начальный индекс каждого измерения задается в массиве lowerBounds. Таким образом, этот метод позволяет создавать массивы, которые начинаются с некоторого индекса, отличного от нуля
public static bool
Возвращает логическое значение true, если мас
Exists<T>(T[]array,
сив array содержит хотя бы один элемент, удо
Predicate<T>match)
влетворяющий условию предиката, задаваемого параметром match, а иначе возвращает логическое значение false
public static T Find<T>(T[]
Возвращает первый элемент массива array, удо
array,Predicate<T>match)
влетворяющий условию предиката, задаваемого параметром’ match, а иначе возвращает значение типа default (Т)
public static T[]
Возвращает все элементы массива array, удо
FindAll<T>(T[]array,
влетворяющие условию предиката, задаваемого
Predicate<T>match)
параметром match, а иначе возвращает массив нулевой длины
public static int
Возвращает индекс первого элемента массива
FindIndex<T>(T[]array,
array, удовлетворяющего условию предиката, за
Predicate<T>match)
даваемого параметром match, иначе возвращает значение -1
public static int
Возвращает индекс первого элемента масси
FindIndex<T>(T[]array,int
ва array, удовлетворяющего убловию предика
startlndex,Predicate<T>
та, задаваемого параметром match. Поиск на
match)
чинается с элемента, указываемого по индексу array [ start Index]. Если ни один из элементов, удовлетворяющих данному условию, не найден, то возвращается значение -1
public static int
Возвращает индекс первого элемента масси
FindIndex<T>(T[]array,
ва array, удовлетворяющего условию предика
intstartlndex,intcount,
та, задаваемого параметром match. Поиск на
Predicate<T>match)
чинается с элемента, указываемого по индексу array [startlndex], и продолжается среди числа элементов, определяемых параметром count. Если ни один из элементов, удовлетворяющих данному условию, не найден, то возвращается значение -1
public static T
Возвращает последний элемент массива array,
FindLast<T>(T[]array,
удовлетворяющий условию предиката, задаваемо
Predicate<T>match)
го параметром match, иначе возвращает значение типа default (Т)
Метод
Назначение
public static int
Возвращает индекс последнего элемента массива
FindLastIndex<T>(T[]
array,
array, удовлетворяющего условию предиката, за
. Predicate<T>match)
даваемого параметром match, иначе возвращает значение -1
public static int
Возвращает индекс последнего элемента массива
FindLastIndex<T>(T[]
array,
array, удовлетворяющего условию предиката, за
intstartlndex,Predicate<T>
даваемого параметром match. Поиск начинается
match)
в обратном порядке с элемента, указываемого по индексу array [startlndex], и оканчивается на элементе array [ 0]. Если ни один из элементов, удовлетворяющих данному условию, не найден, то возвращается значение -1
public static int
Возвращает индекс последнего элемента массива
FindLastIndex<T>(T[]
array,
array, удовлетворяющего условию предиката, за
intstartlndex,int
count,
даваемого параметром v. Поиск начинается в об
Predicate<T>match)
ратном порядке с элемента, указываемого по индексу array[start], и продолжается среди числа элементов, определяемых параметром count. Если ни один из элементов, удовлетворяющих данному условию, не найден, то возвращается значение -1
public static void
Применяет метод, задаваемый параметром
ForEach<T>(T[]array,
action, к каждому элементу массива array
Action<T>action)
public IEnumerator
Возвращает перечислительный объект для масси
GetEnumerator()
ва. Перечислители позволяют опрашивать массив в цикле. Боле подробно перечислители описываются в главе 25
public override int
Возвращает хеш-код для вызывающего объекта
GetHashCode()
public int GetLength(int
Возвращает длину заданного измерения массива.
dimension)
Отсчет измерений начинается с нуля, поэтому для получения длины первого измерения необходимо передать данному методу значение 0 параметра dimension, для получения длины второго измерения — значение 1 и т.д.
public long GetLongLength(int
Возвращает длину заданного измерения массива в
dimension)
виде значения типа long. Отсчет измерений начинается с нуля, поэтому для получения длины первого измерения необходимо передать данному методу значение 0 параметра dimension, для получения длины второго измерения — значение 1 и т.д.
public int GetLowerBound(int
Возвращает начальный индекс заданного измере
dimension)
ния массива, который обычно равен нулю. Параметр dimension определяет отсчет измерений
_Продолжение табл. 21.12
Метод
Назначение
с нуля, поэтому для получения начального индекса
-
первого измерения необходимо передать данному
методу значение 0 параметра dimension, для получения начального индекса второго измерения —
значение 1 и т.д.
public int GetUpperBound(int
Возвращает конечный индекс заданного измере
dimension)
ния массива. Параметр dimension определяет отсчет измерений с нуля, поэтому для получения конечного индекса первого измерения необходимо передать данному методу значение 0 параметра dimension, для получения конечного индекса второго измерения — значение 1 и т.д.
public object GetValue(int
Возвращает значение элемента из вызывающего
index)
массива по индексу index. Массив должен быть одномерным
public object GetValue(long
Возвращает значение элемента из вызывающего
index)
массива по индексу index. Массив должен быть одномерным
public object GetValue(int
Возвращает значение элемента из вызывающего
indexl,intindex2)
массива по индексам [indexl, index2]. Массив должен быть двумерным
public object GetValue(long
Возвращает значение элемента из вызывающего
indexl,longindex2)
массива по индексам [ indexl, index2]. Массив должен быть двумерным
public object GetValue(int
Возвращает значение элемента из вызывающе
indexl,intindex2,int
го массива по индексам [indexl, index2,
index3)
index3]. Массив должен быть трехмерным
public object GetValue(long
Возвращает значение элемента из вызывающе
indexl,longindex2,long
го массива по индексам [indexl, index2,
idx3)
index3]. Массив должен быть трехмерным
public object GetValue(int[]
Возвращает значение элемента из вызывающего
indices)
массива по указанным индексам. Число измерений массива должно соответствовать числу элементов массива indices
public object
Возвращает значение элемента из вызывающего
GetValue(long[]indices)
массива по указанным индексам. Число измерений массива должно соответствовать числу элементов массива indices
public static int
Возвращает индекс первого элемента, имеющего
IndexOf(Arrayarray,object
значение value в одномерном массиве array.
value)
Если искомое значение не найдено, то возвращает -1. (Если же массив имеет ненулевую нижнюю границу, то неудачный исход поиска будет обозначаться значением нижней границы, уменьшенным на 1.)
Метод
Назначение
public static int
Возвращает индекс первого элемента, имеющего
IndexOf<T>(T[] array,T
значение value в одномерном массиве array.
value)
Если искомое значение не найдено, то возвращает
public static int
-L
Возвращает индекс первого элемента, имеющего
IndexOf(Arrayarray,object
значение value в одномерном массиве array.
value,intstartlndex)
Поиск начинается с элемента, указываемого по индексу array [startlndex]. Метод возвращает -1, если искомое значение не найдено. (Если массив имеет ненулевую нижнюю границу, то неудачный исход поиска будет обозначаться значением
нижней границы, уменьшенным на 1.)
public static int
Возвращает индекс первого элемента, имеющего
IndexOf<T>(T[] array,T
значение value в одномерном массиве array.
value,intstartlndex)
Поиск начинается с элемента, указываемого по индексу array [startlndex]. Метод возвращает -1, если искомое значение не найдено
public static int
Возвращает индекс первого элемента, имеющего
IndexOf(Arrayarray,object
значение value в одномерном массиве array.
value,intstartlndex,int
Поиск начинается с элемента, указываемого по
count)
индексу array [startlndex], и продолжается среди числа элементов, определяемых параметром count. Метод возвращает -1, если искомое значение не найдено в заданных пределах. (Если же массив имеет ненулевую нижнюю границу, то неудачный исход поиска будет обозначаться значением нижней границы, уменьшенным на 1.)
public static int
Возвращает индекс первого элемента, имеющего
IndexOf<T>(T[] array, T
значение value в одномерном массиве array.
value,intstartlndex,int
• Поиск начинается с элемента, указываемого по
count)
индексу array [start Index], и продолжается среди числа элементов, определяемых параметром count. Метод возвращает -1, если искомое значение не найдено в заданных пределах
public void4-Initialize ()
Инициализирует каждый элемент вызывающего массива с помощью конструктора, используемого по умолчанию для соответствующего элемента. Этот метод можно использовать только для массивов простых типов значений
public static int
Возвращает индекс последнего элемента, имеюще
LastlndexOf(Arrayarray,
го значение value в одномерном массиве array.
objectvalue)
Если искомое значение не найдено, то возвращает -1. (Если массив имеет ненулевую нижнюю границу, то неудачный исход поиска будет обозначаться значением нижней границы, уменьшенным на 1.)
_Продолжение табл. 21.12
Метод
Назначение
public static int
Возвращает индекс последнего элемента, имею
LastIndexOf-<T> (T [ ]array,T
щего значение value в одномерном массиве
value)
array. Если искомое значение не найдено, то возвращает -1
public static int
Возвращает индекс последнего элемента, имеюще
LastlndexOf(Arrayarray,
го значение value в одномерном массиве array.
objectvalue,intstartlndex)
Поиск начинается в обратном порядке с элемента, указываемого по индексу array [startlndex], и оканчивается на элементе а [ 0]. Метод возвращает -1, если искомое значение не найдено. (Если массив имеет ненулевую нижнюю границу, то неудачный исход поиска будет обозначаться значением нижней границы, уменьшенным на 1.)
public static int
Возвращает индекс последнего элемента, имеюще
LastlndexOf<T>(T[]array,T
го значение value в одномерном массиве array.
value,intstartlndex)
Поиск начинается в обратном порядке с элемента, указываемого по индексу a [startlndex], и оканчивается на элементе а [ 0]. Метод возвращает -1, если искомое значение не найдено
public static int
Возвращает индекс последнего элемента, имеюще
LastlndexOf(Arrayarray,
го значение value в одномерном массиве array.
objectvalue,intstartlndex,
Поиск начинается в обратном порядке с элемента,
intcount)
указываемого по индексу array [start Index], и продолжается среди числа элементов, определяемых параметром count. Метод возвращает -1, если искомое значение не найдено в заданных пределах. (Если массив имеет ненулевую нижнюю границу, то неудачный исход поиска будет обозначаться значением нижней границы, уменьшенным на 1.)
public static int
Возвращает индекс последнего элемента, имеюще
LastlndexOf<T>(T[]array,T
го значение value в одномерном массиве array.
value,intstartlndex,int
Поиск начинается в обратном порядке с элемента,
count)
указываемого по индексу array [start Index], и продолжается среди числа элементов, определяемых параметром count. Метод возвращает -1, если искомое значение не найдено в заданных пределах
public static void
Задает длину newSize массива array
Resize<T>(ref T[]array,int
newSize)
public static void
Меняет на обратный порядок следования элемен
Reverse(Arrayarray)
тов в массиве array
public static void
Меняет на обратный порядок следования эле
Reverse(Arrayarray,int
ментов массива array заданных в пределах,
index,intlength)
начиная с элемента, указываемого по индексу array [ index], и включая число элементов, определяемых параметром lenqth
Метод
Назначение
public
void SetValue(object
Устанавливает значение value элемента вызыва
value,
intindex)
ющего массива по индексу index. Массив должен быть одномерным
public
void SetValue(object
Устанавливает значение value элемента вызыва
value,
longindex)
ющего массива по индексу index. Массив должен быть одномерным
public
void SetValue(object
Устанавливает значение value элемента вы
value,
intindexl,intindex2)
зывающего массива по индексам [indexl, index2]. Массив должен быть двумерным
public
void SetValue(object
Устанавливает значение value элемента вы
value,
longindexl,longindex2)
зывающего массива по индексам [indexl, index2]. Массив должен быть двумерным
public
void SetValue(object
Устанавливает значение value элемента вызыва
value,
intindexl,int 4
ющего массива по индексам [ indexl, index2,
index2,
intindex3)
index3]. Массив должен быть трехмерным
public
void SetValue(object
Устанавливает значение value элемента вызыва
value,
longindexl,long
ющего массива по индексам [ indexl, index2,
index2,
. long index3)
index3]. Массив должен быть трехмерным
public
void SetValue(object
Устанавливает значение value элемента вызы
value,
int[]indices)
вающего массива по указанным индексам. Число измерений массива должно соответствовать числу элементов массива indices
public
void SetValue(object
Устанавливает значение value элемента вызы
value,
long[]indices)
вающего массива по указанным индексам. Число измерений массива должно соответствовать числу элементов массива indices
public
static void
Сортирует массив array по нарастающей. Массив
Sort(Arrayarray)
должен быть одномерным
public
static void
Сортирует массив array по нарастающей. Массив
Sort<T>(T[]array)
должен быть одномерным
public
static void
Сортирует массив array по нарастающей, исполь
Sort(Arrayarray,IComparer
зуя способ сравнения, задаваемый параметром
comparer)
comparer. Массив должен быть одномерным
public
static void
Сортирует массив array по нарастающей, исполь
Sort<T> (T [ ]array,
зуя способ сравнения, задаваемый параметром
Comparison<T>comparer)
comparer. Массив должен быть одномерным
public
static void
Сортирует массив array по нарастающей, исполь
Sort<T> (T [ ]array,
зуя способ сравнения, задаваемый параметром
IComparer<T>comparer)
comparer. Массив должен быть одномерным
public
static void
Сортирует по нарастающей два заданных одномер
Sort(Arraykeys,Array
ных массива. Массив keys содержит ключи сорти
iterns)
ровки, а массив i tems — значения, связанные с этими ключами. Следовательно, оба массива должны содержать пары “ключ-значение”. После сортировки элементы обоих массивов располагаются по порядку нарастания ключей
_Продолжение табл. 21.12
Метод
Назначение
public static void
Сортирует по нарастающей два заданных одномер
Sort<TKey, TValue>(TKey[]
ных массива. Массив keys содержит ключи сорти
keys,TV[]items)
ровки, а массив items — значения, связанные с этими ключами. Следовательно, оба массива должны содержать пары “ключ-значение”. После сортировки элементы обоих массивов располагаются по порядку возрастания ключей
public static void
Сортирует по нарастающей два заданных одномер
Sort(Arraykeys,Array
ных массива, используя способ сравнения, задава-,
items,Icomparercomparer)
емый параметром comparer. Массив keys содержит ключи сортировки, а массив i terns — значения, связанные с этими ключами. Следовательно, оба массива должны содержать пары “ключ-значение”. После сортировки элементы обоих массивов располагаются по порядку возрастания ключей
public static void
Сортирует по нарастающей два заданных одномер
SortCTKey* TValue>(TKey[]
ных массива, используя способ сравнения, задава
keys,TValue[]items,
емый параметром comparer. Массив keys содер
IComparer<TKey>comparer)
жит ключи сортировки, а массив i terns — значения, связанные с этими ключами. Следовательно, оба массива должны содержать пары “ключ-значение”. После сортировки элементы обоих массивов располагаются по порядку возрастания ключей
public static void
Сортирует массив array по нарастающей в задан
Sort(Arrayarray,intindex,
ных пределах, начиная с элемента, указываемого
intlength)
по индексу array [index], и включая число элементов, определяемых параметром length. Массив должен быть одномерным
public static void
Сортирует массив array по нарастающей в задан
Sort<T>(T[]array,int
ных пределах, начиная с элемента, указываемого
index,intlength)
по индексу array [index], и включая число элементов, определяемых параметром length. Массив должен быть одномерным
public static void Sort (Array Сортирует массив array по нарастающей в за-
array, intindex,intlength,
данных пределах, начиная с элемента, указывае
IComparercomparer)
мого по индексу array [index], и включая число элементов, определяемых параметром length, а также используя способ сравнения, задаваемый параметром v. Массив должен быть одномерным
public static void
Сортирует массив array по нарастающей в задан
Sort<T>(T[]array,int
ных пределах, начиная с элемента, указываемого по
index,intlength,
индексу array [ index], и включая число элемен
Icomparer<T>comparer)
тов, определяемых параметром length, а также используя способ сравнения, задаваемый параметром comparer. Массив должен быть одномерным
Метод
Назначение
public static void
Сортирует по нарастающей два одномерных мас
Sort(Array keys, Array
сива в.заданных пределах, начиная с элемента,
items, int index, int
указываемого по индексу index, и включая число
length)
элементов, определяемых параметром length. Массив keys содержит ключи сортировки, а массив i terns — значения, связанные с этими ключами. Следовательно, оба массива должны содержать
пары “ключ-значение". После сортировки элементы обоих массивов располагаются в заданных пределах по порядку возрастания ключей
public static void
Сортирует по нарастающей два одномерных мас
Sort<TKey, TValue>(TKey[]
сива в заданных пределах, начиная с элемента,
keys, TValue[] items, int
указываемого по индексу index, и включая число
index, int length)
элементов, определяемых параметром length. Массив keys содержит ключи сортировки, а массив i terns — значения, связанные с этими ключами. Следовательно, оба массива должны содержать пары “ключ-значение". После сортировки элемен
ты обоих массивов располагаются в заданных пределах по порядку возрастания ключей
public static void
Сортирует по нарастающей два одномерных мас
Sort(Array keys, Array
сива в заданных пределах, начиная с элемента,
items, int index, int
указываемого по индексу index, и включая число
length, IComparer comparer)
элементов, определяемых параметром length, а также используя способ сравнения, задаваемый параметром comparer. Массив keys содержит ключи сортировки, а массив items — значения, связанные с этими ключами. Следовательно, эти два массива должны содержать пары “ключ-значение". После сортировки элементы обоих мас
сивов располагаются в заданных пределах по порядку возрастания ключей
public static void
Сортирует по нарастающей два одномерных мас
Sort<TKey, TValue>(TKey[]
сива в заданных пределах, начиная с элемента,
keys, TV items, int index,
указываемого по индексу index, и включая число
int length, Icomparer<TKey>
элементов, определяемых параметром length, а
comparer)
также используя способ сравнения, задаваемый параметром comparer. Массив keys содержит ключи сортировки,,а массив items — значения, связанные с этими ключами. Следовательно, эти два массива должны содержать пары .“ключ-
значение". После сортировки элементы обоих массивов располагаются в заданных пределах по порядку возрастания ключей
Метод
Назначение
public static bool
Возвращает логическое значение true, если все
TrueForAll<T>(T[] array,
элементы массива array удовлетворяют условию
Predicate<T>match)
предиката, задаваемого параметром match. Если один или более элементов этого массива не удовлетворяют заданному условию, то возвращается логическое значение false
Сортировка и поиск в массивах
Содержимое массива нередко приходится сортировать. Для этой цели в классе Array
предусмотрен обширный ряд сортирующих методов. Так, с помощью разных вариантов метода Sort
() можно отсортировать массив полностью или в заданных пределах либо отсортировать два массива, содержащих соответствующие пары "ключ-значение". После сортировки в массиве можно осуществить эффективный поиск, используя разные варианты метода BinarySearch
(). В качестве примера ниже приведена программа, в которой демонстрируется применение методов Sort
() и BinarySearch
() для сортировки и поиска в массиве значений типа int.
// Отсортировать массив и найти в нем значение. using System; class SortDemo { static void Main() { int [ ] nums = { 5, 4, 6, 3, 14, 9, 8, 17, 1, 24, -1, 0 }; // Отобразить исходный порядок следования. Console.Write("Исходный порядок следования: "); foreach(int i in nums) Console.Write(i + " ") ; Console.WriteLine(); // Отсортировать массив. Array.Sort(nums); // Отобразить порядок следования после сортировки. Console.Write("Порядок следования после сортировки: "); foreach(int i in nums) Console.Write(i + " "); Console.WriteLine (); // Найти значение 14. int idx = Array.BinarySearch(nums, 14); Console.WriteLine("Индекс элемента массива со значением 14: " + idx) ; } } Вот к какому результату приводит выполнение этой программы. Исходный порядок следования: 54 63 14 98 17 124-10 Порядок следования после сортировки: -101345689141724 Индекс элемента массива со значением 14: 9 В приведенном выше примере массив состоит из элементов типаint,который относится к категории типов значений. Все методы, определенные в классеArray,автоматически доступны для обработки массивов всех встроенных в C# типов значений. Но в отношении массивов ссылок на объекты это правило может и не соблюдаться. Так, для сортировки массива ссылок на объекты в классе типа этих объектов должен быть реализован интерфейсIComparableилиIComparable<T>.Если же ни один из этих интерфейсов не реализован в данном классе, то во время выполнения программы может возникнуть исключительная ситуация в связи с попыткой отсортировать подобный массив или осуществить в нем поиск. Правда, реализовать оба интерфейса,IComparableиIComparable<T>,совсем нетрудно. В интерфейсеIComparableопределяется один метод. int CompareTo(objectobj) . В этом методе значение вызывающего объекта сравнивается со значением объекта, определяемого параметромobj.Если значение вызывающего объекта больше, чем у объектаobj,то возвращается положительное значение; если оба значения равны — нулевое значение, а если значение вызывающего объекта меньше, чем у объектаobj,— отрицательное значение. ИнтерфейсIComparable<T>является обобщенным вариантом интерфейсаIComparable.Поэтому в нем определен следующий обобщенный вариант методаCompareTo(). int CompareTo(Тother) Обобщенный вариант методаCompareTo() действует аналогично необобщенному его варианту. В нем значение вызывающего объекта также сравнивается со значением объекта, определяемого параметромother.Если значение вызывающего объекта больше, чем у объектаother,то возвращается положительное значение; если оба значения равны — нулевое значение, а если значение вызывающего объекта меньше, чем у объектаother,— отрицательное значение. Преимущество интерфейсаIComparable<T>заключается в том, что он'обеспечивает типовую безопасность, поскольку в этом случае тип обрабатываемых данных указывается явным образом, а следовательно, никакого приведения типаobjectсравниваемого объекта к нужному типу не требуется. В качестве примера ниже приведена программа, в которой демонстрируются сортировка и поиск в массиве объектов определяемого пользователем класса. // Отсортировать массив объектов и осуществить в нем поиск, using System; class MyClass : IComparable<MyClass> { public int i; public MyClass(int x) { i = x; } // Реализовать интерфейс IComparable<MyClass>. public int CompareTo(MyClass v) { return i - v.i; } public bool Equals(MyClass v) { return i == v.i; } class SortDemo { static void Main() { MyClass[] nums = new MyClass[5]; nums[0] = new MyClass(5); nums[l] = new MyClass (2); nums[2] = new MyClass (3); nums[3] = new MyClass(4); nums[4] = new MyClass(1); // Отобразить исходный порядок следования. Console.Write("Исходный порядок следования: "); foreach(MyClass о in nums) Console.Write(о.i + " "); Console.WriteLine (); // Отсортировать массив. Array.Sort(nums); // Отобразить порядок следования после сортировки. Console.Write("Порядок следования после сортировки: "); foreach(MyClass о in nums) Console.Write(о.i + " "); Console.WriteLine (); // Найти объект MyClass (2). MyClass x = new MyClass (2); int idx = Array.BinarySearch(nums, x); Console.WriteLine("Индекс элемента массива с объектом MyClass(2): " + idx) ; } } При выполнении этой программы получается следующий результат. Исходный порядок следования: 5 2 3 4 1 Порядок следования после сортировки: 12 3 4 5 Индекс элемента массива с объектом MyClass(2): 1 При сортировке или поиске в массиве строк может возникнуть потребность явно указать способ сравнения символьных строк. Так, если массив будет сортироваться с использованием одних настроек культурной среды, а поиск в нем — с помощью других настроек, то во избежание ошибок, скорее всего, придется явно указать способ сравнения. Аналогичная ситуация возникает и в том случае, если требуется отсортировать массив символьных строк при настройках культурной среды, отличающихся от текущих.Длявыхода из подобных ситуаций можно передать экземпляр объекта типаStringComparerпараметру типаIComparer,который поддерживается в целом ряде перегружаемых вариантов методовSort () иBinarySearch (). ПРИМЕЧАНИЕ
Более подробно особенности сравнения строк рассматриваются в главе 22.
Класс StringComparer
объявляется в пространстве имен System
и реализует, среди прочего, интерфейсы IComparer
и I Comparer <Т>.
Поэтому экземпляр объекта типа StringComparer
может быть передан в качестве аргумента параметру типа IComparer.
Кроме того, в классе StringComparer
определен ряд доступных только для чтения свойств, возвращающих экземпляр объекта типа StringComparer
и поддерживающих различные способы сравнения символьных строк. Все эти свойства перечислены ниже.
Свойство
Способ сравнения
public static StringComparer
С учетом регистра и культурной среды
CurrentCulture {get; }
public static StringComparer
Без учета регистра, но с учетом культур
CurrentCulturelgnoreCase {get; }
ной среды
public static StringComparer
С учетом регистра и безотносительно
InvariantCulture {get; }
к культурной среде
public static StringComparer
Без учета регистра и безотносительно
InvariantCulturelgnoreCase {get; }
к культурной среде
public static StringComparer Ordinal
Порядковое сравнение с учетом реги
{get; }
стра
public static StringComparer
Порядковое сравнение без учета реги
OrdinallgnoreCase {get; }
стра
Передавая явным образом экземпляр объекта типа StringComparer,
можно совершенно однозначно определить порядок сортировки или поиска в массиве. Например, в приведенном фрагменте кода сортировка и поиск в массиве символьных строк осуществляется с помощью свойства StringComparer. Ordinal.
string[] strs = { "xyz", "one" , "beta", "Alpha" }; //... Array.Sort(strs, StringComparer.Ordinal); int idx = Array.BinarySearch(strs, "beta", StringComparer.Ordinal) ; Обращение содержимого массива
Иногда оказывается полезно обратить содержимое массива и, в частности, отсортировать по убывающей массив, отсортированный по нарастающей. Для такого обращения массива достаточно вызвать метод Reverse
(). С его помощью можно обратить содержимое массива полностью или частично. Этот процесс демонстрируется в приведенной ниже программе. // Обратить содержимое массива. using System; class ReverseDemo { static void Main() { int[] nums = { 1, 2, 3, 4, 5 }; // Отобразить исходный порядок следования. Console.Write("Исходный порядок следования: "); foreach(int i in nums) Console.Write(i + " "); Console.WriteLine (); // Обратить весь массив. Array.Reverse(nums); // Отобразить обратный порядок следования. Console.Write("Обратный порядок следования: "); foreach(int i in nums) Console.Write (i + " "); Console.WriteLine(); // Обратить часть массива. Array.Reverse(nums, 1, 3); // Отобразить обратный порядок следования. Console.Write("Частично обращенный порядок следования: "); foreach(int i in nums) Console.Write(i + " "); Console.WriteLine(); } } Эта программа дает следующий результат. Исходный порядок следования: 12 3 4 5 Обратный порядок следования: 5 4 3 2 1 Частично обращенный порядок следования: 5 2 3 4 1 Копирование массива
Полное или частичное копирование одного массива в другой — это еще одна весьма распространенная операция с массивами. Для копирования содержимого массива служит методСору(). В зависимости от его варианта копирование элементов исходного массива осуществляется в начало или в средину целевого массива. Применение методаСору() демонстрируется в приведенном ниже примере программы. // Скопировать массив. ^ using System; class CopyDemo { static void Main() { int[] source ={1, 2, 3, 4, 5}; int[] target = { 11, 12, 13, 14, 15 }; int[] source2 = { -1, -2, -3, -4, -5 }; // Отобразить исходный массив. Console.Write("Исходный массив: "); foreach(int i in source) • Console.Write(i + " "); Console.WriteLine (); // Отобразить исходное содержимое целевого массива. Console.Write("Исходное содержимое целевого массива: "); foreach(int i in target) Console.Write(i,+ " "); Console.WriteLine(); // Скопировать весь массив. Array.Copy(source, target, source.Length); // Отобразить копию. Console.Write("Целевой массив после копирования: "); foreach(int i in target) Console.Write(i + " "); Console.WriteLine(); // Скопировать в средину целевого массива. Array.Copy(source2, 2, target, 3, 2); // Отобразить копию. Console.Write("Целевой массив после частичного копирования: "); foreach(int i in target) Console.Write(i + " "); Console.WriteLine(); } } Выполнение этой программы дает следующий результат. Исходный массив: 12 3 4 5 Исходное содержимое целевого массива: 11 12 13 14 15 Целевой массив после копирования: 12 3 4 5 Целевой массив после частичного копирования: 12 3-3-4 Применение предиката
Предикатпредставляет собой делегат типаSystem. Predicate,возвращающий логическое значениеtrueилиfalseв зависимости от некоторого условия. Он объявляется следующим образом. public delegate bool Predicate<T> (Tobj) Объект, проверяемый по заданному условию, передается в качестве параметраobj.Если объектobjудовлетворяет заданному условию, то предикат должен возвратить логическое значениеtrue,в противном случае — логическое значениеfalse.Предикаты используются в ряде методов классаArray,включая:Exists (), Find (), Findlndex() иFindAll(). В приведенном ниже примере программы демонстрируется применение предиката с целью определить, содержится ли в целочисленном массиве отрицательное значение. Если такое значение обнаруживается, то данная программа извлекает первое отрицательное значение, найденное в массиве. Для этого в ней используются методыExists() иFind(). class PredDemo { // Предикатный метод, возвращающий логическое значение true, // если значение переменной v оказывается отрицательным, static bool IsNeg(int v) { if (v < 0) return true; return false; } static void Main() { int[] nums = { 1, 4, -1, 5, -9 }; Console.Write("Содержимое массива nums: "); foreach(int i in nums) Console.Write (i + " "); Console.WriteLine(); // Сначала проверить, содержит ли массив nums отрицательное значение, if(Array.Exists(nums, PredDemo.IsNeg)) { Console.WriteLine("Массив nums содержит отрицательное значение."); // Затем найти первое отрицательное значение" в массиве, int х = Array.Find(nums, PredDemo.IsNeg); Console.WriteLine("Первое отрицательное значение: " + x); } else Console.WriteLine("В массиве nums отсутствуют отрицательные значения."); } } Эта программа дает следующий результат. Содержимое массива nums: 14-15-9 Массив nums содержит отрицательное значение. Первое отрицательное значение: -1 В данном примере программы в качестве предиката методам Exists
() и Find
() передается метод IsNeg ()
. Обратите внимание на следующее объявление метода IsNeg().
static bool IsNeg (int v) { Методы Exists
() и Find
() автоматически и по порядку передают элементы массива переменной v.
Следовательно, после каждого вызова метода IsNeg ()
переменная v
будет содержать следующий элемент массива. Применение делегата Action
Делегат Action
применяется в методе Array. ForEach
() для выполнения заданного действия над каждым элементом массива. Существуют разные формы делегата Action,
отличающиеся числом параметров типа. Ниже приведена одна из таких форм. public delegate void Action<T> (Tobj) В этой форме объект, над которым должно выполняться действие, передается в качестве параметраobj.Когда же эта форма делегата Action
применяется в методе Array
. ForEach (
), то каждый элемент массива передается по порядку объектуobj. Следовательно, используя делегатActionи методFor Each (),можно в одном операторе выполнить заданную операцию над целым массивом. В приведенном ниже примере программы демонстрируется применение делегатаActionи методаForEach ().Сначала в ней создается массив объектов классаMyClass,а затем используется методShow() для отображения значений, извлекаемых из этого массива. Далее эти значения становятся отрицательными с помощью метода Neg (). И наконец, методShow() используется еще раз для отображения отрицательных значений. Все эти операции выполняются посредством вызовов методаForEach(). // Продемонстрировать применение делегата Action. using System; 'class MyClass { public int i; public MyClass(int x) { i = x; } } class ActionDemo { // Метод делегата Action, отображающий значение, которое ему передается, static void Show(MyClass о) { Console.Write(о.i + " "); } // Еще один метод делегата Action, делающий // отрицательным значение, которое ему передается. static void Neg(MyClass о) { * o.i = -o.i; } static void Main() { MyClass[] nums = new MyClass[5]; nums[0] = new MyClass(5); nums[l] = new MyClass(2); nums[2] = new MyClass(3); nums[3] = new MyClass(4); nums[4] = new MyClass(1); Console.Write("Содержимое массива nums: "); // Выполнить действие для отображения значений. Array.ForEach(nums, ActionDemo.Show); Console.WriteLine(); // Выполнить действие для отрицания значений. Array.ForEach(nums, ActionDemo.Neg); Console.Write("Содержимое массива nums после отрицания: "); // Выполнить действие для повторного отображения значений. Array.ForEach(nums, ActionDemo.Show); Ниже приведен результат выполнения этой программы. Содержимое массива nums: 5 2 3 4 1 Содержимое массива nums после отрицания: -5 -2 -3 -4 -1 Класс BitConverter
В программировании нередко требуется преобразовать встроенный тип данных в массив байтов. Допустим, что на некоторое устройство требуется отправить целое значение, но сделать это нужно отдельными байтами, передаваемыми по очереди. Часто возникает и обратная ситуация, когда данные получаются из устройства в виде упорядоченной последовательности байтов, которые требуется преобразовать в один из встроенных типов. Для подобных преобразований в среде .NET предусмотрен отдельный класс BitConverter.
Класс BitConverter
является статическим. Он содержит методы, приведенные в табл. 21.13. Кроме того, в нем определено следующее поле. public static readonly bool IsLittleEndian Это поле принимает логическое значение true,
если в текущей среде сначала сохраняется младший байт слова, а затем старший. Это так называемый формат спрямымпорядком байтов. А если в текущей среде сначала сохраняется старший байт слова, а затем младший, то поле IsLittleEndian
принимает логическое значение false.
Это так называемый формат собратнымпорядком байтов. В компьютерах с процессором Intel Pentium используется формат с прямым порядком байтов. Таблица 21.13. Методы, определенные в классе BitConverter Метод Назначение
public static long Преобразует значение
value в целочисленное значение
DoubleToInt64Bits (double типа long и возвращает результат
value) public static byte [ ] Преобразует значение value в однобайтовый массив и
GetBytes (bool
value) возвращает результат
public static byte [ ] Преобразует значение
value в двухбайтовый массив и
GetBytes (char
value) возвращает результат
public static byte [ ] Преобразует значение
value в восьмибайтовый массив
GetBytes (double
value) и возвращает результат
public static byte [ ] Преобразует значение value в четырехбайтовый массив
GetBytes (float
value) и возвращает результат
public static byte [ ] Преобразует значение
value в четырехбайтовый массив
GetBytes (int
value) и возвращает результат
public static byte [ ] Преобразует значение
value в восьмибайтовый массив
GetBytes (long
value) и возвращает результат
public static byte [ ] Преобразует значение
value в двухбайтовый массив и
GetBytes (short
value) возвращает результат
public static byte [ ] Преобразует значение
value в четырехбайтовый массив
GetBytes (uint
value) и возвращает результат
public static byte [ ] Преобразует значение
value в восьмибайтовый массив
GetBytes (ulong
value)_и возвращает результат_
Метод
Назначение
public static byte[]
Преобразует значение
value в двухбайтовый массив
GetBytes(ushortvalue)
и возвращает результат
public static double
Преобразует значение
value в значение типа
double
Int64BitsToDouble(long
и возвращает результат
value) public static bool
Преобразует байт из элемента массива, указываемого по
ToBoolean(byte[]value,
индексу
value [startlndex], в эквивалентное значе
intstartlndex)
ние типа
bool и возвращает результат. Ненулевое значе
public static char
ние преобразуется в логическое значение
true, а нулевое — в логическое значение
false Преобразует два байта, начиная с элемента массива
ToChar(byte[]value,int
value [ index], в эквивалентное значение типа
char
index)
и возвращает результат
public static double
Преобразует восемь байтов, начиная с элемента массива
ToDouble(byte[]value,
value [startlndex], в эквивалентное значение типа
intstartlndex)
double и возвращает результат
public static short
Преобразует два байта, начиная с элемента массива
Tolntl6(byte[]value,
value [startlndex], в эквивалентное значение типа
intstartlndex)
short и возвращает результат
public static int
Преобразует четыре байта, начиная с элемента массива
ToInt32(byte[]value,
value [startlndex], в эквивалентное значение типа
intstartlndex)
int и возвращает результат
public static long
Преобразует восемь байтов, начиная с элемента массива
ToInt64(byte[]value,
value [startlndex], в эквивалентное значение типа
intstartlndex)
long и возвращает результат
public static float
Преобразует четыре байта, начиная с элемента массива
ToSingle(byte[]value,
value [startlndex], в эквивалентное значение типа
intstartlndex)
float и возвращает результат
public static string
Преобразует байты из массива
value в символьную
ToString(byte[]value)
строку. Строка содержит шестнадцатеричные значения,
public static string
связанные с этими байтами и разделенные дефисами Преобразует байты из массива
value в символьную
ToString(byte[]value,
строку, начиная с элемента
value[startindex]. Стро
intstartlndex)
ка содержит шестнадцатеричные значения, связанные
public static string
с этими байтами и разделенные дефисами Преобразует байты из массива
value в символьную
ToString(byte[]value,
строку, начиная с элемента
value [ startlndex]
intstartlndex,int
и включая число элементов, определяемых параметром
length)
length. Строка содержит шестнадцатеричные значения,
public static ushort
связанные с этими байтами и разделенные дефисами Преобразует два байта, начиная с элемента массива
ToUIntl6(byte[]value,
value [startlndex], в эквивалентное значение типа
intstartlndex)
ushort и возвращает результат
public static uint
Преобразует четыре байта, начиная с элемента массива
ToUInt32(byte[]value,
value[startlndex], в эквивалентное значение типа
intstartlndex)
uint и возвращает результат
Метод
Назначение
public static ulong
Преобразует восемь байтов, начиная с элемента массива
ToUInt64(byte[]
value,
value[startlndex], в эквивалентное значение типа
intstartlndex)
ulong и возвращает результат
Генерирование случайных чисел средствами класса Random
Для генерирования последовательного ряда случайных чисел служит классRandom.Такие последовательности чисел оказываются полезными в самых разных ситуациях, включая имитационное моделирование. Начало последовательности случайных чисел определяется некоторым начальным числом, которое может задаваться автоматически или указываться явным образом. В классеRandomопределяются два конструктора. public Random() public Random(intseed) Первый конструктор создает объект типаRandom,использующий системное время для определения начального числа. А во втором конструкторе используется начальное значениеseed,задаваемое явным образом. Методы, определенные в классеRandom,перечислены в табл. 21.14. Таблица 21.14. Методы, определенные в классе Random
Метод
Назначение
public virtual int Next() public virtual int Next(intmaxValue) public virtual int Next(intminValue,intmaxValue) public virtual void NextBytes(byte[]
buffer) public virtual double NextDouble() protected virtual double Sample()
Возвращает следующее случайное целое число, которое будет находиться в пределах от 0 до
Int32 . MaxValue-1 включительно
Возвращает следующее случайное целое число, которое будет находиться в пределах от 0 до
maxValue-1 включительно
Возвращает следующее случайное целое число, которое будет находиться в пределах от
minValue до
maxValue-1 включительно
Заполняет массив
buffer последовательностью случайных целых чисел. Каждый байт в массиве будет находиться в пределах от 0 до
Byte .MaxValue-1 включительно
Возвращает из последовательности следующее случайное число, которое представлено в форме с плавающей точкой, больше или равно 0,0 и меньше 1,0 Возвращает из последовательности следующее случайное число, которое представлено в форме с плавающей точкой, больше или равно 0,0 и меньше 1,0. Для получения несимметричного или специального распределения случайных чисел этот метод необходимо переопределить в производном классе
Ниже приведена программа, в которой применение классаRandomдемонстрируется на примере создания компьютерного варианта пары игральных костей. // Компьютерный вариант пары играль/ных костей. using System; class RandDice { static void Main() { Random ran = new Random(); Console.Write(ran.Next(1, 7) + " "); Console.WriteLine(ran.Next(1, 7)); } } При выполнении этой программы три раза подряд могут быть получены, например, следующие результаты. 5 2 4 4 1 6 Сначала в этой программе создается объект классаRandom.А затем в ней запрашиваются два случайных значения в пределах от 1
до 6
. Управление памятью и класс GC
В классеGCинкапсулируются средства "сборки мусора". Методы, определенные в этом классе, перечислены в табл. 21.15. Таблица 21.15. Методы, определенные в классе GC
Метод
Назначение
public static voidAddMemoryPressure(longbytesAllocated)public static void CancelFullGCNotification () public static void Collect () public static void Collect(intgeneration) public static void Collect (intgeneration,GCCollectionModemode) public static int CollectionCount (intgeneration) public static int GetGeneration (objectobj)
Задает в качестве параметра
bytes Allocated количество байтов, распределенных в неуправляемой области памяти Отменяет уведомление о “сборке мусора”
Инициализирует процесс “сборки мусора” Инициализирует процесс “сборки мусора” в областях памяти с номерами поколений от 0 до
generation Инициализирует процесс “сборки мусора” в областях памяти с номерами поколений от 0 до
generation в'режиме, определяемом параметром
mode Возвращает количество операций “сборки мусора”, выполненных в области памяти с номером поколения
generation Возвращает номером поколения для области памяти, доступной по ссылке
obj
_Продолжение табл. 21.15
Метод
Назначение
public static int
Возвращает номер поколения для области па
GetGeneration(WeakReferencewo)
мяти, доступной по “слабой" ссылке, задавае
мой параметром
wo. Наличие “слабой" ссылки не защищает объект от “сборки мусора”
public static long
Возвращает общий объем памяти (в байтах),
GetTotalMemory(bool
выделенной на данный момент. Если параметр
forceFullCollection)
forceFullCollection имеет логическое значение
true, то сначала выполняется “сборка мусора”
public static void
Создает ссылку на объект
obj, защищая
KeepAlive(objectobj)
его от “сборки мусора”. Действие этой ссылки оканчивается после выполнения метода
KeepAlive()
public static void Regist
Разрешает уведомление о “сборке мусора”. Зна
erForFullGCNotification(in
чение параметра
maxGenerationThreshold
tmaxGenerationThreshold,int
обозначает количество объектов второго поко
largeObj ectHeapThreshold)
ления в обычной “куче", которые будут инициировать уведомление. А значение параметра
largeObj ectHeapThreshold обозначает количество объектов в крупной “куче", которые будут инициировать уведомление. Оба значения должны быть указаны в пределах от 1 до 99
public static void
Задает в качестве параметра
bytesAllocated
RemoveMemoryPressure(long
количество байтов, освобождаемых в неуправ
bytesAllocated)
ляемой области памяти
public static void
Вызывает деструктор для объекта
obj.
ReRegisterForFinalize(object
Этот метод аннулирует действие метода
obj)
SuppressFinalize()
public static void
Препятствует вызову деструктора для объекта
SuppressFinalize(objectobj)
obj
public static
Ожидает уведомления о том, что должен про
GCNotificationStatus
изойти полный цикл “сборки мусора”. Здесь
WaitForFullGCApproach()
GCNotif icationStatus — перечисление, определенное в пространстве имен
System
public static
Ожидает уведомления о том, что должен
GCNotificationStatus
произойти полный цикл “сборки мусора",
WaitForFullGCApproach(int
в течение времени, задаваемого пара
millisecondsTimeout)
метром
millisecondsTimeout. Здесь
GCNotif icationStatus — перечисление, определенное в пространстве имен
System
public static
Ожидает уведомления о завершении
GCNotificationStatus
полного цикла “сборки мусора". Здесь
WaitForFullGCComplete ()
GCNotif icationStatus — перечисление, определенное в пространстве имен
System
Кроме того, в классеGCопределяется следующее доступное только для чтения свойство: public static int MaxGeneration { get; } СвойствоMaxGenerationсодержит максимальный номер поколения, доступный для системы. Номер поколения обозначает возраст выделенной области памяти. Чем старше выделенная область памяти, тем больше номер ее поколения. Номера поколений позволяют повысить эффективность работы системы " сборки мусора". В большинстве приложений возможности классаGCне используются. Но в особых случаях они оказываются весьма полезными. Допустим, что требуется организовать принудительную "сборку мусора" с помощью методаCollect() в выбранный момент времени. Как правило, "сборка мусора" происходит в моменты, не указываемые специально в программе. А поскольку для ее выполнения требуется некоторое время, то желательно, чтобы она не происходила в тот момент, когда решается критичная по времени задача. С другой стороны, "сборку мусора" и другие вспомогательные операции можно выполнить во время простоя программы. Имеется также возможность регистрировать уведомления о приближении и завершении "сборки мусора". Для проектов с неуправляемым кодом особое значение имеют два следующих метода из классаGC: AddMemoryPressure() иRemoveMemoryPressure(). С их помощью указывается большой объем неуправляемой памяти, выделяемой или освобождаемой в программе. Особое значение этих методов состоит в том, что система управления памятью не контролирует область неуправляемой памяти. Если программа выделяет большой объем неуправляемой памяти, то это может сказаться на производительности, поскольку системе ничего неизвестно о таком сокращении объема свободно доступной памяти. Если же большой объем неуправляемой памяти выделяется с помощью методаAddMemoryPressure(), то система CLR уведомляется о сокращении объема свободно доступной памяти. А если выделенная область памяти освобождается с помощью методаRemoveMemoryPressure(), то система CLR уведомляется о соответствующем восстановлении объема свободно доступной памяти. Следует, однако, иметь в виду, что методRemoveMemoryPressure() необходимо вызывать только для уведомления об освобождении области неуправляемой памяти, выделенной с помощью методаAddMemoryPressure(). Класс object
В основу типаobjectв C# положен классobject.Члены классаObjectподробно рассматривались в главе 11, но поскольку он играет главную роль в С#, то его методы ради удобства повторно перечисляются в табл. 21.16. В классеobjectопределен конструктор public Object() который создает пустой объект. Таблица 21.16. Методы, определенные в классе Object
Метод
Назначение
public virtual bool
Возвращает логическое значение
true, если вы
Equals(objectobj)
зывающий объект оказывается таким же, как и объект, определяемый параметром
obj. В противном случае возвращается значение
false
public static bool Equals(object
Возвращает логическое значение
true, если
obj A,objectobjB)
объект
obj А оказывается таким же, как и объект
objB. В противном случае возвращается значение
false
protected Finalize()
Выполняет завершающие действия перед процессом “сборки мусора”. В C# метод
Finalize () доступен через деструктор
public virtual int
Возвращает хеш-код, связанный с вызывающим
GetHashCode()
объектом
public Type GetTypeO
Получает тип объекта во время выполнения программы
protected object
Создает “неполную” копию объекта. При этом ко
MemberwiseClone()
пируются члены, но не объекты, на которые ссылаются эти члены
public static bool
Возвращает логическое значение
true, если
ReferenceEquals(objectobjA,
объекты
obj А и
objB ссылаются на один и тот
objectobjB)
же объект. В противном случае возвращается логическое значение
false
public virtual string
Возвращает строку, описывающую объект
ToString()
Класс Tuple
В версии .NET Framework 4.0 внедрен удобный способ создания групп объектов (так называемых кортежей). В основу этого способа положен статический класс Tuple, в котором определяется несколько вариантов метода Create () для создания кортежей, а также различные обобщенные классы типа Tuple<. . . >, в которых инкапсулируются кортежи. В качестве примера ниже приведено объявление варианта метода Create (), возвращающего кортеж с тремя членами.
public static Tuple<Tl, T2, T3>
Create<Tl, Т2, Т3> (Tliteml,Т2item2,ТЗitem3)
Следует заметить, что данный метод возвращает объект типа Tuple<Tl, Т2, Т3>, в котором инкапсулируются члены кортежаiteml,item2иitem3.Вообще говоря, кортежи оказываются полезными в том случае, если группу значений нужно интерпретировать как единое целое. В частности, кортежи можно передавать методам, возвращать из методов или же сохранять в коллекции либо в массиве.
Интерфейсы IComparable и IComparable<T>
Во многих классах приходится реализовывать интерфейс IComparable или IComparable<T>, поскольку он позволяет сравнивать один объект с другим, используя различные методы, определенные в среде .NET Framework. Интерфейсы IComparable и IComparable<T> были представлены в главе 18, где они использовались в примерах программ для сравнения двух объектов, определяемых параметрами обобщенного типа. Кроме того, они упоминались при рассмотрении класса Array ранее в этой главе. Но поскольку эти интерфейсы имеют особое значение и применяются во многих случаях, то ниже приводится их краткое описание.
Интерфейс IComparable реализуется чрезвычайно просто, потому что он состоит всего лишь из одного метода.
int CompareTo(objectobj) /
В этом методе значение вызывающего объекта сравнивается со значением объекта, определяемого параметромobj.Если значение вызывающего объекта больше, чем у объекта obj, то возвращается положительное значение; если оба значения равны — нулевое значение, а если значение вызывающего объекта меньше, чем у объектаobj, — отрицательное значение.
Обобщенный вариант интерфейса IComparable объявляется следующим образом.
public interface IComparable<T>
В данном варианте тип сравниваемых данных передается параметру Т в качестве аргумента типа. В силу этого объявление метода CompareTo () претерпевает изменения и выглядит так, как показано ниже.
int CompareTo(Тother)
В этом объявлении тип данных, которыми оперирует метод CompareTo (), может быть указан явным образом. Следовательно, интерфейс IComparable<T> обеспечивает типовую безопасность. Именно по этой причине он теперь считается более предпочтительным в программировании на С#, чем интерфейс IComparable.
Интерфейс IEquatable<T>
Интерфейс IEquatable<T> реализуется в тех классах, где требуется определить порядок сравнения двух объектов на равенство их значений. В этом интерфейсе определен только один метод, Equals (), объявление которого приведено ниже.
bool Equals(Тother)
Этот метод возвращает логическое значениеtrue,если значение вызывающего объекта оказывается равным значению другого объектаother, в противном случае — логическое значениеfalse.
ИнтерфейсIEquatable<T>реализуется в нескольких классах и структурах среды .NET Framework, включая структуры числовых типов и классString.Для реализации интерфейсаIEquatable<T>обычно требуется также переопределять методыEquals (Object)иGetHashCode (), определенные в классеObject.
Интерфейс IConvertible
ИнтерфейсIConvertibleреализуется в структурах всех типов значений,StringиDateTime.В нем определяются различные преобразования типов. Реализовывать этот интерфейс в создаваемых пользователем классах, как правило, не требуется.
Интерфейс ICloneable
Реализовав интерфейсICloneable,можно создать все условия для копирования объекта. В интерфейсеICloneableопределен только один метод,Clone (), объявление которого приведено ниже.
object Clone()
В этом методе создается копия вызывающего объекта, а конкретная его реализация зависит от способа создания копии объекта. Вообще говоря, существуют две разновидности копий объектов: полная и неполная. Если создается полная копия, то копия совершенно не зависит от оригинала. Так, если в исходном объекте содержится ссылка на другой объектО,то при его копировании создается также копия объектаО.А при создании неполной копии осуществляется копирование одних только членов, но не объектов, на которые эти члены ссылаются. Так, после создания неполной копии объекта, ссылающегося на другой объектО,копия и оригинал будут ссылаться на один и тот же объектО,причем любые изменения в объектеОбудут оказывать влияние как на копию, так и на оригинал. Как правило, методClone() реализуется для получения полной копии. А неполные копии могут быть созданы с помощью методаMemberwiseClone (), определенного в классеObj ect.
Ниже приведен пример программы, в которой демонстрируется применение интерфейсаICloneable.В ней создается классTest,содержащий ссылку на объект класса X. В самом классеTestиспользуется методClone() для создания полной копии.
// Продемонстрировать применение интерфейса ICloneable.
using System;
class X {
public int a;
public X(int x) { a = x; }
}
class Test : ICloneable {
public X о; public int b;
public Test (int x, int y) { о = new X(x); b = y;
}
public void Show(string name) {
Console.Write("Значения объекта " + name + ": ");
Console.WriteLine("o.a: {0}, b: {1}", o.a, b);
}
// Создать полную копию вызывающего объекта, public object Clone() {
Test temp = new Test(o.a, b); return temp;
}
}
class CloneDemo {
static void Main() {
Test obi = new Test(10, 20);
obi.Show("obi");
Console.WriteLine("Сделать объект ob2 копией объекта obi.");
Test ob2 = (Test) obi.Clone ();
ob2.Show("ob2");
Console.WriteLine("Изменить значение obi.о.а на 99, " +
" а значение obl.b — на 88.");
obi.о.a = 99; obl.b = 88;
obi.Show("obi"); ob2.Show("ob2");
}
}
, Ниже приведен результат выполнения этой программы.
Значения объекта оЫ: о.а: 10, Ь: 20 Сделать объект оЬ2 копией объекта оЫ.
Значения объекта оЬ2: о.а: 10, Ь: 20
Изменить значение obi.о.а на 99, а значение obl.b — на 88.
Значения объекта оЫ: о.а: 99, Ь: 88 Значения объекта оЬ2: о.а: 10, Ь: 20
Как следует из результата выполнения приведенной выше программы, объект оЬ2 является копией объекта оЫ, но это совершенно разные объекты. Изменения в одном из них не оказывают никакого влияния на другой. Это достигается конструированием нового объекта типаTest,который выделяет новый объект типаXдля копирования. При этом новому экземпляру объекта типаXприсваивается такое же значение, как и у объекта типаXв оригинале.
Дляполучения неполной копии достаточно вызвать методMemberwiseClone (),определяемый в классеObjectиз методаClone (). В качестве упражнения попробуйте заменить методClone() в предыдущем примере программы на следующий его вариант.
// Сделать неполную копию вызывающего объекта, public object Clone () {
Test temp = (Test) MemberwiseClone(); return temp;
}
После этого изменения результат выполнения данной программы будет выглядеть следующим образом.
Значения объекта obi: о.а: 10, Ь: 20 Сделать объект оЬ2 копией объекта оЫ.
Значения объекта оЬ2: о.а: 10, Ь: 20
Изменить значение obi.о.а на 99, а значение obl.b — на 88.
Значения объекта obi: о.а: 99, Ь: 88 Значения объекта оЬ2: о.а: 99, Ь: 20
Как видите, обе переменные экземпляраов объектахоЫиоЬ2ссылаются на один и тот же объект типа X. Поэтому изменения в одном объекте оказывают влияние на другой. Но в то же время поляbтипаintв каждом из них разделены, поскольку типы значений недоступны по ссылке.
Интерфейсы I Forma tProvider и I Format table
В интерфейсеI Forma tProviderопределен единственный методGet Format (), который возвращает объект, определяющий форматирование данных в удобочитаемой форме текстовой строки. Ниже приведена общая форма методаGet Format():
object GetFormat(TypeformatType)
гдеformatType— это объект, получаемый для форматирования.
ИнтерфейсI Format tableподдерживает форматирование выводимых результатов в удобочитаемой форме. В нем определен следующий метод:
string ToString(stringformat,IFormatProviderformatProvider)
гдеformatобозначает инструкции для форматирования, aformatProvider —поставщик формата.
ПРИМЕЧАНИЕ
Подробнее о форматировании речь пойдет в главе 22.
Интерфейсы IObservable<T> и IObserver<T>
В версию .NET Framework 4.0 добавлены еще два интерфейса, поддерживающие шаблон наблюдателя:IObservable<T>иIObserver<T>.В шаблоне наблюдателя один класс (в роли наблюдаемого) предоставляет уведомления другому классу (в роли наблюдателя). С этой целью объект наблюдаемого класса регистрирует объект наблюдающего класса. Для регистрации наблюдателя вызывается методSubscribe (), который определен в интерфейсеIObservable<T>и которому передается объект типаIObserver<T>,принимающий уведомление. Для получения уведомлений можно зарегистрировать несколько наблюдателей. А для отправки уведомлений всем зарегистрированным наблюдателям применяются три метода, определенные в интерфейсеIObserver<T>.Так, методOnNext () отправляет данные наблюдателю, методOnError() сообщает об ошибке, а методOnCompleted() указывает на то, что наблюдаемый объект прекратил отправку уведомлений.
ГЛАВА 22 Строки и форматирование
В этой главе рассматривается классString,положенный в основу встроенного в C# типаstring.Как известно, обработка символьных строк является неотъемлемой частью практически всех программ. Именно по этой причине в классеStringопределяется обширный ряд методов, свойств и полей, обеспечивающих наиболее полное управление процессом построения символьных строк и манипулирования ими. С обработкой строк тесно связано форматирование данных в удобочитаемой форме. Используя подсистему форматирования, можно отформатировать данные всех имеющихся в C# числовых типов, а также дату, время и перечисления.
Строки в с#
Вопросы обработки строк уже обсуждались в главе 7, и поэтому не стоит повторяться. Вместо этого целесообразно дать краткий обзор реализации символьных строк в С#, прежде чем переходить к рассмотрению классаString.
Во всех языках программированиястрокапредставляет собой последовательность символов, но конкретная ее реализация отличается в разных языках. В некоторых языках программирования, например в C++, строки представляют собой массивы символов, тогда как в C# они являются объектами встроенного типа данныхstring.Следовательно,stringявляется ссылочным типом. Более того,string— это имя стандартного для среды .NET строкового типаSystem. String.Это означает, что в C# строке как объекту доступны все методы, свойства, поля и операторы, определенные в классеString.
После создания строки последовательность составляющих ее символов не может быть изменена. Благодаря этому ограничению строки реализуются в C# более эффективно. И хотя такое ограничение кажется на первый взгляд серьезным препятствием, на самом деле оно таковым не является. Когда требуется получить строку как разновидность уже существующей строки, достаточно создать новую строку, содержащую требующиеся изменения, и "отвергнуть" исходную строку, если она больше не нужна. А поскольку ненужные строковые объекты автоматически утилизируются средствами "сборки мусора'7, то беспокоиться о дальнейшей судьбе ''отвергнутых77строк не приходится. Следует, однако, подчеркнуть, что переменные ссылок на строки могут, безусловно, изменить объект, на который они ссылаются. Но сама последовательность символов в конкретном строковом объекте не подлежит изменению после его создания.
Для создания строк, которые нельзя изменить, в C# предусмотрен классStringBuilder,находящийся в пространстве именSystem. Text.Но на практике для этой цели чаще используется типstring,а не классStringBuilder.
Класс String
КлассStringопределен в пространстве именSystem.В нем реализуются следующие интерфейсы:IComparable, IComparable<string>, ICloneable, IConvertible, IEnumerable, IEnumerable<char>иIEquatable<string>.Кроме того,String —герметичный класс, а это означает, что он не может наследоваться. В классеStringпредоставляются все необходимые функциональные возможности для обработки символьных строк в С#. Он служит основанием для встроенного в C# типаstringи является составной частью среды .NET Framework. В последующих разделах представлено подробное описание классаString.
Конструкторы класса String
В классеStringопределено несколько конструкторов, позволяющих создавать строки самыми разными способами. Для создания строки из символьного массива служит один из следующих конструкторов.
public String(char[ ]value)
public String(char[ ]value,intstartlndex,intlength)
Первая форма конструктора позволяет создать строку, состоящую из символов массиваvalue.А во второй форме для этой цели из массиваvalueизвлекается определенное количество символов(length),начиная с элемента, указываемого по индексу
startlndex.
С помощью приведенного ниже конструктора можно создать строку, состоящую из отдельного символа, повторяющегося столько раз, сколько потребуется:
public String(charс,intcount)
гдесобозначает повторяющийся символ; acount— количество его повторений.
Кроме того, строку можно создать по заданному указателю на символьный массив, используя один из следующих конструкторов.
public String(char*value)
public String(char*value,intstartlndex,intlength)
Первая форма конструктора позволяет создать строку из символов, доступных из массива по указателюvalue. При этом предполагается, что массив, доступный по указателюvalue, завершается пустым символом, обозначающим конец строки. А во второй форме конструктора для этой цели из массива, доступного по указателюvalue, извлекается определенное количество символов(length),начиная с элемента, указываемого по индексуstartlndex.В этих конструкторах применяются указатели, поэтому их можно использовать только в небезопасном коде.
И наконец, строку можно построить по заданному указателю на байтовый массив, используя один из следующих конструкторов.
public String(sbyte*value)
public String(sbyte*value, intstartlndex,intlength)
public String(sbyte*value, intstartlndex,intlength,Encodingenc)
Первая форма конструктора позволяет построить строку из отдельных байтов символов, доступных из массива по указателюvalue.При этом предполагается, что массив, доступный по указателюvalue,завершается признаком конца строки. Во второй форме конструктора для этой цели из массива, доступного по указателюvalue,извлекается определенное количество байтов символов(length),начиная с элемента, указываемого по индексуstartlndex.А третья форма конструктора позволяет указать количество кодируемых байтов. КлассEncodingнаходится в пространстве именSystem. Text.В этих конструкторах применяются указатели, и поэтому их можно использовать только в небезопасном коде.
При объявлении строкового литерала автоматически создается строковый объект. Поэтому для инициализации строкового объекта зачастую оказывается достаточно присвоить ему строковый литерал, как показано ниже.
string str = "новая строка";
Поле, индексатор и свойство класса String
В классеStringопределено единственное поле.
public static readonly string Empty
ПолеEmptyобозначает пустую строку, т.е. такую строку, которая не содержит символы. Этим оно отличается от пустой ссылки типаString,которая просто делается на несуществующий объект.
Помимо этого, в классеStringопределен единственный индексатор, доступный только для чтения.
public char this[intindex] {get; }
Этот индексатор позволяет получить символ по указанному индексу. Индексация строк, как и массивов, начинается с нуля. Объекты типаStringотличаются постоянством и не изменяются, поэтому вполне логично, что в классеStringподдерживается индексатор, доступный только для чтения.
И наконец, в классеStringопределено единственное свойство, доступное только для чтения.
public int Length { get; }
СвойствоLengthвозвращает количество символов в строке.
Операторы класса String
В классеStringперегружаются два следующих оператора: == и ! =. Оператор == служит для* проверки двух символьных строк на равенство. Когда оператор == применяется к ссылкам на объекты, он обычно проверяет, делаются ли обе ссылки на один и тот же объект. А когда оператор == применяется к ссылкам на объекты типаString,то на предмет равенства сравнивается содержимое самих строк. Это же относится и к оператору!=. Когда он применяется к ссылкам на объекты типаString,то на предмет неравенства сравнивается содержимое самих строк. В то же время другие операторы отношения, в том числе < и >=, сравнивают ссылки на объекты типаStringтаким же образом, как и на объекты других типов. А для того чтобы проверить, является ли одна строка больше другой, следует вызвать методCompare(), определенный в классеString.
Как станет ясно дальше, во многих видах сравнения символьных строк используются сведения о культурной среде. Но это не относится к операторам = = и ! =. Ведь они просто сравнивают порядковые значения символов в строках. (Иными словами, они сравнивают двоичные значения символов, не видоизмененные нормами культурной среды, т.е. региональными стандартами.) Следовательно, эти операторы выполняют сравнение строк без учета регистра и настроек культурной среды.
Сравнение строк
Вероятно, из всех операций обработки символьных строк чаще всего выполняется сравнение одной строки с другой. Прежде чем рассматривать какие-либо методы сравнения строк, следует подчеркнуть следующее: сравнение строк может быть выполнено в среде .NET Framework двумя основными способами. Во-первых, сравнение может отражать обычаи и нормы отдельной культурной среды, которые зачастую представляют собой настройки культурной среды, вступающие в силу при выполнении программы. Это стандартное поведение некоторых, хотя и не всех методов сравнения. И во-вторых, сравнение может быть выполнено независимо от настроек культурной среды только по порядковым значениям символов, составляющих строку. Вообще говоря, при сравнении строк без учета культурной среды используется лексикографический порядок (и лингвистические особенности), чтобы определить, является ли одна строка больше, меньше или равной другой строке. При порядковом сравнении строки просто упорядочиваются на основании невидоизмененного значения каждого символа.
ПРИМЕЧАНИЕ
В силу отличий способов сравнения строк с учетом культурной среды и порядкового сравнения, а также последствий каждого такого сравнения настоятельно рекомендуется руководствоваться лучшими методиками, предлагаемыми в настоящее время корпорацией Microsoft. Ведь выбор неверного способа сравнения строк может привести к неправильной работе программы, когда она эксплуатируется в среде, отличающей от той, в которой она разработана.
Выбор способа сравнения символьных строк представляет собой весьма ответственное решение. Как правило и без всякий исключений, следует выбирать сравнение строк с учетом культурной среды, если это делается для целей отображения результата пользователю (например, для вывода на экран ряда строк, отсортированных в лексикографическом порядке). Но если строки содержат фиксированную информацию, не предназначенную для видоизменения с учетом отличий в культурных средах, например, имя файла, ключевое слово, адрес веб-сайта или значение, связанное с обеспечением безопасности, то следует выбрать порядковое сравнение строк. Разумеется, особенности конкретного разрабатываемого приложения будут диктовать выбор подходящего способа сравнения символьных строк.
В классеStringпредоставляются самые разные методы сравнения строк, перечисленные в табл. 22.1. Наиболее универсальным среди них является методCompare ().Он позволяет сравнивать две строки полностью или частично, с учетом или без учета регистра, способа сравнения, определяемого параметром типаStringComparison,а также сведений о культурной среде, предоставляемых с помощью параметра типаCulturelnfo.Те перегружаемые варианты методаCompare(), которые не содержат параметр типаStringComparison,выполняют сравнение символьных строк с учетом регистра и культурной среды. А в тех перегружаемых его вариантах, которые не содержат параметр типаCulturelnfo,сведения о культурной среде определяются текущей средой выполнения. В примерах программ, приведенных в этой главе, параметр типаCulturelnfoне используется, а большее внимание уделяется использованию параметра типаStringComparison.
Таблица 22.1. Методы сравнения символьных строк
Метод
Назначение
public static int
Сравнивает строку strA со строкой strB. Возвращает поло
Compare(stringstrA,
жительное значение, если строка strA больше строки strB;
stringstrB)
отрицательное значение, если строка strA меньше строки strB; и нуль, если строки strA и strB равны. Сравнение выполняется с учетом регистра и культурной среды
public static int
Сравнивает строку strA со строкой strB. Возвращает поло
Compare(stringstrA,
жительное значение, если строка strA больше строки strB;
stringstrB,bool
отрицательное значение, если строка strA меньше строки
ignoreCase)
strB; и нуль, если строки strA и strB равны. Если параметр ignoreCase принимает логическое значение true, то при сравнении не учитываются различия между прописным и строчным вариантами букв. В противном случае эти различия учитываются. Сравнение выполняется с учетом культурной среды
public static int
Сравнивает строку strA со строкой strB. Возвращает положи
Compare(string
тельное значение, если строка strA больше строки strB; отрица
strA,stringstrB,
тельное значение, если строка strA меньше строки strB-, и нуль,
StringComparison
если строки strA и strB равны. Параметр comparisonType
comparisonType)
определяет конкретный способ сравнения строк
public static int.
Сравнивает строку strA со строкой strB, используя информа
Compare(string
цию о культурной среде, определяемую параметром culture.
strA,stringstrB,
Возвращает положительное значение, если строка strA боль
boolignoreCase,
ше строки strB; отрицательное значение, если строка strA
Culturelnfoculture)
меньше строки strB; и нуль, если строки strA и strB равны. Если параметр ignoreCase принимает логическое значение true, то при сравнении не учитываются различия между прописным и строчным вариантами букв. В противном случае эти различия учитываются. Класс Culturelnfo определен в пространстве имен System.Globalization
Метод
Назначение
public static int
Сравнивает части строк strA и strB. Сравнение начинается
Compare(stringstrA,
со строковых элементов strA[ indexA] и strB[indexB]
intindexA,string
и включает количество символов, определяемых параметром
strB,intindexB,
length. Метод возвращает положительное значение, если
intlength)
часть строки strA больше части строки strB] отрицательное значение, если часть строки strA меньше части строки strB; и нуль, если сравниваемые части строк strA и strB равны. Сравнение выполняется с учетом регистра и культурной среды
public static int
Сравнивает части строк strA и strB. Сравнение начинается
Compare(stringstrA,
СО строковых элементов str А[ indexA] и strB[indexB]
intindexA,string
и включает количество символов, определяемых параметром
strB,intindexB,
length. Метод возвращает положительное значение, если
intlength,bool
часть строки strA больше части строки strB; отрицатель
ignoreCase)
ное значение, если часть строки strA меньше части строки strB; и нуль, если сравниваемые части строк strA и strB равны. Если параметр ignoreCase принимает логическое значение true, то при сравнении не учитываются различия между прописным и строчным вариантами букв. В противном случае эти различия учитываются. Сравнение выполняется с учетом культурной среды
public static int
Сравнивает части строк strA и strB. Сравнение начинается
Compare(string
со строковых элементов strA[ indexA] и strB[indexB]
strA,intindexA,
и включает количество символов, определяемых параметром
stringstrB,int
length. Метод возвращает положительное значение, если
indexB,intlength,
часть строки strA больше части строки strB; отрицатель
StringComparison
ное значение, если часть строки strA меньше части строки
comparisonType)
strB; и нуль, если сравниваемые части строк strA и strB равны. Параметр comparisonType определяет конкретный способ сравнения строк
public static int
Сравнивает части строк strA и strB, используя инфор
Compare(string
мацию о культурной среде, определяемую параметром
strA,intindexA,
culture. Сравнение начинается со строковых элементов
stringstrB,int
strA[indexA] и strB[indexB] и включает количество
indexB,intlength,
символов, определяемых параметром length. Метод воз
boolignoreCase,
вращает положительное значение, если часть строки strA
Culturelnfoculture)
больше части строки strB; отрицательное значение, если часть строки strA меньше части строки strB; и нуль, если сравниваемые части строк strA и strB равны. Если параметр ignoreCase принимает логическое значение true, то при сравнении не учитываются различия между прописным и строчным вариантами букв. В противном случае эти различия учитываются. Класс Culturelnfo определен в пространстве имен System.Globalization
_Продолжение табл. 22.1
Метод
Назначение
public static int
Сравнивает строку strA со строкой strB, используя ин
Compare(string
формацию о культурной среде, обозначаемую параметром
strAfstringstrB,
culture, а также варианты сравнения, передаваемые в ка
Culturelnfoculture,
честве параметра options. Возвращает положительное зна
CompareOptions
чение, если строка strA больше строки strB; отрицательное
options)
значение, если строка strA меньше строки strB; и нуль, если строки strA и strB равны. Классы Culturelnfo и CompareOptions определены в пространстве имен
System.Globalization
public static int
Сравнивает части строк strA и strB, используя информацию
Compare(string
о культурной среде, обозначаемую параметром culture,
strA,intindexA,
а также варианты сравнения, передаваемые в качестве
stringstrB,int
параметра options. Сравнение начинается со строковых
indexB,intlength,
элементов strA[ indexA] и strB[indexB] и включает
Culturelnfoculture,
количество символов, определяемых параметром length.
CompareOptions
Метод возвращает положительное значение, если часть стро
options)
ки strA больше части строки strB; отрицательное значение, если часть строки strA меньше части строки strB; и нуль, если сравниваемые части строк strA и strB равны. Классы
Culturelnfo и CompareOptions определены в пространстве имен System.Globalization
public static int
Сравнивает строку strAco строкой strB независимо от куль
CompareOrdinal(string
турной среды, языка и региональных стандартов. Возвращает
strA,stringstrB)
положительное значение, если строка strA больше строки strB; отрицательное значение, если строка strA меньше строки strB; и нуль, если строки strA и strB равны
public static int
Сравнивает части строк strA и strB независимо от культурной
CompareOrdinal(string
среды, языка и региональных стандартов. Сравнение начинает
strA,intindexA,
ся со строковых элементов strА[ indexA] и strB[ indexB]
stringstrB,int
и включает количество символов, определяемых параметром
indexB,intcount)
count. Метод возвращает положительное значение, если часть строки strA больше части строки strB; отрицательное значение, если часть строки strA меньше части строки strB; и нуль, если сравниваемые части строк strA и strB равны
public int
Сравнивает вызывающую строку со строковым представле
CompareTo(object
нием объекта value. Возвращает положительное значение,
value)
если вызывающая строка больше строки value; отрицательное значение, если вызывающая строка меньше строки value; и нуль, если сравниваемые строки равны
public int
Сравнивает вызывающую строку со строкой strB. Возвра
CompareTo(string
щает положительное значение, если вызывающая строка
strB)
больше строки strB; отрицательное значение, если вызывающая строка меньше строки strB; и нуль, если сравниваемые строки равны
Метод
Назначение
public override bool
Возвращает логическое значение true, если вызывающая
Equals(object
obj)
строка содержит ту же последовательность символов, что и строковое представление объекта obj. Выполняется порядковое сравнение с учетом регистра, но без учета культурной среды
public bool
Возвращает логическое значение true, если вызывающая
Equals(string
value)
строка содержит ту же последовательность символов, что и строка value. Выполняется порядковое сравнение с учетом регистра, но без учета культурной среды
public bool
Возвращает логическое значение true, если вызывающая
Equals(string
value,
строка содержит ту же последовательность символов, что и
StringComparison
строка value. Параметр comparison Туре определяет кон
comparisonType)
кретный способ сравнения строк
public static
bool
Возвращает логическое значение true, если строка а содер
Equals(string
a,
жит ту же последовательность символов, что и строка Ь. Вы
stringb)
полняется порядковое сравнение с учетом регистра, но без учета культурной среды
public static
bpol
Возвращает логическое значение true, если строка а со
Equals(string
a,
держит ту же последовательность символов, что и строка Ь.
stringb,
Параметр comparisonType определяет конкретный способ
StringComparison
сравнения строк
comparison Type)
ТипStringComparisonпредставляет собой перечисление, в котором определяются значения, приведенные в табл. 22.2. Используя эти значения, можно организовать сравнение строк, удовлетворяющее потребностям конкретного приложения. Следовательно, добавление параметра типаStringComparisonрасширяет возможности методаCompare() и других методов сравнения, например,Equals(). Это дает также возможность однозначно указывать способ предполагаемого сравнения строк. В силу имеющих отличий между сравнением строк с учетом культурной среды и порядковым сравнением очень важно быть предельно точным в этом отношении. Именно по этой причине в примерах программ, приведенных в данной книге, параметр типаStringComparisonявно указывается в вызовах тех методов, в которых он поддерживается.
Таблица 22.2. Значения, определяемые в перечислении StringComparison
Значение
Описание
CurrentCulture
Сравнение строк производится с использованием текущих настроек параметров культурной среды
CurrentCultureIgnoreCase
Сравнение строк производится с использованием текущих настроек параметров культурной среды, но без учета регистра
InvariantCulture
Сравнение строк производится с использованием неизменяемых, т.е. универсальных данных о культурной среде
Значение
Описание
InvariantCulturelngoreCase
Сравнение строк производится с использованием не
-
изменяемых, т.е. универсальных данных о культурной среде и без учета регистра
Ordinal
Сравнение строк производится с использованием порядковых значений символов в строке. При этом лексикографический порядок может нарушиться, а условные обозначения, принятые в отдельной культурной среде, игнорируются
OrdinalIgnoreCase
Сравнение строк производится с использованием порядковых значений символов в строке, но без учета регистра. При этом лексикографический порядок может нарушиться, а условные обозначения, принятые в отдельной культурной среде, игнорируются
В любом случае методCompare() возвращает отрицательное значение, если первая сравниваемая строка оказывается меньше второй; положительное значение, если первая сравниваемая строка больше второй; и наконец, нуль, если обе сравниваемые строки равны. Несмотря на то что методCompare() возвращает нуль, если сравниваемые строки равны, для определения равенства символьных строк, как правило, лучше пользоваться методомEquals() или же оператором = =. Дело в том, что методCompare() определяет равенство сравниваемых строк на основании порядка их сортировки. Так, если выполняется сравнение строк с учетом культурной среды, то обе строки могут оказаться одинаковыми по порядку их сортировки, но не равными по существу. По умолчанию равенство строк определяется в методеEquals(), исходя из порядковых значений символов и без учета культурной среды. Следовательно, по умолчанию обе строки сравниваются в этом методе на абсолютное, посимвольное равенство подобно тому, как это делается в операторе = =.
Несмотря на большую универсальность метода Compare (), для простого порядкового сравнения символьных строк проще пользоваться методом CompareOrdinal (). И наконец, следует иметь в виду, что метод CompareTo () выполняет сравнение строк только с учетом культурной среды. На момент написания этой книги отсутствовали перегружаемые варианты этого метода, позволявшие указывать другой способ сравнения символьных строк.
В приведенной ниже программе демонстрируется применение методовCompare (), Equals(), CompareOrdinal(), а также операторов = = и ! = для сравнения символьных строк. Обратите внимание на то, что два первых примера сравнения наглядно демонстрируют отличия между сравнением строк с учетом культурной среды и порядковым сравнением в англоязычной среде.
// Продемонстрировать разные способы сравнения символьных строк.
using System;
class CompareDemo { 1 static void Main() { string strl = "alpha"; string str2 = "Alpha";
string str3 = "Beta"; string str4 = "alpha"; string str5 = "alpha, beta"; int result;
// Сначала продемонстрировать отличия между сравнением строк
// с учетом культурной среды и порядковым сравнением.
result = String.Compare(strl, str2, StringComparison.CurrentCulture)
Console.Write("Сравнение строк с учетом культурной среды: ");
if(result < 0)
Console.WriteLine(strl + " меньше " + str2); else if(result > 0)
Console.WriteLine(strl + " больше " + str2); else
Console.WriteLine(strl + " равно " + str2);
result = String.Compare(strl, 'str2, StringComparison.Ordinal); Console.Write("Порядковое сравнение строк: "); if(result < 0)
Console.WriteLine(strl + " меньше " + str2); else if(result > 0)
Console.WriteLine(strl + " больше " + str2); else
Console.WriteLine(strl + " равно " + str4); i
// Использовать метод CompareOrdinal(). result = String.CompareOrdinal(strl, str2);
Console.Write("Сравнение строк методом CompareOrdinal():\n") ; if(result < 0)
Console.WriteLine(strl + " меньше " + str2) ; else if(result > 0)
Console.WriteLine(strl + " больше " + str2); else
Console.WriteLine(strl + " равно " + str4);
Console.WriteLine();
// Определить равенство строк с помощью оператора = = .
// Это порядковое сравнение символьных строк, if(strl == str4) Console.WriteLine(strl + " == " + str4);
// Определить неравенство строк с помощью оператора !=. if(strl != str3) Console.WriteLine(strl + " != " + str3); if(strl != str2) Console.WriteLine(strl + " != " + str2);
Console.WriteLine();
// Выполнить порядковое сравнение строк без учета регистра,
// используя метод Equals().
if(String.Equals(strl, str2, StringComparison.OrdinallgnoreCase)) Console.WriteLine("Сравнение строк методом Equals() с " + "параметром OrdinallgnoreCase:\n" + strl + " равно " + str2);
Console.WriteLine ();
// Сравнить части строк, if (String.Compare(str2, 0, str5, 0, 3,
StringComparison.CurrentCulture) >0) {
Console.WriteLine("Сравнение строк с учетом текущей культурной среды:" + "\пЗ первых символа строки " + str2 +
" больше, чем 3 первых символа строки " + str5);
}
}
}
Выполнение этой программы приводит к следующему результату.
Сравнение строк с учетом культурной среды: alpha меньше Alpha Порядковое сравнение строк: alpha больше Alpha Сравнение строк методом CompareOrdinal(): alpha больше Alpha
alpha == alpha alpha != Beta alpha != Alpha
Сравнение строк методом Equals() с параметром OrdinallgnoreCase: alpha равно Alpha
Сравнение строк с учетом текущей культурной среды:
3 первых символа строки Alpha больше, чем 3 первых символа строки alpha, beta
Сцепление строк ,
Строки можно сцеплять, т.е. объединять вместе, двумя способами. Во-первых, с помощью оператора +, как было показано в главе 7. И во-вторых, с помощью одного из методов сцепления, определенных в классеString.Конечно, для этой цели проще всего воспользоваться оператором +, тем не менее методы сцепления служат неплохой альтернативой такому подходу.
Метод, выполняющий сцепление строк, называетсяConcat (). Ниже приведена одна из самых распространенных его форм.
public static string Concat(stringstrO,stringstrl)
Этот метод возвращает строку, состоящую из строкиstrl, присоединяемой путем сцепления в конце строкиstrO.Ниже приведена еще одна форма методаConcat (),в которой сцепляются три строки.
public static string Concat(stringstrO,stringstrl,stringstr2)
В данной форме методConcat() возвращает строку, состоящую из последовательно сцепленных строкstrOf strl и str2.
Имеется также форма методаConcat (), в которой сцепляются четыре строки.
public static string Concat(stringstrO,stringstrl, stringstr2,stringstr3)
В этой форме методConcat() возвращает строку, состоящую из четырех последовательно сцепленных строк.
А в приведенной ниже еще одной форме методаСо neat() сцепляется произвольное количество строк:
public static string Concat(params string[]values)
гдеvaluesобозначает переменное количество аргументов, сцепляемых для получения возвращаемого результата. Если в этой форме методаConcat() допускается сцепление произвольного количества строк, то зачем нужны все остальные его формы? Они существуют ради повышения эффективности. Ведь передача методу от одного до четырех аргументов оказывается намного эффективнее, чем использование для этой цели переменного списка аргументов.
В приведенном ниже примере программы демонстрируется применение методаConcat() в форме с переменным списком аргументов.
// Продемонстрировать применение метода Concat().
using System;
class ConcatDemo { static void Main() {
string result = String.Concat("Это ", "тест ", "метода ",
"сцепления ", "строк ",
"из класса ", "String." );
Console.WriteLine("Результат: " + result);
}
}
Эта программа дает следующий результат.
Результат: Это тест метода сцепления строк из класса String.
Кроме того, существуют варианты методаConcat (), в которых он принимает в качестве параметров ссылки на объекты, а не на строки. В этих вариантах методConcat() получает строковые представления вызывающих объектов, а возвращает объединенную строку, сцепленную из этих представлений. (Строковые представления объектов получаются с помощью методаToStringO,вызываемого для этих объектов.) Ниже приведены все подобные варианты и формы методаConcat ().
public static string Concat(object argO)
public static string Concat(object argO, object argl)
public static string Concat(object argO, object argl, object arg2)
public static string Concat(object argO, object argl, object arg2,objectarg3)
public static string Concat(params object[]args)
В первой форме методConcat() возвращает строку, эквивалентную объектуargO, ав остальных формах — строку, получаемую в результате сцепления всех аргументов данного метода. Объектные формы методаConcat (), т.е. относящиеся к типуobj ect,очень удобны, поскольку они исключают получение вручную строковых представлений объектов перед их сцеплением. В приведенном ниже примере программы наглядно демонстрируется польза от подобных форм методаConcat ().
.// Продемонстрировать применение объектной формы метода Concat()." using System;
public static int Count = 0; public MyClassO { Count++; }
}
class ConcatDemo { static void Main() {
string result = String.Concat("значение равно " + 19);
Console.WriteLine("Результат: " + result);
result = String.Concat("привет ", 88, " ", 20.0,
" ", false, " ", 23.45M);
Console.WriteLine("Результат: " + result);
MyClass me = new MyClassO;
result = String.Concat(me, " текущий счет равен ",
MyClass.Count);
Console.WriteLine("Результат: " + result);
}
}
Вот к какому результату приводит выполнение этой программы.
Результат: значение равно 19 Результат: привет 88 20 False 23.45 Результат: MyClass текущий счет равен 1
В данном примере методConcat() сцепляет строковые представления различных типов данных. Для каждого аргумента этого метода вызывается соответствующий методToString (), с помощью которого получается строковое представление аргумента. Следовательно, в следующем вызове методаConcat():
string result = String.Concat("значение равно " + 19);
методInt32.ToString()вызывается для получения строкового представления целого значения 19, а затем методConcat() сцепляет строки и возвращает результат.
Обратите также внимание на применение объекта определяемого пользователем классаMyClassв следующем вызове методаConcat ().
result = String.Concat(me, " текущий счет равен ",
MyClass.Count);
В данном случае возвращается строковое представление объекта типаMyClass,сцепленное с указываемой строкой. По умолчанию это просто имя класса. Но если переопределить методToString (), то вместо строки с именем классаMyClassможет быть возвращена другая строка. В качестве упражнения попробуйте ввести в приведенный выше пример программы следующий фрагмент кода.
public override string ToString() {
return "Объект типа MyClass";
}
В этом случае последняя строка результата выполнения программы будет выглядеть так, как показано ниже.
Результат: Объект типа MyClass текущий счет равен 1
В версию 4.0 среды .NET Framework добавлены еще две формы методаConcat (),приведенные ниже.
public static string Concat<T>(IEnumerable<T>values)public static string Concat(IEnumerable<string>values)
В первой форме этого метода возвращается символьная строка, состоящая из сцепленных строковых представлений ряда значений, имеющихся в объекте, который обозначается параметромvaluesи может быть объектом любого типа, реализующего интерфейсIEnumerable<T>.А во второй форме данного метода сцепляются строки, обозначаемые параметромvalues.(Следует, однако, иметь в виду, что если приходится выполнять большой объем операций сцепления символьных строк, то для этой цели лучше воспользоваться средствами классаStringBuilder.)
Поиск в строке
В классеStringпредоставляется немало методов для поиска в строке. С их помощью можно, например, искать в строке отдельный символ, строку, первое или последнее вхождение того и другого в строке. Следует, однако, иметь в виду, что поиск может осуществляться либо с учетом культурной среды либо порядковым способом.
Для обнаружения первого вхождения символа или подстроки в исходной строке служит методIndexOf (). Для него определено несколько перегружаемых форм. Ниже приведена одна из форм для поиска первого вхождения символа в исходной строке.
public int IndexOf(charvalue)
В этой форме методаIndexOf() возвращается первое вхождение символаvalueв вызывающей строке. Если символvalueв ней не найден,joвозвращается значение -1. При таком поиске символа настройки культурной среды игнорируются. Следовательно, в данном случае осуществляется порядковый поиск первого вхождения символа. .
Ниже приведены еще две формы методаIndexOf (), позволяющие искать первое вхождение одной строки в другой.
public int IndexOf(Stringvalue)
public int IndexOf(Stringvalue,StringComparisoncomparisonType)
В первой форме рассматриваемого здесь метода поиск первого вхождения строки, обозначаемой параметромvalue,осуществляется с учетом культурной среды. А во второй форме предоставляется возможность указать значение типаStringComparison,обозначающее способ поиска. В если искомая строка не найдена, то в обеих формах данного метода возвращается значение -1.
Для обнаружения последнего вхождения символа или строки в исходной строке служит методLast IndexOf(). И для этого метода определено несколько перегружаемых форм. Ниже приведена одна из форм для поиска последнего вхождения символа в вызывающей строке.
public int LastlndexOf(charvalue)
В этой форме методаLastlndexOf() осуществляется порядковый поиск, а в итоге возвращается последнее вхождение символаvalueв вызывающей строке или же значение -1, если искомый символ не найден.
Ниже приведены еще две формы методаLastlndexOf (), позволяющие искать последнее вхождение одной строки в другой.
public int LastlndexOf(stringvalue)
public int LastlndexOf(stringvalue,StringComparisoncomparisonType)
В первой форме рассматриваемого здесь метода поиск последнего вхождения строки, обозначаемой параметромvalue, осуществляется с учетом культурной среды. А во второй форме предоставляется возможность указать значение типаStringComparison,обозначающее способ поиска. Если же искомая строка не найдена, то в обеих формах данного метода возвращается значение -1.
В классеStringпредоставляются еще два интересных метода поиска в строке:IndexOf Any () иLastlndexOf Any (). Оба метода обнаруживают первый символ, совпадающий с любым набором символов. Ниже приведены простейшие формы этих методов.
public int IndexOfAny(char[]anyOf)public int LastlndexOfAny(char[]anyOf)
МетодIndexOf Any() возвращает индекс первого вхождения любого символа из массиваanyOf,обнаруженного в вызывающей строке, а методLastlndexOfAny () —индекс последнего вхождения любого символа из массиваanyOf,обнаруженного в вызывающей строке. Если совпадение символов не обнаружено, то в обоих случаях возвращается значение -1. Кроме того, в обоих рассматриваемых здесь методах осуществляется порядковый поиск.
При обработке символьных строк нередко оказывается полезно знать, начинается ли строка заданной подстрокой или же оканчивается ею. Для этой цели служат методыStartsWith () иEndsWith (). Ниже приведены их простейшие формы.
public bool StartsWith(stringvalue)public bool EndsWith(stringvalue)
МетодStartsWith() возвращает логическое значениеtrue,если вызывающая строка начинается с подстроки, переданной ему в качестве аргументаvalue. А методEndsWith() возвращает логическое значениеtrue,если вызывающая строка оканчивается подстрокой, переданной ему в качестве аргументаvalue.В противном случае оба метода возвращают логическое значениеfalse.
В обоих рассматриваемых здесь методах поиск осуществляется с учетом культурной среды. Для того чтобы указать конкретный способ поиска подстроки, можно воспользоваться приведенными ниже вариантами этих методов с дополнительным параметром типаStringComparison.
public bool StartsWith(stringvalue,StringComparisoncomparisonType)public bool EndsWith(stringvalue,StringComparisoncomparisonType)
Оба варианта рассматриваемых здесь методов поиска действуют таким же образом, как и предыдущие их варианты. Но в то же время они позволяют явно указать конкретный способ поиска.
В приведенном ниже примере программы демонстрируется применение нескольких методов поиска в строке.
// Продемонстрировать поиск в строке.
using System;
class StringSearchDemo { static void Main() {
string str = "C# обладает эффективными средствами обработки строк."; int idx;
Console.WriteLine("Строка str: " + str); idx = str.IndexOf('o');
Console.WriteLine("Индекс первого вхождения символа 'o': " + idx); idx = str.LastlndexOf('о');
Console.WriteLine("Индекс последнего вхождения символа 'o': " + idx); idx = str.IndexOf("ми", StringComparison.Ordinal);
Console.WriteLine("Индекс первого вхождения подстроки \"ми\": " + idx); idx = str.LastlndexOf("ми", StringComparison.Ordinal);
Console.WriteLine("Индекс последнего вхождения подстроки \"ми\": " + idx);
char[] chrs = { 1 a', '6', 1 в' };
idx = str.IndexOfAny(chrs);
Console.WriteLine("Индекс первого вхождения символов " +
" 'а1, 'б' или 'в': " + idx);
if(str.StartsWith("C# обладает", StringComparison.Ordinal))
Console.WriteLine("Строка str начинается с подстроки \"C# обладает\"");
if(str.EndsWith("строк.", StringComparison.Ordinal))
Console.WriteLine("Строка str оканчивается подстрокой \"строк.\"") ;
}
}
Ниже приведен результат выполнения этой программы.
Строка str: C# обладает эффективными средствами обработки строк.
Индекс первого вхождения символа ' о' : 3 Индекс последнего вхождения символа 'о': 49 Индекс первого вхождения подстроки "ми": 22 Индекс последнего вхождения подстроки "ми": 33 Индекс первого вхождения символов 1 а', 'б' или 'в': 4
Строка str начинается с подстроки "C# обладает"
Строка str оканчивается подстрокой "строк."
Во многих случаях полезным для поиска в строке оказывается метод Contains(). Его общая форма выглядит следующим образом.
public bool Contains(stringvalue)
Метод Contains () возвращает логическое значение true, если вызывающая строка содержит подстроку, обозначаемую параметромvalue,в противном случае — логическое значение false. Поиск указываемой подстроки осуществляется порядковым способом. Этот метод особенно полезен, если требуется только выяснить, находится ли конкретная подстрока в другой строке. В приведенном ниже примере программы демонстрируется применение метода Contains ().
// Продемонстрировать применение метода Contains().
class ContainsDemo { static void Main() {
string str = "C# сочетает эффективность с производительностью.";
if(str.Contains("эффективность"))
Console.WriteLine("Обнаружена подстрока \"эффективность\".");
if(str.Contains("эффе"))
Console.WriteLine("Обнаружена подстрока \"эффе\".");
if(!str.Contains("эффективный"))
Console.WriteLine("Подстрока \"эффективный\" не обнаружена.");
}
}
Выполнение этой программы приводит к следующему результату.
Обнаружена подстрока "эффективность".
Обнаружена подстрока "эффе".
Подстрока "эффективный" не обнаружена.
Как следует из результата выполнения приведенной выше программы, методContains() осуществляет поиск на совпадение произвольной последовательности символов, а не только целых слов. Поэтому в вызывающей строке обнаруживается и подстрока"эффективность",и подстрока"эффе".Но поскольку в вызывающей строке отсутствует подстрока"эффективный",то она и не обнаруживается.
У некоторых методов поиска в строке имеются дополнительные формы, позволяющие начинать поиск по указанному индексу или указывать пределы для поиска в строке. В табл. 22.3 сведены все варианты методов поиска в строке, которые поддерживаются в классеString.
Таблица 22.3. Методы поиска в строке, поддерживаемые в классе String
Метод
Назначение
public bool Contains(stringvalue)
public bool EndsWith(stringvalue)
public bool EndsWith(stringvalue,StringComparisoncomparisonType)
public bool EndsWith(stringvalue,boolignoreCase,Culturelnfoculture)
Возвращает логическое значение true, если вызывающая строка содержит подстроку value. Если же подстрока value не обнаружена, возвращается логическое значение false
Возвращает логическое значение* true, если вызывающая строка оканчивается подстрокой value. В противном случае возвращает логическое значение false Возвращает логическое значение true, если вызывающая строка оканчивается подстрокой value. В противном случае возвращает логическое значение false. Параметр comparisonType определяет конкретный способ поиска Возвращает логическое значение true, если вызывающая строка оканчивается подстрокой value, иначе возвращает
Метод
Назначение
public int
IndexOf(charvalue)
логическое значение false. Если параметр ignoreCase принимает логическое значение true, то при сравнении не учитываются различия между прописным и строчным вариантами букв. В противном случае эти различия учитываются. Поиск осуществляется с использованием информации о культурной среде, обозначаемой параметром culture Возвращает индекс первого вхождения
public int
IndexOf(stringvalue)
символа value в вызывающей строке. Если искомый символ не обнаружен, то возвращается значение -1 Возвращает индекс первого вхождения под
public int
IndexOf(charvalue,int
строки value в вызывающей строке. Если искомая подстрока не обнаружена, то возвращается значение -1 Возвращает индекс первого вхождения
startlndex)
символа value в вызывающей строке. По
public int
IndexOf(stringvalue,
иск начинается с элемента, указываемого по индексу startlndex. Метод возвращает значение -1, если искомый символ не обнаружен
Возвращает индекс первого вхождения
intstartlndex)
подстроки value в вызывающей строке.
public int
IndexOf(charvalue,int
Поиск начинается с элемента, указываемого по индексу startlndex. Метод возвращает значение -1, если искомая подстрока не обнаружена
Возвращает индекс первого вхождения
startlndex,
intcount)
символа value в вызывающей строке. По
public in.t
IndexOf(stringvalue,
иск начинается с элемента, указываемого по индексу startlndex, и охватывает число элементов, определяемых параметром count. Метод возвращает значение -1, если искомый символ не обнаружен Возвращает индекс первого вхождения под
intstartlndex,intcount)
строки value в вызывающей строке. По
public int
IndexOf(stringvalue,
иск начинается с элемента, указываемого по индексу startlndex, и охватывает число элементов, определяемых параметром count. Метод возвращает значение -1, если искомая подстрока не обнаружена Возвращает индекс первого вхождения
StringComparisoncomparisonType)
подстроки value в вызывающей строке.
Продолжение табл. 22.3
Метод
Назначение
Параметр comparisonType определяет
конкретный способ выполнения поиска. Метод возвращает значение -1, если искомая подстрока не обнаружена
public int IndexOf(stringvalue,
Возвращает индекс первого вхождения
intstartlndex, StringComparison
подстроки value в вызывающей строке.
comparison Type)
Поиск начинается с элемента, указываемого по индексу startlndex. Параметр comparisonType определяет конкретный способ выполнения поиска. Метод возвращает значение -1, если искомая подстрока не обнаружена
public int IndexOf(stringvalue,
Возвращает индекс первого вхождения
intstartlndex,intcount,
подстроки value в вызывающей строке.
StringComparisoncomparisonType)
Поиск начинается с элемента, указываемого по индексу startlndex, и охватывает число элементов, определяемых параметром count. Параметр comparisonType определяет конкретный способ выполнения поиска. Метод возвращает значение -1, если искомая подстрока не обнаружена
public int LastlndexOf(charvalue)
Возвращает индекс последнего вхождения символа value в вызывающей строке. Если искомый символ не обнаружен, возвращается значение -1
public int IndexOfAny(char[]anyOf)
Возвращает индекс первого вхождения любого символа из массива anyOf, обнаруженного в вызывающей строке. Метод возвращает значение -1, если не обнаружено совпадение ни с одним из символов из массива anyOf. Поиск осуществляется порядковым способом
public int IndexOfAny(char[]anyOf,
Возвращает индекс первого вхождения лю
intstartlndex)
бого символа из массива anyOf, обнаруженного в вызывающей строке. Поиск начинается с элемента, указываемого по индексу startlndex Метод возвращает значение -1, если не обнаружено совпадение ни с одним из символов из массива anyOf. Поиск осуществляется порядковым способом
public int IndexOfAny(char[] anyOf,
Возвращает индекс первого вхождения
intstartlndex,intcount)
любого символа из массива anyOf, обнаруженного в вызывающей строке. Поиск начинается с элемента, указываемого по^ индексу startlndex, и охватывает число
Метод
Назначение
элементов, определяемых параметром count. Метод возвращает значение -1, если не обнаружено совпадение ни с одним из символов из массива anyOf. Поиск осуществляется порядковым способом
public int LastlndexOf(stringvalue)
Возвращает индекс последнего вхождения подстроки value в вызывающей строке. Если искомая подстрока не обнаружена, возвращается значение -1
public int LastlndexOf(charvalue,
Возвращает индекс последнего вхождения
intstartlndex)
символа value в части вызывающей строки. Поиск осуществляется в обратном порядке, начиная с элемента, указываемого по индексу startlndex, и заканчивая элементом с нулевым индексом. Метод возвращает значение -1, если искомый символ не обнаружен
public int LastlndexOf(stringvalue,
Возвращает индекс последнего вхождения
intstartlndex)
подстроки value в части вызывающей строки. Поиск осуществляется в обратном порядке, начиная с элемента, указываемого по индексу startlndex, и заканчивая элементом с нулевым индексом. Метод возвращает значение -1, если искомая подстрока не обнаружена
public int LastlndexOf(charvalue,
Возвращает индекс последнего вхождения
intstartlndex,intcount)
символа value в части вызывающей строки. Поиск осуществляется в обратном порядке, начиная с элемента, указываемого по индексу startlndex, и охватывает число элементов, определяемых параметром count. Метод возвращает значение -1, если искомый символ не обнаружен
public int LastlndexOf(stringvalue,
Возвращает индекс последнего вхождения
intstartlndex,intcount)
подстроки value в части вызывающей строки. Поиск осуществляется в обратном порядке, начиная с элемента, указываемого по индексу startlndex, и охватывает число элементов, определяемых параметром count. Метод возвращает значение -1, если искомая подстрока не обнаружена
public int LastlndexOf(stringvalue,
Возвращает индекс последнего вхождения
StringComparisoncomparisonType)
подстроки value в вызывающей строке. Параметр comparisonType определяет конкретный способ выполнения поиска. Метод возвращает значение -1, если искомая подстрока не обнаружена
_Продолжение табл. 22.3
Метод
Назначение
public int LastlndexOf(stringvalue,
Возвращает индекс последнего вхождения
intstartlndex,StringComparison
подстроки value в части вызывающей
comparisonType)
строки. Поиск осуществляется в обратном порядке, начиная с элемента, указываемого по индексу startlndex, и заканчивая элементом с нулевым индексом. Параметр comparisonType определяет конкретный способ выполнения поиска. Метод возвращает значение -1, если искомая подстрока не обнаружена
public int LastlndexOf(stringvalue,
Возвращает индекс последнего вхождения
intstartlndex,intcount,
подстроки value в части вызывающей
StringComparisoncomparisonType)
строки. Поиск осуществляется в обратном порядке, начиная с элемента, указываемого по индексу startlndex, и охватывает число элементов, определяемых параметром count. Параметр comparisonType определяет конкретный способ выполнения поиска. Метод возвращает значение -1, если искомая подстрока не обнаружена
public int LastlndexOfAny(char[]
Возвращает индекс последнего вхождения
anyOf)
любого символа из массива anyOf, обнаруженного в вызывающей строке. Метод возвращает значение -1, если не обнаружено совпадение ни с одним из символов из массива anyOf. Поиск осуществляется порядковым способом
public int LastlndexOfAny(char[]
• Возвращает индекс последнего вхождения
anyOf,intstartlndex)
любого символа из массива anyOf, обнаруженного в вызывающей строке. Поиск начинается в обратном порядке с элемента, указываемого по индексу startlndex, и заканчивая элементом с нулевым индексом. Метод возвращает значение -1, если не обнаружено совпадение ни с одним из символов из массива anyOf. Поиск осуществляется порядковым способом
public int LastlndexOfAny(char[]
Возвращает индекс последнего вхождения
anyOf,intstartlndex,intcount)
любого символа из массива anyOf, обнаруженного в вызывающей строке. Поиск осуществляется в обратном порядке, начиная с элемента, указываемого по индексу startlndex, и охватывает число элементов, определяемых параметром count, число элементов, определяемых параметром count. Метод возвращает значение -1,
Метод
Назначение
public bool StartsWith(stringvalue)
public bool StartsWith(stringvalue,StringComparisoncomparisonType)
public bool StartsWith(stringvalue,boolignoreCase,Culturelnfoculture)
если не обнаружено совпадение ни с одним из символов из массива anyOf. Поиск осуществляется порядковым способом Возвращает логическое значение true, если вызывающая строка начинается с подстроки value. В противном случае возвращается логическое значение false Возвращает логическое значение true, если вызывающая строка начинается с подстроки value. В противном случае возвращается логическое значение false. Параметр comparisonType определяет конкретный способ выполнения поиска Возвращает логическое значение true, если вызывающая строка начинается с подстроки value. В противном случае возвращается логическое значение false. Если параметр ignoreCase принимает логическое значение true, то при сравнении не учитываются различия между прописным и строчным вариантами букв. В противном случае эти различия учитываются. Поиск осуществляется с использованием информации о культурной среде, обозначаемой параметром culture
Разделение и соединение строк
К основным операциям обработки строк относятся разделение и соединение. Приразделениистрока разбивается на составные части, а при соединении строка составляется из отдельных частей. Для разделения строк в классеStringопределен методSplit(), а для соединения — методJoin ().
Существует несколько вариантов методаSplit(). Ниже приведены две формы этого метода, ставшие наиболее часто используемыми, начиная с версии C# 1.0.
public string[ ] Split(params char[ ]separator)
public string[ ] Split(params char[ ]separator,intcount)
В первой форме методаSplit() вызывающая строка разделяется на составные
части. В итоге возвращается массив, содержащий подстроки, полученные из вызы
вающей строки. Символы, ограничивающие эти подстроки, передаются в массивеseparator.Если массивseparatorпуст или ссылается на пустую строку, то в качестве разделителя подстрок используется пробел. А во второй форме .данного метода возвращается количество подстрок, определяемых параметромcount.
Существует несколько форм методаJoin(). Ниже приведены две формы, ставшие доступными, начиная с версии 2.0 среды .NET Framework.
public static string Join(stringseparator, string[]value)public static string Join(stringseparator,string[]value,
intstartlndex,intcount)
В первой форме методаJoin() возвращается строка, состоящая из сцепляемых подстрок, передаваемых в массивеvalue.Во второй форме также возвращается строка, состоящая из подстрок, передаваемых в массивеvalue,но они сцепляются в определенном количествеcount, начиная с элемента массиваvalue [startlndex]. В обеих формах каждая последующая строка отделяется от предыдущей разделительной строкой, определяемой параметромseparator.
В приведенном ниже примере программы демонстрируется применение методовSplit() иJoin().
// Разделить и соединить строки.
using System;
class SplitAndJoinDemo { static void Main() {
string str = "Один на суше, другой на море."; char[] seps = { ' ', '.', ',' };
// Разделить строку на части, string[] parts = str.Split(seps);
Console.WriteLine("Результат разделения строки: "); for(int i=0; i < parts.Length; i++)
Console.WriteLine (parts [i]);
//А теперь соединить части строки, string whole = String.Join(" | ", parts);
Console.WriteLine("Результат соединения строки: ");
Console.WriteLine(whole);
}
}
Ниже приведен результат выполнения этой программы.
Результат разделения строки:
Один
на
суше
другой
на
море
Результат соединения строки:
Один | на | суше | | другой | на | море
Обратите внимание на пустую строку между словами "суше" и "другой". Дело в том, что в исходной строке после слова "суше" следует запятая и пробел, как в подстроке "суше, другой". Но запятая и пробел указаны в качестве разделителей. Поэтому при разделении данной строки между двумя разделителями (запятой и пробелом) оказывается пустая строка.
Существует ряд других форм методаSplit (), принимающих параметр типаStringSplitOptions.Этот параметр определяет, являются ли пустые строки частью разделяемой в итоге строки. Ниже приведены все эти формы методаSplit ().
public string[] Split(params char[]separator,StringSplitOptionsoptions)public string[] Split(string[]separator,StringSplitOptionsoptions)public string[] Split(params char[]separator,intcount,
StringSplitOptionsoptions)public string[] Split(string[]separator,intcount,
StringSplitOptionsoptions)
В двух первых формах методаSplit() вызывающая строка разделяется на части и возвращается массив, содержащий подстроки, полученные из вызывающей строки. Символы, разделяющие эти подстроки, передаются в массивеseparator.Если массивseparatorпуст, то в качестве разделителя используется пробел. А в третьей и четвертой формах данного метода возвращается количество строк, ограничиваемое параметромcount.Но во всех формах параметрoptionsобозначает конкретный способ обработки пустых строк, которые образуются в том случае, если два разделителя оказываются рядом. В перечисленииStringSplitOptionsопределяются только два значения:NoneиRemoveEmptyEntries.Если параметрoptionsпринимает значениеNone,то пустые строки включаются в конечный результат разделения исходной строки, как показано в предыдущем примере программы. А если параметрoptionsпринимает значениеRemoveEmptyEntries,то пустые строки исключаются из конечного результата разделения исходной строки.
Для того чтобы стали понятнее последствия исключения пустых строк, попробуем заменить в предыдущем примере программы строку кода
string[] parts = str.Split (seps);
следующим фрагментом кода.
string[] parts = str.Split(seps, StringSplitOptions.RemoveEmptyEntries) ;
При выполнении данной программы получится следующий результат.
Результат разделения строки:
Один
на
суше
другой
на
море
Результат соединения строки:
Один | на | суше | другой | на | море
Как видите, пустая строка, появлявшаяся ранее из-за того, что после слова "суше" следовали запятая и пробел, теперь исключена.
Разделение является очень важной процедурой обработки строк, поскольку с его помощью нередко получают отдельныелексемы,составляющие исходную строку. Так, в программе ведения базы данных может возникнуть потребность разделить с помощью методаSplit() строку запроса "показать все остатки больше 100" на отдельные части, включая подстроки "показать" и "100". В процессе разделения исключаются разделители, поэтому в итоге получается пбдстрока "показать" (без начальных и конечных пробелов), а не подстрока " показать". Этот принцип демонстрируется
в приведенном ниже примере программы, где строки, содержащие такие бинарные математические операции, как 10 + 5, преобразуются в лексемы, а затем эти операции выполняются и выводится конечный результат.
// Преобразовать строки в лексемы.
using System;
class TokenizeDemo { static void Main() { string[] input = {
"100 + 19",
"100 / 3,3",
«_3 * 9..r
"100 - 87"
};
char[] seps = {' '};
for (int i=0; i < input.Length; i++) {
// разделить строку на части
string[] parts = input[i].Split(seps);
Console.Write("Команда: ");
for (int j=0; j < parts.Length; j++)
Console.Write(parts[j] + " ");
Console.Write(", результат: "); double n = Double.Parse(parts[0]); double n2 = Double.Parse(parts[2]);
switch(parts[1]) { case
Console.WriteLine(n + n2); break; case
Console.WriteLine (n - n2); break; case
Console.WriteLine(n * n2); break; case "/":
Console.WriteLine(n / n2); break;
}
}
}
}
Вот к какому результату приводит выполнение этой программы.
Команда: 100 + 19,результат: 119
Команда: 100/ 3,3 ,результат: 30,3030303030303
Команда: -3*9, результат: -27
Команда: 100 - 87,результат: 13
Начиная с версии 4.0, в среде .NET Framework стали доступными следующие дополнительные формы методаJoin ().
public static string Join(stringseparator,params object[]values)public static string Join(stringseparator,IEnumerable<string>[]values)public static string Join<T>(stringseparator,IEnumerable<T>[]values)
В первой форме рассматриваемого здесь метода возвращается строка, содержащая строковое представление объектов из массиваvalues.Во второй форме возвращается строка, содержащая результат сцепления коллекции строк, обозначаемой параметромvalues.И в третьей форме возвращается строка, содержащая результат сцепления строковых представлений объектов из коллекции, обозначаемой параметром values. Во всех трех случаях каждая предыдущая строка отделяется от последующей разделителем, определяемым параметромseparator.
Заполнение и обрезка строк
Иногда в строке требуется удалить начальные и конечные пробелы. Такая операция называетсяобрезкойи нередко требуется в командных процессорах. Например, программа ведения базы данных способна распознавать команду "print'7, но пользователь может ввести эту команду с одним или несколькими начальными и конечными пробелами. Поэтому перед распознаванием введенной команды необходимо удалить все подобные пробелы. С другой стороны, строку иногда требуется заполнить пробелами, чтобы она имела необходимую минимальную длину. Так, если подготавливается вывод результатов в определенном формате, то каждая выводимая строка должна иметь определенную длину, чтобы сохранить выравнивание строк. Для упрощения подобных операций в C# предусмотрены соответствующие методы.
Для обрезки строк используется одна из приведенных ниже форм метода Trim ().
public string Trim()
public string Trim(params char[]trimChars)
В первой форме метода Trim () из вызывающей строки удаляются начальные и конечные пробелы. А во второй форме этого метода удаляются начальные и конечные вхождения в вызывающей строке символов из массиваtrimChars.В обеих формах возвращается получающаяся в итоге строка.
Строку можно заполнить символами слева или справа. Для заполнения строки слева служат такие формы метода PadLef t ().
public string PadLeft(inttotalWidth)
public string PadLeft(inttotalWidth,char paddingChar)
В первой форме метода PadLef t () вводятся пробелы с левой стороны вызывающей строки, чтобы ее общая длина стала равной значению параметраtotalWidth.А во второй форме данного метода символы, обозначаемые параметромpaddingChar, вводятся с левой стороны вызывающей строки, чтобы ее общая длина стала равной значению параметраtotalWidth. В обеих формах возвращается получающаяся в итоге строка. Если значение параметраtotalWidthменьше длины вызывающей строки, то возвращается копия неизмененной вызывающей строки.
Для заполнения строки справа служат следующие формы метода PadRight ().
public string PadRight(inttotalWidth)
public string PadRight(inttotalWidth, char paddingChar)
В первой форме метода PadLef t () вводятся пробелы с правой стороны вызывающей строки, чтобы ее общая длина стала равной значению параметраtotalWidth.
А во второй форме данного метода символы, обозначаемые параметромpaddingChar,вводятся с правой стороны вызывающей строки, чтобы ее общая длина стала равной значению параметраtotalWidth.Bобеих формах возвращается получающаяся в итоге строка. Если значение параметраtotalWidthменьше длины вызывающей строки, то возвращается копия неизмененной вызывающей строки.
В приведенном ниже примере программы демонстрируются обрезка и заполнение строк.
// Пример обрезки и заполнения строк.
using System;
class TrimPadDemo { static void Main() { string str = "тест";
Console.WriteLine("Исходная строка: " + str);
// Заполнить строку пробелами слева, str = str.PadLeft(10);
Console.WriteLine("I" + str + "I");
// Заполнить строку пробелами справа, str = str.PadRight(20);
Console.WriteLine("I" + str + "I");
// Обрезать пробелы, str = str.Trim();
Console.WriteLine("|" + str + "I");
// Заполнить строку символами # слева, str = str.PadLeft(10, '#');
Console.WriteLine ("|" + str + "I");
// Заполнить строку символами # справа, str = str.PadRight(20, '#');
Console.WriteLine("|" + str + "I");
// Обрезать символы #. str = str.Trim('#');
Console.WriteLine("|" + str + "|");
}
}
Эта программа дает следующий результат.
Исходная строка: тест I тест|
| тест |
|тест|
I ######тест|
|######тест##########|
I тест|
Вставка, удаление и замена строк
Для вставки одной строки в другую служит приведенный ниже методInsert():public string Insert(intstartlndex,stringvalue)
гдеvalueобозначает строку, вставляемую в вызывающую строку по индексуstartlndex.Метод возвращает получившуюся в итоге строку.
Для удаления части строки служит методRemove (). Ниже приведены две его формы.
public string Remove(intstartlndex)
public string Remove(intstartlndex,intcount)
В первой форме методаRemove() удаление выполняется, начиная с места, указываемого по индексуstartlndex, и продолжается до конца строки. А во второй форме данного метода из строки удаляется количество символов, определяемое параметромcount, начиная с места, указываемого по индексуstartlndex.В обеих формах возвращается получающаяся в итоге строка.
Для замены части строки служит методReplace (). Ниже приведены две его формы.
public string Replace(charoldChar,charnewChar)public string Replace(stringoldValue,stringnewValue)
В первой форме методаReplace() все вхождения символаoldCharв вызывающей строке заменяются символомnewChar.А во второй форме данного метода все вхождения строкиoldValueв вызывающей строке заменяются строкойnewValue.В обеих формах возвращается получающаяся в итоге строка.
В приведенном ниже примере демонстрируется применение методовInsert (), Remove() иReplace().
// Пример вставки, замены и удаления строк.
using System;
class InsRepRevDemO { static void Main() {
string str = "Это тест";
Console.WriteLine("Исходная строка: " + str);
// Вставить строку.
str = str.Insert(4, "простой ");
Console.WriteLine(str) ;
// Заменить строку.
str = str.Replace("простой", "непростой ");
Console.WriteLine (str);
// Заменить символы в строке str = str.Replace('т', 'X');
Console.WriteLine(str);
// Удалить строку, str = str.Remove(4, 5);
Console.WriteLine(str);
Ниже приведен результат выполнения этой программы.
Исходная строка: Это тест Это простой тест Это непростой _тест ЭХо непросХой ХесХ ЭХо сХой ХесХ
Смена регистра
В классеStringпредоставляются два удобных метода, позволяющих сменить регистр букв в строке, —ToUpper () иToLower (). Диже приведены их простейшие формы.
public string ToLower() public string ToUpper()
МетодToLower() делает строчными все буквы в вызывающей строке, а методToUpper() делает их прописными. В обоих случаях возвращается получающаяся в итоге строка. Имеются также следующие формы этих методов, в которых можно указывать информацию о культурной среде и способы преобразования символов.
public string ToLower (Culturelnfoculture)public string ToUpper(Culturelnfoculture)
С помощью этих форм можно избежать неоднозначности в исходном коде по отношению к правилам смены регистра. Именно для таких целей эти формы и рекомендуется применять.
Кроме того, имеются следующие методыToUpper Invariant( ) иToLowerlnvariant().
public string ToUpperlnvariant () public string ToLowerlnvariant()
Эти методы аналогичны методамToUpper () иToLower (), за исключением того, что они изменяют регистр букв в вызывающей строке безотносительно к настройкам культурной среды.
Применение метода Substring ()
Для получения части строки служит методSubstringO.Ниже приведены две его формы.
public string Substring(intstartlndex)
public string Substring(intstartlndex,intlength)
В первой форме методаSubstring() подстрока извлекается, начиная с места, обозначаемого параметромstartlndex,и до конца вызывающей строки. А во второй форме данного метода извлекается подстрока, состоящая из количества символов, определяемых параметромlength,начиная с места, обозначаемого параметромstartlndex.В обеих формах возвращается получающаяся в итоге подстрока.
В приведенном ниже примере программы демонстрируется применение методаSubstring().
// Использовать метод Substring().
using System;
class SubstringDemo { static void Main() {
string str - "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
Console.WriteLine("Строка str: " + str);
Console.Write("Подстрока str.Substring(15): "); string substr = str.Substring(15);
Console.WriteLine(substr);
Console.Write("Подстрока str.Substring(0, 15): "); substr = str.Substring(0, 15);
Console.WriteLine (substr);
}
}
Эта программа дает следующий результат.
Строка str: ABCDEFGHIJKLMNOPQRSTUVWXYZ Подстрока str.Substring(15): PQRSTUVWXYZ Подстрока' str. Substring (0, 15): ABCDEFGHIJKLMNO
Методы расширения класса String
Как упоминалось ранее, в классеStringреализуется обобщенный интерфейсIEnumerable<T>.Это означает, что, начиная с версии C# 3.0, для объекта классаStringможно вызывать методы расширения, определенные в классахEnumerableиQueryable,которые находятся в пространстве именSystem. Linq.Эти методы расширения служат главным образом для поддержки LINQ, хотя некоторые из них могут использоваться в иных целях, в том числе и в определенных видах обработки строк. Подробнее о методах расширения см. в главе 19.
Форматирование
Когда данные встроенных в C# типов, напримерintилиdouble,требуется отобразить в удобочитаемой форме, приходится формировать их строковое представление. Несмотря на то что в C# для такого представления данных автоматически предоставляется формат, используемый по умолчанию, имеется также возможность указать выбранный формат вручную. Так, в части I этой книги было показано, что числовые данные можно выводить в формате выбранной денежной единицы. Для форматирования данных числовых типов в C# предусмотрен целый ряд методов, включая методыConsole.WriteLine (),String.Format() иToString(). Во всех этих методах применяется один и тот же подход к форматированию. Поэтому освоив один из них, вы сможете без особого труда применять и другие.
Общее представление о форматировании
Форматирование осуществляется с помощью двух компонентов:спецификаторов форматаипоставщиков формата.Конкретная форма строкового представления отдельного значения зависит от спецификатора формата. Следовательно, спецификатор формата определяет, в какой именно удобочитаемой форме будут представлены данные. Например, для вывода числового значения в экспоненциальном представлении (т.е. в виде^мантиссы и порядка числа) используется спецификатор форматаЕ.
Как правило, конкретный формат значения зависит от культурных и языковых особенностей локализации программного обеспечения. Например, в Соединенных Штатах Америки денежные суммы указываются в долларах, а в странах ЕС — в евро. Для учета культурных и языковых отличий в C# предусмотрены поставщики формата. В частности, поставщик формата определяет порядок интерпретации спецификатора формата. Поставщик формата создается путем реализации интерфейсаI Format Provi der,в котором определяется методGet Format (). Для всех встроенных числовых типов и многих других типов данных в среде .NET Framework предопределены соответствующие поставщики формата. Вообще говоря, данные можно отформатировать, не указывая конкретный поставщик формата, поэтому поставщики формата не рассматриваются далее в этой книге.
Для того чтобы отформатировать данные, достаточно включить спецификатор формата в метод, поддерживающий форматирование. О применении спецификаторов формата речь уже шла в главе 3, тем не менее к этому вопросу стоит вернуться вновь. Применение спецификаторов формата рассматривается далее на примере методаConsole.WriteLine (), хотя аналогичный подход применим и к другим методам, поддерживающим форматирование.
Для форматирования выводимых данных служит следующая форма методаWriteLine().
WriteLine("форматирующая строка",argO, argl,... ,argN);
В этой форме аргументы методаWriteLine() разделяются запятой, а не знаком +. А форматирующая строка состоит из двух следующих элементов: обычных печатаемых символов, отображаемых в исходном виде, а также команд форматирования.
Ниже приведена общая форма команд форматирования:
{argnum, width: fmt}
гдеargnum— это номер отображаемого аргумента, начиная с нуля;width— минимальная ширина поля, afmt—спецификатор формата. Параметрыwidthиfmtне являются обязательными. Поэтому в своей простейшей форме команда форматирования просто указывает конкретные аргументы для отображения. Например, команда{ 0 }указывает аргументагдО,команда{1} —аргументarglи т.д.
Если во время выполнения программы в форматирующей строке встречается команда форматирования, то вместо нее подставляется и затем отображается соответствующий аргумент, определяемый параметромargnum.Следовательно, от положения спецификатора формата в форматирующей строке зависит, где именно будут отображаться соответствующие данные. А номер аргумента определяет конкретный форматируемый аргумент.
Если в команде форматирования указывается параметрfmt,то данные отображаются в указываемом формате. В противном случае используется формат, выбираемый по умолчанию. Если же в команде форматирования указывается параметрwidth,то выводимые данные дополняются пробелами для достижения минимально необходимой ширины поля. При положительном значении параметраwidthвыводимые данные выравниваются по правому краю, а при отрицательном значении — по левому краю.
Оставшаяся часть данной главы посвящена вопросам форматирования и отдельным спецификаторам формата.
Спецификаторы формата числовых данных
Для числовыхданных определено несколько спецификаторов формата, сведенных в табл. 22.4. Каждый спецификатор формата может включать в себя дополнительный, но необязательный спецификатор точности. Так, если числовое значение требуется указать в формате с фиксированной точкой и двумя десятичными разрядами в дробной части, то для этой цели служит спецификаторF2.
Таблица 22.4. СпеыисЬикатооы (Ьоомата числовых данных
Спецификатор
Формат
Назначение спецификатора точности
с
Денежная единица
Задает количество десятичных разрядов
с
То же, что и С
D
Целочисленный (используется только с
Задает минимальное количество
целыми числами)
цифр. При необходимости результат дополняется начальными нулями
d
То же, что и D
Е
Экспоненциальное представление чи
Задает количество десятичных раз
сел (в обозначении используется про
рядов. По умолчанию используется
писная буква Е)
шесть рязрядов
е
Экспоненциальное представление
Задает количество десятичных раз
чисел (в обозначении используется
рядов. По умолчанию используется
строчная буква е)
шесть рязрядов
F
Представление чисел с фиксирован
Задает количество десятичных раз
ной точкой
рядов
f
То же, что и F
G
Используется более короткий из двух форматов: Е или F
См. спецификаторы Е и F
g
Используется более короткий из двух форматов: е или f
См. спецификаторы е и f
N
Представление чисел с фиксирован
Задает количество десятичных раз
ной точкой (и запятой в качестве раз
рядов
делителя групп разрядов)
-
n
То же, что и N
р
Проценты
Задает количество десятичных разрядов
р
То же, что и Р
RИЛИг
Числовое значение, которое преобразуется с помощью метода Parse () в эквивалентную внутреннюю форму. (Это так называемый “круговой” формат)
Не используется
X
Шестнадцатеричный (в обозначении
Задает минимальное количество
используются прописные буквы A-F)
цифр. При необходимости результат дополняется начальными нулями
X
Шестнадцатеричный (в обозначении
Задает минимальное количество
используются строчные буквы A-F)
цифр. При необходимости результат дополняется начальными нулями
Как пояснялось выше, конкретное действие спецификаторов формата зависит от текущих настроек параметров культурной среды. Например, спецификатор денежной единицы С автоматически отображает числовое значение в формате денежной единицы, выбранном для локализации программного обеспечения в конкретной культурной среде. Для большинства пользователей используемая по умолчанию информация о культурной среде соответствует их региональным стандартам и языковым особенностям. Поэтому один и тот же спецификатор формата может использоваться без учета культурного контекста, в котором выполняется программа.
В приведенной ниже программе демонстрируется применение нескольких спецификаторов формата числовых данных.
// Продемонстрировать применение различных // спецификаторов формата числовых данных.
using System;
class FormatDemo {
static void Main() { double v = 17688.65849; double v2 = 0.15; int x = 21;
Console.WriteLine("{0:F2}", v);
Console.WriteLine("{0:N5}", v);
Console.WriteLine ("{0:e}", v);
Console.WriteLine("{0:r}", v);
Console.WriteLine("{0:p}", v2);
Console.WriteLine("{0:X}", x);
Console.WriteLine("{0:D12}", x);
Console.WriteLine("{0:C}", 189.99);
}
}
Эта программа дает следующий результат.
17688.66
17.688.65849 1.768866е+004
17688.65849 15.00 %
15
000000000021
$189.99
Обратите внимание на действие спецификатора точности в нескольких форматах.
Представление о номерах аргументов
Следует иметь в виду, что аргумент, связанный со спецификатором формата, определяется номером аргумента, а не его позицией в списке аргументов. Это означает, что один и тот же аргумент может указываться неоднократно в одном вызове методаWriteLine().Эта также означает, что аргументы могут отображаться в той последовательности, в какой они указываются в списке аргументов. В качестве примера рассмотрим следующую программу.
using System;
class FormatDemo2 { static void Main() {
// Форматировать один и тот же аргумент тремя разными способами.
Console.WriteLine("{0:F2} {0:F3} {0:е}", 10.12345);
// Отобразить аргументы не по порядку.
Console.WriteLine("{2:d} {0:d} {l:d}", 1,2,3) ;
}
}
Ниже приведен результат выполнения этой программы.
10.12 10.123 1.012345е+001 3 12
В первом операторе вызова методаWriteLine() один и тот же аргумент10.12345форматируется тремя разными способами. Это вполне допустимо, поскольку каждый спецификатор формата в этом вызове обозначает первый и единственный аргумент. А во втором вызове методаWriteLine() три аргумента отображаются не по порядку. Не следует забывать, что каких-то особых правил, предписывающих обозначать аргументы в спецификаторах формата в определенной последовательности, не существует. Любой спецификатор формата может обозначать какой угодно аргумент.
Применение методов String. Format () и ToString () для форматирования данных
Несмотря на все удобства встраивания команд форматирования выводимых данных в вызовы методаWriteLine (), иногда все же требуется сформировать строку, содержащую отформатированные данные, но не отображать ее сразу. Это дает возможность отформатировать данные заранее, чтобы вывести их в дальнейшем на выбранное устройство. Такая возможность особенно полезна для организации работы в среде с графическим пользовательским интерфейсом, подобной Windows, где ввод-вывод на консоль применяется редко, а также для подготовки вывода на веб-страницу.
Вообще говоря, отформатированное строковое представление отдельного значения может быть получено двумя способами. Один из них состоит в применении методаString. Format (), а другой — в передаче спецификатора формата методуToStringO,относящемуся к одному из встроенных в C# числовых типов данных. Оба способа рассматриваются далее по порядку.
Применение метода String. Format () для форматирования значений
Дляполучения отформатированного значения достаточно вызвать методFormat (),определенный в классеString,в соответствующей его форме. Все формы этого метода перечислены в табл. 22.5. МетодFormat() аналогичен методуWriteLine(), за исключением того, что он возвращает отформатированную строку, а не выводит ее на консоль.
Метод
Описание
public
static string
Форматирует объект argO в соответствии с первой ко
Format
(stringformat,
мандой форматирования, которая содержится в строке
obj ect
argO)
format. Возвращает копию строки format, в которой команда форматирования заменена отформатированными данными
public
static string
Форматирует объект argO в соответствии с первой
Format
(stringformat,
командой форматирования, содержащейся в строке
object
argO,objectargl)
format, а объект argl— в соответствии со второй командой. Возвращает копию строки format, в которой команды форматирования заменены отформатированными данными
public
static string
Форматирует объекты argO, argl и агд2 по соответ
Format
(stringformat,
ствующим командам форматирования, содержащимся
obj ect
argO,objectargl,
в строке format. Возвращает копию строки format, в
obj ect
arg2)
которой команды форматирования заменены отформатированными данными
public
static string
Форматирует значения, передаваемые в массиве args,
Format
(stringformat,
в соответствии с командами форматирования, содержа
params
object[]args)
щимися в строке format. Возвращает копию строки format, в которой команды форматирования заменены отформатированными данными
public
static string
Форматирует значения, передаваемые в массиве args,
Format
(IFormatProvider
в соответствии с командами форматирования, содержа
provider,stringformat,
щимися в строке format, используя поставщик фор
params
object[]args)
мата provider. Возвращает копию строки format, в которой команды форматирования заменены отформатированными данными
Ниже приведен вариант предыдущего примера программы форматирования, измененный с целью продемонстрировать применение методаString.Format(). Этот вариант дает такой же результат, как и предыдущий.
// Использовать метод String.Format() для форматирования значений, using System;
class FormatDemo { static void Main() {
double v = 17688.65849; double v2 = 0.15; int x = 21;
string str = String.Format("{0:F2}", v);
Console.WriteLine(str) ;
str = String.Format("{0:N5}", v); Console.WriteLine(str);
str = String.Format("{0:e}", v); Console.WriteLine(str);
str = String.Format("{0:r}", v); Console.WriteLine (str);
str = String.Format("{0:p}", v2); Console.WriteLine(str);
str = String.Format("{0:X}", x) ; Console.WriteLine(str);
str = String.Format("{0:D12}", x) ; Console.WriteLine(str);
str = String.Format("{0:C}", 189.99); Console.WriteLine (str);
}
}
Аналогично методу WriteLine (), метод String. Format () позволяет встраивать в свой вызов обычный текст вместе со спецификаторами формата, причем в вызове данного метода может быть указано несколько спецификаторов формата и значений. В качестве примера рассмотрим еще одну программу, отображающую текущую сумму и произведение чисел от 1 до 10.
// Еще один пример применения метода Format().
using System;
class FormatDemo2 { static void Main() { int i;
int sum = 0; int prod = 1; string str;
/* Отобразить текущую сумму и произведение чисел от 1 до 10. */ for(i=l; i <= 10; i++) {
sum += i; prod *= i;
str = String.Format("Сумма:{0,3:D} Произведение:{1,8:D}", sum, prod);
Console.WriteLine(str);
}
Сумма: 45 Произведение: 362880
Сумма: 55 Произведение: 3628800
Обратите особое внимание в данной программе на следующий оператор.
str = String.Format("Сумма:{0,3:D} Произведение:{1,8:D}", sum, prod);
В этом операторе содержится вызов методаFormat() с двумя спецификаторами формата: одним — для суммы (в переменнойsum),а другим — для произведения (в переменнойprod).Обратите также внимание на то, что номера аргументов указываются таким же образом, как и в вызове методаWriteLine(),nчто в вызов методаFormat() включается обычный текст, как, например, строка"Сумма: ". Этот текст
передается данному методу и становится частью выводимой строки.
Применение метода ToString () для форматирования данных
Дляполучения отформатированного строкового представления отдельного значения любого числового типа, которому соответствует встроенная структура, напримерInt32илиDouble,можно воспользоваться методомToString(). Этой цели служит приведенная ниже форма методаToString().
public string ToString("форматирующая строка")
В этой форме метод ToString () возвращает строковое представление вызывающего объекта в том формате, который определяет спецификатор"форматирующая строка",передаваемый данному методу. Например, в следующей строке кода формируется строковое представление значения 188.99 в формате денежной единицы с помощью спецификатора форматаС.
string str = 189.99.ToString("С");
Обратите внимание на то, что спецификатор формата передается методуToString() непосредственно. В отличие от встроенных команд форматирования, используемых в вызовах методовWriteLine() иFormat(), где для этой цели дополнительно указываются номер аргумента и ширина поля, в вызове методаToString() достаточно указать только спецификатор формата.
Ниже приведен вариант примера предыдущей программы форматирования, измененный с целью продемонстрировать применение методаToString() для получения отформатированных строк. Этот вариант дает такой же результат, как и предыдущий.
// Использовать метод ToString() для форматирования значений.
using System;
class ToStringDemo { static void Main() {
double v = 17688.65849; double v2 = 0.15; int x = 21;
string str = v.ToString("F2") ; Console.WriteLine(str);
str = v.ToString("N5");
Console.WriteLine(str);
str = v.ToString("e");
Console.WriteLine(str);
str = v.ToString("r");
Console.WriteLine(str);
str = v2.ToString("p");
Console.WriteLine(str);
str = x.ToString("X");
Console.WriteLine(str);
str = x.ToString("D12"); Console.WriteLine(str);
str = 189.99.ToString("C"); Console.WriteLine(str);
}
}
Определение пользовательского формата числовых данных
Несмотря на всю полезность предопределенных спецификаторов формата числовых данных, в C# предоставляется также возможность определить пользовательский, т.е. свой собственный, формат, используя средство, называемоеформатом изображения. Своим происхождением терминформат изображенияобязан тому обстоятельству, что специальный формат пользователь определяет, задавая пример внешнего вида (т.е. изображение) выводимых данных. Такой подход вкратце упоминался в части I этой книги, а здесь он рассматривается более подробно.
Символы-заполнители специального формата числовых данных
I
Когда пользователь определяет специальный формат, он задает этот формат в виде примера (или изображения) того, как должны выглядеть выводимые данные. Для этой цели используются символы, перечисленные в табл. 22.6. Они служат в качестве заполнителей и рассматриваются далее по очереди.
Символ точки обозначает местоположение десятичной точки.
Символ-заполнитель # обозначает цифровую позицию, или разряд числа. Этот символ может указываться слева или справа от десятичной точки либо отдельно. Так, если справа от десятичной точки указывается несколько символов #, то они обозначают количество отображаемых десятичных цифр в дробной части числа. При необходимости форматируемое числовое значение округляется. Когда же символы # указываются слева от десятичной точки, то они обозначают количество отображаемых десятичных цифр в целой части числа. При необходимости форматируемое числовое значение дополняется начальными нулями. Если целая часть числового значения состоит из большего количества цифр, чем количество указываемых символов #, то она отображается полностью, но в любом случае целая часть числового значения не усекается. В отсутствие десятичной точки наличие символа # обусловливает округление соответствующего целого значения. А нулевое значение, которое не существенно, например конечный нуль, не отображается. Правда, это обстоятельство несколько усложняет дело, поскольку при указании такого формата, как # . # #, вообще ничего не отображается, если форматируемое числовое значение равно нулю. Для вывода нулевого значения служит рассматриваемый далее символ-заполнитель 0.
Таблица 22.6. Символы-заполнители специального формата числовых данных
Символ-заполнитель
Назначение
#
Цифра
Десятичная точка
,
Разделитель групп разрядов
%
Процент
0
Используется для дополнения начальными и конечными нулями
;
Выделяет разделы, описывающие формат для положительных, отрицательных и нулевых значений
Е0 Е+0 Е-0 еО е+0 е-0
Экспоненциальное представление чисел
Символ-заполнитель 0 обусловливает дополнение форматируемого числового значения начальными или конечными нулями, чтобы обеспечить минимально необходимое количество цифр в строковом представлении данного значения. Этот символ может указываться как слева, как и справа от десятичной точки. Например, следующая строка кода:
Console.WriteLine("{0:00##.#00}", 21.3);выводит такой результат.
0021.300
Значения, состоящие из большего количества цифр, будут полностью отображаться слева от десятичной точки, а округленные — справа.
При отображении больших числовых значений отдельные группы цифр могут отделяться друг от друга запятыми, для чего достаточно вставить запятую в шаблон, состоящий из символов #. Например, следующая строка кода:
Console.WriteLine("{0:#,###.#}", 3421.3);
выводит такой результат.
3,421.3.
Указывать запятую на каждой позиции совсем не обязательно. Если указать запятую в шаблоне один раз, то она будет автоматически вставляться в форматируемом числовом значении через каждые три цифры слева от десятичной запятой. Например, следующая строка кода:
Console.WriteLine ("{0:#,###.#}", 8763421.3);дает такой результат.
8,763,421.3.
У запятой имеется и другое назначение. Если запятая вставляется непосредственно перед десятичной точкой, то она выполняет роль масштабного коэффициента. Каждая запятая делит форматируемое числовое значение на 1000. Например, следующая строка кода:
Console.WriteLine("Значение в тысячах: {0:#,###,.#}", 8763421.3);дает такой результат.
Значение в тысячах: 8,763.4
Как показывает приведенный выше результат, числовое значение выводится масштабированным в тысячах.
Помимо символов-заполнителей, пользовательский спецификатор формата может содержать любые другие символы, которые появляются в отформатированной строке без изменения на тех местах, где они указаны в спецификаторе формата. Например, при выполнении следующего фрагмента кода:
Console.WriteLine("КПД топлива: {0:##.# миль на галлон }", 21.3);выводится такой результат.
КПД топлива: 21.3 миль на галлон
При необходимости в форматируемой строке можно также указывать такие управляющие последовательности, как \t или \п.
Символы-заполнителиЕиеобусловливают отображение числовых значений в экспоненциальном представлении. В этом случае после символаЕилиедолжен быть указан хотя бы один нуль, хотя их может быть и больше. Нули обозначают количество отображаемых десятичных цифр. Дробная часть числового значения округляется в соответствии с заданным форматом отображения. Если указывается символЕ,то он отображается прописной буквой "Е". А если указывается символе,то он отображается строчной буквой "е". Для того чтобы знак порядка отображался всегда, используются формыЕ+илие+.А для отображения знака порядка только при выводе отрицательных значений служат формыЕ, е, Е-илие-.
Знак ; служит разделителем в различных форматах вывода положительных, отрицательных и нулевых значений. Ниже приведена общая форма пользовательского спецификатора формата, в котором используется знак ;.
положительный_формат;отрицательный_формат;нулевой_формат
Рассмотрим следующий пример.
Console.WriteLine("{0:#.##; (#.##);0.00}", num);
Если значение переменнойnumположительно, то оно отображается с двумя разрядами после десятичной точки. Если же значение переменнойnumотрицательно, то оно также отображается с двумя разрядами после десятичной точки, но в круглых скобках. А если значение переменнойnumравно нулю, то оно отображается в виде строки 0.00. Когда используются разделители, указывать все части приведенной выше фррмы пользовательского спецификатора формата совсем не обязательно. Так, если требуется вывести только положительные или отрицательные значения,нулевой_ форматможно опустить. (В данном случае нуль форматируется как положительное значение.) С другой стороны, можно опуститьотрицательный_формат.И в этом случаеположительный_форматинулевой_форматдолжны разделяться точкой с запятой. А в итогеположительный_форматбудет использоваться для форматирования не только положительных, но и отрицательных значений.
В приведенном ниже примере программы демонстрируется лишь несколько специальных форматов, которые могут быть определены пользователем.
// Пример применения специальных форматов, using System;
class PictureFormatDemo { static void Main() {
double num = 64354.2345;
Console.WriteLine("Формат по умолчанию: " + num);
// Отобразить числовое значение с 2 разрядами после десятичной точки. Console.WriteLine("Значение с 2 десятичными разрядами: " +
"{0:#.##}", num);
// Отобразить числовое значение с 2 разрядами после // десятичной точки и запятыми перед ней.
Console.WriteLine("Добавить запятые: {0:#,###.##}", num);
// Отобразить числовое значение в экспоненциальном представлении.
Console.WriteLine("Использовать экспоненциальное представление: " +
"{0:#.###е+00}", num);
// Отобразить числовое значение, масштабированное в тысячах.
Console.WriteLine("Значение в тысячах: " + "{0:#0,}", num) ;
/* Отобразить по-разному положительные, отрицательные и нулевые значения. */
Console.WriteLine("Отобразить по-разному положительные," +
"отрицательные и нулевые значения.");
Console.WriteLine("{0:#.#; (#.##);0.00}", num); num = -num;
Console.WriteLine("{0:#.##;(#.##);0.00}", num); num = 0.0;
Console.WriteLine ("{0:#.##; (#.##);0.00} ", num) ;
// Отобразить числовое значение в процентах, num = 0.17;
Console.WriteLine("Отобразить в процентах: {0:#%}", num);
}
}
Ниже приведен результат выполнения этой программы.
Формат по умолчанию: 64354.2345
Значение с 2 десятичными разрядами: 64354.23
Добавить запятые: 64,354.23
Использовать экспоненциальное представление: 6.435е+04 Значение в тысячах: 64
Отобразить по-разному положительные, отрицательные и нулевые значения.
64354.2
(64354.23)
0.00
Отобразить в процентах: 17%
Форматирование даты и времени
Помимо числовых значений, форматированию нередко подлежит и другой тип данных:DateTime.Это структура, представляющая дату и время. Значения даты и времени могут отображаться самыми разными способами. Ниже приведены лишь некоторые примеры их отображения.
06/05/2005
Friday, January 1, 2010
12:59:00
12:59:00 PM
Кроме того, дата и время могут быть по-разному представлены в отдельных странах. Для этой цели в среде .NET Framework предусмотрена обширная подсистема форматирования значений даты и времени.
Форматирование даты и времени осуществляется с помощью спецификаторов формата. Спецификаторы формата даты и времени сведены в табл. 22.7. Конкретное представление даты и времени может отличаться в силу региональных и языковых особенностей и поэтому зависит от настройки параметров культурной среды.
Таблица 22.7. Спецификаторы формата даты и времени
Спецификатор
Формат
D
Дата в длинной форме
d
Дата в краткой форме
F
Дата и время в длинной форме
f
Дата и время в краткой форме
G
Дата — в краткой форме, время — в длинной
gg
Дата и время — в краткой форме
м
Месяц и день
m
То же, что и м
0
Формат даты и времени, включая часовой пояс. Строка, составленная в формате 0, может быть преобразована обратно в эквивалентную форму вывода даты и времени. Это так называемый “круговой” формат
о
То же, что и о
R
Дата и время в стандартной форме по Гринвичу
г
То же, что и R
s
Сортируемый формат представления даты и времени
T
Время в длинной форме
t
Время в краткой форме
Окончание табл. 22.7
Спецификатор
Формат
и
Длинная форма универсального представления даты и времени; время ото
бражается как универсальное синхронизированное время (UTC)
и
Краткая форма универсального представления даты и времени
Y
Месяц и год
У
То же, что и Y
В приведенном ниже примере программы демонстрируется применение спецификаторов формата даты и времени.
// Отформатировать дату и время, используя стандартные форматы, using System;
class TimeAndDateFormatDemo { static void Main() {
DateTime dt = DateTime.Now; // получить текущее время
Console.WriteLine("Формат d: {0:d}", dt);
Console.WriteLine("Формат D: {0:D}", dt);
Console.WriteLine("Формат t: {0:t}", dt) ;
Console.WriteLine("Формат'T: {0:T}", dt);
Console.WriteLine("Формат f: {0:f}", dt);
Console.WriteLine("Формат F: {0:F}", dt);
Console.WriteLine("Формат g: {0:g}", dt);
Console.WriteLine("Формат G: {0:G}", dt);
Console.WriteLine("Формат m: {0:m}", dt);
Console.WriteLine("Формат M: {0:M}", dt);
Console.WriteLine("Формат о: {0:o}", dt);
Console.WriteLine("Формат 0: {0:0}", dt);
Console.WriteLine("Формат r: {0:r}", dt);
Console.WriteLine("Формат R: {0:R}", dt);
Console.WriteLine("Формат s: {0:s}", dt);
Console.WriteLine("Формат u: {0:u}", dt);
Console.WriteLine("Формат U: {0:U}", dt);
Console.WriteLine("Формат у: {0:y}", dt);
Console.WriteLine("Y format: {0:Y}", dt) ;
}
}
Эта программа дает следующий результат, который, впрочем, зависит от настроек языковых и региональных параметров локализации базового программного обеспечения.
Формат d: 2/11/2010
Формат D: Thursday, February 11, 2010 Формат t: 11:21 AM f
Формат T: 11:21:23 AM
Формат f: Thursday, February 11, 2010 11:21 AM
Формат F: Thursday, February 11, 2010 11:21:23 AM
Формат g: 2/11/2010 11:21 AM
Формат G: 2/11/2010 11:21:23 AM
Формат m: February 11
Формат M: February 11
Формат о: 2010-02-11T11:21:23.3768153-06:00 Формат О: 2010-02-11T11:21:23.3768153-06:00 Формат r: Thu, 11 Feb 2010 11:21:23 GMT Формат R: Thu, 11 Feb 2010 11:21:23 GMT Формат s: 2010-02-11T11:21:23 Формат u: 2010-02-11 11:21:23Z
Формат U: Thursday, February 11, 2010 5:21:23 PM Формат у: February, 2010 Формат Y: February, 2010
В следующем примере программы воспроизводятся очень простые часы. Время обновляется каждую секунду, и каждый час компьютер издает звонок. Для получения отформатированного строкового представления времени перед его выводом в этой программе используется метод ToStringO из структуры DateTime. Через каждый час символ звукового предупреждающего сигнала присоединяется к отформатированной строке, представляющей время, в результате чего звенит звонок.
// Пример простых часов.
using System;
class SimpleClock { static void Main() { string t; int seconds;
DateTime dt = DateTime.Now; seconds = dt.Second;
for(;;) {
dt = DateTime.Now;
// обновлять время через каждую секунду if(seconds != dt.Second) { seconds = dt.Second;
t = dt.ToString("T");
if(dt.Minute==0 && dt.Second==0) •
t = t + "\a"; // производить звонок через каждый час
Определение пользовательского формата даты и времени
Несмотря на то что стандартные спецификаторы формата даты и времени предусмотрены практически на все случаи жизни, пользователь может определить свои собственные специальные форматы. Процесс определения пользовательских форматов даты и времени мало чем отличается от описанного выше для числовых типов значений. По существу, пользователь создает пример (т.е. изображение) того, как должны выглядеть выводимые данные даты и времени. Для определения пользовательского формата даты и времени служат символы-заполнители, перечисленные в табл. 22.8.
Таблица 22.8. Символы-заполнители специального формата даты и времени
Символ-заполнитель
Назначение
d
День месяца в виде числа в пределах от 1 до 31
dd
День месяца в виде числа в пределах от 1 до 31. Числовые значения в пределах от 1 до 9 дополняются начальным нулем
ddd
Сокращенное название дня недели
dddd
Полное название дня недели
f, ff,
fff, ffff,
Дробная часть числового значения, обозначающего секун
fffff,
ffffff, fffffff
ды. Количество десятичных разрядов определяется числом заданных символов f
g
Эра
h
Часы в виде числа в пределах от 1 до 12
hh
Часы в виде числа в пределах от 1 до 12. Числовые значения в пределах от 1 до 9 дополняются начальным нулем
H
Часы в виде числа в пределах от 0 до 23
HH
Часы в виде числа в пределах от 0 до 23. Числовые значения в пределах от 1 до 9 дополняются начальным нулем
К
Часовой пояс, указываемый в часах. Для автоматической коррекции местного времени и универсального синхронизированного времени (UTC) используется значение свойства DateTime. Kind. (Этот спецификатор формата рекомендуется теперь вместо спецификаторов с символами-заполнителями Z.)
m
Минуты
mm
Минуты. Числовые значения в пределах от 1 до 9 дополняются начальным нулем
M
Месяц в виде числа в пределах от 1 до 12
MM
Месяц в виде числа в пределах от 1 до 12. Числовые значения в пределах от 1 до 9 дополняются начальным нулем
МММ
Сокращенное название месяца
MMMM
Полное название месяца
s
Секунды
ss
Секунды. Числовые значения в пределах от 1 до 9 дополняются начальным нулем
t
Символ “А” или “Р”, обозначающий время А.М. (до полудня) или P.M. (после полудня) соответственно
Символ-заполнитель
Назначение
tt
A.M. или P.M.
У
Год в виде двух цифр, если недостаточно одной
УУ
Год в виде двух цифр. Числовые значения в пределах от 1 до 9 дополняются начальным нулем
УУУ
Год в виде трех цифр
УУУУ
Год в виде четырех цифр
УУУУУ
Год в виде пяти цифр
z
Смещение часового пояса в часах
zz
Смещение часового пояса в часах. Числовые значения в пределах от 1 до 9 дополняются начальным нулем
zzz
Смещение часового пояса в часах и минутах
:
Разделитель для составляющих значения времени
/
Разделитель для составляющих значения даты
%fmt
Стандартный формат, соответствующий спецификатору формата fmt
Глядя на табл. 22.8, можно заметить, что символы-заполнители d, f, g, m, М, s и t выполняют ту же функцию, что и аналогичные символы-заполнители из табл. 22.7. Вообще говоря, если один из этих символов указывается отдельно, то он интерпретируется как спецификатор формата. В противном случае он считается символом-заполнителем. Поэтому если требуется указать несколько таких символов отдельно, но интерпретировать их как символы-заполнители, то перед каждым из них следует поставить знак %.
В приведенном ниже примере программы демонстрируется применение нескольких форматов даты и времени.
// Отформатировать дату и время, используя специальные форматы, using System;
class CustomTimeAndDateFormatsDemo { static void Main() {
DateTime dt = DateTime.Now;
Console.WriteLine("Время: {0:hh:mm tt}", dt) ;
Console.WriteLine("Время в 24-часовом формате: {0:HH:mm}", dt) ;
Console.WriteLine("Дата: {0:ddd МММ dd, yyyy}", dt) ;
Console.WriteLine("Эра: {0:gg}", dt) ;
Console.WriteLine("Время в секундах: " +
"{0:HH:mm:ss tt}", dt) ;
Console.WriteLine("День месяца в формате m: {0:m}", dt);
Console.WriteLine("Минуты в формате m: {0:%m}", dt);
Вот к какому результату приводит выполнение этой программы (опять же все зависит от конкретных настроек языковых и региональных параметров локализации базового программного обеспечения).
Время: 11:19 AM
Время 24-часовом формате: 11:19 Дата: Thu Feb 11, 2010 Эра: A.D.
Время в секундах: 11:19:40 AM День месяца в формате m: February 11 Минуты в формате ш: 19
Форматирование промежутков времени
Начиная с версии 4.0, в среде .NET Framework появилась возможность форматировать объекты типаTime Span— структуры, представляющей промежуток времени. Объект типаTime Spanможет быть получен самыми разными способами, в том числе и в результате вычитания одного объекта типаDateTimeиз другого. И хотя форматировать объекты типаTime Spanприходится нечасто, о такой возможности все же стоит упомянуть вкратце.
По умолчанию в структуреTime Spanподдерживаются три стандартных спецификатора формата даты и времени:с, gиG.Они обозначают инвариантную форму промежутка времени, короткую и длинную форму с учетом культурной среды соответственно (последняя форма всегда включает в себя дни). Кроме того, в структуреTime Spanподдерживаются специальные спецификаторы формата даты и времени, приведенные в табл. 22.9. Вообще говоря, если один из этих спецификаторов используется в отдельности, его нужно предварить символом %.
Таблица 22.9. Символы-заполнители специального формата промежутка времени
Символ-заполнитель
Назначение
d, dd,
ddd, dddd,
Целые дни. Если указано несколько символов-заполнителей
ddddd,
dddddd,
ddddddd
d, то отображается, по крайней мере, указанное количество цифр с начальными нулями, если требуется
h, hh
Часы (не считая тех, что составляют часть целого дня). Если указано hh, то отображаются две цифры с начальными нулями, если требуется
m, mm
Минуты (не считая тех, что составляют часть целого часа). Если указано mm, то отображаются две цифры с начальными нулями, если требуется
s, ss
Секунды (не считая тех, что составляют часть целой минуты). Если указано ss, то отображаются две цифры с начальными нулями, если требуется
f, ff,
fff, ffff,
Дробные доли секунды. Количество символов-заполнителей
fffff,
ffffff,
fffffff
f обозначает точность представления, а остальные цифры отбрасываются
F, FF,
FFF, FFFF,
Дробные доли секунды. Количество символов-заполнителей
FFFFF,
FFFFFF,
FFFFFFF
F обозначает точность представления, а остальные цифры отбрасываются и конечные нули не отображаются
В приведенной ниже программе демонстрируется форматирование объектов типаTimeSpanна примере отображения времени, которое приблизительно требуется для вывода на экран 1000 целых значений в циклеfor.
// Отформатировать объект типа TimeSpan.
using System;
class TimeSpanDemo { static void Main() {
DateTime start = DateTime.Now;
// Вывести числа от 1 до 1000. for(int i = 1; i <= 1000; i++) {
Console.Write(i + " ");
if((i % 10) == 0) Console.WriteLine();
}
Console.WriteLine();
DateTime end = DateTime.Now;
TimeSpan span = end - start;
Console.WriteLine("Время выполнения: {0:c}", span);
Console.WriteLine("Время выполнения: {0:g}", span);
Console.WriteLine("Время выполнения: {0:G}", span);
Console.WriteLine("Время выполнения: 0.{0:fff} секунды", span);
}
}
Выполнение этой программы приводит к следующему результату, который и в этом случае зависит от конкретных настроек языковых и региональных параметров локализации базового программного обеспечения, а также от загрузки системы задачами и ее быстродействия.
981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 Время выполнения: 00:00:00.0140000 Время выполнения: 0:00:00.014 Время выполнения: 0:00:00:00.0140000 Время выполнения: 0.014 секунды
Форматирование перечислений
В C# допускается также форматировать значения, определяемые в перечислении. Вообще говоря, значения из перечисления могут отображаться как по имени, так и по значению. Спецификаторы формата перечислений сведены в табл. ”22.10. Обратите особое внимание на форматыGи F. Перед перечислениями, которые должны представлять битовые поля, следует указывать атрибутFlags.Как правило, в битовых полях хранятся значения, обозначающие отдельные двоичные разряды и упорядоченные по степени числа 2. При наличии атрибутаFlagsимена всех битовых составляющих форматируемого значения, если, конечно, это действительное значение, отображаются с помощью спецификатораG.А с помощью спецификатораFотображаются имена всех битовых составляющих форматируемого значения, если оно составляется путем логического сложения по ИЛИ двух или более полей, определяемых в перечислении.
Таблица 22.10. Спецификаторы формата перечислений Спецификатор Назначение
D Отображает значение в виде десятичного целого числа
d То же, что и D
F Отображает имя значения. Если это значение можно создать путем логиче
ского сложения по ИДИ двух или более полей, определенных в перечислении, то данный спецификатор отображает имена всех битовых составляющих заданного значения, причем независимо оттого, задан атрибут Flags или нет
f То же, что и F
G Отображает имя значения. Если перед форматируемым перечислением ука
зывается атрибут Flags, то данный спецификатор отображает имена всех битовых составляющих заданного значения, если, конечно, это допустимое значение g То же, что и G
X Отображает значение в виде шестнадцатеричного целого числа. Для отобра
жения как минимум восьми цифр форматируемое значение дополняется (при необходимости) начальными нулями х_То же, что и X_
В приведенной ниже программе демонстрируется применение спецификаторов формата перечислений.
// Отформатировать перечисление.
using System;
class EnumFmtDemo {
enum Direction { North, South, East, West }
[Flags] enum Status { Ready=0xl, OffLine=Ox2,
Waiting=0x4, TransmitOK=Ox8,
ReceiveOK=OxlO, OnLine=0x20 }
static void Main() {
Direction d = Direction.West;
Console.WriteLine("{0:G}", d);
Console.WriteLine("{0:F}", d); 1
Console.WriteLine("{0:D}", d);
Console.WriteLine("{0:X}", d) ;
Status s = Status.Ready | Status.TransmitOK;
Console.WriteLine("{0:G}", s);
Console.WriteLine("{0:F}", s);
Console.WriteLine("{0:D}", s);
Console.WriteLine("{0:X}", s);
}
}
Ниже приведен результат выполнения этой программы.
West
West
3
00000003
Ready, TransmitOK Ready, TransmitOK 9
ГЛАВА 23 Многопоточное программирование. Часть первая: основы
Среди многих замечательных свойств языка C# особое место принадлежит поддержкемногопоточного программирования.Многопоточная программа состоит из двух или более частей, выполняемых параллельно. Каждая часть такой программы называетсяпотокоми определяет отдельный путь выполнения команд. Таким образом, многопоточная обработка является особой формой многозадачности.
Многопоточное программирование опирается на целый ряд средств, предусмотренных для этой цели в самом языке С#, а также на классы, определенные в среде .NET Framework. Благодаря встроенной в C# поддержке многопоточной обработки сводятся к минимуму или вообще устраняются многие трудности, связанные с организацией многопоточной обработки в других языках программирования. Как станет ясно из дальнейшего, поддержка в C# многопоточной обработки четко организована и проста для понимания.
С выпуском версии 4.0 в среде .NET Framework появились два важных дополнения, имеющих отношение к многопоточным приложениям. Первым из них является TPL (Task Parallel Library — Библиотека распараллеливания задач), а вторым — PLINQ (Parallel LINQ — Параллельный язык интегрированных запросов). Оба дополнения поддерживают параллельное программирование и позволяют использовать преимущества, предоставляемые многопроцессорными (многоядерными) компьютерами в отношении обработки данных. Кроме того, библиотека TPL упрощает создание многопоточных приложений и управление ими. В силу этого многопоточная обработка, опирающаяся на
TPL, рекомендуется теперь как основной подход к разработке многопоточных приложений. Тем не менее накопленный опыт создания исходной многопоточной подсистемы по-прежнему имеет значение по целому ряду причин. Во-первых, уже существует немалый объем унаследованного кода, в котором применяется первоначальный подход к многопоточной обработке. Если приходится работать с таким кодом или сопровождать его, то нужно знать, как работает исходная многопоточная система. Во-вторых, в коде, опирающемся на TPL, могут по-прежнему использоваться элементы исходной многопоточной системы, и особенно ее средства синхронизации. И в-третьих, несмотря на то что сама библиотека TPL основывается на абстракции, называемойзадачей, она по-прежнему неявно опирается на потоки и потоковые средства, описываемые в этой главе. Поэтому для полного усвоения и применения TPL потребуются твердые знания материала, излагаемого в этой главе.
И наконец, следует особо подчеркнуть, что многопоточная обработка представляет собой довольно обширную тему, и поэтому подробное ее изложение выходит за рамки этой книги. В этой и последующей главах представлен лишь беглый обзор данной темы и демонстрируется ряд основополагающих методик. Следовательно, материал этих глав может служить введением в эту важную тему и основанием для дальнейшего ее самостоятельного изучения.
Основы многопоточной обработки
Различают две разновидности многозадачности: на основе процессов и на основе потоков. В связи с этим важно понимать отличия между ними.Процессфактически представляет собой исполняемую программу. Поэтомумногозадачность на основе процессов— это средство, благодаря которому на компьютере могут параллельно выполняться две программы и более. Так, многозадачность на основе процессов позволяет одновременно выполнять программы текстового редактора, электронных таблиц и просмотра содержимого в Интернете. При организации многозадачности на основе процессов программа является наименьшей единицей кода, выполнение которой может координировать планировщик задач.
Потокпредставляет собой координируемую единицу исполняемого кода. Своим происхождением этот термин обязан понятию "поток исполнения'7. При организации многозадачности на основе потоков у каждого процесса должен быть по крайней мере один поток, хотя их может быть и больше. Это означает, что в одной программе одновременно могут решаться две задачи и больше. Например, текст может форматироваться в редакторе текста одновременно с его выводом на печать, при условии, что оба эти действия выполняются в двух отдельных потоках.
Отличия в многозадачности на основе процессов и потоков могут быть сведены к следующему: многозадачность на основе процессов организуется для параллельного выполнения программ, а многозадачность на основе потоков — для параллельного выполнения отдельных частей одной программы.
Главное преимущество многопоточной обработки заключается в том, что она позволяет писать программы, которые работают очень эффективно благодаря возможности выгодно использовать время простоя, неизбежно возникающее в ходе выполнения большинства программ. Как известно, большинство устройств ввода-вывода, будь то устройства, подключенные к сетевым портам, накопители на дисках или клавиатура, работают намного медленнее, чем центральный процессор (ЦП). Поэтому большую часть своего времени программе приходится ожидать отправки данных на устройство ввода-вывода или приема информации из него. А благодаря многопоточной обработке программа может решать какую-нибудь другую задачу во время вынужденного простоя. Например, в то время как одна часть программы отправляет файл через соединение с Интернетом, другая ее часть может выполнять чтение текстовой информации, вводимой с клавиатуры, а третья — осуществлять буферизацию очередного блока отправляемых данных.
Поток может находиться в одном из нескольких состояний. В целом, поток мол<:ет бытьвыполняющимся; готовым к выполнению,как только он получит время и ресурсы ЦП;приостановленным, т.е. временно не выполняющимся;возобновленнымв дальнейшем;заблокированнымв ожидании ресурсов для своего выполнения; а такжезавершенным,когда его выполнение окончено и не может быть возобновлено.
В среде .NET Framework определены две разновидности потоков:приоритетныйифоновый.По умолчанию создаваемый поток автоматически становится приоритетным, но его можно сделать фоновым. Единственное отличие приоритетных потоков от фоновых заключается в том, что фоновый поток автоматически завершается, если в его процессе остановлены все приоритетные потоки.
В связи с организацией многозадачности на основе потоков возникает потребность в особого рода режиме, который называетсясинхронизациейи позволяет координировать выполнение потоков вполне определенным образом. Для такой синхронизации в C# предусмотрена отдельная подсистема, основные средства которой рассматриваются в этой главе.
Все процессы состоят хотя бы из одного потока, который обычно называютосновным,поскольку именно с него начинается выполнение программы. Следовательно, в основном потоке выполнялись все приведенные ранее примеры программ. Из основного потока можно создать другие потоки.
В языке C# и среде .NET Framework поддерживаются обе разновидности многозадачности: на основе процессов и на основе потоков. Поэтому средствами C# можно создавать как процессы, так и потоки, а также управлять и теми и другими. Для того чтобы начать новый процесс, от программирующего требуется совсем немного усилий, поскольку каждый предыдущий процесс совершенно обособлен от последующего. Намного более важной оказывается поддержка в C# многопоточной обработки, благодаря которой упрощается написание высокопроизводительных, многопоточных программ на C# по сравнению с некоторыми другими языками программирования.
Классы, поддерживающие многопоточное программирование, определены в пространстве имен System. Threading. Поэтому любая многопоточная программа на C# включает в себя следующую строку кода.
using System.Threading;
Класс Thread
Система многопоточной обработки основывается на классе Thread, который инкапсулирует поток исполнения. Класс Thread являетсягерметичным,т.е. он не может наследоваться. В классе Thread определен ряд методов и свойств, предназначенных для управления потоками. На протяжении всей этой главы будут рассмотрены наиболее часто используемые члены данного класса.
Создание и запуск потока
Длясоздания потока достаточно получить экземпляр объекта типаThread,т.е. класса, определенного в пространстве именSystem.Threading.Ниже приведена простейшая форма конструктора классаThread:
public Thread(ThreadStartзапуск)
гдезапуск— это имя метода, вызываемого с целью начать выполнение потока, aThreadStart— делегат, определенный в среде .NET Framework, как показано ниже.
public delegate void ThreadStart()
Следовательно, метод, указываемый в качестве точки входа в поток, должен иметь возвращаемый типvoidи не принимать никаких аргументов.
Вновь созданный новый поток не начнет выполняться до тех пор, пока не будет вызван его методStart (), определяемый в классеThread.Существуют две формы объявления методаStart (). Ниже приведена одна из них.
public void Start()
Однажды начавшись, поток будет выполняться до тех пор, пока не произойдет возврат из метода, на который указываетзапуск.Таким образом, после возврата из этого метода поток автоматически прекращается. Если же попытаться вызвать методStart() для потока, который уже начался, это приведет к генерированию исключенияThreadStateException.
В приведенном ниже примере программы создается и начинает выполняться новый поток.
// Создать поток исполнения.
using System;
using System.Threading;
class MyThread { public int Count; string thrdName;
public MyThread(string name) {
Count = 0; thrdName = name;
}
// Точка входа в поток, public void Run() {
Console.WriteLine(thrdName + " начат.");
do {
Thread.Sleep(500);
Console.WriteLine("В потоке " + thrdName + ", Count = " + Count);
Count++;
} while(Count < 10);
Console.WriteLine(thrdName + " завершен.");
}
class MultiThread { static void Main() {
Console.WriteLine("Основной поток начат.");
// Сначала сконструировать объект типа MyThread.
MyThread mt = new MyThread("Потомок #1");
// Далее сконструировать поток из этого объекта.
Thread newThrd = new Thread(mt.Run);
// И наконец, начать выполнение потока. newThrd.Start(); do {
Console.Write(".");
Thread.Sleep(100);
} while (mt.Count != 10);
Console.WriteLine("Основной поток завершен.");
}
}
Рассмотрим приведенную выше программу более подробно. В самом ее начале определяется классMyThread,предназначенный для создания второго потока исполнения. В методе Run () этого класса организуется цикл для подсчета от 0 до 9. Обратите внимание на вызов статического методаSleep (), определенного в классеThread.Этот метод обусловливает приостановление того потока, из которого он был вызван, на определенный период времени, указываемый в миллисекундах. Когда приостанавливается один поток, может выполняться другой. В данной программе используется следующая форма методаSleep():
public static void Sleep(intмиллисекунд_простоя)
гдемиллисекунд_простояобозначает период времени, на который приостанавливается выполнение потока. Если указанное количествомиллисекунд_простояравно нулю, то вызывающий поток приостанавливается лишь для того, чтобы предоставить возможность для выполнения потока, ожидающего своей очереди.
В методеMain() новый объект типаThreadсоздается с помощью приведенной ниже последовательности операторов.
// Сначала сконструировать объект типа MyThread.
MyThread mt = new MyThread("Потомок #1");
// Далее сконструировать поток из этого объекта.
Thread newThrd = new Thread(mt.Run);
// И наконец, начать выполнение потока. newThrd.Start();
Как следует из комментариев к приведенному выше фрагменту кода, сначала создается объект типаMyThread.Затем этот объект используется для создания объекта типаThread,для чего конструктору этого объекта в качестве точки входа передается методmt.Run (). И наконец, выполнение потока начинается с вызова методаStart ().
Благодаря этому методmt. Run() выполняется в своем собственном потоке. После вызова методаStart () выполнение основного потока возвращается к методуMain (),где начинается циклdo-while.Оба потока продолжают выполняться, совместно используя ЦП, вплоть до окончания цикла. Ниже приведен результат выполнения данной программы. (Он может отличаться в зависимости от среды выполнения, операционной системы и степени загрузки задач.)
Основной поток начат.
Потомок #1 начат.
Потомок #1 завершен.
Основной поток завершен.
Зачастую в многопоточной программе требуется, чтобы основной поток был последним потоком, завершающим ее выполнение. Формально программа продолжает выполняться до тех пор, пока не завершатся все ее приоритетные потоки. Поэтому требовать, чтобы основной поток завершал выполнение программы, совсем не обязательно. Тем не менее этого правила принято придерживаться в многопоточном программировании, поскольку оно явно определяет конечную точку программы. В рассмотренной выше программе предпринята попытка сделать основной поток завершающим ее выполнение. Для этой цели значение переменнойCountпроверяется в циклеdo-whileвнутри методаMain (), и как только это значение оказывается равным 10, цикл завершается и происходит поочередный возврат из методовSleep ().Но такой подход далек от совершенства, поэтому далее в этой главе будут представлены более совершенные способы организации ожидания одного потока до завершения другого.
Простые способы усовершенствования многопоточной программы
Рассмотренная выше программа вполне работоспособна, но ее можно сделать более эффективной, внеся ряд простых усовершенствований, (to-первых, можно сделать так, чтобы выполнение потока начиналось сразу же после его создания. Для этого достаточно получить экземпляр объекта типаThreadв конструкторе классаMyThread.И во-вторых, в классеMyThreadсовсем не обязательно хранить имя потока, поскольку для этой цели в классеThreadспециально определено свойствоName.
public string Name { get; set; }
СвойствоNameдоступно для записи и чтения и поэтому может служить как для запоминания, так и для считывания имени потока.
Ниже приведена версия предыдущей программы, в которую внесены упомянутые выше усовершенствования.
// Другой способ запуска потока.
using System;
using System.Threading;
class MyThread { public int Count; public Thread Thrd;
public MyThread(string name) {
Count = 0;
Thrd = new Thread(this.Run);
Thrd.Name = name; // задать имя потока Thrd.Start(); // начать поток
}
// Точка входа в поток, void Run() {
Console.WriteLine(Thrd.Name + " начат."); do {
Thread.Sleep (500);
Console.WriteLine ("В потоке " + Thrd.Name + ", Count = " + Count);
Count++;
} while(Count < 10);
Console.WriteLine(Thrd.Name + " завершен.");
}
}
class MultiThreadlmproved { static void Main() {
Console.WriteLine("Основной поток начат.");
// Сначала сконструировать объект типа MyThread.
MyThread mt = new MyThread("Потомок #1");
do {
Console.Write (".");
Thread.Sleep (100);
} while (mt.Count != 10);
Console.WriteLine("Основной поток завершен.");
}
}
Эта версия программы дает такой же результат, как и предыдущая. Обратите внимание на то, что объект потока сохраняется в переменной Thrd из класса MyThread.
Создание нескольких потоков
В предыдущих примерах программ был создан лишь один порожденный поток. Но в программе можно породить столько потоков, сколько потребуется. Например, в следующей программе создаются три порожденных потока.
using System;
using System.Threading;
class MyThread { public int Count; public Thread Thrd;
public MyThread(string name) {
Count = 0;
Thrd = new Thread(this.Run);
Thrd.Name = name;
Thrd.Start() ;
}
// Точка входа в поток, void Run() {
Console.WriteLine(Thrd.Name + " начат."); do {
Thread.Sleep (500);
Console.WriteLine("В потоке " + Thrd.Name + ", Count = " + Count); Count++;
} while(Count < 10);
Console.WriteLine(Thrd.Name + " завершен.");
}
}
class MoreThreads { static void Main() {
Console.WriteLine("Основной поток начат.");
// Сконструировать три потока.
do {
Console.Write(".");
Thread.Sleep(100) ;
} while (mtl.Count <10 I | mt2.Count <10 || mt3.Count < 10);
Console.WriteLine("Основной поток завершен.");
}
}
Ниже приведен один из возможных результатов выполнения этой программы
Основной поток начат.
.Потомок #1 начат.
Потомок #2 начат.
Потомок #3 начат.
....В потоке Потомок #1, Count = 0 В потоке Потомок #2, Count = 0 В потоке Потомок #3, Count = 0
.....В потоке Потомок #1, Count = 1
Поток #1 завершен.
В потоке Потомок #2, Count = 9 Поток #2 завершен.
В потоке Потомок #3, Count = 9 Поток #3 завершен.
Основной поток завершен.
Как видите, после того как все три потока начнут выполняться, они будут совместно использовать ЦП. Приведенный выше результат может отличаться в зависимости от среды выполнения, операционной системы и других внешних факторов, влияющих на выполнение программы.
Определение момента окончания потока
Нередко оказывается полезно знать, когда именно завершается поток. В предыдущих примерах программ для этой цели отслеживалось значение переменнойCount.Но ведь это далеко не лучшее и не совсем пригодное для обобщения решение. Правда, в классеThreadимеются два других средства для определения момента окончания потока. С этой целью можно, прежде всего, опросить доступное только для чтения свойствоIs Alive,определяемое следующим образом.
public bool IsAlive { get; }
СвойствоIsAliveвозвращает логическое значениеtrue,если поток, для которого оно вызывается, по-прежнему выполняется. Для "опробования" свойстваIsAliveподставьте приведенный ниже фрагмент кода вместо кода в классеMore Threadиз предыдущей версии многопоточной программы, как показано ниже.
// Использовать свойство IsAlive для отслеживания момента окончания потоков, class MoreThreads { static void Main() {
Console.WriteLine("Основной поток начат.");
// Сконструировать три потока.
do {
Console.Write(".");
Thread.Sleep(100);
} while (mtl.Thrd.IsAlive && mt2.Thrd.IsAlive && mt3.Thrd.IsAlive);
Console.WriteLine("Основной поток завершен.");
}
}
При выполнении этой версии программы результат получается таким же, как и прежде. Единственное отличие заключается в том, что в ней используется свойство IsAlive для отслеживания момента окончания порожденных потоков.
Еще один способ отслеживания момента окончания состоит в вызове метода Join (). Ниже приведена его простейшая форма.
public void Join()
Метод Join () ожидает до тех пор, пока поток, для которого он был вызван, не завершится. Его имя отражает принцип ожидания до тех пор, пока вызывающий поток неприсоединитсяк вызванному методу. Если же данный поток не был начат, то генерируется исключение ThreadStateException. В других формах метода Join () можно указать максимальный период времени, в течение которого следует ожидать завершения указанного потока.
В приведенном ниже примере программы метод Join () используется для того, чтобы основной поток завершился последним.
// Использовать метод Join().
using System;
using System.Threading;
class MyThread { public int Count; public Thread Thrd;
public MyThread(string name) {
Count = 0;
Thrd = new Thread(this.Run);
Thrd.Name = name;
Thrd.Start ();
}
// Точка входа в поток, void Run() {
Console.WriteLine(Thrd.Name + " начат."); do {
Thread.Sleep(500);
Console .WriteLine ("В потоке " + Thrd.Name + ", Count = " + Count);
Count++;
} Vhile(Count < 10);
Console.WriteLine(Thrd.Name + " завершен.");
}
}
// Использовать метод Join() для ожидания до тех пор,
// пока потоки не завершатся, class JoinThreads { static void Main() {
Console.WriteLine("Основной поток начат.");
// Сконструировать три потока.
mtl.Thrd.Join();
Console.WriteLine("Потомок #1 присоединен."); mt2.Thrd.Join();
Console.WriteLine("Потомок #2 присоединен."); mt3.Thrd.Join();
Console.WriteLine("Потомок #3 присоединен.");
Console.WriteLine("Основной поток завершен.");
}
}
Ниже приведен один из возможных результатов выполнения этой программы. Напомним, что он может отличаться в зависимости от среды выполнения, операционной системы и прочих факторов, влияющих на выполнение программы.
Основной поток начат.
Потомок #1 начат. •
Потомок #2 начат.
Потомок #3 начат.
в
потоке
Потомок
#3,
Count
=
3
в
потоке
Потомок
#1,
Count
=
4
в
потоке
Потомок
#2,
Count
=
4
в
потоке
Потомок
#3,
Count
=
4
в
потоке
Потомок
#1,
Count
=
5
в
потоке
Потомок
#2,
Count
=
5
в
потоке
Потомок
#3,
Count
=
5
в
потоке
Потомок
#1,
Count
=
6
в
потоке
Потомок
#2,
Count
=
6
в
потоке
Потомок
#3,
Count
=
6
в
потоке
Потомок
#1,
Count
=
7
в
потоке
Потомок
#2,
Count
=
7
в
потоке
Потомок
#3,
Count
=
7
в
потоке
Потомок
#1/
Count
=
8
в
потоке
Потомок
#2,
Count
=
8
в
потоке
Потомок
#3,
Count
=
8
в
потоке
Потомок
#1/
Count
=
9
Потомок #1 завершен.
В
потоке
Потомок
#2,
Count
=
9
Потомок #2 завершен.
В
потоке
Потомок
#3,
Count
=
9
Потомок #3 завершен.
Потомок #1 присоединен.
Потомок #2 присоединен.
Потомок #3 присоединен.
Основной поток завершен.
Как видите, выполнение потоков завершилось после возврата из последовательного ряда вызовов методаJoin ().
Передача аргумента потоку
Первоначально в среде .NET Framework нельзя было передавать аргумент потоку, когда он начинался, поскольку у метода, служившего в качестве точки входа в поток, не могло быть параметров. Если же потоку требовалось передать какую-то информацию, то к этой цели приходилось идти различными обходными путями, например использовать общую переменную. Но этот недостаток был впоследствии устранен, и теперь аргумент может быть передан потоку. Для этого придется воспользоваться другими формами методаStart (), конструктора классаThread,а также метода, служащего в качестве точки входа в поток.
Аргумент передается потоку в следующей форме методаStart ().
public void Start(objectпараметр)
Объект, указываемый в качестве аргументапараметр, автоматически передается методу, выполняющему роль точки входа в поток. Следовательно, для того чтобы передать аргумент потоку, достаточно передать его методуStart ().
Для применения параметризированной формы методаStart() потребуется следующая форма конструктора классаThread:
public Thread(ParameterizedThreadStartзапуск)
гдезапускобозначает метод, вызываемый с целью начать выполнение потока. Обратите внимание на то, что в этой форме конструкторазапускимеет тип
ParameterizedThreadStart,а неThreadStart,как в форме, использовавшейся в предыдущих примерах. В данном случаеParameter izedThreadS tartявляется делегатом, объявляемым следующим образом.
public delegate void ParameterizedThreadStart(objectobj)
Как видите, этот делегат принимает аргумент типаobj ect.Поэтому для правильного применения данной формы конструктора классаThreadу метода, служащего в качестве точки входа в поток, должен быть параметр типаobj ect.
В приведенном ниже примере программы демонстрируется передача аргумента потоку.
// Пример передачи аргумента методу потока.
using System;
using System.Threading;
class MyThread { public int Count; public Thread Thrd;
// Обратите внимание на то, что конструктору класса // MyThread передается также значение типа int. public MyThread(string name, int num) {
Count = 0;
// Вызвать конструктор типа ParameterizedThreadStart // явным образом только ради наглядности примера.
Thrd = new Thread(this.Run);
Thrd.Name = name;
// Здесь переменная num передается методу Start ()
// в качестве аргумента.
Thrd.Start(num);
}
// Обратите внимание на то, что в этой форме метода Run()
// указывается параметр типа object.
’ void Run(object num) {
Console.WriteLine(Thrd.Name + " начат со счета " + num);
do {
Thread.Sleep (500);
Console.WriteLine("В потоке " + Thrd.Name + ", Count = " + Count);
Count++;
} while(Count < (int) num);
Console.WriteLine(Thrd.Name + " завершен.");
}
}
class PassArgDemo { static void Main() {
// Обратите внимание на то, что число повторений // передается этим двум объектам типа MyThread. MyThread mt = new MyThread("Потомок #1", 5); MyThread mt2 = new MyThread("Потомок #2", 3);
do {
Thread.Sleep(100);
} while (mt.Thrd.IsAlive'| mt2.Thrd.IsAlive);
Console.WriteLine("Основной поток завершен.");
}
}
Ниже приведен результат выполнения данной программы, хотя у вас он может оказаться несколько иным.
Потомок #1 завершен.
Основной поток завершен.
Как следует из приведенного выше результата, первый поток повторяется пять раз, а второй—три раза. Число повторений указывается в конструкторе классаMyThreadи затем передается методуRun (), служащему в качестве точки входа в поток, с помощью параметризированной формыParameterizedThreadStartметодаStart ().
Свойство IsBackground
Как упоминалось выше, в среде .NET Framework определены две разновидности потоков: приоритетный и фоновый. Единственное отличие между ними заключается в том, что процесс не завершится до тех пор, пока не окончится приоритетный поток, тогда как фоновые потоки завершаются автоматически по окончании всех приоритетных потоков. По умолчанию создаваемый поток становится приоритетным. Но его можно сделать фоновым, используя свойствоIsBackground,определенное в классеThread,следующим образом.
public bool IsBackground { get; set; }
Для того чтобы сделать поток фоновым, достаточно присвоить логическое значениеtrueсвойствуIsBackground.А логическое значениеfalseуказывает на то, что поток является приоритетным.
Приоритеты потоков
У каждого потока имеется свой приоритет, который отчасти определяет, насколько часто поток получает доступ к ЦП. Вообще говоря, низкоприоритетные потоки получают доступ к ЦП реже, чем высокоприоритетные. Таким образом, в течение заданного промежутка времени низкоприоритетному потоку будет доступно меньше времени ЦП, чем высокоприоритетному. Как и следовало ожидать, время ЦП, получаемое потоком, оказывает определяющее влияние на характер его выполнения и взаимодействия с другими потоками, исполняемыми в настоящий момент в системе.
Следует иметь в виду, что, помимо приоритета, на частоту доступа потока к ЦП оказывают влияние и другие факторы. Так, если высокоприоритетный поток ожидает доступа к некоторому ресурсу, например для ввода с клавиатуры, он блокируется, а вместо него выполняется низкоприоритетный поток. В подобной ситуации низкоприоритетный поток может получать доступ к ЦП чаще, чем высокоприоритетный поток в течение определенного периода времени. И наконец, конкретное планирование задач на уровне операционной системы также оказывает влияние на время ЦП, выделяемое для потока.
Когда порожденный поток начинает выполняться, он получает приоритет, устанавливаемый по умолчанию. Приоритет потока можно изменить с помощью свойстваPriority,являющегося членом классаThread.Ниже приведена общая форма данного свойства:
public ThreadPriority Priority{ get; set; }
гдеThreadPriorityобозначает перечисление, в котором определяются приведенные ниже значения приоритетов.
ThreadPriority.Highest ThreadPriority.AboveNormal ThreadPriority.Normal ThreadPriority.BelowNormal ThreadPriority.Lowest
По умолчанию для потока устанавливается значение приоритетаThreadPriority. Normal.
Для того чтобы стало понятнее влияние приоритетов на исполнение потоков, обратимся к примеру, в котором выполняются два потока: один с более высоким приоритетом. Оба потока создаются в качестве экземпляров объектов классаMyThread.В методеRun() организуется цикл, в котором подсчитывается определенное число повторений. Цикл завершается, когда подсчет достигает величины 1000000000 или когда статическая переменнаяstopполучает логическое значениеtrue.Первоначально переменнаяstopполучает логическое значениеfalse.В первом потоке, где производится подсчет до 1000000000, устанавливается логическое значениеtrueпеременнойstop.В силу этого второй поток оканчивается на следующем своем интервале времени. На каждом шаге цикла строка в переменнойcurrentNameпроверяется на наличие имени исполняемого потока. Если имена потоков не совпадают, это означает, что произошло переключение исполняемых задач. Всякий раз, когда происходит переключение задач, имя нового потока отображается и присваивается переменнойcurrentName.Это дает возможность отследить частоту доступа потока к ЦП. По окончании обоих потоков отображается число повторений цикла в каждом из них.
// Продемонстрировать влияние приоритетов потоков.
using System;
using System.Threading;
class MyThread { public int Count; public Thread Thrd;
static bool stop = false; static string currentName;
/* Сконструировать новый поток. Обратите внимание на то, что данный конструктор еще не начинает выполнение потоков. */ public MyThread(string name) {
Count = 0;
Thrd = new Thread(this.Run);
Thrd.Name = name; currentName = name;
}
// Начать выполнение нового потока, void Run() {
Console.WriteLine("Поток " + Thrd.Name + " начат."); do {
Count++;
if(currentName != Thrd.Name) { currentName = Thrd.Name;
Console.WriteLine("В потоке " + currentName);
} .
} while(stop == false && Count < 1000000000); stop = true;
Console.WriteLine("Поток " + Thrd.Name + " завершен.");
}
}
class PriorityDemo { static void Main() {
MyThread mtl = new MyThread("с высоким приоритетом"); MyThread mt2 = new MyThread("с низким приоритетом");
// Установить приоритеты для потоков.
mtl.Thrd.Priority = ThreadPriority.AboveNormal;
mt2.Thrd.Priority = ThreadPriority.BelowNormal;
// Начать потоки, mtl.Thrd.Start(); mt2.Thrd.Start() ;
mtl.Thrd.Join (); mt2.Thrd.Join();
Console.WriteLine();
Console.WriteLine("Поток " + mtl.Thrd.Name +
" досчитал до " + mtl.Count);
Console.WriteLine("Поток " + mt2.Thrd.Name +
" досчитал до " + mt2.Count);
}
}
Вот к какому результату может привести выполнение этой программы.
Поток с высоким приоритетом начат.
В потоке с высоким приоритетом Поток с низким приоритетом начат.
В потоке с низким приоритетом В потоке с высоким приоритетом В потоке с низким приоритетом В потоке с высоким приоритетом В потоке с низким приоритетом В потоке с высоким приоритетом В потоке с низким приоритетом В потоке с высоким приоритетом В потоке с низким приоритетом В потоке с высоким приоритетом Поток с высоким приоритетом завершен.
Поток с низким приоритетом завершен.
Поток с высоким приоритетом досчитал до 1000000000 Поток с низким приоритетом досчитал до 23996334
Судя по результату, высокоприоритетный поток получил около 98% всего времени, которое было выделено для выполнения этой программы. Разумеется, конкретный результат может отличаться в зависимости от быстродействия ЦП и числа других задач, решаемых в системе, а также от используемой версии Windows.
Многопоточный код может вести себя по-разному в различных средах, поэтому никогда не следует полагаться на результаты его выполнения только в одной среде. Так, было бы ошибкой полагать, что низкоприоритетный поток из приведенного выше примера будет всегда выполняться лишь в течение небольшого периода времени до тех пор, пока не завершится высокоприоритетный поток. В другой среде высокоприоритетный поток может, например, завершиться еще до того, как низкоприоритетный поток выполнится хотя бы один раз.
Синхронизация
Когда используется несколько потоков, то иногда приходится координировать действия двух или более потоков. Процесс достижения такой координации называетсясинхронизацией.Самой распространенной причиной применения синхронизации служит необходимость разделять среди двух или более потоков общий ресурс, который может быть одновременно доступен только одному потоку. Например, когда в одном потоке выполняется запись информации в файл, второму потоку должно быть запрещено делать это в тот же самый момент времени. Синхронизация требуется и в том случае, если один поток ожидает событие, вызываемое другим потоком. В подобной ситуации требуются какие-то средства, позволяющие приостановить один из потоков до тех пор, пока не произойдет событие в другом потоке. После этого ожидающий поток может возобновить свое выполнение.
В основу синхронизации положено понятиеблокировки, посредством которой организуется управление доступом к кодовому блоку в объекте. Когда объект заблокирован одним потоком, остальные потоки не могут получить доступ к заблокированному кодовому блоку. Когда же блокировка снимается одним потоком, объект становится доступным для использования в другом потоке.
Средство блокировки встроено в язык С#. Благодаря этому все объекты могут быть синхронизированы. Синхронизация организуется с помощью ключевого слова lock. Она была предусмотрена в C# с самого начала, и поэтому пользоваться ею намного проще, чем кажется на первый взгляд. В действительности синхронизация объектов во многих программах на C# происходит практически незаметно.
Ниже приведена общая форма блокировки:
lock(lockObj){
// синхронизируемые операторы
}
гдеlockObjобозначает ссылку на синхронизируемый объект. Если же требуется синхронизировать только один оператор, то фигурные скобки не нужны. Оператор lock гарантирует, что фрагмент кода, защищенный блокировкой для данного объекта, будет использоваться только в потоке, получающем эту блокировку. А все остальные потоки блокируются до тех пор, пока блокировка не будет снята. Блокировка снимается по завершении защищаемого ею фрагмента кода.
Блокируемым считается такой объект, который представляет синхронизируемый ресурс. В некоторых случаях им оказывается экземпляр самого ресурса или же произвольный экземпляр объекта, используемого для синхронизации. Следует, однако, иметь в виду, что блокируемый объект не должен быть общедоступным, так как в противном случае он может быть заблокирован из другого, неконтролируемого в программе фрагмента кода и в дальнейшем вообще не разблокируется. В прошлом для блокировки объектов очень часто применялась конструкция lock (this). Но она пригодна только в том случае, если this является ссылкой на закрытый объект. В связи с возможными программными и концептуальными ошибками, к которым может привести конструкция lock (this), применять ее больше не рекомендуется. Вместо нее лучше создать закрытый объект, чтобы затем заблокировать его. Именно такой подход принят в примерах программ, приведенных далее в этой главе. Но в унаследованном коде C# могут быть обнаружены примеры применения конструкции lock (this). В одних случаях такой код оказывается безопасным, а в других — требует изменений во избежание серьезных осложнений при его выполнении.
В приведенной ниже программе синхронизация демонстрируется на примере управления доступом к методу Sumlt (), суммирующему элементы целочисленного массива.
// Использовать блокировку для синхронизации доступа к объекту.
using System;
using System.Threading;
class SumArray { int sum;
object lockOn = new object(); // закрытый объект, доступный
// для последующей блокировки
lock(lockOn) { // заблокировать весь метод
sum =0; // установить исходное значение суммы
for(int i=0; i < nums.Length; i++) {
sum +- nums[i];
Console.WriteLine("Текущая сумма для потока " +
Thread.CurrentThread.Name + " равна " + sum); Thread.Sleep(10); // разрешить переключение задач
}
return sum;
}
}
}
class MyThread {
public Thread Thrd; int[] a; int answer;
// Создать один объект типа SumArray для всех // экземпляров класса MyThread. static SumArray sa = new SumArray();
// Сконструировать новый поток, public MyThread(string name, int [ ] nums) { a = nums;
Thrd = new Thread(this.Run);
Thrd.Name = name;
Thrd.Start(); // начать поток
}
// Начать выполнение нового потока, void Run() {
Console.WriteLine(Thrd.Name + " начат.");
answer = sa.Sumlt(a);
Console.WriteLine("Сумма для потока " + Thrd.Name + " равна " + answer); Console.WriteLine(Thrd.Name + " завершен.");
}
}
class Sync {
static void Main() {
int[] a = {1, 2, 3, 4, 5};
MyThread mtl = new MyThread ("Потомок #1", a);.
MyThread mt2 = new MyThread("Потомок #2", a);
mtl.Thrd.Join(); mt2.Thrd.Join();
Ниже приведен результат выполнения данной программы, хотя у вас он может оказаться несколько иным.
Потомок #1 начат.
Сумма для потока Потомок #2 равна 15 Потомок #2 завершен.
Как следует из приведенного выше результата, в обоих потоках правильно подсчитывается сумма, равная 15.
Рассмотрим эту программу более подробно. Сначала в ней создаются три класса. Первым из них оказывается классSumArray,в котором определяется методSumlt (),суммирующий элементы целочисленного массива. Вторым создается классMyThread,в котором используется статический объектsaтипаSumArray.Следовательно, единственный объект типаSumArrayиспользуется всеми объектами типаMyThread.С помощью этого объекта получается сумма элементов целочисленного массива. Обратите внимание на то, что текущая сумма запоминается в полеsumобъекта типаSumArray.Поэтому если методSumlt() используется параллельно в двух потоках, то оба потока попытаются обратиться к полюsum,чтобы сохранить в нем текущую сумму. А поскольку это может привести к ошибкам, то доступ к методуSumlt() должен быть синхронизирован. И наконец, в третьем классе,Sync,создаются два потока, в которых подсчитывается сумма элементов целочисленного массива.
Операторlockв методеSumlt() препятствует одновременному использованию данного метода в разных потоках. Обратите внимание на то, что в оператореlockобъектlockOnиспользуется в качестве синхронизируемого. Это закрытый объект, предназначенный исключительно для синхронизации. МетодSleep() намеренно вызывается для того, чтобы произошло переключение задач, хотя в данном случае это невозможно. Код в методеSumlt() заблокирован, и поэтому он может быть одновременно использован только в одном потоке. Таким образом, когда начинает выполняться второй порожденный поток, он не сможет войти в методSumlt() до тех пор, пока из него не выйдет первый порожденный поток. Благодаря этому гарантируется получение правильного результата.
Для того чтобы полностью уяснить принцип действия блокировки, попробуйте удалить из рассматриваемой здесь программы тело методаSumlt(). В итоге методSumlt() перестанет быть синхронизированным, а следовательно, он_может параллельно использоваться в любом числе потоков для одного и того же объекта. Поскольку текущая сумма сохраняется в полеsum,она может быть изменена в каждом потоке, вызывающем методSumlt(). Это означает, что если два потока одновременно вызывают методSumlt() для одного и того же объекта, то конечный результат получается неверным, поскольку содержимое поля sum отражает смешанный результат суммирования в обоих потоках. В качестве примера ниже приведен результат выполнения рассматриваемой здесь программы после снятия блокировки с метода Sumlt ().
Потомок #1 начат.
Сумма для потока Потомок #1 равна 2 9 Потомок #1 завершен.
Текущая сумма для потока Потомок #2 равна 29
Потомок #2 завершен.
Как следует из приведенного выше результата, в обоих порожденных потоках методSumlt() используется одновременно для одного и того же объекта, а это приводит к искажению значения в полеsum.
Ниже подведены краткие итоги использования блокировки.
• Если блокировка любого заданного объекта получена в одном потоке, то после блокировки объекта она не может быть получена в другом потоке.
• Остальным потокам, пытающимся получить блокировку того же самого объекта, придется ждать до тех пор, пока объект не окажется в разблокированном состоянии.
• Когда поток выходит из заблокированного фрагмента кода, соответствующий объект разблокируется.
Другой подход к синхронизации потоков
Несмотря на всю простоту и эффективность блокировки кода метода, как показано в приведенном выше примере, такое средство синхронизации оказывается пригодным далеко не всегда. Допустим, что требуется синхронизировать доступ к методу класса, который был создан кем-то другим и сам не синхронизирован. Подобная ситуация вполне возможна при использовании чужого класса, исходный код которого недоступен. В этом случае оператор lock нельзя ввести в соответствующий метод чужого класса. Как же тогда синхронизировать объект такого класса? К счастью, этот вопрос разрешается довольно просто: доступ к объекту может быть заблокирован из внешнего кода по отношению к данному объекту, для чего достаточно указать этот объект в оператореlock.В качестве примера ниже приведен другой вариант реализации предыдущей программы. Обратите внимание на то, что код в методеSumlt() уже не является заблокированным, а объектlockOnбольше не объявляется. Вместо этого вызовы методаSumlt() блокируются в классеMyThread.
// Другой способ блокировки для синхронизации доступа к объекту, using System;
using System.Threading;
class SumArray { int sum;
public int Sumlt(int[] nums) {
sum =0; // установить исходное значение суммы
for(int i=0; i < nums.Length; i++) { sum += nums[i];
Console.WriteLine("Текущая сумма для потока " +
Thread.CurrentThread.Name + " равна " + sum); Thread.Sleep(10); // разрешить переключение задач
}
return sum;
}
}
class MyThread {
public Thread Thrd; int[] a; int answer;
/* Создать один объект типа SumArray для всех экземпляров класса MyThread. */ static SumArray sa = new SumArray();
// Сконструировать новый поток, public MyThread(string name, int[] nums) { a = nums;
Thrd = new Thread(this.Run);
Thrd.Name = name;
Thrd.Start(); // начать поток
}
// Начать выполнение нового потока, void Run() {
Console.WriteLine(Thrd.Name + " начат.");
// Заблокировать вызовы метода Sumlt(). lock(sa) answer = sa.Sumlt(a);
Console.WriteLine("Сумма для потока " + Thrd.Name +
" равна " + answer);
Console.WriteLine(Thrd.Name + " завершен.");
}
}
class Sync {
static void Main() {
int[] a = {1, 2, 3, 4, 5};
MyThread mtl = new MyThread("Потомок #1", a);
MyThread mt2 = new MyThread("Потомок #2", a);
mtl.Thrd.Join(); mt2.Thrd.Join();
В данной программе блокируется вызов методаsa.Sum It (), а не сам методSum It (). Ниже приведена соответствующая строка кода, в которой осуществляется подобная блокировка.
// Заблокировать вызовы метода Sumlt() . lock(sa) answer = sa.Sumlt(a);
Объект sa является закрытым, и поэтому он может быть благополучно заблокирован. При таком подходе к синхронизации потоков данная программа дает такой же правильный результат, как и при первоначальном подходе.
Класс Monitor и блокировка
Ключевое слово lock на самом деле служит в C# быстрым способом доступа к средствам синхронизации, определенным в классеMonitor,который находится в пространстве именSystem. Threading.В этом классе определен, в частности, ряд методов для управления синхронизацией. Например, для получения блокировки объекта вызывается методEnter (), а для снятия блокировки — методExit (). Ниже приведены общие формы этих методов:
public static void Enter(objectobj)public static void Exit (objectobj)
гдеobjобозначает синхронизируемый объект. Если же объект недоступен, то после вызова методаEnter() вызывающий поток ожидает до тех пор, пока объект не станет доступным. Тем не менее методыEnter() иExit() применяются редко, поскольку операторlockавтоматически предоставляет эквивалентные средства синхронизации потоков. Именно поэтому операторlockоказывается "более предпочтительным" для получения блокировки объекта при программировании на С#.
Впрочем, один метод из классаMonitorможет все же оказаться полезным. Это методTryEnter (), одна из общих форм которого приведена ниже.
public static bool TryEnter(objectobj)
Этот метод возвращает логическое значениеtrue,если вызывающий поток получает блокировку для объектаobj,а иначе он возвращает логическое значениеfalse.Но в любом случае вызывающему потоку придется ждать своей очереди. С помощью методаTryEnter() можно реализовать альтернативный вариант синхронизации потоков, если требуемый объект временно недоступен.
Кроме того, в классеMonitorопределены методыWait (), Pulse () иPulseAll (),которые рассматриваются в следующем разделе.
Сообщение между потоками с помощью методов Wait (), Pulse () и PulseAll ()
Рассмотрим следующую ситуацию. ПотокГвыполняется в кодовом блокеlock,и ему требуется доступ к ресурсуR,который временно недоступен. Что же тогда делать потоку7?Если потокГвойдет в организованный в той или иной форме цикл опроса, ожидая освобождения ресурсаR,то тем самым он свяжет соответствующий объект, блокируя доступ к нему других потоков. Это далеко не самое оптимальное решение, поскольку оно лишает отчасти преимуществ программирования для многопоточной
среды. Более совершенное решение заключается в том, чтобы временно освободить объект и тем самым дать возможность выполняться другим потокам. Такой подход основывается на некоторой форме сообщения между потоками, благодаря которому один поток может уведомлять другой о том, что он заблокирован и что другой поток может возобновить свое выполнение. Сообщение между потоками организуется в C# с помощью методовWait (), Pulse () иPulseAll ().
МетодыWait(), Pulse () и PulseAll () определены в классеMonitorи могут вызываться только из заблокированного фрагмента блока. Они применяются следующим образом. Когда выполнение потока временно заблокировано, он вызывает методWait(). В итоге поток переходит в состояние ожидания, а блокировка с соответствующего объекта снимается, что дает возможность использовать этот объект в другом потоке. В дальнейшем ожидающий поток активизируется, когда другой поток войдет в аналогичное состояние блокировки, и вызывает метод Pulse () или PulseAll (). При вызове метода Pulse () возобновляется выполнение первого потока, ожидающего своей очереди на получение блокировки. А вызов метода PulseAll () сигнализирует о снятии блокировки всем ожидающим потокам.
Ниже приведены две наиболее часто используемые формы методаWait ().
public static bool Wait(objectobj)
public static bool Wait(objectobj,intмиллисекунд_простоя)
В первой форме ожидание длится вплоть до уведомления об освобождении объекта, а во второй форме — как до уведомления об освобождении объекта, так и до истечения периода времени, на который указывает количествомиллисекунд_простоя.В обеих формахobjобозначает объект, освобождение которого ожидается.
Ниже приведены общие формы методовPulse() иPulseAll():
public static void Pulse(objectobj)public static void PulseAll(objectobj)
гдеobjобозначает освобождаемый объект.
Если методыWait(),Pulse() nPulseAll()вызываются из кода, находящегося за пределами синхронизированного кода, например из блокаlock,то генерируется исключениеSynchronizationLockException.
Пример использования методов Wait () и Pulse ()
Для тогочтобы стало понятнее назначение методовWait() иPulse(), рассмотрим пример программы, имитирующей тиканье часов и отображающей этот процесс на экране словами "тик" и "так". Для этой цели в программе создается классTickTock,содержащий два следующих метода:Tick()иТоск().МетодTick() выводит на экран слово "тик", а методТоск () — слово "так". Для запуска часов далее в программе создаются два потока: один из них вызывает методTick(), а другой — методТоск(). Преследуемая в данном случае цель состоит в том, чтобы оба потока выполнялись, поочередно выводя на экран слова "тик" и "так", из которых образуется повторяющийся ряд "тик-так", имитирующий ход часов/
using System;
using System.Threading;
class TickTock {
object lockOn = new object (); public void Tick(bool running) { lock(lockOn) {
if(!running) { // остановить часы ‘Monitor.Pulse(lockOn); // уведомить любые ожидающие потоки return;
}
Console.Write("тик ");
Monitor.Pulse(lockOn); // разрешить выполнение метода Tock() Monitor.Wait(lockOn); // ожидать завершения метода Tock()
}
}
public void Tock(bool running) { lock(lockOn) {
if(!running) { // остановить часы
Monitor.Pulse(lockOn); // уведомить любые ожидающие потоки return;
}
Console.WriteLine("так");
Monitor.Pulse(lockOn); // разрешить выполнение метода Tick() Monitor.Wait(lockOn); // ожидать завершения метода Tick()
}
}
}
class MyThread {
public Thread Thrd;
TickTock ttOb;
// Сконструировать новый поток.
public MyThread(string name, TickTock tt) {
Thrd = new Thread(this.Run); ttOb = tt;
Thrd.Name = name;
Thrd.Start(); '
}
// Начать выполнение нового потока, void Run() {
if(Thrd.Name == "Tick") {
for(int i=0; i<5; i++) ttOb.Tick(true); ttOb.Tick(false) ;
}
else {
for(int i=0; i<5; i++) ttOb.Tock(true); ttOb.Tock(false);
class TickingClock { static void Main() {
TickTock tt = new TickTock ();
MyThread mtl = new MyThread("Tick", tt);
MyThread mt2 = new MyThread("Tock", tt) ; mtl.Thrd.Join(); mt2.Thrd.Join();
Console.WriteLine("Часы остановлены");
}
}
Ниже приведен результат выполнения этой программы.
тик так тик так тик так тик так тик так
Часы остановлены
Рассмотрим эту программу более подробно. В методе Main () создается объект tt типа TickTock, который используется для запуска двух потоков на выполнение. Если в методе Run () из класса MyThread обнаруживается имя потока Tick, соответствующее ходу часов "тик", то вызывается метод Tick (). А если это имя потока Tock, соответствующее ходу часов "так", то вызывается метод Tock (). Каждый из этих методов вызывается пять раз подряд с передачей логического значения true в качестве аргумента. Часы идут до тех пор, пока этим методам передается логическое значение true, и останавливаются, как только передается логическое значение false.
Самая важная часть рассматриваемой здесь программы находится в методах Tick () и Tock (). Начнем с метода Tick (), код которого для удобства приводится ниже.
public void Tick(bool running) { lock(lockOn) {
if(!running) { // остановить часы
Monitor.Pulse(lockOn); // уведомить любые ожидающие потоки return;
}
Console.Write("тик ");
Monitor.Pulse(lockOn); // разрешить выполнение метода Tock()
Monitor.Wait(lockOn); // ожидать завершения метода Tock()
}
}
Прежде всего обратите внимание на код метода Tick () в блоке lock. Напомним, что методы Wait () и Pulse () могут использоваться только в синхронизированных блоках кода. В начале метода Tick () проверяется значение текущего параметра, которое служит явным признаком остановки часов. Если это логическое значение false, то часы остановлены. В этом случае вызывается метод Pulse (), разрешающий выполнение любого потока, ожидающего своей очереди. Мы еще вернемся к этому моменту в дальнейшем. Если же часы идут при выполнении метода Tick (), то на экран выводится слово "тик" с пробелом, затем вызывается метод Pulse (), а после него — метод
Wait (). При вызове метода Pulse () разрешается выполнение потока для того же самого объекта, а при вызове метода Wait () выполнение метода Tick () приостанавливается до тех пор, пока метод Pulse () не будет вызван из другого потока. Таким образом, когда вызывается метод Tick (), отображается одно слово "тик" с пробелом, разрешается выполнение другого потока, а затем выполнение данного метода приостанавливается.
Метод То с к () является точной копией метода Tick (), за исключением того, что он выводит на экран слово "так". Таким образом, при входе в метод То с к () на экран выводится слово "так", вызывается метод Pulse (), а затем выполнение метода Тоск () приостанавливается. Методы Tick () иТоск() можно рассматривать как поочередно сменяющие друг друга, т.е. они взаимно синхронизированы.
Когда часы остановлены, метод Pulse () вызывается для того, чтобы обеспечить успешный вызов метода Wait (). Напомним, что метод Wait () вызывается в обоих методах, Tick () и Тоск (), после вывода соответствующего слова на экран. Но дело в том, что когда часы остановлены, один из этих методов все еще находится в состоянии ожидания. Поэтому завершающий вызов метода Pulse () требуется, чтобы выполнить ожидающий метод до конца. В качестве эксперимента попробуйте удалить этот вызов метода Pulse () и понаблюдайте за тем, что при этом произойдет. Вы сразу же обнаружите, что программа "зависает", и для выхода из нее придется нажать комбинацию клавиш <Ctrl+C>. Дело в том, что когда метод Wait () вызывается в последнем вызове метода Тоск (), соответствующий ему метод Pulse () не вызывается, а значит, выполнение метода Тоск () оказывается незавершенным, и он ожидает своей очереди до бесконечности.
Прежде чем переходить к чтению следующего раздела, убедитесь сами, если, конечно, сомневаетесь, в том, что следует обязательно вызывать методы Wait () и Pulse (), чтобы имитируемые часы шли правильно. Для этого подставьте приведенный ниже вариант класса TickTock в рассматриваемую здесь программу. В этом варианте все вызовы методов Wait () и Pulse () исключены.
// Нерабочий вариант класса TickTock. class TickTock {
object lockOn = new object ();
public void Tick(bool running) { lock(lockOn) {
if (!running) { // остановить часы return;
}
Console.Write("тик ") ;
}
}
public void Tock (bool running) { lock(lockOn) {
if(!running) { // остановить часы return;
}
После этой подстановки результат выполнения данной программы будет выглядеть следующим образом.
тик тик тик тик тик так
так
так
так
так
Часы остановлены
Очевидно, что методыTick () иTock () больше не синхронизированы!
Взаимоблокировка и состояние гонки
При разработке многопоточных программ следует быть особенно внимательным, чтобы избежать взаимоблокировки и состояний гонок.Взаимоблокировка,как подразумевает само название, — это ситуация, в которой один поток ожидает определенных действий от другого потока, а другой поток, в свою очередь, ожидает чего-то от первого потока. В итоге оба потока приостанавливаются, ожидая друг друга, и ни один из них не выполняется. Эта ситуация напоминает двух слишком вежливых людей, каждый из которых настаивает на том, чтобы другой прошел в дверь первым!
На первый взгляд избежать взаимоблокировки нетрудно, но на самом деле не все так просто, ведь взаимоблокировка может возникать окольными путями. В качестве примера рассмотрим классTickTockиз предыдущей программы. Как пояснялось выше, в отсутствие завершающего вызова методаPulse() из методаTick() илиTock() тот или другой будет ожидать до бесконечности, что приведет к "зависанию" программы вследствие взаимоблокировки. Зачастую причину взаимоблокировки не так-то просто выяснить, анализируя исходный код программы, поскольку параллельно действующие процессы могут взаимодействовать довольно сложным образом во время выполнения.Дляисключения взаимоблокировки требуется внимательное программирование и тщательное тестирование. В целом, если многопоточная программа периодически "зависает", то наиболее вероятной причиной этого является взаимоблокировка.
Состояние гонкивозникает в том случае, когда два потока или больше пытаются одновременно получить доступ к общему ресурсу без должной синхронизации. Так, в одном потоке может сохраняться значение в переменной, а в другом — инкрементироваться текущее значение этой же переменной. В отсутствие синхронизации конечный результат будет зависеть от того, в каком именно порядке выполняются потоки: инкрементируется ли значение переменной во втором потоке или же оно сохраняется в первом. О подобной ситуации говорят, что потоки "гоняются друг за другом", причем конечный результат зависит от того, какой из потоков завершится первым. Возникающее состояние гонок, как и взаимоблокировку, непросто обнаружить. Поэтому его лучше предотвратить, синхронизируя должным образом доступ к общим ресурсам при программировании.
Применение атрибута MethodlmplAttribute
Метод может быть полностью синхронизирован с помощью атрибутаMethodlmplAttribute.Такой подход может стать альтернативой операторуlockв тех случаях, когда метод требуется заблокировать полностью. Атрибут
MethodlmplAttгibuteопределен в пространстве именSystem . Runtime . CompilerServices.Ниже приведен конструктор, применяемый для подобной синхронизации:
public MethodlmplAttribute(MethodlmplOptionsmethodlmplOptions)
гдеmethodlmplOptionsобозначает атрибут реализации. Для синхронизации метода достаточно указать атрибутMethodlmplOptions. Synchronized.Этот атрибут вызывает блокировку всего метода для текущего экземпляра объекта, доступного по ссылкеthis.Если же метод относится к типуstatic,то блокируется его тип. Поэтому данный атрибут непригоден для применения в открытых объектах или классах.
Ниже приведена еще одна версия программы, имитирующей тиканье часов, с переделанным вариантом классаTickTock,в котором атрибутMethodlmplOptionsобеспечивает должную синхронизацию.
// Использовать атрибут MethodlmplAttribute для синхронизации метода.
using System;
using System.Threading;
using System.Runtime.CompilerServices;
// Вариант класса TickTock, переделанный с целью // использовать атрибут MethodlmplOptions.Synchronized, class TickTock {
/* Следующий атрибут полностью синхронизирует метод Tick(). */ [MethodlmplAttribute(MethodlmplOptions.Synchronized)] public void Tick (bool running) { if(!running) { // остановить часы
Monitor.Pulse(this); // уведомить любые ожидающие потоки return;
}
Console.Write("тик ");
Monitor.Pulse(this); // разрешить выполнение метода Tock()
Monitor.Wait(this); // ожидать завершения метода Tock()
}
/* Следующий атрибут полностью синхронизирует метод Тоск(). */ [MethodlmplAttribute(MethodlmplOptions.Synchronized)] public void Tock(bool running) { if (!running) { // остановить часы
Monitor.Pulse(this); // уведомить любые ожидающие потоки return;
}'
Console.WriteLine("так");
Monitor.Pulse(this); // разрешить выполнение метода Tick()
Monitor.Wait(this); // ожидать завершения метода Tick()
}
}
class MyThread {
public Thread Thrd;
TickTock ttOb;
// Сконструировать новый поток.
public MyThread(string name, TickTock tt) {
Thrd = new Thread(this.Run); ttOb = tt;
Thrd.Name = name;
Thrd.Start();
}
// Начать выполнение нового потока, void Run() {
if(Thrd.Name == "Tick") {
for(int i=0; i<5; i++) ttOb.Tick(true); ttOb.Tick(false);
}
else {
for(int i=0; i<5; i++) ttOb.Tock(true); ttOb.Tock(false);
}
}
}
class TickingClock { static void Main() {
TickTock tt = new TickTock();
MyThread mtl = new MyThread("Tick", tt) ;
MyThread mt2 = new MyThread("Tock", tt) ;
mtl.Thrd.Join(); mt2.Thrd.Join();
Console.WriteLine("Часы остановлены");
}
}
Эта версия программы дает такой же результат, как и предыдущая. Синхронизируемый метод не определен в открытом классе и не вызывается для открытого объекта, поэтому применение оператора lock или атрибута MethodlmplAttribute зависит от личных предпочтений. Ведь и тот и другой дает один и тот же результат. Но поскольку ключевое слово lock относится непосредственно к языку С#, то в примерах, приведенных в этой книге, предпочтение отдано именно ему.
ПРИМЕЧАНИЕ
Не применяйте атрибут MethodlmplAttribute в открытых классах или экземплярах открытых объектов. Вместо этого пользуйтесь оператором lock, чтобы заблокировать метод для закрытого объекта, как пояснялось ранее.
Применение мьютекса и семафора
В большинстве случаев, когда требуется синхронизация, оказывается достаточно и оператора lock. Тем не менее в некоторых случаях, как, например, при ограничении доступа к общим ресурсам, более удобными оказываются механизмы синхронизации, встроенные в среду .NET Framework. Ниже рассматриваются по порядку два таких механизма: мьютекс и семафор.
Мьютекс
Мьютекспредставляет собой взаимно исключающий синхронизирующий объект. Это означает, что он может быть получен потоком только по очереди. Мьютекс предназначен для тех ситуаций, в которых общий ресурс может быть одновременно использован только в одном потоке. Допустим, что системный журнал совместно используется в нескольких процессах, но только в одном из них данные могут записываться в файл этого журнала в любой момент времени. Для синхронизации процессов в данной ситуации идеально подходит мьютекс.
Мьютекс поддерживается в классеSystem. Threading.Mutex.У него имеется несколько конструкторов. Ниже приведены два наиболее употребительных конструктора.
public Mutex()
public Mutex(boolinitiallyOwned)
В первой форме конструктора создается мьютекс, которым первоначально никто не владеет. А во второй форме исходным состоянием мьютекса завладевает вызывающий поток, если параметрini tiallyOwnedимеет логическое значениеtrue.В противном случае мьютексом никто не владеет.
Для того чтобы получить мьютекс, в коде программы следует вызвать методWaitOne() для этого мьютекса. МетодWaitOne() наследуется классомMutexот классаThread. WaitHandle.Ниже приведена его простейшая форма.
public bool WaitOne ();
МетодWaitOne() ожидает до тех пор, пока не будет получен мьютекс, для которого он был вызван. Следовательно, этот метод блокирует выполнение вызывающего потока до тех пор, пока не станет доступным указанный мьютекс. Он всегда возвращает логическое значениеtrue.
Когда же в коде больше не требуется владеть мьютексом, он освобождается посредством вызова методаReleaseMutex (), форма которого приведена ниже.
public void ReleaseMutex()
В этой форме методReleaseMutex() освобождает мьютекс, для которого он был вызван, что дает возможность другому потоку получить данный мьютекс.
Для применения мьютекса с целью синхронизировать доступ к общему ресурсу упомянутые выше методыWaitOne() иReleaseMutex() используются так, как показано в приведенном ниже фрагменте кода.
Mutex myMtx = new Mutex();
// ...
myMtx.WaitOne() ; // ожидать получения мьютекса // Получить доступ к общему ресурсу.
myMtx.ReleaseMutex(); // освободить мьютекс
При вызове методаWaitOne() выполнение соответствующего потока приостанавливается до тех пор, пока не будет получен мьютекс. А при вызове методаReleaseMutex() мьютекс освобождается и затем может быть получен другим потоком. Благодаря такому подходу к синхронизации одновременный доступ к общему ресурсу ограничивается только одним потоком.
В приведенном ниже примере программы описанный выше механизм синхронизации демонстрируется на практике. В этой программе создаются два потока в виде классовIncThreadn DecThread,которым требуется доступ к общему ресурсу: переменнойSharedRes . Count.В потокеIncThreadпеременнаяSharedRes . Countинкрементируется, а в потокеDecThread— декрементируется. Во избежание одновременного доступа обоих потоков к общему ресурсуSharedRes . Countэтот доступ синхронизируется мьютексомMtx,также являющимся членом классаSharedRes.
// Применить мьютекс.
using System;
using System.Threading;
//В этом классе содержится общий ресурс(переменная Count),
// а также мьютекс (Mtx), управляющий доступом к ней. class SharedRes {
public static int Count = 0;
public static Mutex Mtx = new Mutex();
}
// В этом потоке переменная SharedRes.Count инкрементируется, class IncThread { int num;
public Thread Thrd;
public IncThread(string name, int n) {
Thrd = new Thread(this.Run); num = n;
Thrd.Name = name;
Thrd.Start();
}
// Точка входа в поток, void Run() {
Console.WriteLine(Thrd.Name + " ожидает мьютекс.");
// Получить мьютекс.
SharedRes.Mtx.WaitOne();
Console.WriteLine(Thrd.Name + " получает мьютекс."); do {
Thread.Sleep (500);
SharedRes.Count++;
Console.WriteLine("В потоке " + Thrd.Name +
", SharedRes.Count = " + SharedRes.Count);
num— ;
} while(num > 0);
Console.WriteLine(Thrd.Name + " освобождает мьютекс.");
// Освободить мьютекс.
SharedRes.Mtx.ReleaseMutex();
}
}
// В этом потоке переменная SharedRes.Count декрементируется, class DecThread { int num;
public Thread Thrd;
public DecThread(string name, int n) {
Thrd = new Thread(new ThreadStart(this.Run)); num = n;
Thrd.Name = name;
Thrd.Start();
}
// Точка входа в поток, void Run() {
Console.WriteLine(Thrd.Name + " ожидает мьютекс.");
// Получить мьютекс.
SharedRes.Mtx.WaitOne();
Console.WriteLine(Thrd.Name + " получает мьютекс."); do {
Thread.Sleep(500) ;
SharedRes.Count—;
Console.WriteLine("В потоке " + Thrd.Name +
", SharedRes.Count = " + SharedRes.Count);
num— ;
} while(num > 0);
Console.WriteLine(Thrd.Name + " освобождает мьютекс.");
// Освободить мьютекс.
SharedRes.Mtx.ReleaseMutex();
}
}
class MutexDemo {
static void Main() {
// Сконструировать два потока.
IncThread mtl = new IncThread("Инкрементирующий Поток", 5); Thread.Sleep(1); // разрешить инкрементирующему потоку начаться DecThread mt2 = new DecThread("Декрементирующий Поток", 5);
mtl.Thrd.Join();
mt2.Thrd.Join();
}
}
Эта программа дает следующий результат.
Инкрементирующий Поток ожидает мьютекс.
Инкрементирующий Поток получает мьютекс.
Декрементирующий Поток ожидает мьютекс.
Декрементирующий Поток освобождает мьютекс.
Как следует из приведенного выше результата, доступ к общему ресурсу (переменнойSharedRes . Count)синхронизирован, и поэтому значение данной переменной может быть одновременно изменено только в одном потоке.
Для того чтобы убедиться в том, что мьютекс необходим для получения приведенного выше результата, попробуйте закомментировать вызовы методовWaitOne() иReleaseMutex() в исходном коде рассматриваемой здесь программы. При ее последующем выполнении вы получите следующий результат, хотя у вас он может оказаться несколько иным.
Как следует из приведенного выше результата, без мьютекса инкрементирование и декрементирование переменнойSharedRes .Countпроисходит, скорее, беспорядочно, чем последовательно.
Мьютекс, созданный в предыдущем примере, известен только тому процессу, который его породил. Но мьютекс можно создать и таким образом, чтобы он был известен где-нибудь еще. Для этого он должен быть именованным. Ниже приведены формы конструктора, предназначенные для создания такого мьютекса.
public Mutex(boolinitiallyOwned,stringимя)
public Mutex(boolinitiallyOwned,stringимя,out boolcreatedNew)
В обеих формах конструктораимяобозначает конкретное имя мьютекса. Если в первой форме конструктора параметрini tiallyOwnedимеет логическое значение
true,то владение мьютексом запрашивается. Но поскольку мьютекс может принадлежать другому процессу на системном уровне, то для этого параметра лучше указать логическое значениеfalse.А после возврата из второй формы конструктора параметрcreatedNewбудет иметь логическое значениеtrue,если владение мьютексом было запрошено и получено, и логическое значениеfalse,если запрос на владение был отклонен. Существует и третья форма конструктора типаMutex,в которой допускается указывать управляющий доступом объект типаMutexSecurity.С помощью именованных мьютексов можно синхронизировать взаимодействие процессов.
И последнее замечание: в потоке, получившем мьютекс, допускается делать один или несколько дополнительных вызовов метода Wait One () перед вызовом метода ReleaseMutex (), причем все эти дополнительные вызовы будут произведены успешно. Это означает, что дополнительные вызовы метода Wait One () не будут блокировать поток, который уже владеет мьютексом. Но количество вызовов метода Wait One () должно быть равно количеству вызовов метода ReleaseMutex () перед освобождением мьютекса.
Семафор
Семафорподобен мьютексу, за исключением того, что он предоставляет одновременный доступ к общему ресурсу не одному, а нескольким потокам. Поэтому семафор пригоден для синхронизации целого ряда ресурсов. Семафор управляет доступом к общему ресурсу, используя для этой цели счетчик. Если значение счетчика больше нуля, то доступ к ресурсу разрешен. А если это значение равно нулю, то доступ к ресурсу запрещен. С помощью счетчика ведется подсчет количестваразрешений.Следовательно, для доступа к ресурсу поток должен получить разрешение от семафора.
Обычно поток, которому требуется доступ к общему ресурсу, пытается получить разрешение от семафора. Если значение счетчика семафора больше нуля, то поток получает разрешение, а счетчик семафора декрементируется. В противном случае поток блокируется до тех пор, пока не получит разрешение. Когда же потоку больше не требуется доступ к общему ресурсу, он высвобождает разрешение, а счетчик семафора инкрементируется. Если разрешения ожидает другой поток, то он получает его в этот момент. Количество одновременно разрешаемых доступов указывается при создании семафора. Так, если создать семафор, одновременно разрешающий только один доступ, то такой семафор будет действовать как мьютекс.
Семафоры особенно полезны в тех случаях, когда общий ресурс состоит из группы или пула ресурсов. Например, пул ресурсов может состоять из целого ряда сетевых соединений, каждое из которых служит для передачи данных. Поэтому потоку, которому требуется сетевое соединение, все равно, какое именно соединение он получит. В данном случае семафор обеспечивает удобный механизм управления доступом к сетевым соединениям.
Семафор реализуется в классеSystem. Threading. Semaphore,у которого имеется несколько конструкторов. Ниже приведена простейшая форма конструктора данного класса:
public Semaphore(intinitialCount,intmaximumCount)
гдеinitialCount— это первоначальное значение для счетчика разрешений семафора, т.е. количество первоначально доступных разрешений;maximumCount—максимальное значение данного счетчика, т.е. максимальное количество разрешений, которые может дать семафор.
Семафор применяется таким же образом, как и описанный ранее мьютекс. В целях получения доступа к ресурсу в коде программы вызывается методWaitOne() для семафора. Этот метод наследуется классомSemaphoreот классаWaitHandle.МетодWaitOne() ожидает до тех пор, пока не будет получен семафор, для которого он вызывается. Таким образом, он блокирует выполнение вызывающего потока до тех пор, пока указанный семафор не предоставит разрешение на доступ к ресурсу.
Если коду больше не требуется владеть семафором, он освобождает его, вызывая методRelease (). Ниже приведены две формы этого метода.
public int Release()
public int Release(intreleaseCount)
В первой форме методRelease() высвобождает только одно разрешение, а во второй форме — количество разрешений, определяемых параметромreleaseCount.В обеих формах данный метод возвращает подсчитанное количество разрешений, существовавших до высвобождения.
МетодWaitOne() допускается вызывать в потоке несколько раз перед вызовом методаRelease(). Но количество вызовов методаWaitOne() должно быть равно количеству вызовов методаRelease() перед высвобождением разрешения. С другой стороны, можно воспользоваться формой вызова методаRelease(intпит),чтобы передать количество высвобождаемых разрешений, равное количеству вызовов методаWaitOne ().
Ниже приведен пример программы, в которой демонстрируется применение семафора. В этой программе семафор используется в классеMyThreadдля одновременного выполнения только двух потоков типаMyThread.Следовательно, разделяемым ресурсом в данном случае является ЦП.
// Использовать семафор.
using System;
using System.Threading;
// Этот поток разрешает одновременное выполнение // только двух своих экземпляров, class MyThread {
public Thread Thrd;
// Здесь создается семафор, дающий только два // разрешения из двух первоначально имеющихся, static Semaphore sem = new Semaphore(2, 2);
public MyThread(string name) {
Thrd = new Thread(this.Run);
Thrd.Name = name;
Thrd. Start () ;
}
// Точка входа в поток, void Run() {
Console.WriteLine(Thrd.Name + " ожидает разрешения.");
sem.WaitOne();
Console.WriteLine(Thrd.Name + " получает разрешение.");
for(char ch='A'; ch < 'D'; ch++) {
Console.,WriteLine (Thrd.Name + " : " + ch + " ");
Thread.Sleep(500);
}
Console.WriteLine(Thrd.Name + " высвобождает разрешение.");
// Освободить семафор, sem.Release();
}
}
class SemaphoreDemo { static void Main() {
// Сконструировать три потока.
mtl.Thrd.Join(); mt2.Thrd.Join() ; mt3.Thrd.Join() ;
}
}
В классеMyThreadобъявляется семафорsem,как показано ниже.
static Semaphore sem = new Semaphore(2f 2);
При этом создается семафор, способный дать не более двух разрешений на доступ к ресурсу из двух первоначально имеющихся разрешений.
Обратите внимание на то, что выполнение методаMyThread. Run() не может быть продолжено до тех пор, пока семафорsemне даст соответствующее разрешение. Если разрешение отсутствует, то выполнение потока приостанавливается. Когда же разрешение появляется, выполнение потока возобновляется. В методеIn Main() создаются три потока. Но выполняться могут только два первых потока, а третий должен ожидать окончания одного из этих двух потоков. Ниже приведен результат выполнения рассматриваемой здесь программы, хотя у вас он может оказаться несколько иным.
Поток #1 ожидает разрешения.
Поток #1 получает разрешение.
Поток #1 : А
Поток #2 ожидает разрешения.
Поток #2 получает разрешение.
Поток #2 : А
Поток #3 ожидает разрешения.
Поток #1 : В Поток #2 : В Поток #1 : С Поток #2 : С
Поток #1 высвобождает разрешение.
Поток #3 получает разрешение.
Поток #3 : А
Поток #2 высвобождает разрешение.
Поток #3 : В Поток #3 : С
Поток #3 высвобождает разрешение.
Семафор, созданный в предыдущем примере, известен только тому процессу, который его породил. Но семафор можно создать и таким образом, чтобы он был известен где-нибудь еще. Для этого он должен быть именованным. Ниже приведены формы конструктора классаSemaphore,предназначенные для создания такого семафора.
public Semaphore(intinitialCount,intmaximumCountfstringимя)public Semaphore(intinitialCount,intmaximumCount,stringимя,out boolcreatedNew)
В обеих формахимяобозначает конкретное имя, передаваемое конструктору. Если в первой форме семафор, на который указываетимя,еще не существует, то он создается с помощью значений, определяемых параметрамиinitialCountиmaximumCount.А если он уже существует, то значения параметровinitialCountиmaximumCountигнорируются. После возврата из второй формы конструктора параметрcreatedNewбудет иметь логическое значениеtrue,если семафор был создан. В этом случае значения параметровini tialCountиmaximumCountиспользуются для создания семафора. Если же параметрcreatedNewбудетиметь логическое значениеfalse,значит, семафор уже существует и значения параметровinitialCountиmaximumCountигнорируются. Существует и третья форма конструктора классаSemaphore,в которой допускается указывать управляющий доступом объект типаSemaphoreSecurity.С помощью именованных семафоров можно синхронизировать взаимодействие процессов.
Применение событий
Для синхронизации в C# предусмотрен еще один тип объекта: событие. Существуют две разновидности событий: устанавливаемые в исходное состояние вручную и автоматически. Они поддерживаются в классахManualResetEventиAutoResetEventсоответственно. Эти классы являются производными от классаEventWaitHandle,находящегося на верхнем уровне иерархии классов, и применяются в тех случаях, когда один поток ожидает появления некоторого события в другом потоке. Как только такое событие появляется, второй поток уведомляет о нем первый поток, позволяя тем самым возобновить его выполнение.
Ниже приведены конструкторы классовManualResetEventиAutoResetEvent.
public ManualResetEvent(boolinitialState)public AutoResetEvent(boolinitialState)
Если в обеих формах параметрini tialStateимеет логическое значениеtrue,то о событии первоначально уведомляется. А если он имеет логическое значениеfalse,то о событии первоначально не уведомляется.
Применяются события очень просто. Так, для события типаManualResetEventпорядок применения следующий. Поток, ожидающий некоторое событие, вызывает методWaitOne() для событийного объекта, представляющего данное событие. Если событийный объект находится в сигнальном состоянии, то происходит немедленный
возврат из методаWait One(). В противном случае выполнение вызывающего потока приостанавливается до тех пор, пока не будет получено уведомление о событии. Как только событие произойдет в другом потоке, этот поток установит событийный объект в сигнальное состояние, вызвав методSet(). Поэтому методSet() следует рассматривать как уведомляющий о том, что событие произошло. После установки событийного объекта в сигнальное состояние произойдет немедленный возврат из методаWaitOne(), и первый поток возобновит свое выполнение. А в результате вызова методаReset() событийный объект возвращается в несигнальное состояние.
Событие типаAutoResetEventотличается от события типаManualResetEventлишь способом установки в исходное состояние. Если для события типаManualResetEventсобытийный объект остается в сигнальном состоянии до тех пор, пока не будет вызван методReset(), то для события типаAutoResetEventсобытийный объект автоматически переходит в несигнальное состояние, как только поток, ожидающий это событие, получит уведомление о нем и возобновит свое выполнение. Поэтому если применяется событие типаAutoResetEvent,то вызывать методReset() необязательно.
В приведенном ниже примере программы демонстрируется применение события типаManualResetEvent.
// Использовать событийный объект, устанавливаемый // в исходное состояние вручную.
using System;
using System.Threading;
// Этот поток уведомляет о том, что событие передано его конструктору, class MyThread {
public Thread Thrd;
ManualResetEvent mre;
public MyThread(string name, ManualResetEvent evt) {
Thrd = new Thread(this.Run);
Thrd.Name = name; ,
mre = evt;
Thrd.Start();
}
// Точка входа в поток, void Run() {
Console.WriteLine("Внутри потока " + Thrd.Name);
for(int i=0; i<5; i++) {
Console.WriteLine(Thrd.Name);
Thread.Sleep(500) ;
}
Console.WriteLine(Thrd.Name + " завершен!");
// Уведомить о событии, mre.Set();
class ManualEventDemo { static void Main() {
ManualResetEvent evtObj = new ManualResetEvent(false);
MyThread mtl = new MyThread("Событийный Поток 1", evtObj);
Console.WriteLine("Основной поток ожидает событие.");
// Ожидать уведомления о событии. evtObj.WaitOne();
Console.WriteLine("Основной поток получил " +
"уведомление о событии от первого потока.");
// Установить событийный объект в исходное состояние. evtObj.Reset();
mtl = new MyThread("Событийный Поток 2", evtObj);
// Ожидать уведомления о событии. evtObj.WaitOne();
Console.WriteLine("Основной поток получил " +
"уведомление о событии от второго потока.");
}
}
Ниже приведен результат выполнения рассматриваемой здесь программы, хотя у вас он может оказаться несколько иным.
В потоке Событийный Поток 1
Событийный Поток 1
Основной поток ожидает событие.
Событийный Поток 1 Событийный Поток 1 Событийный Поток 1 Событийный Поток 1 Событийный Поток 1 завершен!
Основной поток получил уведомление о событии от первого потока.
В потоке Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 Событийный Поток 2 завершен!
Основной поток получил уведомление о событии от второго потока.
Прежде всего обратите внимание на то, что событие типаManualResetEventпередается непосредственно конструктору классаMyThread.Когда завершается методRun() из классаMyThread,он вызывает для событийного объекта методSet (),устанавливающий этот объект в сигнальное состояние. В методеMain() формируется событийный объектevtObjтипаManualResetEvent,первоначально устанавливаемый в исходное, несигнальное состояние. Затем создается экземпляр объекта типа
MyThread,которому передается событийный объектevtObj.После этого основной поток ожидает уведомления о событии. А поскольку событийный объектevtObjпервоначально находится в несигнальном состоянии, то основной поток вынужден ожидать до тех пор, пока для экземпляра объекта типаMyThreadне будет вызван методSet() ^устанавливающий событийный объектevtObjв сигнальное состояние. Это дает возможность основному потоку возобновить свое выполнение. Затем событийный объект устанавливается в исходное состояние, и весь процесс повторяется, но на этот раз для второго потока. Если бы не событийный объект, то все потоки выполнялись бы одновременно, а результаты их выполнения оказались бы окончательно запутанными. Для того чтобы убедиться в этом, попробуйте закомментировать вызов методаWaitOne () в методеMain ().
Если бы в рассматриваемой здесь программе событийный объект типаAutoResetEventиспользовался вместо событийного объекта типаManualResetEvent,то вызывать методReset() в методеMain() не пришлось бы. Ведь в этом случае событийный объект автоматически устанавливается в несигнальное состояние, когда поток, ожидающий данное событие, возобновляет свое выполнение. Для опробования этой разновидности события замените в данной программе все ссылки на объект типаManualResetEventссылками на объект типаAutoResetEventи удалите все вызовы методаReset (). Видоизмененная версия программы будет работать так же, как и прежде.
Класс Interlocked
Еще одним классом, связанным с синхронизацией, является классInterlocked.Этот класс служит в качестве альтернативы другим средствам синхронизации, когда требуется только изменить значение общей переменной. Методы, доступные в классеInterlocked,гарантируют, что их действие будет выполняться как единая, непрерываемая операция. Это означает, что никакой синхронизации в данном случае вообще не требуется. В классеInterlockedпредоставляются статические методы для сложения двух целых значений, инкрементирования и декрементирования целого значения, сравнения и установки значений объекта, обмена объектами и получения 64-разрядно-го значения. Все эти операции выполняются без прерывания.
В приведенном ниже примере программы демонстрируется применение двух методов из классаInterlocked: Increment () иDecrement (). При этом используются следующие формы обоих методов:
public static int Increment(ref intlocation)public static int Decrement(ref intlocation)
гдеlocation —это переменная, которая подлежит инкрементированию или декрементированию.
// Использовать блокируемые операции.
using System;
using System.Threading;
// Общий ресурс, class SharedRes {
public static int Count = 0;
// В этом потоке переменная SharedRes.Count инкрементируется, class IncThread { '
public Thread Thrd;
public IncThread(string name) {
Thrd = new Thread(this.Run);
Thrd.Name = name;
Thrd.Start();
}
// Точка входа в поток, void Run() {
for(int i=0; i<5; i++) {
Interlocked.Increment(ref SharedRes.Count);
Console.WriteLine(Thrd.Name + " Count = " + SharedRes.Count);
}
}
}
// В этом потоке переменная SharedRes.Count декрементируется, class DecThread { public Thread Thrd;
public DecThread(string name) {
Thrd = new Thread(this.Run);
Thrd.Name = name;
Thrd.Start();
}
// Точка входа в поток, void Run() {
for(int i=0; i<5; i++) {
Interlocked.Decrement(ref SharedRes.Count);
Console.WriteLine(Thrd.Name + " Count = " + SharedRes.Count);
}
}
}
class InterlockedDemo { static void Main() {
// Сконструировать два потока.
IncThread mtl = new IncThread("Инкрементирующий Поток"); DecThread mt2 = new DecThread("Декрементирующий Поток");
mtl.Thrd.Join(); mt2.Thrd.Join();
}
}
Классы синхронизации, внедренные в версии .NET Framework 4.0
Рассматривавшиеся ранее классы синхронизации, в том числеSemaphoreиAutoResetEvent,были доступны в среде .NET Framework, начиная с версии 1.1.
Таким образом, эти классы образуют основу поддержки синхронизации в среде .NET Framework. Но после выпуска версии .NET Framework 4.0 появился ряд новых альтернатив этим классам синхронизации. Все они перечисляются ниже.
Класс
Назначение
Barrier
Вынуждает потоки ожидать появления всех остальных пото
ков в указанной точке, называемой барьерной
CountdownEvent
Выдает сигнал, когда обратный отсчет завершается
ManualResetEventSlim
Это упрощенный вариант класса ManualResetEvent
semaphoreslim
Это упрощенный вариант класса Semaphore
Если вам понятно, как пользоваться основными классами синхронизации, описанными ранее в этой главе, то у вас не должно возникнуть затруднений при использовании их новых альтернатив и дополнений.
Прерывание потока
Иногда поток полезно прервать до его нормального завершения. Например, отладчику может понадобиться прервать вышедший из-под контроля поток. После прерывания поток удаляется из системы и не может быть начат снова.
Для прерывания потока до его нормального завершения служит методThread. Abort (). Ниже приведена простейшая форма этого метода.
public void Abort()
МетодAbort() создает необходимые условия Для генерирования исключенияThreadAbortExceptionв том потоке, для которого он был вызван. Это исключение приводит к прерыванию потока и может быть перехвачено и в коде программы, но в этом случае оно автоматически генерируется еще раз, чтобы остановить поток. МетодAbort() не всегда способен остановить поток немедленно, поэтому если поток требуется остановить перед тем, как продолжить выполнение программы, то после методаAbort() следует сразу же вызвать методJoin(). Кроме того, в самых редких случаях методуAbort() вообще не удается остановить поток. Это происходит, например, в том случае, если кодовый блокfinallyвходит в бесконечный цикл.
В приведенном ниже примере программы демонстрируется применение методаAbort() для прерывания потока.
// Прервать поток с помощью метода Abort().
using System;
using System.Threading;
class MyThread {
public Thread Thrd;
public MyThread(string name) {
Thrd = new Thread(this.Run);
Thrd.Name = name;
Thrd.Start();
// Это точка входа в поток, void Run() {
Console.WriteLine(Thrd.Name + " начат.");
for (int i = 1; i <= 1000; i++) {
Console.Write(i + " "); if((i %10)==0) {
Console.WriteLine();
Thread.Sleep(250);
}
}
Console .WriteLine (Thrd.Name + 11 завершен.");
}
}
class StopDemo {
static void Main() {
MyThread mtl = new MyThread("Мой Поток");
Thread.Sleep (1000); // разрешить порожденному потоку начать свое выполнение
Console.WriteLine("Прерывание потока."); mtl.Thrd.Abort();
mtl.Thrd.Join(); // ожидать прерывания потока Console.WriteLine("Основной поток прерван.");
}
}
Вот к какому результату приводит выполнение этой программы.
Прерывание потока.
Основной поток прерван.
ПРИМЕЧАНИЕ
Метод Abort () не следует применять в качестве обычного средства прерывания потока, поскольку он предназначен для особых случаев. Обычно поток должен завершаться естественным образом, чтобы произошел возврат из метода, выполняющего роль точки входа в него.
Другая форма метода Abort ()
В некоторых случаях оказывается полезной другая форма метода Abort (), приведенная ниже в общем виде:
public void Abort(objectstatelnfo)
гдеstatelnfoобозначает любую информацию, которую требуется передать потоку, когда он останавливается. Эта информация доступна посредством свойстваExceptionStateиз класса исключенияThreadAbortException.Подобным образом потоку можно передать код завершения. В приведенном ниже примере программы демонстрируется применение данной формы методаAbort ().
// Использовать форму метода Abort (objectstatelnfo).
using System;
using System.Threading;
class MyThread {
public Thread Thrd;
public MyThread(string name) {
Thrd = new Thread(this.Run);
Thrd.Name = name;
Thrd.Start();
}
// Это точка входа в поток, void Run() { try {
Console.WriteLine(Thrd.Name + " начат.");
for (int i = 1; i <= 1000; i++) {
Console.Write(i + " "); if((i%10)==0) {
Console.WriteLine();
Thread.Sleep(250);
}
}
Console.WriteLine(Thrd.Name + " завершен нормально.");
} catch(ThreadAbortException exc) {
Console.WriteLine("Поток прерван, код завершения " + exc.ExceptionState);
>
)
}
class UseAltAbort { static void Main() {
MyThread mtl = new MyThread("Мой Поток");
Thread.Sleep(1000) ; // разрешить порожденному потоку начать свое выполнение Console.WriteLine("Прерывание потока."); mtl.Thrd.Abort(100);
Console.WriteLine("Основной поток прерван.");
}
}
Эта программа дает следующий результат.
Прерывание потока.
Поток прерван, код завершения 100 Основной поток прерван.
Как следует из приведенного выше результата, значение 100 передается методуAbort() в качестве кода прерывания. Это значение становится затем доступным посредством свойстваExceptionStateиз класса исключенияThreadAbortException,которое перехватывается потоком при его прерывании.
Отмена действия метода Abort ()
Запрос на преждевременное прерывание может быть переопределен в самом потоке. Для этого необходимо сначала перехватить в потоке исключениеThreadAbortException,а затем вызвать методResetAbort (). Благодаря этому исключается повторное генерирование исключения по завершении обработчика исключения, прерывающего данный поток. Ниже приведена форма объявления методаResetAbort().
public static void ResetAbort()
Вызов методаResetAbort() может завершиться неудачно, если в потоке отсутствует надлежащий режим надежной отмены преждевременного прерывания потока.
В приведенном ниже примере программы демонстрируется применение методаResetAbort().
// Использовать метод ResetAbort().
using System;
using System.Threading;
class MyThread {
public Thread Thrd;
public MyThread(string name) {
Thrd = new Thread(this.Run);
Thrd.Name = name;
Thrd.Start();
}
// Это точка входа в поток, void Run() {
Console.WriteLine(Thrd.Name + ".начат.");
try {
Console.Write(i + " "); if((i %10)==0) {
Console.WriteLine ();
Thread.Sleep (250);
}
} catch(ThreadAbortException exc) { if((int)exc.ExceptionState ==0) {
Console.WriteLine("Прерывание потока отменено! " +
"Код завершения " + exc.ExceptionState);
Thread.ResetAbort();
}
else
Console.WriteLine("Поток прерван, код завершения " + exc.ExceptionState);
' }
}
Console.WriteLine(Thrd.Name + " завершен нормально.");
}
}
class ResetAbort { static void Main() {
MyThread mtl = new MyThread("Мой Поток");
Thread.Sleep(1000); // разрешить порожденному потоку начать свое выполнение
Console.WriteLine("Прерывание потока."); mtl.Thrd.Abort(0); // это не остановит поток
Thread.Sleep(1000); // разрешить порожденному потоку выполняться подольше
Console.WriteLine("Прерывание потока."); i
mtl.Thrd.Abort(100); // а это остановит поток
mtl.Thrd.Join(); // ожидать прерывания потока
Console.WriteLine("Основной поток прерван.");
}
}
Ниже приведен результат выполнения этой программы.
Поток прерван, код завершения 100
Основной поток прерван.
Если вданном примере программы методAbort() вызывается с нулевым аргументом, то запрос на преждевременное прерывание отменяется потоком, вызывающим методResetAbort (), и выполнение этого потока продолжается. Любое другое значение аргумента приведет к прерыванию потока.
Приостановка и возобновление потока
В первоначальных версиях среды .NET Framework поток можно было приостановить вызовом методаThread. Suspend() и возобновить вызовом методаThread. Resume (). Но теперь оба эти метода считаются устаревшими и не рекомендуются к применению в новом коде. Объясняется это, в частности, тем, что пользоваться методомSuspend() на самом деле небезопасно, так как с его помощью можно приостановить поток, который в настоящий момент удерживает блокировку, что препятствует ее снятию, а следовательно, приводит к взаимоблокировке. Применение обоих методов может стать причиной серьезных осложнений на уровне системы. Поэтому для приостановки и возобновления потока следует использовать другие средства синхронизации, в том числе мьютекс и семафор.
Определение состояния потока
Состояние потока может быть получено из свойстваThreadstate,доступного в классеThread.Ниже приведена общая форма этого свойства.
public ThreadState ThreadState{ get; }
Состояние потока возвращается в виде значения, определенного в перечисленииThreadState.Ниже приведены значения, определенные в этом перечислении.
ThreadState.Aborted
ThreadState.AbortRequested
ThreadState.Background
ThreadState.Running
ThreadState.Stopped
ThreadState.StopRequested
ThreadState.Suspended
ThreadState.SuspendRequested
ThreadState.Unstarted
ThreadState.WaitSleepJoin
Все эти значения не требуют особых пояснений, за исключением одного. ЗначениеThreadState . WaitsleepJoinобозначает состояние, в которое поток переходит во время ожидания в связи с вызовом методаWait (), Sleep ()илиJoin ().
Применение основного потока
Как пояснялось в самом начале этой главы, у всякой программы на C# имеется хотя бы один поток исполнения, называемыйосновным.Этот поток программа получает автоматически, как только начинает выполняться. С основным потоком можно обращаться таким же образом, как и со всеми остальными потоками.
Для доступа к основному потоку необходимо получить объект типаThread,который ссылается на него. Это делается с помощью свойстваCurrentThread,являющегося членом классаThread.Ниже приведена общая форма этого свойства.
Данное свойство возвращает ссылку на тот поток, в котором оно используется. Поэтому если свойствоCurrentThreadиспользуется при выполнении кода в основном потоке, то с его помощью можно получить ссылку на основной поток. Имея в своем распоряжении такую ссылку, можно управлять основным потоком так же, как и любым другим потоком.
В приведенном ниже примере программы сначала получается ссылка на основной поток, а затем получаются и устанавливаются имя и приоритет основного потока.
// Продемонстрировать управление основным потоком.
using System;
using System.Threading;
class UseMain {
static void Main() {
Thread Thrd;
// Получить основной поток.
Thrd = Thread.CurrentThread;
// Отобразить имя основного потока, if(Thrd.Name == null)
Console.WriteLine("У основного потока нет имени."); else
Console.WriteLine("Основной поток называется: " + Thrd.Name);
// Отобразить приоритет основного потока.
Console.WriteLine("Приоритет: " + Thrd.Priority);
Console.WriteLine();
// Установить имя и приоритет.
Console.WriteLine("Установка имени и приоритета.\п");
Thrd.Name = "Основной Поток";
Thrd. Priority = ThreadPriority. AboveNormal^-
Console. WriteLine ("Теперь основной поток называется: " +
Thrd.Name);
Console.WriteLine("Теперь приоритет: " + Thrd.Priority);
}
}
Ниже приведен результат выполнения этой программы.
У основного потока нет имени.
Приоритет: Normal
Установка имени и приоритета.
Теперь основной поток называется: Основной Поток Теперь приоритет: AboveNormal
Следует, однако, быть очень внимательным, выполняя операции с основным потоком. Так, если добавить в конце методаMain() следующий вызов методаJoin():
Thrd.Join ();
программа никогда не завершится, поскольку она будет ожидать окончания основного потока!
Дополнительные средства многопоточной обработки, внедренные в версии .NET Framework 4.0
В версии .NET Framework 4.0 внедрен ряд новых средств многопоточной обработки, которые могут оказаться весьма полезными. Самым важным среди них является новая система отмены. В этой системе поддерживается механизм отмены потока простым, вполне определенным и структурированным способом. В основу этого механизма положено понятиепризнака отмены, с помощью которого указывается состояние отмены потока. Признаки отмены поддерживаются в классеCancellationTokenSourceи в структуреCancellationToken.Система отмены полностью интегрирована в новую библиотеку распараллеливания задач (TPL), и поэтому она подробнее рассматривается вместе с TPL в главе 24.
В классSystem. Threadingдобавлена структураSpinWait,предоставляющая методыSpinOnce () иSpinUntil (), которые обеспечивают более полный контроль над ожиданием в состоянии занятости. Вообще говоря, структураSpinWaitоказывается непригодной для однопроцессорных систем. А для многопроцессорных систем она применяется в цикле. Еще одним элементом, связанным с ожиданием в состоянии занятости, является структураSpinLock,которая применяется в цикле ожидания до тех пор, пока не станет доступной блокировка. В классThreadдобавлен методYield(),который просто выдает остаток кванта времени, выделенного потоку. Ниже приведена общая форма объявления этого метода.
public static bool Yield()
Этот метод возвращает логическое значениеtrue,если происходит переключение контекста. В отсутствие другого потока, готового для выполнения, переключение контекста не произойдет.
Рекомендации по многопоточному программированию
Для эффективного многопоточного программирования самое главное — мыслить категориями параллельного, а не последовательного выполнения кода. Так, если в одной программе имеются две подсистемы, которые могут работать параллельно, их следует организовать в отдельные потоки. Но делать это следует очень внимательно и аккуратно, поскольку если создать слишком много потоков, то тем самым можно значительно снизить,.а не повысить производительность программы. Следует также иметь в виду дополнительные издержки, связанные с переключением контекста. Так, если создать слишком много потоков, то на смену контекста уйдет больше времени ЦП, чем на выполнение самой программы! И наконец, для написания нового кода, предназначенного для многопоточной обработки, рекомендуется пользоваться библиотекой распараллеливания задач (TPL), о которой речь пойдет в следующей главе.
Запуск отдельной задачи
Многозадачность на основе потоков чаще всего организуется при программировании на С#. Но там, где это уместно, можно организовать и многозадачность на основе процессов. В этом случае вместо запуска другого потока в одной и той же программе одна программа начинает выполнение другой. При программировании на C# это делается с помощью классаProcess,определенного в пространстве именSystem. Diagnostics.В заключение этой главы вкратце будут рассмотрены особенности запуска и управления другим процессом.
Простейший способ запустить другой процесс — воспользоваться методомStart (), определенным в классеProcess.Ниже приведена одна из самых простых форм этого метода:
public static Process Start(stringимя_файла)
гдеимя_файлаобозначает конкретное имя файла, который должен исполняться или же связан с исполняемым файлом.
Когда созданный процесс завершается, следует вызвать методClose (), чтобы освободить память, выделенную для этого процесса. Ниже приведена форма объявления методаClose ().
public void Close ()
Процесс может быть прерван двумя способами. Если процесс является приложением Windows с графическим пользовательским интерфейсом, то для прерывания такого процесса вызывается методCloseMainWindow (), форма которого приведена ниже.
public bool CloseMainWindow()
Этот метод посылает процессу сообщение, предписывающее ему остановиться. Он возвращает логическое значениеtrue,если сообщение получено, и логическое значениеfalse,если приложение не имеет графического пользовательского интерфейса или главного окна. Следует, однако, иметь в виду, что методCloseMainWindow() служит только для запроса остановки процесса. Если приложение проигнорирует такой запрос, то оно не будет прервано как процесс.
Для безусловного прерывания процесса следует вызвать методKill (), как показано ниже.
public void Kill()
Но методомKill() следует пользоваться аккуратно, так как он приводит к неконтролируемому прерыванию процесса. Любые несохраненные данные, связанные с прерываемым процессом, будут, скорее всего, потеряны.
Для того чтобы организовать ожидание завершения процесса, можно воспользоваться методомWaitForExit (). Ниже приведены две его формы.
public void WaitForExit()
public bool WaitForExit(intмиллисекунд)
В первой форме ожидание продолжается до тех пор, пока процесс не завершится, а во второй форме — только в течение указанного количествамиллисекунд.В последнем случае методWaitForExit() возвращает логическое значениеtrue,если процесс завершился, и логическое значениеfalse,если он все еще выполняется.
В приведенном ниже примере программы демонстрируется создание, ожидание и закрытие процесса. В этой программе сначала запускается стандартная сервисная программа Windows: текстовый редактор WordPad.exe, а затем организуется ожидание завершения программы WordPad как процесса.
// Продемонстрировать запуск нового процесса.
using System;
using System.Diagnostics;
class StartProcess { static void Main() {
Process newProc =r Process.Start("wordpad.exe");
Console.WriteLine("Новый процесс запущен.");
newProc.WaitForExit();
newProc.Close(); // освободить выделенные ресурсы Console.WriteLine("Новый процесс завершен.");
}
}
При выполнении этой программы запускается стандартное приложение WordPad, и на экране появляется сообщение "Новый процесс запущен. ". Затем программа ожидает закрытия WordPad. По окончании работы WordPad на экране появляется заключительное сообщение "Новый процесс завершен.11.
ГЛАВА 24 Многопоточное программирование. Часть вторая: библиотека TPL
Вероятно, самым главным среди новых средств, внедренных в версию 4.0 среды .NET Framework, является библиотека распараллеливания задач (TPL). Эта библиотека усовершенствует многопоточное программирование двумя основными способами. Во-первых, она упрощает создание и применение многих потоков. И во-вторых, она позволяет автоматически использовать несколько процессоров. Иными словами, TPL открывает возможности для автоматического масштабирования приложений с целью эффективного использования ряда доступных процессоров. Благодаря этим двух особенностям библиотеки TPL она рекомендуется в большинстве случаев к применению для организации многопоточной обработки.
Еще одним средством параллельного программирования, внедренным в версию 4.0 среды .NET Framework, является параллельный язык интегрированных запросов (PLINQ). Язык PLINQ дает возможность составлять запросы, для обработки которых автоматически используется несколько процессоров, а также принцип параллелизма, когда это уместно. Как станет ясно из дальнейшего, запросить параллельную обработку запроса очень просто. Следовательно, с помощью PLINQ можно без особого труда внедрить параллелизм в запрос.
Главной причиной появления таких важных новшеств, как TPL и PLINQ, служит возросшее значение параллелизма в современном программировании. В настоящее время многоядерные процессоры уже стали обычным явлением.
Кроме того, постоянно растет потребность в повышении производительности программ. Все это, в свою очередь, вызвало растущую потребность в механизме, который
позволялбы с выгодой использовать несколько процессов для повышения производительности программного обеспечения. Но дело в том, что в прошлом это было не так-то просто сделать ясным и допускающим масштабирование способом. Изменить это положение, собственно, и призваны TPL и PLINQ. Ведь они дают возможность легче (и безопаснее) использовать системные ресурсы.
Библиотека TPL определена в пространстве именSystem. Threading . Tasks.Но для работы с ней обычно требуется также включать в программу классSystem. Threading,поскольку он поддерживает синхронизацию и другие средства многопоточной обработки, в том числе и те, что входят в классInterlocked.
В этой главе рассматривается и TPL, и PLINQ. Следует, однако, иметь в виду, что и та и другая тема довольно обширны. Поэтому в этой главе даются самые основы и рассматриваются некоторые простейшие способы применения TPL и PLINQ. Таким образом, материал этой главы послужит вам в качестве удобной отправной точки для дальнейшего изучения TPL и PLINQ. Если параллельное программирование входит в сферу ваших интересов, то именно эти средства .NET Framework вам придется изучить более основательно.
ПРИМЕЧАНИЕ
Несмотря на то что применение TPL и PLINQ рекомендуется теперь для разработки большинства многопоточных приложений, организация многопоточной обработки на основе класса Thread, представленного в главе 23, по-прежнему находит широкое распространение. Кроме того, многое из того, что пояснялось в главе 23, применимо и к TPL. Поэтому усвоение материала главы 23 все еще необходимо для полного овладения особенностями организации многопоточной обработки на С#.
Два подхода к параллельному программированию
Применяя TPL, параллелизм в программу можно ввести двумя основными способами. Первый из них называетсяпараллелизмом данных.При таком подходе одна операция над совокупностью данных разбивается на два параллельно выполняемых потока или больше, в каждом из которых обрабатывается часть данных. Так, если изменяется каждый элемент массива, то, применяя параллелизм данных, можно организовать параллельную обработку разных областей массива в двух или больше потоках. Нетрудно догадаться, что такие параллельно выполняющиеся действия могут привести к значительному ускорению обработки данных по сравнению с последовательным подходом. Несмотря на то что параллелизм данных был всегда возможен и с помощью классаThread,построение масштабируемых решений средствами этого класса требовало немало усилий и времени. Это положение изменилось с появлением библиотеки TPL, с помощью которой масштабируемый параллелизм данных без особого труда вводится в программу.
Второй способ ввода параллелизм называетсяпараллелизмом задач.При таком подходе две операции или больше выполняются параллельно. Следовательно, параллелизм задач представляет собой разновидность параллелизма, который достигался в прошлом средствами классаThread.А к преимуществам, которые сулит применение TPL, относится простота применения и возможность автоматически масштабировать исполнение кода на несколько процессоров.
Класс Task
В основу TPL положен классTask.Элементарная единица исполнения инкапсулируется в TPL средствами классаTask,а неThread.КлассTaskотличается от классаThreadтем, что он является абстракцией, представляющей асинхронную операцию. А в классеThreadинкапсулируется поток исполнения. Разумеется, на системном уровне поток по-прежнему остается элементарной единицей исполнения, которую можно планировать средствами операционной системы. Но соответствие экземпляра объекта классаTaskи потока исполнения не обязательно оказывается взаимно-однозначным. Кроме того, исполнением задач управляет планировщик задач, который работает с пулом потоков. Это, например, означает, что несколько задач могут разделять один и тот же поток. КлассTask(и вся остальная библиотека TPL) определены в пространстве именSystem.Threading.Tasks.
Создание задачи
Создать новую задачу в виде объекта классаTaskи начать ее исполнение можно самыми разными способами. Для начала создадим объект типаTaskс помощью конструктора и запустим его, вызвав методStart (). Для этой цели в классеTaskопределено несколько конструкторов. Ниже приведен тот конструктор, которым мы собираемся воспользоваться:
public Task(Actionдействие)
гдедействиеобозначает точку входа в код, представляющий задачу, тогда какAction— делегат, определенный в пространстве именSystem.Форма делегатаAction,которой мы собираемся воспользоваться, выглядит следующим образом.
public delegate void Action()
Таким образом, точкой входа должен служить метод, не принимающий никаких параметров и не возвращающий никаких значений. (Как будет показано далее, делегатуActionможно также передать аргумент.)
Как только задача будет создана, ее можно запустить на исполнение, вызвав методStart (). Ниже приведена одна из его форм.
public void Start()
После вызова метода Start () планировщик задач запланирует исполнение задачи. В приведенной ниже программе все изложенное выше демонстрируется на практике. В этойпрограмме отдельная задача создается на основе метода MyTask ().Послетого как начнет выполняться метод Main (), задача фактически создается и запускается на исполнение. Оба метода MyTask () и Main () выполняются параллельно.
// Создать и запустить задачу на исполнение.
using System;
using System.Threading;
using System.Threading.Tasks;
class DemoTask {
static void MyTask() {
Console.WriteLine("MyTask() запущен");
for(int count = 0; count < 10; count++) {
Thread.Sleep(500);
Console.WriteLine("В методе MyTask(), подсчет равен " + count);
}
Console.WriteLine("MyTask завершен");
}
static void Main() {
Console.WriteLine("Основной поток запущен.");
// Сконструировать объект задачи.
Task tsk = new Task(MyTask);
// Запустить задачу на исполнение, tsk.Start ();
// метод Main() активным до завершения метода MyTask(). for(int i = 0; i < 60; i++) {
Console.Write(".");
Thread.Sleep(100);
}
Console.WriteLine("Основной поток завершен.");
}
}
Ниже приведен результат выполнения этой программы. (У вас он может несколько отличаться в зависимости от загрузки задач, операционной системы и прочих факторов.)
Основной поток запущен.
.MyTask() запущен
MyTask завершен
.........Основной поток завершен.
Следует иметь в виду, что по умолчанию задача исполняется в фоновом потоке. Следовательно, при завершении создающего потока завершается и сама задача. Именно поэтому в рассматриваемой здесь программе методThread. SleepС) использован для сохранения активным основного потока до тех пор, пока не завершится выполнение методаMyTask(). Как и следовало ожидать, организовать ожидание завершения задачи можно и более совершенными способами, что и будет показано далее.
В приведенном выше примере программы задача, предназначавшаяся для параллельного исполнения, обозначалась в виде статического метода. Но такое требование к задаче не является обязательным. Например, в приведенной ниже программе, которая является переработанным вариантом предыдущей, метод MyTask (), выполняющий роль задачи, инкапсулирован внутри класса.
// Использовать метод экземпляра в качестве задачи.
using System;
using System.Threading;
using System.Threading.Tasks;
class MyClass {
// Метод выполняемый в качестве задачи, public void MyTask() {
Console.WriteLine("MyTask() запущен");
for(int count = 0; count < 10; count++) {
Thread.Sleep(500);
Console.WriteLine("В методе MyTask(), подсчет равен " + count);
}
Console.WriteLine("MyTask завершен ");
}
}
class DemoTask {
static void Main() {
Console.WriteLine("Основной поток запущен.");
// Сконструировать объект типа MyClass.
MyClass me = new MyClass();
// Сконструировать объект задачи для метода mc.MyTask().
Task tsk = new Task(me.MyTask);
// Запустить задачу на исполнение, tsk.Start();
// Сохранить метод Main() активным до завершения метода MyTask(). for(int i = 0; i < 60; i++) {
Console.Write (".");
Thread.Sleep (100);
}
Console.WriteLine("Основной поток завершен.");
}
}
Результат выполнения этой программы получается таким же, как и прежде. Единственное отличие состоит в том, что метод MyTask () вызывается теперь для экземпляра объекта класса MyClass.
В отношении задач необходимо также иметь в виду следующее: после того, как задача завершена, она не может быть перезапущена. Следовательно, иного способа повторного запуска задачи на исполнение, кроме создания ее снова, не существует.
Применение идентификатора задачи
В отличие от классаThread;в классеTaskотсутствует свойствоNameдля хранения имени задачи. Но вместо этого в нем имеется свойствоIdдля хранения идентификатора задачи, по которому можно распознавать задачи. Свойство Id доступно только для чтения и относится к типуint.Оно объявляется следующим образом.
public int Id { get; }
Каждая задача получает идентификатор, когда она создается. Значения идентификаторов уникальны, но не упорядочены. Поэтому один идентификатор задачи может появиться перед другим, хотя он может и не иметь меньшее значение.
Идентификатор исполняемой в настоящий момент задачи можно выявить с помощью свойстваCurrentld.Это свойство доступно только для чтения, относится к типуstaticи объявляется следующим образом.
public static Nullable<int> CurrentID { get; }
Оно возвращает исполняемую в настоящий момент задачу или же пустое значение, если вызывающий код не является задачей.
В приведенном ниже примере программы создаются две задачи и показывается, какая из них исполняется.
// Продемонстрировать применение свойств Id и Currentld.
using System;
using System.Threading;
using System.Threading.Tasks;
class DemoTask {
// Метод, исполняемый как задача, static void MyTaskO {
Console.WriteLine("MyTask() №" + Task.Currentld + " запущен");
for (int count = 0; count < 10; count++) {
Thread.Sleep(500);
Console.WriteLine("В методе MyTaskO #" + Task.Currentld +
", подсчет равен " + count );
}
Console.WriteLine("MyTask №" + Task.Currentld + " завершен");
}
static void Main() {
Console.WriteLine("Основной поток запущен.");
// Сконструировать объекты двух задач.
Task tsk = new Task(MyTask);
Task tsk2 = new Task(MyTask);
// Запустить задачи на исполнение, tsk.Start(); tsk2.Start();
Console.WriteLine("Идентификатор задачи tsk: " + tsk.Id);
Console.WriteLine("Идентификатор задачи tsk2: " + tsk2.Id);
// Сохранить метод Main() активным до завершения остальных задач, for(int i = 0; i < 60; i++) {
Console.Write(".");
Thread.Sleep (100);
}
Console.WriteLine("Основной поток завершен.");
}
}
Выполнение этой программы приводит к следующему результату.
Основной поток запущен Идентификатор задачи tsk: 1 Идентификатор задачи tsk2: 2 .MyTask() №1 запущен MyTask() №2 запущен
MyTask №1 завершен
В методе MyTask() №2, подсчет равен 9 MyTask №2 завершен .........Основной поток завершен.
Применение методов ожидания
В приведенных выше примерах основной поток исполнения, а по существу, методMain(), завершался потому, что такой результат гарантировали вызовы методаThread. Sleep(). Но подобный подход нельзя считать удовлетворительным.
Организовать ожидание завершения задач можно и более совершенным способом, применяя методы ожидания, специально предоставляемые в классеTask.Самым простым из них считается методWait (), приостанавливающий исполнение вызывающего потока до тех пор, пока не завершится вызываемая задача. Ниже приведена простейшая форма объявления этого метода.
public void Wait()
При выполнении этого метода могут быть сгенерированы два исключения. Первым из них является исключениеOb j ectDisposedException.Оно генерируется в том случае, если задача освобождена посредством вызова методаDispose(). А второе исключение,AggregateException,генерируется в том случае, если задача сама генерирует исключение или же отменяется. Как правило, отслеживается и обрабатывается именно это исключение. В связи с тем что задача может сгенерировать не одно исключение, если, например, у нее имеются порожденные задачи, все подобные исключения собираются в единое исключение типаAggregateException.Для того чтобы выяснить, что же произошло на самом деле, достаточно проанализировать внутренние исключения, связанные с этим совокупным исключением. А до тех пор в приведенных далее примерах любые исключения, генерируемые задачами, будут обрабатываться во время выполнения.
Ниже приведен вариант предыдущей программы, измененный с целью продемонстрировать применение методаWait() на практике. Этот метод используется внутри методаMain (), чтобы приостановить его выполнение до тех пор, пока не завершатся обе задачиtskиtsk2.
// Применить метод Wait().
using System;
using System.Threading;
using System.Threading.Tasks;
class DemoTask {
// Метод, исполняемый как задача, static void MyTask() {
Console.WriteLine("MyTask() №" + Task.Currentld + " запущен");
for(int count = 0; count < 10; count++) {
Thread.Sleep(500);
Console.WriteLine("В методе MyTask() #" + Task.Currentld +
", подсчет равен " + count );
}
Console.WriteLine("MyTask №" + Task.Currentld + " завершен");
}
static void Main() {
Console.WriteLine("Основной поток запущен.");
// Сконструировать объекты двух задач.
Task tsk = new Task(MyTask);
Task tsk2 = new Task(MyTask);
// Запустить задачи на исполнение, tsk.Start(); tsk2.Start() ;
Console.WriteLine("Идентификатор задачи tsk: " + tsk.Id); Console.WriteLine("Идентификатор задачи tsk2: " + tsk2.Id);
// Приостановить выполнение метода Main() до тех пор,
// пока не завершатся обе задачи tsk и tsk2 tsk.Wait () ; tsk2 .Wait () ;
Console.WriteLine("Основной поток завершен.");
}
}
При выполнении этой программы получается следующий результат.
Основной поток запущен Идентификатор задачи tsk: 1 Идентификатор задачи tsk2: 2 MyTask() №1 запущен MyTask() №2 запущен
MyTask №1 завершен
В методе MyTaskO №2, подсчет равен 9
MyTask №2 завершен Основной поток завершен.
Как следует из приведенного выше результата, выполнение метода Main () приостанавливается до тех пор, пока не завершатся обе задачи tsk и tsk2. Следует, однако, иметь в виду, что в рассматриваемой здесь программе последовательность завершения задач tsk и tsk2 не имеет особого значения для вызовов метода Wait (). Так, если первой завершается задача tsk2, то в вызове метода tsk. Wait () будет по-прежнему ожидаться завершение задачи tsk. В таком случае вызов метода tsk2 . Wait () приведет к выполнению и немедленному возврату из него, поскольку задача tsk2 уже завершена.
В данном случае оказывается достаточно двух вызовов метода Wait (), но того же результата можно добиться и более простым способом, воспользовавшись методом Wait АН (). Этот метод организует ожидание завершения группы задач. Возврата из него не произойдет до тех пор, пока не завершатся все задачи. Ниже приведена простейшая форма объявления этого метода.
public static void WaitAll(params Task[]tasks)
Задачи, завершения которых требуется ожидать, передаются с помощью параметра в виде массива tasks. А поскольку этот параметр относится к типу params, то данному методу можно отдельно передать массив объектов типа Task или список задач. При этом могут быть сгенерированы различные исключения, включая и AggregateException.
Для того чтобы посмотреть, как метод WaitAll () действует на практике, замените в приведенной выше программе следующую последовательность вызовов.
tsk.Wait (); tsk2.Wait ();
на
Task.WaitAll(tskf tsk2);
Программа будет работать точно так же, но логика ее выполнения станет более понятной.
Организуя ожидание завершения нескольких задач, следует быть особенно внимательным, чтобы избежать взаимоблокировок. Так, если две задачи ожидают завершения друг друга, то вызов метода WaitAll () вообще не приведет к возврату из него. Разумеется, условия для взаимоблокировок возникают в результате ошибок программирования, которых следует избегать. Следовательно, если вызов метода WaitAll () не приводит к возврату из него, то следует внимательно проанализировать, могут ли две задачи или больше взаимно блокироваться. (Вызов метода Wait (), который не приводит к возврату из него, также может стать причиной взаимоблокировок.)
Иногда требуется организовать ожидание до тех пор, пока не завершится любая из группы задач. Для этой цели служит метод Wait Ап у (). Ниже приведена простейшая форма его объявления.
public static int WaitAny(params Task[]tasks)
Задачи, завершения которых требуется ожидать, передаются с помощью параметра в виде массива tasks объектов типа Task или отдельного списка аргументов типа Task. Этот метод возвращает индекс задачи, которая завершается первой. При этом могут быть сгенерированы различные исключения.
Попробуйте применить метод WaitAny () на практике, подставив в предыдущей программе следующий вызов.
Task.WaitAny(tsk, tsk2);
Теперь, выполнение метода Main () возобновится, а программа завершится, как только завершится одна из двух задач.
Помимо рассматривавшихся здесь форм методов Wait(), WaitAll () и WaitAny (), имеются и другие их варианты, в которых можно указывать период простоя или отслеживать признак отмены. (Подробнее об отмене задач речь пойдет далее в этой главе.)
Вызов метода Dispose ()
В классеTaskреализуется интерфейсIDisposable,Bкотором определяется методDispose (). Ниже приведена форма его объявления.
public void Dispose ()
Метод Dispose () реализуется в классе Task, освобождая ресурсы, используемые этим классом. Как правило, ресурсы, связанные с классом Task, освобождаются автоматически во время "сборки мусора" (или по завершении программы). Но если эти ресурсы требуется освободить еще раньше, то для этой цели служит метод Dispose (). Это особенно важно в тех программах, где создается большое число задач, оставляемых на произвол судьбы.
Следует, однако, иметь в виду, что метод Dispose () можно вызывать для отдельной задачи только после ее завершения. Следовательно, для выяснения факта завершения отдельной задачи, прежде чем вызывать метод Dispose (), потребуется некоторый механизм, например, вызов метода Wait (). Именно поэтому так важно было рассмотреть метод Wait (), перед тем как обсуждать метод Dispose (). Ели же попытаться вызвать Dispose () для все еще активной задачи, то будет сгенерировано исключение InvalidOperationException.
Во всех примерах, приведенных в этой главе, создаются довольно короткие задачи, которые сразу же завершаются, и поэтому применение метода Dispose () в этих примерах не дает никаких преимуществ. (Именно по этой причине вызывать метод Dispose () в приведенных выше программах не было никакой необходимости. Ведь все они завершались, как только завершалась задача, что в конечном итоге приводило к освобождению от остальных задач.) Но в целях демонстрации возможностей данного метода и во избежание каких-либо недоразумений метод Dispose () будет вызываться явным образом при непосредственном обращении с экземплярами объектов типа Task во всех последующих примерах программ. Если вы обнаружите отсутствие вызовов метода Dispose () в исходном коде, полученном из других источников, то не удивляйтесь этому. Опять же, если программа завершается, как только завершится задача, то вызывать метод Dispose () нет никакого смысла — разве что в целях демонстрации его применения.
Применение класса TaskFactory для запуска задачи
Приведенные выше примеры программы были составлены не так эффективно, как следовало бы, поскольку задачу можно создать и сразу же начать ее исполнение, вызвав методStartNew (), определенный в классеTaskFactory.В классеTaskFactoryпредоставляются различные методы, упрощающие создание задач и управление ими. По умолчанию объект классаTaskFactoryможет быть получен из свойстваFactory,доступного только для чтения в классеTask.Используя это свойство, можно вызвать любые методы классаTaskFactory.МетодStartNew() существует во множестве форм. Ниже приведена самая простая форма его объявления:
public Task StartNew(Actionaction)
гдеaction— точка входа в исполняемую задачу. Сначала в методеStartNew () автоматически создается экземпляр объекта типаTaskдля действия, определяемого параметромaction, а затем планируется запуск задачи на исполнение. Следовательно, необходимость в вызове методаStart() теперь отпадает.
Например, следующий вызов метода StartNew () в рассматривавшихся ранее программах приведет к созданию и запуску задачи tsk одним действием.
Task tsk = Task.Factory.StartNew(MyTask);
После этого оператора сразу же начнет выполняться метод MyTask ().
Метод StartNew () оказывается более эффективным в тех случаях, когда задача создается и сразу же запускается на исполнение. Поэтому именно такой подход и применяется в последующих примерах программ.
Применение лямбда-выражения в качестве задачи
Кроме использования обычного метода в качестве задачи, существует и другой, более рациональный подход: указать лямбда-выражение как отдельно решаемую задачу. Напомним, что лямбда-выражения являются особой формой анонимных функций. Поэтому они могут исполняться как отдельные задачи. Лямбда-выражения оказываются особенно полезными в тех случаях, когда единственным назначением метода является решение одноразовой задачи. Лямбда-выражения могут составлять отдельную задачу или же вызывать другие методы. Так или иначе, применение лямбда-выражения в качестве задачи может стать привлекательной альтернативой именованному методу.
В приведенном ниже примере программы демонстрируется применение лямбда-выражения в качестве задачи. В этой программе код метода MyTask () из предыдущих примеров программ преобразуется в лямбда-выражение.
// Применить лямбда-выражение в качестве задачи.
using System;
using System.Threading;
using System.Threading.Tasks;
class DemoLambdaTask { static void Main() {
Console.WriteLine("Основной поток запущен.");
// Далее лямбда-выражение используется для определения задачи.
Task tsk = Task.Factory.StartNew( () => {
Console.WriteLine("Задача запущена");
for (int count = 0; count < 10; count++) {
Thread.Sleep(500);
Console.WriteLine("Подсчет в задаче равен " + count );
}
Console.WriteLine("Задача завершена");
} );
// Ожидать завершения задачи tsk. tsk.Wait();
У/ Освободить задачу tsk. tsk.Dispose();
Console.WriteLine("Основной поток завершен.");
}
}
Ниже приведен результат выполнения этой программы.
Основной поток запущен.
Задача завершена Основной поток завершен.
Помимо применения лямбда-выражения для описания задачи, обратите также внимание в данной программе на то, что вызов метода tsk. Dispose () не делается до тех пор, пока не произойдет возврат из метода tsk. Wait (). Как пояснялось в предыдущем разделе, метод Dispose () можно вызывать только по завершении задачи. Для того чтобы убедиться в этом, попробуйте поставить вызов метода tsk. Dispose () в рассматриваемой здесь программе перед вызовом метода tsk .Wait (). Вы сразу же заметите, что это приведет к исключительной ситуации.
Создание продолжения задачи
Одной из новаторских и очень удобных особенностей библиотеки TPL является возможность создавать продолжение задачи.Продолжение —это одна задача, которая автоматически начинается после завершения другой задачи. Создать продолжение можно, в частности, с помощью методаContinueWith(), определенного в классеTask.Ниже приведена простейшая форма его объявления:
public Task ContinueWith(Action<Task>действие_продолженмя)
гдедействие_продолженияобозначает задачу, которая будет запущена на исполнение по завершении вызывающей задачи. У делегатаActionимеется единственный параметр типаTask.Следовательно, вариант делегатаAction,применяемого в данном методе, выглядит следующим образом.
public delegate void Action<in T>(T obj)
Вданном случае обобщенный параметр Т обозначает класс Task.
Продолжение задачи демонстрируется на примере следующей программы.
// Продемонстрировать продолжение задачи.
using System;
using System.Threading;
using System.Threading.Tasks;
class ContinuationDemo {
// Метод, исполняемый как задача, static void MyTaskO {
Console.WriteLine("MyTask() запущен");
for(int count = 0; count < 5; count++) {
Thread.Sleep(500);
Console.WriteLine("В методе MyTaskO подсчет равен " + count );
}
Console.WriteLine("MyTask завершен");
}
// Метод, исполняемый как продолжение задачи, static void ContTask(Task t) {
Console.WriteLine("Продолжение запущено");
for(int count = 0; count < 5; count++) {
Thread.Sleep(500);
Console.WriteLine("В продолжении подсчет равен " + count );
}
Console.WriteLine("Продолжение завершено");
}
static void Main() {
Console.WriteLine("Основной поток запущен.");
// Сконструировать объект первой задачи.
Task tsk = new Task(MyTask);
//А теперь создать продолжение задачи.
Task taskCont = tsk.ContinueWith(ContTask);
// Начать последовательность задач, tsk.Start () ;
// Ожидать завершения продолжения. taskCont.Wait();
tsk.Dispose(); taskCont.Dispose();
Console.WriteLine("Основной поток завершен.");
}
}
Ниже приведен результата выполнения данной программы.
Основной поток запущен.
MyTas к() запущен
В методе MyTaskO подсчет равен 0
MyTask() завершен Продолжение запущено В продолжении подсчет равен О В продолжении подсчет равен 1 В продолжении подсчет равен 2 В продолжении подсчет равен 3 В продолжении подсчет равен 4 Продолжение завершено Основной поток завершен.
Как следует из приведенного выше результата, вторая задача не начинается до тех пор, пока не завершится первая. Обратите также внимание на то, что в методеMain() пришлось ожидать окончания только продолжения задачи. Дело в том, что методMyTask() как задача завершается еще до начала методаContTaskкак продолжения задачи. Следовательно, ожидать завершения методаMyTask() нет никакой надобности, хотя если и организовать такое ожидание, то в этом будет ничего плохого.
Любопытно, что в качестве продолжения задачи нередко применяется лямбда-выражение. Для примера ниже приведен еще один способ организации продолжения задачи из предыдущего примера программы.
//В данном случае в качестве продолжения задачи применяется лямбда-выражение. Task taskCont = tsk.ContinueWith((first) =>
{
Console.WriteLine("Продолжение запущено"); for(int count = 0; count < 5; count++) {
Thread.Sleep (500);
Console.WriteLine("В продолжении подсчет равен " + count );
}
Console.WriteLine("Продолжение завершено");
}
);
В этом фрагменте кода параметр first принимает предыдущую задачу (в данном случае — tsk).
Помимо методаContinueWith(), в классеTaskпредоставляются идругиеметоды, поддерживающие продолжение задачи, обеспечиваемое классомTaskFactory.К их числу относятся различные формы методовContinueWhenAny ()иContinueWhenAll (),которые продолжают задачу, если завершится любая или все указанные задачи соответственно.
Возврат значения из задачи
Задача может возвращать значение. Это очень удобно по двум причинам. Во-первых, это означает, что с помощью задачи можно вычислить некоторый результат. Подобным образом поддерживаются параллельные вычисления. И во-вторых, вызывающий процесс окажется блокированным до тех пор, пока не будет получен результат. Это означает, что для организации ожидания результата не требуется никакой особой синхронизации.
Длятого чтобы возвратить результат из задачи, достаточно создать эту задачу, используя обобщенную формуTask<TResult>классаTask.Ниже приведены два конструктора этой формы классаTask:
public Task(Func<TResult>функция)
public Task(FuncCObject, TResult>функция,Objectсостояние)
гдефункцияобозначает выполняемый делегат. Обратите внимание на то, что он должен быть типаFunc,а неAction.ТипFuncиспользуется именно в тех случаях, когда задача возвращает результат. В первом конструкторе создается задача без аргументов, а во втором конструкторе — задача, принимающая аргумент типаObject,передаваемый каксостояние.Имеются также другие конструкторы данного класса.
Как и следовало ожидать, имеются также другие варианты методаStartNew (),доступные в обобщенной форме классаTaskFactory<TResult>и поддерживающие возврат результата из задачи. Ниже приведены те варианты данного метода, которые применяются параллельно с только что рассмотренными конструкторами классаTask.
public Task<TResult> StartNew(Func<TResult>функция)
public Task<TResult> StartNew(Func<Object,TResult>функция,Objectсостояние)
В любом случае значение, возвращаемое задачей, получается из свойстваResultв классеTask,которое определяется следующим образом.
public TResult Result { get; internal set; }
Аксессорsetявляется внутренним для данного свойства, и поэтому оно оказывается доступным во внешнем коде, по существу, только для чтения. Следовательно, задача получения результата блокирует вызывающий код до тех пор, пока результат не будет вычислен.
В приведенном ниже примере программы демонстрируется возврат задачей значений. В этой программе создаются два метода. Первый из них,MyTask (), не принимает параметров, а просто возвращает логическое значениеtrueтипаbool.Второй метод,Sumlt (), принимает единственный параметр, который приводится к типуint,и возвращает сумму из значения, передаваемого в качестве этого параметра.
// Возвратить значение из задачи.
using System;
using System.Threading;
using System.Threading.Tasks;
class DemoTask {
// Простейший метод, возвращающий результат и не принимающий аргументов, static bool MyTask() {
return true;
}
// Этот метод возвращает сумму из положительного целого значения,
// которое ему передается в качестве единственного параметра static int Sumlt(object v) { int x = (int) v; int sum = 0;
\
for(; х > 0; х—) sum += х;
return sum;
}
static void Main() {
Console.WriteLine("Основной поток запущен.");
// Сконструировать объект первой задачи.
Task<bool> tsk = Task<bool>.Factory.StartNew(MyTask);
Console.WriteLine("Результат после выполнения задачи MyTask: " + tsk.Result);
// Сконструировать объект второй задачи.
Task<int> tsk2 = Task<int>.Factory.StartNew(Sumlt, 3);
Console.WriteLine("Результат после выполнения задачи Sumlt: " + tsk2.Result);
tsk.Dispose(); tsk2.Dispose();
Console.WriteLine("Основной поток завершен.");
}
}
Выполнение этой программы приводит к следующему результату.
Основной поток запущен.
Результат после выполнения задачи MyTask: True Результат после выполнения Sumlt: 6 Основной поток завершен.
Помимо упомянутых выше форм классаTask<TResult>и методаStartNew<TResult>,имеются также другие формы. Они позволяют указывать другие дополнительные параметры.
Отмена задачи и обработка исключения AggregateException
В версии 4.0 среды .NET Framework внедрена новая подсистема, обеспечивающая структурированный, хотя и очень удобный способ отмены задачи. Эта новая подсистема основывается на понятиипризнака отмены.Признаки отмены поддерживаются в классеTask,среди прочего, с помощью фабричного методаStartNew ().
ПРИМЕЧАНИЕ
Новую подсистему отмены можно применять и для отмены потоков, рассматривавшихся в предыдущей главе, но она полностью интегрирована в TPL и PLINQ. Именно поэтому эта подсистема рассматривается в этой главе.
Отмена задачи, как правило, выполняется следующим образом. Сначала получается признак отмены из источника признаков отмены. Затем этот признак передается задаче, после чего она должна контролировать его на предмет получения запроса на отмену. (Этот запрос может поступить только из источника признаков отмены.) Если получен запрос на отмену, задача должна завершиться. В одних случаях этого оказывается достаточно для простого прекращения задачи без каких-либо дополнительных действий, а в других — из задачи должен быть вызван методThrowIfCancellationRequested() для признака отмены. Благодаря этому в отменяющем коде становится известно, что задача отменена. А теперь рассмотрим процесс отмены задачи более подробно.
'Признак отмены является экземпляром объекта типаCancellationToken,т.е. структуры, определенной в пространстве именSystem. Threading.В структуреCancellationTokenопределено несколько свойств и методов, но мы воспользуемся двумя из них. Во-первых, это доступное только для чтения свойствоIsCancellation Re quested,которое объявляется следующим образом.
public bool IsCancellationRequested { get; }
Оно возвращает логическое значениеtrue,если отмена задачи была запрошена для вызывающего признака, а иначе — логическое значениеfalse.И во-вторых, это методThrowIfCancellationRequested (), который объявляется следующим образом.
public void ThrowIfCancellationRequested()
Если признак отмены, для которого вызывается этот метод, получил запрос на отмену, то в данном методе генерируется исключениеOperationCanceledException.В противномслучаеникаких действий не выполняется. В отменяющем коде можно организовать отслеживание упомянутого исключения с целью убедиться в том, что отмена задачи действительно произошла. Как правило, с этой целью сначала перехватывается исключениеAggregateException,а затем его внутреннее исключение анализируется с помощью свойстваInnerExceptionилиInnerExceptions.(СвойствоInnerExceptionsпредставляет собой коллекцию исключений. Подробнее о коллекциях речь пойдет в главе 25.)
Признак отмены получается из источника признаков отмены, который представляет собой объект классаCancellationTokenSource,определенного в пространстве именSystem. Threading.Для того чтобы получить данный признак, нужно создать сначала экземпляр объекта типаCancellationTokenSource.(С этой целью можно воспользоваться вызываемым по умолчанию конструктором классаCancellationTokenSource.)Признак отмены, связанный сданным источником, оказывается доступным через используемое только для чтения свойствоToken,которое объявляется следующим образом.
public CancellationToken Token { get; }
Это и есть тот признак, который должен быть передан отменяемой задаче.
Для отмены в задаче должна быть получена копия признака отмены и организован контроль этого признака с целью отслеживать саму отмену. Такое отслеживание можно организовать тремя способами: опросом, методом обратного вызова и с помощью дескриптора ожидания. Проще всего организовать опрос, и поэтому здесь будет рассмотрен именно этот способ. С целью опроса в задаче проверяется упомянутое выше свойствоIsCancellationRequestedпризнака отмены. Если это свойство содержит логическое значениеtrue,значит, отмена была запрошена, и задача должна быть завершена. Опрос может оказаться весьма эффективным, если организовать его правильно. Так, если задача содержит вложенные циклы, то проверка свойстваIsCancellationRequestedво внешнем цикле зачастую дает лучший результат, чем его проверка на каждом шаге внутреннего цикла.
Для создания задачи, из которой вызывается методThrowIfCancellationRequested (),когда она отменяется, обычно требуется передать признак отмены как самой задаче, так и конструктору классаTask,будь то непосредственно или же косвенно через методStartNew (). Передача признака отмены самой задаче позволяет изменить состояние отменяемой задачи в запросе на отмену из внешнего кода. Далее будет использована следующая форма методаStartNew().
public Task StartNew(Action<Object> action, Objectсостояние,
CancellationTokenпризнак_отмены)
В этой форме признак отмены передается через параметры, обозначаемые каксостояниеипризнак_отмены.Это означает, что признак отмены будет передан как делегату, реализующему задачу, так и самому экземпляру объекта типаTask.Ниже приведена форма, поддерживающая делегатAction.
public delegate void Actioncin T>(Tobj)
В данном случае обобщенный параметрТобозначает типObject.В силу этого объектobjдолжен быть приведен внутри задачи к типуCancel lationToken.
И еще одно замечание: по завершении работы с источником признаков отмены следует освободить его ресурсы, вызвав методDispose ().
Факт отмены задачи может быть проверен самыми разными способами. Здесь применяется следующий подход: проверка значения свойстваIsCanceledдля экземпляра объекта типаTask.Если это логическое значениеtrue,то задача была отменена.
В приведенной ниже программе демонстрируется отмена задачи. В ней применяется опрос для контроля состояния признака отмены. Обратите внимание на то, что методThrowIfCancellationRequested() вызывается после входа в методMyTask (). Это дает возможность завершить задачу, если она была отмена еще до ее запуска. Внутри цикла проверяется свойствоIsCancellationRequested.Если это свойство содержит логическое значениеtrue,а оно устанавливается после вызова методаCancel() для экземпляра источника признаков отмены, то на экран выводится сообщение об отмене и далее вызывается методThrowIfCancellationRequested() для отмены задачи.
// Простой пример отмены задачи с использованием опроса.
using System;
using System.Threading;
using System.Threading.Tasks;
class DemoCancelTask {
// Метод, исполняемый как задача, static void MyTask(Object ct) {
CancellationToken cancelTok = (CancellationToken) ct;
// Проверить, отменена ли задача, прежде чем запускать ее. cancelTok.ThrowIfCancellationRequested();
Console.WriteLine("MyTask() запущен");
for(int count = 0; count < 10; count++) {
// В данном примере для отслеживания отмены задачи применяется опрос, if(cancelTok.IsCancellationRequested) {
Console.WriteLine("Получен запрос на отмену задачи."); cancelTok.ThrowIfCancellationRequested();
}
Thread.Sleep(500);
Console.WriteLine("В методе MyTask() подсчет равен " + count );
}
Console.WriteLine("MyTask завершен");
}
static void Main() {
Console.WriteLine("Основной поток запущен.");
// Создать объект источника признаков отмены.
CancellationTokenSource cancelTokSrc = new CancellationTokenSource();
// Запустить задачу, передав признак отмены ей самой и делегату.
Task tsk = Task.Factory.StartNew(MyTask, cancelTokSrc.Token,
cancelTokSrc.Token);
// Дать задаче возможность исполняться вплоть до ее отмены.
Thread.Sleep(2000); try {
// Отменить задачу. cancelTokSrc.Cancel();
// Приостановить выполнение метода Main() до тех пор,
// пока не завершится задача tsk. tsk.Wait();
} catch (AggregateException exc) { if(tsk.IsCanceled)
Console.WriteLine("ХпЗадача tsk отменена\п");
// Для просмотра исключения снять комментарии со следующей строки кода:
// Console.WriteLine(exc);
} finally {
tsk.Dispose(); cancelTokSrc.Dispose();
}
Console.WriteLine("Основной поток завершен.");
}
}
Ниже приведен результат выполнения этой программы. Обратите внимание на то что задача отменяется через 2 секунды.
Основной поток запущен.
MyTask() запущен
Получен запрос на отмену задачи.
Задача tsk отменена
Основной поток завершен.
Как следует из приведенного выше результата, выполнение методаMyTask ()отменяется в методеMain() лишь две секунды спустя. Следовательно, в методеMyTask() выполняются четыре шага цикла. Когда же перехватывается исключениеAggregateException,проверяется состояние задачи. Если задачаtskотменена, что и должно произойти в данном примере, то об этом выводится соответствующее сообщение. Следует, однако, иметь в виду, что когда сообщениеAggregateExceptionгенерируется в ответ на отмену задачи, то это еще не свидетельствует об ошибке, а просто означает, что задача была отменена.
Выше были изложены лишь самые основные принципы, положенные в основу отмены задачи и генерирования исключенияAggregateException.Тем не менее эта тема намного обширнее и требует от вас самостоятельного и углубленного изучения, если вы действительно хотите создавать высокопроизводительные, масштабируемые приложения.
Другие средства организации задач
В предыдущих разделах был описан ряд понятий и основных способов организации и исполнения задач. Но имеются и другие полезные средства. В частности, задачи можно делать вложенными, когда одни задачи способны создавать другие, или же порожденными, когда вложенные задачи оказываются тесно связанными с создающей их задачей.
В предыдущем разделе было дано краткое описание исключенияAggregateException,но у него имеются также другие особенности, которые могут оказаться весьма полезными. К их числу относится методFlatten(), применяемый для преобразования любых внутренних исключений типаAggregateExceptionв единственное исключениеAggregateException.Другой метод,Handle(), служит для обработки исключения, составляющего совокупное исключениеAggregateException.
При создании задачи имеется возможность указать различные дополнительные параметры, оказывающие влияние на особенности ее исполнения. Для этой цели указывается экземпляр объекта типаTaskCreationOptionsв конструкторе классаTaskили же в фабричном методеStartNew(). Кроме того, в классеTaskFactoryдоступно целое семейство методовFromAsync(), поддерживающих модель асинхронного программирования (АРМ — Asynchronous Programming Model).
Как упоминалось ранее в этой главе, задачи планируются на исполнение экземпляром объекта классаTaskScheduler.Как правило, для этой цели предоставляется планировщик, используемый по умолчанию в среде .NET Framework. Но этот планировщик может быть настроен под конкретные потребности разработчика. Кроме того, допускается применение специализированных планировщиков задач.
Класс Parallel
В примерах, приведенных до сих пор в этой главе, демонстрировались ситуации, в которых библиотека TPL использовалась таким же образом, как и классThread.Но это было лишь самое элементарное ее применение, поскольку в TPL имеются и другие средства. К их числу относится классParallel,который упрощает параллельное исполнение кода и предоставляет методы, рационализирующие оба вида параллелизма: данных и задач.
КлассParallelявляется статическим, и в нем определены методыFor(),For Each() иInvoke(). У каждого из этих методов имеются различные формы. В частности, методFor () выполняет распараллеливаемый циклfor,а методForEach () —распараллеливаемый циклforeach,и оба метода поддерживают параллелизм данных. А методInvoke() поддерживает параллельное выполнение двух методов или больше. Как станет ясно дальше, эти методы дают преимущество реализации на практике распространенных методик параллельного программирования, не прибегая к управлению задачами или потоками явным образом. В последующих разделах каждый из этих методов будет рассмотрен более подробно.
Распараллеливание задач методом Invoke ()
МетодInvoke(), определенный в классеParallel,позволяет выполнять один или несколько методов, указываемых в виде его аргументов. Он также масштабирует исполнение кода, используя доступные процессоры, если имеется такая возможность. Ниже приведена простейшая форма его объявления.
public static void Invoke(params Action[]actions)
Выполняемые методы должны быть совместимы с описанным ранее делегатомAction.Напомним, что делегатActionобъявляется следующим образом.
public delegate void Action()
Следовательно, каждый метод, передаваемый методуInvoke() в качестве аргумента, не должен ни принимать параметров, ни возвращать значение. Благодаря тому что параметрactionsданного метода относится к типуparams,выполняемые методы могут быть указаны в виде переменного списка аргументов. Для этой цели можно также воспользоваться массивом объектов типаAction,но зачастую оказывается проще указать список аргументов.
МетодInvoke() сначала инициирует выполнение, а затем ожидает завершения всех передаваемых ему методов. Это, в частности, избавляет от необходимости (да и не позволяет) вызывать методWait (). Все функции параллельного выполнения методWait() берет на себя. И хотя это не гарантирует, что методы будут действительно выполняться параллельно, тем не менее, именно такое их выполнение предполагается, если система поддерживает несколько процессоров. Кроме того, отсутствует возможность указать порядок выполнения методов от первого и до последнего, и этот порядок не может быть таким же, как и в списке аргументов.
В приведенном ниже примере программы демонстрируется применение методаInvoke() на практике. В этой программе два методаMyMeth() иMyMeth2() выполняются параллельно посредством вызова методаInvoke (). Обратите внимание на простоту организации данного процесса.
// Применить метод Parallel.Invoke() для параллельного выполнения двух методов.
using System;
using System.Threading;
using System.threading.Tasks;
class DemoParallel {
// Метод, исполняемый как задача, static void MyMeth() {
Console.WriteLine("MyMeth запущен");
for (int count = 0; count < 5; count++) {
Thread.Sleep(500);
Console.WriteLine("В методе MyMeth подсчет равен " + count );
}
Console.WriteLine("MyMeth завершен");
}
// Метод, исполняемый как задача, static void MyMeth2() {
Console.WriteLine("MyMeth2 запущен");
for(int count = 0; count < 5; count++) {
Thread.Sleep(500);
Console.WriteLine("В методе MyMeth2, подсчет равен " + count );
}
Console.WriteLine("MyMeth2 завершен");
}
static void Main() {
Console.WriteLine("Основной поток запущен.");
// Выполнить параллельно два именованных метода.
Parallel.Invoke(MyMeth, MyMeth2);
Console.WriteLine("Основной поток завершен.");
}
}
Выполнение этой программы может привести к следующему результату.
Основной поток запущен.
MyMeth() запущен MyMeth2() запущен В методе MyMeth() подсчет равен 0 В методе MyMeth2() подсчет равен 0 В методе MyMeth() подсчет равен 1 В методе MyMeth2() подсчет равен 1 В методе MyMeth() подсчет равен 2 В методе MyMeth2() подсчет равен 2 В методе MyMeth() подсчет равен 3
В методе MyMeth2() подсчет равен 3 В методе MyMethO подсчет равен 4 MyMeth() завершен
В методе MyMeth2() подсчет равен 4 MyMeth2() завершен Основной поток завершен.
В данном примере особое внимание обращает на себя следующее обстоятельство: выполнение метода Main () приостанавливается до тех пор, пока не произойдет возврат из метода Invoke (). Следовательно, метод Main (), в отличие от методов MyMeth () и MyMeth2 (), не выполняется параллельно. Поэтому применять метод Invoke () показанным здесь способом нельзя в том случае, если требуется, чтобы исполнение вызывающего потока продолжалось.
В приведенном выше примере использовались именованные методы, но для вызова методаInvoke() это условие не является обязательным. Ниже приведен переделанный вариант той же самой программы, где в качестве аргументов в вызове методаInvoke() применяются лямбда-выражения.
// Применить метод Parallel.Invoke() для параллельного выполнения двух методов. // В этой версии программы применяются лямбда-выражения.
using System;
using System.Threading;
using System.Threading.Tasks;
class DemoParallel {
static void Main() {
Console.WriteLine("Основной поток запущен.");
// Выполнить два анонимных метода, указываемых в лямбда-выражениях.
Parallel.Invoke( () => {
Console.WriteLine("Выражение #1 запущено");
for(int count = 0; count < 5; count++) {
Thread.Sleep(500);
Console.WriteLine("В выражении #1 подсчет равен " + count );
}
Console.WriteLine("Выражение #1 завершено");
},
О => {
Console.WriteLine("Выражение #2 запущено");
for (int count = 0; count < 5; count++) {
Thread.Sleep(500);
Console.WriteLine("В выражении #2 подсчет равен " + count );
}
Console.WriteLine("Основной поток завершен.");
}
}
Эта программа дает результат, похожий на результат выполнения предыдущей программы.
Применение метода For ()
В TPL параллелизм данных поддерживается, в частности, с помощью методаFor (),определенного в классеParallel.Этот метод существует в нескольких формах. Его рассмотрение мы начнем с самой простой формы, приведенной ниже:
public static ParallelLoopResult
For(intfromlnclusive,inttoExclusive,Action<int>body)
гдеfromlnclusiveобозначает начальное значение того, что соответствует переменной управления циклом; оно называется также итерационным, или индексным, значением; atoExclusive —значение, на единицу больше конечного. На каждом шаге цикла переменная управления циклом увеличивается на единицу. Следовательно, цикл постепенно продвигается от начального значенияfromlnclusiveк конечному значениюtoExclusiveминус единица. Циклически выполняемый код указывается методом, передаваемым через параметрbody.Этот метод должен быть совместим с делегатомAction<int>,объявляемым следующим образом.
public delegate void Action<in T>(Tobj)
ДляметодаFor() обобщенный параметрТдолжен быть, конечно, типаint.Значение, передаваемое через параметрobj,будет следующим значением переменной управления циклом. А метод, передаваемый через параметрbody, может быть именованным или анонимным. МетодFor() возвращает экземпляр объекта типаParallelLoopResult,описывающий состояние завершения цикла. Для простых циклов этим значением можно пренебречь. (Более подробно это значение будет рассмотрено несколько ниже.)
Главная особенность методаFor() состоит в том, что он позволяет, когда такая возможность имеется, распараллелить исполнение кода в цикле. А это, в свою очередь, может привести к повышению производительности. Например, процесс преобразования массива в цикле может быть разделен на части таким образом, чтобы разные части массива преобразовывались одновременно. Следует, однако, иметь в виду, что повышение производительности не гарантируется из-за отличий в количестве доступных процессоров в разных средах выполнения, а также из-за того, что распараллеливание мелких циклов может составить издержки, которые превышают сэкономленное время.
В приведенном ниже примере программы демонстрируется применение методаFor() на практике. В начале этой программы создается массивdata,состоящий из 1000000000 целых значений. Затем вызывается методFor (), которому в качестве "тела" цикла передается методMyTransf оггп (). Этот метод состоит из ряда операторов, выполняющих произвольные преобразования в массивеdata.Его назначение — сымитировать конкретную операцию. Как будет подробнее пояснено несколько ниже, выполняемая операция должна быть нетривиальной, чтобы параллелизм данных принес какой-то положительный эффект. В противном случае последовательное выполнение цикла может завершиться быстрее.
11Применить метод Parallel.For() для организации параллельно // выполняемого цикла обработки данных.
using System;
using System.Threading.Tasks;
class DemoParallelFor { static int[] data;
// Метод, служащий в качестве тела параллельно выполняемого цикла.
// Операторы этого цикла просто расходуют время ЦП для целей демонстрации, static void MyTrlansform (int i) { data[i] = data[i] / 10;
if(data[i] < 10000) data[i] = 0;
if(data[i] > 10000 & data[i] < 20000) data[i] = 100; if(data[i] > 20000 & data[i] < 30000) data[i] = 200; if(data[i] > 30000) data[i] = 300;
}
static void Main() {
Console.WriteLine("Основной поток запущен.");
data = new int[100000000];
// Инициализировать данные в обычном цикле for. for (int i=0; i < data.Length; i++) data[i] = i;
// Распараллелить цикл методом For().
Parallel.For(0, data.Length, MyTransform);
Console.WriteLine("Основной поток завершен.");
}
}
Эта программа состоит из двух циклов. В первом, стандартном, циклеforинициализируется массивdata.А во втором цикле, выполняемом параллельно методомFor (), над каждым элементом массиваdataпроизводится преобразование. Как упоминалось выше, это преобразование носит произвольный характер и выбрано лишь для целей демонстрации. МетодFor() автоматически разбивает вызовы методаMyTransform() на части для параллельной обработки отдельных порций данных, хранящихся в массиве. Следовательно, если запустить данную программу на компьютере с двумя доступными процессорами или больше, то цикл преобразования данных в массиве может быть выполнен методомFor() параллельно.
Следует, однако, иметь в виду, что далеко не все циклы могут выполняться эффективно, когда они распараллеливаются. Как правило, мелкие циклы, а также циклы, состоящие из очень простых операций, выполняются быстрее последовательным способом, чем параллельным. Именно поэтому циклforинициализации массива данных не распараллеливается методомFor() в рассматриваемой здесь программе. Распараллеливание мелких и очень простых циклов может оказаться неэффективным потому, что время, требующееся для организации параллельных задач, а также время, расходуемое на переключение контекста, превышает время, экономящееся благодаря параллелизму. В подтверждение этого факта в приведеннрм ниже примере программы
создаются последовательный и параллельный варианты цикла for, а для сравнения на экран выводится время выполнения каждого из них.
// Продемонстрировать отличия во времени последовательного //и параллельного выполнения цикла for.
using System;
using System.Threading.Tasks; using System.Diagnostics;
class DemoParallelFor { static int[] data;
// Метод, служащий в качестве тела параллельно выполняемого цикла.
// Операторы этого цикла просто расходуют время ЦП для целей демонстрации, static void MyTransform(int i) { data[i] = data[i] / 10;
if(data[i] < 1000) data[i] = 0;
if(data[i] > 1000 & data[i] < 2000) data[i] = 100; if(data[i] > 2000 & data[i] < 3000) data[i] = 200; if(data[i] > 3000) data[i] = 300;
}
static void Main() {
Console.WriteLine("Основной поток запущен.");
// Create экземпляр объекта типа Stopwatch // для хранения времени выполнения цикла.
Stopwatch sw = new Stopwatch ();
data = new int[100000000];
// Инициализировать данные, sw.Start ()';
// Параллельный вариант инициализации массива в цикле.
Parallel.For(0, data.Length, (i) => data[i] = i );
sw.Stop () ;
Console.WriteLine("Параллельно выполняемый цикл инициализации: " +
"{0} секунд", sw.Elapsed.TotalSeconds);
sw.Reset () ; sw.Start () ;
// Последовательный вариант инициализации массива в цикле, for(int i=0; i < data.Length; i++) data[i] = i;
sw.Stop ();
Console.WriteLine("Последовательно выполняемый цикл инициализации: " +
"{0} секунд", sw.Elapsed.TotalSeconds);
// Выполнить преобразования, sw.Start();
// Параллельный вариант преобразования данных в цикле.
Parallel.For(0, data.Length, MyTransform);
sw.Stop();
Console.WriteLine("Параллельно выполняемый цикл преобразования: " +
"{0} секунд", sw.Elapsed.TotalSeconds);
sw.Reset () ; sw.Start();
// Последовательный вариант преобразования данных в цикле, for(int i=0; i < data.Length; i++) MyTransform(i);
sw.Stop ();
Console.WriteLine("Последовательно выполняемый цикл преобразования: " +
"{0} секунд", sw.Elapsed.TotalSeconds);
Console.WriteLine("Основной поток завершен.");
}
}
При выполнении этой программы на двухъядерном компьютере получается следующий результат.
Основной поток запущен.
Параллельно выполняемый цикл инициализации: 1.0537757 секунд Последовательно выполняемый цикл инициализации: 0.3457628 секунд
Параллельно выполняемый цикл преобразования: 4.2246675 секунд Последовательно выполняемый цикл преобразования: 5.3849959 секунд Основной поток завершен.
Прежде всего, обратите внимание на то, что параллельный вариант цикла инициализации массива данных выполняется приблизительно в три раза медленнее, чем последовательный. Дело в том, что в данном случае на операцию присваивания расходуется так мало времени, что издержки на дополнительно организуемое распараллеливание превышают экономию, которую оно дает. Обратите далее внимание на то, что параллельный вариант цикла преобразования данных выполняется быстрее, чем последовательный. В данном случае экономия от распараллеливания с лихвой возмещает издержки на его дополнительную организацию.
ПРИМЕЧАНИЕ
Как правило, в отношении преимуществ, которые дает распараллеливание различных видов циклов, следует руководствоваться текущими рекомендациями корпорации Microsoft. Кроме того, необходимо убедиться в том, что распараллеливание цикла действительно приводит к повышению производительности, прежде чем использовать такой цикл в окончательно выпускаемом прикладном коде.
Что касается приведенной выше программы, то необходимо упомянуть о двух других ее особенностях. Во-первых, обратите внимание на то, что в параллельно выполняемом цикле для инициализации данных применяется лямбда-выражение, как показано ниже.
Parallel.For(0, data.Length, (i) => data[i] = i );
Здесь "тело" цикла указывается в лямбда-выражении. (Напомним, что в лямбда-выражении создается анонимный метод.) Следовательно, для параллельного выполнения методомFor() совсем не обязательно указывать именованный метод.
И во-вторых, обратите внимание на применение классаStopwatchдля вычисления времени выполнения цикла. Этот класс находится в пространстве именSystem. Diagnostics.Для того чтобы воспользоваться им, достаточно создать экземпляр его объекта, а затем вызвать методStart (), начинающий отчет времени, и далее — методStop(), завершающий отсчет времени. А с помощью методаReset() отсчет времени сбрасывается в исходное состояние. Продолжительность выполнения можно получить различными способами. В рассматриваемой здесь программе для этой цели использовано свойствоElapsed,возвращающее объект типаTimeSpan.С помощью этого объекта и свойстваTotalSecondsвремя отображается в секундах, включая и доли секунды. Как показывает пример рассматриваемой здесь программы, классStopwatchоказывается весьма полезным при разработке параллельно исполняемого кода.
Как упоминалось выше, методFor() возвращает экземпляр объекта типаParallelLoopResult.Это структура, в которой определяются два следующих свойства.
public bool IsCompleted { get; }
public Nullable<long> LowestBreaklteration { get; }
СвойствоIsCompletedбудет иметь логическое значениеtrue,если выполнены все шаги цикла. Иными словами, при нормальном завершении цикла это свойство будет содержать логическое значениеtrue.Если же выполнение цикла прервется раньше времени, то данное свойство будет содержать логическое значениеfalse.СвойствоLowestBreaklterationбудет содержать наименьшее значение переменной управления циклом, если цикл прервется раньше времени вызовом методаParallelLoopState.Break().
Для доступа к объекту типаParallelLoopStateследует использовать форму методаFor (), делегат которого принимает в качестве второго параметра текущее состояние цикла. Ниже эта форма методаFor() приведена в простейшем виде.
public static ParallelLoopResult For(intfromlnclusive,inttoExclusive,
Action<int, ParallelLoopState>body)
В данной форме делегатAction,описывающий тело цикла, определяется следующим образом.
public delegate void Action<in Tl, in T2>(Targl,T2arg2)
Для методаFor() обобщенный параметрTlдолжен быть типаint,а обобщенный параметрТ2— типаParallelLoopState.Всякий раз, когда делегатActionвызывается, текущее состояние цикла передается в качестве аргументаагд2.
Для преждевременного завершения цикла следует воспользоваться методомBreak (),вызываемым для экземпляра объекта типаParallelLoopStateвнутри тела цикла, определяемого параметромbody.МетодBreak() объявляется следующим образом.
Вызов методаBreak() формирует запрос на как можно более раннее прекращение параллельно выполняемого цикла, что может произойти через несколько шагов цикла после вызова методаBreak(). Но все шаги цикла до вызова методаBreak() все же выполняются. Следует, также иметь в виду, что отдельные части цикла могут и не выполняться параллельно. Так, если выполнено 10 шагов цикла, то это еще не означает, что все эти 10 шагов представляют 10 первых значений переменной управления циклом.
Прерывание цикла, параллельно выполняемого методомFor (), нередко оказывается полезным при поиске данных. Так, если искомое значение найдено, то продолжать выполнение цикла нет никакой надобности. Прерывание цикла может оказаться полезным и в том случае, если во время очередной операции встретились недостоверные данные.
В приведенном ниже примере программы демонстрируется применение методаBreak() для прерывания цикла, параллельно выполняемого методомFor().Это вариант предыдущего примера, переработанный таким образом, чтобы методMyTransform() принимал теперь объект типаParallelLoopStateв качестве своего параметра, а методBreak() вызывался при обнаружении отрицательного значения в массиве данных. Отрицательное значение, по которому прерывается выполнение цикла, вводится в массивdataвнутри методаMain (). Далее проверяется состояние завершения цикла преобразования данных. СвойствоIsCompletedбудет содержать логическое значениеfalse,поскольку в массивеdataобнаруживается отрицательное значение. При этом на экран выводится номер шага, на котором цикл был прерван. (В этой программе исключены все избыточные циклы, применявшиеся в ее предыдущей версии, а оставлены только самые эффективные из них: последовательно выполняемый цикл инициализации и параллельно выполняемый цикл преобразования.)
// Использовать объекты типа ParallelLoopResult и ParallelLoopState, а также А/ метод Break() вместе с методом For() для параллельного выполнения цикла.
using System;
using System.Threading.Tasks;
class DemoParallelForWithLoopResult { static int[] data;
// Метод, служащий в качестве тела параллельно выполняемого цикла.
// Операторы этого цикла просто расходуют время ЦП для целей демонстрации, static void MyTransform(int i, ParallelLoopState pis) {
// Прервать цикл при обнаружении отрицательного значения, if(data[i] < 0) pis.Break();
data[i] = data[i] / 10;
if(data[i] < 1000) data[i] = 0;
if(data[i] > 1000 & data[i] < 2000) data[i] = 100; if(data[i] > 2000 & data[i] < 3000) data[i] = 200; if(data[i] > 3000) data[i] = 300;
}
static void Main() {
Console.WriteLine("Основной поток запущен."); data = new int[100000000];
// Инициализировать данные.
for(int i=0; i < data.Length; i++) data[i] = i;
// Поместить отрицательное значение в массив data, data[1000] = -10;
// Параллельный вариант инициализации массива в цикле.
ParallelLoopResult loopResult = Parallel.For(0, data.Length, MyTransform);
// Проверить, завершился ли цикл, if(!loopResult.IsCompleted)
Console.WriteLine("\пЦикл завершился преждевременно из-за того, " +
"что обнаружено отрицательное значение\п" +
"на шаге цикла номер " +
loopResult.LowestBreaklteration + ".\n");
Console.WriteLine("Основной поток завершен.");
}
}
Выполнение этой программы может привести, например, к следующему результату.
Основной поток запущен.
Цикл завершился преждевременно из-за того, что обнаружено отрицательное значение на шаге цикла номер 1000
Основной поток завершен.
Как следует из приведенного выше результата, цикл преобразования данных преждевременно завершается после 1000 шагов. Дело в том, что методBreak() вызывается внутри методаMyTransform() при обнаружении в массиве данных отрицательного значения.
Помимо двух описанных выше форм методаFor() существует и ряд других его форм. В одних из этих форм допускается указывать различные дополнительные параметры, а в других — использовать параметры типаlongвместоintдля пошагового выполнения цикла. Имеются также формы методаFor (), предоставляющие такие дополнительные преимущества, как, например, возможность указывать метод, вызываемый по завершении потока каждого цикла.
И еще одно, последнее замечание: если требуется остановить цикл, параллельно выполняемый методомFor (), не обращая особого внимания на любые шаги цикла, которые еще могут быть в нем выполнены, то для этой цели лучше воспользоваться методомStop (), чем методомBreak ().
Применение метода ForEach ()
Используя методForEach(), можно создать распараллеливаемый вариант циклаforeach.Существует несколько форм методаForEach (). Ниже приведена простейшая форма его объявления:
public static ParallelLoopResult
ForEach<TSource>(IEnumerable<TSource>source,
Action<TSource>body)
гдеsourceобозначает коллекцию данных, обрабатываемых в цикле, abody —метод, который будет выполняться на каждом шаге цикла. Как пояснялось ранее в этой книге, во всех массивах, коллекциях (описываемых в главе 25) и других источниках данных поддерживается интерфейсIEnumerable<T>.Метод, передаваемый через параметрbody,принимает в качестве своего аргумента значение или ссылку на каждый обрабатываемый в цикле элемент массива, но не его индекс. А в итоге возвращаются сведения
о состоянии цикла.
Аналогично методуFor( ) , параллельное выполнение цикла методомForEach() можно остановить, вызвав методBreak() для экземпляра объекта типаParallelLoopState,передаваемого через параметрbody,при условии, что используется приведенная ниже форма методаFor Each ().
public static ParallelLoopResult
ForEach<TSource>(IEnumerable<TSource>source,
ActiorKTSource, ParallelLoopState>body)
В приведенном ниже примере программы демонстрируется применение методаFor Each() на практике. Как и прежде, в данном примере создается крупный массив целых значений. А отличается данный пример от предыдущих тем, что метод, выполняющийся на каждом шаге цикла, просто выводит на консоль значения из массива. Как правило, методWriteLine() в распараллеливаемом цикле не применяется, потому что ввод-вывод на консоль осуществляется настолько медленно, что цикл оказывается полностью привязанным к вводу-выводу. Но в данном примере методWriteLine() применяется исключительно в целях демонстрации возможностей методаForEach ().При обнаружении отрицательного значения выполнение цикла прерывается вызовом методаBreak(). Несмотря на то что методBreak() вызывается в одной задаче,другаязадача может по-прежнему выполняться в течение нескольких шагов цикла, прежде чем он будет прерван, хотя это зависит от конкретных условий работы среды выполнения.
// Использовать объекты типа ParallelLoopResult и ParallelLoopState, а также // метод Break () вместе с методом ForEachO для параллельного выполнения цикла.
using System;
using System.Threading.Tasks;
class DemoParallelForWithLoopResult { static int[] data;
// Метод, служащий в качестве тела параллельно выполняемого цикла.
// В данном примере переменной v передается значение элемента массива // данных, а не индекс этого элемента.
static void DisplayData(int v, ParallelLoopState pis) {
// Прервать цикл при обнаружении отрицательного значения, if (v < 0) pls.Break();
Console.WriteLine("Значение: " + v);
static void Main() {
Console.WriteLine("Основной поток запущен."); data = new int[100000000];
// Инициализировать данные.
for (int i=0; i < data.Length; i++) data[i] = i;
// Поместить отрицательное значение в массив data, data[100000] = -10;
// Использовать цикл, параллельно выполняемый методом ForEachO,
// для отображения данных на экране.
ParallelLoopResult loopResult = Parallel.ForEach(data, DisplayData);
// Проверить, завершился ли цикл, if(!loopResult.IsCompleted)
Console.WriteLine("ХпЦикл завершился преждевременно из-за того, " +
"что обнаружено отрицательное значение\п" +
"на шаге цикла номер " +
loopResult.LowestBreaklteration + ".\n");
Console.WriteLine("Основной поток завершен.");
}
}
В приведенной выше программе именованный метод применяется в качестве делегата, представляющего "тело" цикла. Но иногда удобнее применять анонимный метод. В качестве примера ниже приведено реализуемое в виде лямбда-выражения "тело" цикла, параллельно выполняемого методомForEach ().
// Использовать цикл, параллельно выполняемый методом ForEachO,
// для отображения данных на экране.
ParallelLoopResult loopResult =
Parallel.ForEach(data, (v, pis) => {
Console.WriteLine("Значение: " + v); if (v < 0) pis.Break ();
});
Исследование возможностей PLINQ
PLINQ представляет собой параллельный вариант языка интегрированных запросов LINQ и тесно связан с библиотекой TPL. PLINQ применяется, главным образом, для достижения параллелизма данных внутри запроса. Как станет ясно из дальнейшего, сделать это совсем не трудно. Как и TPL, тема PLINQ довольно обширна и многогранна, поэтому в этой главе представлены лишь самые основные понятия данного языка.
Класс ParallelEnumerable
Основу PLINQ составляет классParallelEnumerable,определенный в пространстве именSystem. Linq.Это статический класс, в котором определены многие методы расширения, поддерживающие параллельное выполнение операций. По существу, он представляет собой параллельный вариант стандартного для LINQ классаEnumerable.Многие его методы являются расширением классаParallelQuery,а некоторые из них возвращают объект типаParallelQuery.В классеParallelQueryинкапсулируется последовательность операций, поддерживающая параллельное выполнение. Имеются как обобщенный, так и необобщенный варианты данного класса. Мы не будем обращаться к классуParallelQueryнепосредственно, а воспользуемся несколькими методами классаParallelEnumerable.Самый главный из них, методAs Parallel (), описывается в следующем разделе.
Распараллеливание запроса методом AsParallel ()
Едва ли не самым удобным средством PLINQ является возможность просто создавать параллельный запрос. Нужно лишь вызвать методAsParallel() для источника данных. МетодAsParallel() определен в классеParallelEnumerableи возвращает источник данных, инкапсулированный в экземпляре объекта типаParallelQuery.Это дает возможность поддерживать методы расширения параллельных запросов. После вызова данного метода запрос разделяет источник данных на части и оперирует с каждой из них таким образом, чтобы извлечь максимальную выгоду из распараллеливания. (Если распараллеливание оказывается невозможным или неприемлемым, то запрос, как обычно, выполняется последовательно.) Таким образом, добавления в исходный код единственного вызова методаAsParallel() оказывается достаточно для того, чтобы превратить последовательный запрос LINQ в параллельный запрос LINQ. Для простых запросов это единственное необходимое условие.
Существуют как обобщенные, так и необобщенные формы методаAsParallel ().Ниже приведена простейшая обобщенная его форма:
public static ParallelQuery AsParallel(this IEnumerablesource)public static ParallelQuery<TSource>
AsParallel<TSource>(this IEnumerable<TSource>source)
гдеTSourceобозначает тип элементов в последовательном источнике данных
source.
Ниже приведен пример, демонстрирующий простой запрос PLINQ.
// Простой запрос PLINQ.
using System; using System.Linq;
class PLINQDemo {
static void Main() {
int[] data = new int[10000000];
I
// Инициализировать массив данных положительными значениями, for(int i=0; i < data.Length; i++) data[i] = i;
//А теперь ввести в массив данных ряд отрицательных значений
data[1000] = -1;
data[14000] = -2;
data[15000] = -3;
data[676000] = -4;
data[8024540] = -5; data[9908000] = -6;
// Использовать запрос PLINQ для поиска отрицательных значений, var negatives = from val in data.AsParallel() where val < 0 select val;
foreach(var v in negatives)
Console.Write(v + " ");
Console.WriteLine();
}
}
Эта программа начинается с создания крупного массиваdata,инициализируемого целыми положительными значениями. Затем в него вводится ряд отрицательных значений. А далее формируется запрос на возврат последовательности отрицательных значений. Ниже приведен этот запрос.
var negatives = from val in data.AsParallel() where val < 0 select val;
В этом запросе методAsParallel() вызывается для источника данных, в качестве которого служит массивdata.Благодаря этому разрешается параллельное выполнение операций над массивомdata,а именно: поиск отрицательных значений параллельно в нескольких потоках. По мере обнаружения отрицательных значений они добавляются в последовательность вывода. Это означает, что порядок формирования последовательности вывода может и не отражать порядок расположения отрицательных значений в массивеdata.В качестве примера ниже приведен результат выполнения приведенного выше кода в двухъядерной системе.
-5 -6 -1 -2 -3 -4
Как видите, в том потоке, где поиск выполнялся в верхней части массива, отрицательные значения -5 и -6 были обнаружены раньше, чем значение -1 в том потоке, где поиск происходил в нижней части массива. Следует, однако, иметь в виду, что из-за отличий в степени загрузки задачами, количества доступных процессоров и прочих факторов системного характера могут быть получены разные результаты. А самое главное, что результирующая последовательность совсем не обязательно будет отражать порядок формирования исходной последовательности.
Применение метода AsOrdered ()
Как отмечалось в предыдущем разделе, по умолчанию порядок формирования результирующей последовательности в параллельном запросе совсем не обязательно должен отражать порядок формирования исходной последовательности. Более того, результирующую последовательность следует рассматривать как практически неупорядоченную. Если же результат должен отражать порядок организации источника данных, то его нужно запросить специально с помощью методаAsOrdered (), определенного в классеParallelEnumerable.Ниже приведены обобщенная и необобщенная формы этого метода:
public static ParallelQuery AsOrdered(this ParallelQuerysource)public static ParallelQuery<TSource>
AsOrdered<TSource>(this ParallelQuery<TSource>source)
гдеTSourceобозначает тип элементов в источнике данныхsource.МетодAsOrdered() можно вызывать только для объекта типаParallelQuery,поскольку он является методом расширения классаParallelQuery.
Для того чтобы посмотреть, к какому результату может привести применение методаAsOrdered (), подставьте его вызов в приведенный ниже запрос из предыдущего примера программы.
// Использовать метод AsOrdered() для сохранения порядка // в результирующей последовательности.
var negatives = from val in data.AsParallel().AsOrdered() where val < 0 select val;
После выполнения программы порядок следования элементов в результирующей последовательности будет отражать порядок их расположения в исходной последовательности.
Отмена параллельного запроса
Параллельный запрос отменяется таким же образом, как и задача. И в том и в другом случае отмена опирается на структуруCancellationToken,получаемую из классаCancellationTokenSource.Получаемый в итоге признак отмены передается запросу с помощью методаWithCancellation (). Отмена параллельного запроса производится методомCancel (), который вызывается для источника признаков отмены. Главное отличие отмены параллельного запроса от отмены задачи состоит в следующем: когда параллельный запрос отменяется, он генерирует исключениеOperationCanceledException,а неAggregateException.Но в тех случаях, когда запрос способен сгенерировать несколько исключений, исключениеOperationCanceledExceptionможет быть объединено в совокупное исключениеAggregateException.Поэтому отслеживать лучше оба вида исключений.
Ниже приведена форма объявления методаWithCancellation():
public static ParallelQuery<TSource>
WithCancellation<TSource> (
this ParallelQuery<TSource>source,
CancellationTokencancellationToken)
гдеsourceобозначает вызывающий запрос, acancellationToken— признак отмены. Этот метод возвращает запрос, поддерживающий указанный признак отмены.
В приведенном ниже примере программы демонстрируется порядок отмены параллельного запроса, сформированного в программе из предыдущего примера. В данной программе организуется отдельная задача, которая ожидает в течение 100 миллисекунд, а затем отменяет запрос. Отдельная задача требуется потому, что циклforeach,в котором выполняется запрос, блокирует выполнение методаMain() до завершения цикла.
using System.Linq; using System.Threading; using System.Threading.Tasks;
class PLINQCancelDemo {
static void Main() {
CancellationTokenSource cancelTokSrc = new CancellationTokenSource(); int[] data = new int[10000000];
// Инициализировать массив данных положительными значениями, for (int i=0; i < data.Length; i++) data[i] = i;
//А теперь ввести в массив данных ряд отрицательных значений, data[1000] = -1; data [14000] = -2; data[15000] = -3;
data[676000] = -4; ч
data[8024540] = -5; data [9908000] = -6;
// Использовать запрос PLINQ для поиска отрицательных значений, var negatives = from val in data.AsParallel().
WithCancellation(cancelTokSrc.Token) where val < 0 select val;
// Создать задачу для отмены запроса по истечении 100 миллисекунд.
Task cancelTsk = Task.Factory.StartNew( () => {
Thread.Sleep(100); • cancelTokSrc.Cancel();
});
try {
foreach(var v in negatives)
Console.Write(v + " ");
} catch(OperationCanceledException exc) {
Console.WriteLine(exc.Message);
} catch(AggregateException exc) {
Console.WriteLine (exc);
} finally {
cancelTsk.Wait (); cancelTokSrc.Dispose(); cancelTsk.Dispose();
}
Console.WriteLine();
}
}
Ниже приведен результат выполнения этой программы. Если запрос отменяется до его завершения, то на экран выводится только сообщение об исключительной ситуации.
Запрос отменен с помощью маркера, переданного в метод WithCancellation.
Другие средства PLINQ
Как упоминалось ранее, PLINQ представляет собой довольно крупную подсистему. Это объясняется отчасти той гибкостью, которой обладает PLINQ. В PLINQ доступны и многие другие средства, помогающие подстраивать параллельные запросы под конкретную ситуацию. Так, при вызове методаWithDegreeOf Parallelism() можно указать максимальное количество процессоров, выделяемых для обработки запроса, а при вызове методаAs Sequential () — запросить последовательное выполнение части параллельного запроса. Если вызывающий поток, ожидающий результатов от циклаforeach,не требуется блокировать, то для этой цели можно воспользоваться методомForAll(). Все эти методы определены в классеParallelEnumerable.А в тех случаях, когда PLINQ должен по умолчанию поддерживать последовательное выполнение, можно воспользоваться методомWithExecutionMode (), передав ему в качестве параметра признакParallelExecutionMode . ForceParallelism.
Вопросы эффективности PLINQ
Далеко не все запросы выполняются быстрее только потому, что они распараллелены. Как пояснялось ранее в отношении TPL, издержки, связанные с созданием параллельных потоков и управлением их исполнением, могут "перекрыть" все преимущества, которые дает распараллеливание. Вообще говоря, если источник данных оказывается довольно мелким, а требующаяся обработка данных — очень короткой, то внедрение параллелизма может и не привести к ускорению обработки запроса. Поэтому за рекомендациями по данному вопросу следует обращаться к информации корпорации Microsoft.
ГЛАВА 25 Коллекции, перечислители и итераторы
Вэтой главе речь пойдет об одной из самых важных составляющих среды .NET Framework: коллекциях. В C#коллекцияпредставляет собой совокупность объектов. В среде .NET Framework имеется немало интерфейсов и классов, в которых определяются и реализуются различные типы коллекций. Коллекции упрощают решение многих задач программирования благодаря тому, что предлагают готовые решения для создания целого ряда типичных, но порой трудоемких для разработки структур данных. Например, в среду .NET Framework встроены коллекции, предназначенные для поддержки динамических массивов, связных списков, стеков, очередей и хеш-таблиц. Коллекции являются современным технологическим средством, заслуживающим пристального внимания всех, кто программирует на С#.
Первоначально существовали только классы необобщенных коллекций. Но с внедрением обобщений в версии C# 2.0 среда .NET Framework была дополнена многими новыми обобщенными классами и интерфейсами. Благодаря введению обобщенных коллекций общее количество классов и интерфейсов удвоилось. Вместе с библиотекой распараллеливания задач (TPL) в версии 4.0 среды .NET Framework появился ряд новых классов коллекций, предназначенных для применения в тех случаях, когда доступ к коллекции осуществляется из нескольких потоков. Нетрудно догадаться, что прикладной интерфейс Collections API составляет значительную часть среды .NET Framework.
Кроме того, в настоящей главе рассматриваются два средства, непосредственно связанные с коллекциями: перечислители и итераторы. И те и другие позволяют поочередно обращаться к содержимому класса коллекции в цикле foreach.
Краткий обзор коллекций
Главное преимущество коллекций заключается в том, что они стандартизируют обработку групп объектов в программе. Все коллекции разработаны на основе набора четко определенных интерфейсов. Некоторые встроенные реализации таких интерфейсов, в том числеArrayList, Hashtable, StackиQueue,могут применяться в исходном виде и без каких-либо изменений. Имеется также возможность реализовать собственную коллекцию, хотя потребность в этом возникает крайне редко.
В среде .NET Framework поддерживаются пять типов коллекций: необобщенные, специальные, с поразрядной организацией, обобщенные и параллельные. Необобщен-ные коллекции реализуют ряд основных структур данных, включая динамический массив, стек, очередь, а такжесловари,в которых можно хранить пары "ключ-значение". В отношении необобщенных коллекций важно иметь в виду следующее: они оперируют данными типаobj ect.
Таким образом, необобщенные коллекции могут служить для хранения данных любого типа, причем в одной коллекции допускается наличие разнотипных данных. Очевидно, что такие коллекции не типизированы, поскольку в них хранятся ссылки на данные типаobject.Классы и интерфейсы необобщенных коллекций находятся в пространстве именSystem.Collections.
Специадьные коллекции оперируют данными конкретного типа или же делают это каким-то особым образом. Например, имеются специальные коллекции для символьных строк, а также специальные коллекции, в которых используется однонаправленный список. Специальные коллекции объявляются в пространстве именSystem. Collections.Specialized.
В прикладном интерфейсе Collections API определена одна коллекция с поразрядной организацией — этоBit Array.Коллекция типаBit Arrayподдерживает поразрядные операции, т.е. операции над отдельными двоичными разрядами, например И или исключающее ИЛИ, а следовательно, она существенно отличается своими возможностями от остальных типов коллекций. Коллекция типаBit Arrayобъявляется в пространстве именSystem. Collections.
Обобщенные коллекции обеспечивают обобщенную реализацию нескольких стандартных структур данных, включая связные списки, стеки, очереди и словари. Такие коллекции являются типизированными в силу их обобщенного характера. Это означает, что в обобщенной коллекции могут храниться только такие элементы данных, которые совместимы по типу с данной коллекцией. Благодаря этому исключается случайное несовпадение типов. Обобщенные коллекции объявляются в пространстве именSystem.Collections.Generic.
Параллельные коллекции поддерживают многопоточный доступ к коллекции. Это обобщенные коллекции, определенные в пространстве именSystem. Collections . Concurrent.
В пространстве именSystem. Collections . Obj ectModelнаходится также ряд классов, поддерживающих создание пользователями собственных обобщенных коллекций.
Основополагающим для всех коллекций является понятиеперечислителя,который поддерживается в необобщенных интерфейсахIEnumeratorиIEnumerable,а также в обобщенных интерфейсахIEnumerator<T>иIEnumerable<T>.Перечислитель обеспечивает стандартный способ поочередного доступа к элементам коллекции. Следовательно, онперечисляетсодержимое коллекции. В каждой коллекции должна быть реализована обобщенная или необобщенная форма интерфейсаI Enumerable,поэтому элементы любого класса коллекции должны быть доступны посредством методов, определенных в интерфейсеIEnumeratorилиIEnumerator<T>.Это означает, что, внеся минимальные изменения в код циклического обращения к коллекции одного типа, его можно использовать для аналогичного обращения к коллекции другого типа. Любопытно, что для поочередного обращения к содержимому коллекции в циклеforeachиспользуется перечислитель.
Основополагающим для всех коллекций является понятиеперечислителя,который поддерживается в необобщенных интерфейсахIEnumeratorиIEnumerable,а также в обобщенных интерфейсахIEnumerator<T>иIEnumerable<T>.Перечислитель обеспечивает стандартный способ поочередного доступа к элементам коллекции. Следовательно, онперечисляетсодержимое коллекции. В каждой коллекции должна быть реализована обобщенная или необобщенная форма интерфейсаIEnumerable,поэтому элементы любого класса коллекции должны быть доступны посредством методов, определенных в интерфейсеIEnumeratorилиIEnumerator<T>.Это означает, что, внеся минимальные изменения в код циклического обращения к коллекции одного типа, его можно использовать для аналогичного обращения к коллекции другого типа. Любопытно, что для поочередного обращения к содержимому коллекции в циклеforeachиспользуется перечислитель.
С перечислителем непосредственно связано другое средство, называемоеитератором.Это средство упрощает процесс создания классов коллекций, например специальных, поочередное обращение к которым организуется в циклеforeach.Итераторы также рассматриваются в этой главе.
И последнее замечание: если у вас имеется некоторый опыт программирования на C++, то вам, вероятно, будет полезно знать, что классы коллекций по своей сути подобны классам стандартной библиотеки шаблонов (Standard Template Library — STL), определенной в C++. То, что в программировании на C++ называетсяконтейнером, в программировании на C# называетсяколлекцией.Это же относится и к Java. Если вы знакомы с библиотекой Collections Framework для Java, то научиться пользоваться коллекциями в C# не составит для вас большого труда.
В силу характерных отличий каждый из пяти типов коллекций (необобщенных, обобщенных, специальных, с поразрядной организацией и параллельных) будет рассмотрен далее в этой главе отдельно.
Необобщенные коллекции
Необобщенные коллекции вошли в состав среды .NET Framework еще в версии 1.0. Они определяются в пространстве именSystem. Collections.Необобщенные коллекции представляют собой структуры данных общего назначения, оперирующие ссылками на объекты. Таким образом, они позволяют манипулировать объектом любого типа, хотя и не типизированным способом. В этом состоит их преимущество и в то же время недостаток. Благодаря тому что необобщенные коллекции оперирухрт ссылками на объекты, в них можно хранить разнотипные данные. Это удобно в тех случаях, когда требуется манипулировать совокупностью разнотипных объектов или же когда типы хранящихся в коллекции объектов заранее неизвестны. Но если коллекция предназначается для хранения объекта конкретного типа, то необобщенные коллекции не обеспечивают типовую безопасность, которую можно обнаружить в обобщенных коллекциях.
Необобщенные коллекции определены в ряде интерфейсов и классов, реализующих эти интерфейсы. Все они рассматриваются далее по порядку.
Интерфейсы необобщенных коллекций
В пространстве именSystem. Collectionsопределен целый ряд интерфейсов необобщенных коллекций. Начинать рассмотрение необобщенных коллекций следует именно с интерфейсов, поскольку они определяют функциональные возможности, которые являются общими для всех классов необобщенных коллекций. Интерфейсы, служащие опорой для необобщенных коллекций, сведены в табл. 25.1. Каждый из этих интерфейсов подробно описывается далее.
Таблица 25.1. Интерфейсы необобщенных коллекций
Интерфейс
Описание
ICollection
Определяет элементы, которые должны иметь все необобщенные коллекции
IComparer
Определяет метод Compare () для сравнения объектов, хранящихся в коллекции
IDictionary
Определяет коллекцию, состоящую из пар “ключ-значение”
IDictionaryEnumerator
Определяет перечислитель для коллекции, реализующей интерфейс IDictionary
IEnumerable
Определяет метод GetEnumerator (), предоставляющий перечислитель для любого класса коллекции
IEnumerator
Предоставляет методы, позволяющие получать содержимое коллекции по очереди
IEqualityComparer
Сравнивает два объекта на предмет равенства
IHashCodeProvider
Считается устаревшим. Вместо него следует использовать интерфейс IEqualityComparer
IList
Определяет коллекцию, доступ к которой можно получить с помощью индексатора
IStructuralComparable
Определяет метод CompareTo (), применяемый для структурного сравнения
IStructuralEquatable
Определяет метод Equals (), применяемый для выяснения структурного, а не ссылочного равенства. Кроме того, определяет метод GetHashCode ()
Интерфейс ICollection
ИнтерфейсICollectionслужит основанием, на котором построены все необобщенные коллекции. В нем объявляются основные методы и свойства для всех необобщенных коллекций. Он также наследует от интерфейсаIEnumerable.
В интерфейсеICollectionопределяются перечисленные ниже свойства. СвойствоCountиспользуется чаще всего, поскольку оно содержит количество элементов, хранящихся в коллекции на данный момент. Если значение свойстваCountравно нулю, то коллекция считается пустой.
В интерфейсеICollectionопределяется следующий метод.
void CopyTo(Arraytarget, intstartldx)
Свойство Назначение
int Count { get; } Содержит количество элементов в коллекции на дан
ный момент
bool isSynchronized { get; } Принимает .логическое значение true, если коллек
ция синхронизирована, а иначе — логическое значение false. По умолчанию коллекции не синхронизированы. Но для большинства коллекций можно получить синхронизированный вариант object SyncRoot { get; } Содержит объект, для которого коллекция может
_быть синхронизирована_
МетодCopyTo() копирует содержимое коллекции в массивtarget,начиная с элемента, указываемого по индексуstartldx.Следовательно, методCopyTo() обеспечивает в C# переход от коллекции к стандартному массиву.
Благодаря тому что интерфейсICollectionнаследует от интерфейсаI Enumerable,в его состав входит также единственный метод, определенный в интерфейсеIEnumerable.Это методGetEnumerator (), объявляемый следующим образом.
IEnumerator GetEnumerator()
Он возвращает перечислитель для коллекции.
Вследствие того же наследования от интерфейсаIEnumerableв интерфейсеICollectionопределяются также четыре следующих метода расширения:AsParallel(),AsQueryable(),Cast() иOf Type(). В частности, методAsParallel() объявляется в классеSystem. Linq. ParallelEnumerable,методAsQueryable()—в классеSystem. Linq. Queryable,а методыCast() иOf Type() — в классеSystem. Linq.Enumerable.Эти методы предназначены главным образом для поддержки LINQ, хотя их можно применять и в других целях.
Интерфейс IList
В интерфейсеIListобъявляется такое поведение необобщенной коллекции, которое позволяет осуществлять доступ к ее элементам по индексу с отсчетом от нуля. Этот интерфейс наследует от интерфейсовICollectionиIEnumerable.Помимо методов, определенных в этих интерфейсах, в интерфейсеIListопределяется ряд собственных методов. Все эти методы сведены в табл. 25.2. В некоторых из них предусматривается модификация коллекции. Если же коллекция доступна только для чтения или имеет фиксированный размер, то в этих методах генерируется исключениеNotSupportedException.
Tafuuiia 9fv9_ Мртплы пппрлрлрнныр r интрпгЬрйпр TLisI-
Метод
Описание
int Add(objectvalue)void Clear ()
bool Contains(objectvalue)
Добавляет объект value в вызывающую коллекцию. Возвращает индекс, по которому этот объект сохраняется
Удаляет все элементы из вызывающей коллекции Возвращает логическое значение true, если вызывающая коллекция содержит объект value, а иначе — логическое значение false
Метод
Описание
int IndexOf(objectvalue)
void Insert (intindex,objectvalue)
void Remove(objectvalue)
void RemoveAt(intindex)
Возвращает индекс объекта value, если этот объект содержится в вызывающей коллекции. Если же объект value не обнаружен, то метод возвращает значение -1
Вставляет в вызывающую коллекцию объект value по индексу index. Элементы, находившиеся до этого по индексу index и дальше, смещаются вперед, чтобы освободить место для вставляемого объекта
value
Удаляет первое вхождение объекта value в вызывающей коллекции. Элементы, находившиеся до этого за удаленным элементом, смещаются назад, чтобы устранить образовавшийся “пробел”
Удаляет из вызывающей коллекции объект, расположенный по указанному индексу index. Элементы, находившиеся до этого за удаленным элементом, смещаются назад, чтобы устранить образовавшийся “пробел”
Объекты добавляются в коллекцию типаIListвызовом методаAdd(). Обратите внимание на то, что методAdd() принимает аргумент типаobj ect.А посколькуobjectявляется базовым классом для всех типов, то в необобгценной коллекции может быть сохранен объект любого типа, включая и типы значений, в силу автоматической упаковки и распаковки.
Для удаления элемента из коллекции служат методыRemove() иRemoveAt(). В частности, методRemove() удаляет указанный объект, а методRemoveAt() удаляет объект по указанному индексу. И для опорожнения коллекции вызывается методClear().
Для того чтобы выяснить, содержится ли в коллекции конкретный объект, вызывается методContains(). Для получения индекса объекта вызывается методIndexOf (), а для вставки элемента в коллекцию по указанному индексу — методInsert().
В интерфейсе IList определяются следующие свойства.
bool IsFixedSize { get; } bool IsReadOnly { get; }
Если коллекция имеет фиксированный размер, то свойствоIsFixedSizeсодержит логическое значениеtrue.Это означает, что в такую коллекцию нельзя ни вставлять элементы, ни удалять их из нее. Если же коллекция доступна только для чтения, то свойствоIsReadOnlyсодержит логическое значениеtrue.Это означает, что содержимое такой коллекции не подлежит изменению.
Кроме того, в интерфейсе IList определяется следующий индексатор.
object this[int index] { get; set; }
Этот индексатор служит для получения и установки значения элемента коллекции. Но его нельзя использовать для добавления в коллекцию нового элемента. С этой целью обычно вызывается методAdd(). Как только элемент будет добавлен в коллекцию, он станет доступным посредством индексатора.
Интерфейс IDictionary
В интерфейсеIDictionaryопределяется такое поведение необобщенной коллекции, которое позволяет преобразовать уникальные ключи в соответствующие значения. Ключ представляет собой объект, с помощью которого значение извлекается впоследствии. Следовательно, в коллекции, реализующей интерфейсIDictionary,хранятся пары "ключ-значение". Как только подобная пара будет сохранена, ее можно извлечь с помощью ключа. ИнтерфейсIDictionaryнаследует от интерфейсовICollectionиIEnumerable.Методы, объявленные в интерфейсеIDictionary,сведены в табл. 25.3. Некоторые из них генерируют исключениеArgumentNullExceptionпри попытке указать пустой ключ, поскольку пустые ключи не допускаются.
Таблица 25.3. Методы, определенные в интерфейсе IDictionary
Метод
Описание
void Add(objectkey,
Добавляет в вызывающую коллекцию пару “ключ-
objectvalue)
значение", определяемую параметрами key и value
void Clear()
Удаляет все пары “ключ-значение” из вызывающей коллекции
bool Contains(objectkey)
Возвращает логическое значение true, если вызывающая коллекция содержит объект key в качестве ключа, в противном случае — логическое значение false
IDictionaryEnumerator
Возвращает перечислитель для вызывающей коллек
GetEnumerator()
ции
void Remove(objectkey)
Удаляет из коллекции элемент, ключ которого равен значению параметра key
Длядобавления пары "ключ-значение" в коллекцию типаIDictionaryслужит методAdd(). Обратите внимание на то, что ключ и его значение указываются отдельно. А для удаления элемента из коллекции следует указать ключ этого объекта при вызове методаRemove(). И для опорожнения коллекции вызывается методClear ().
Для того чтобы выяснить, содержит ли коллекция конкретный объект, вызывается методContains() с указанным ключом искомого элемента. С помощью методаGetEnumerator() получается перечислитель, совместимый с коллекцией типаIDictionary.Этот перечислитель оперирует парами "ключ-значение".
В интерфейсеIDictionaryопределяются перечисленные ниже свойства.
Свойство
Назначение
bool IsFixedSize
{ get; }
Принимает логическое значение true, если словарь имеет фиксированный размер
bool IsReadOnly {
get; }
Принимает логическое значение true, если словарь доступен только для чтения
ICollection Keys
{ get; }
Получает коллекцию ключей
ICollection Values
{ get; }
Получает коллекцию значений
Следует иметь в виду, что ключи и значения, содержащиеся в коллекции, доступны в отдельных списках с помощью свойствKeysиValues.
Кроме того, в интерфейсеIDictionaryопределяется следующий индексатор.
object this[objectkey]{ get; set; }
Этот индексатор служит для получения и установки значения элемента коллекции, а также для добавления в коллекцию нового элемента. Но в качестве индекса в данном случае служит ключ элемента, а не собственно индекс.
Интерфейсы IEnumerable, IEnumerator и IDictionaryEnumerator
ИнтерфейсIEnumerableявляется необобщенным, и поэтому он должен быть реализован в классе для поддержки перечислителей. Как пояснялось выше, интерфейсIEnumerableреализуется во всех классах необобщенных коллекций, поскольку он наследуется интерфейсомICollection.Ниже приведен единственный методGetEnumerator (), определяемый в интерфейсеIEnumerable.
IEnumerator GetEnumerator()
Он возвращает коллекцию. Благодаря реализации интерфейсаIEnumerableможно также получать содержимое коллекции в циклеforeach.
В интерфейсеIEnumeratorопределяются функции перечислителя. С помощью методов этого интерфейса можно циклически обращаться к содержимому коллекции. Если в коллекции содержатся пары "ключ-значение" (словари), то методGetEnumerator() возвращает объект типаIDictionaryEnumerator,а не типаIEnumerator.ИнтерфейсIDictionaryEnumeratorнаследует от интерфейсаIEnumeratorи вводит дополнительные функции, упрощающие перечисление словарей.
В интерфейсеIEnumeratorопределяются также методыMoveNext() иReset() и свойствоCurrent.Способы их применения подробнее описываются далее в этой главе. А до тех пор следует отметить, что свойствоCurrentсодержит элемент, получаемый в текущий момент. МетодMoveNext() осуществляет переход к следующему элементу коллекции, а методReset() возобновляет перечисление с самого начала.
Интерфейсы IComparer и IEqualityComparer
В интерфейсеIComparerопределяется методCompare() для сравнения двух объектов.
int Compare(object х, object у)
Он возвращает положительное значение, если значение объекта х больше, чем у объектау;отрицательное — если значение объекта х меньше, чем у объектау;и нулевое — если сравниваемые значения равны. Данный интерфейс можно использовать для указания способа сортировки элементов коллекции.
В интерфейсеIEqualityComparerопределяются два метода.
bool Equals(objectх,object у) int GetHashCode(objectobj)
МетодEquals() возвращает логическое значениеtrue,если значения объектов х иуравны. А методGetHashCode() возвращает хеш-код для объектаobj.
Интерфейсы IStructuralComparable и IStructuralEquatable
. Оба интерфейсаIStructuralComparableиIStructuralEquatableдобавлены в версию 4.0 среды .NET Framework. В интерфейсеIStructuralComparableопределяется методCompareTo (), который задает способ структурного сравнения двух объектов для целей сортировки. (Иными словами, МетодCompareTo() сравнивает содержимое объектов, а не ссылки на них.) Ниже приведена форма объявления данного метода.
int CompareTo(objectother,IComparercomparer)
Он должен возвращать -1, если вызывающий объект предшествует другому объектуother; 1, если вызывающий объект следует после объектаother; и наконец, 0, если значения обоих объектов одинаковы для целей сортировки. А само сравнение обеспечивает объект, передаваемый через параметрcomparer.
ИнтерфейсIStructuralEquatableслужит для выяснения структурного равенства путем сравнения содержимого двух объектов. В этом интерфейсе определены следующие методы.
bool Equals(objectother,IEqualityComparercomparer)int GetHashCode(IEqualityComparercomparer)
МетодEquals() должен возвращать логическое значениеtrue,если вызывающий объект и другой объектotherравны. А методGetHashCode() должен возвращать хеш-код для вызывающего объекта. Результаты, возвращаемые обоими методами, должны быть совместимы. Само сравнение обеспечивает объект, передаваемый через параметрcomparer.
Структура DictionaryEntry
В пространстве именSystem. Collectionsопределена структураDictionaryEntry.Необобщенные коллекции пар "ключ-значение" сохраняют эти пары в объекте типаDictionaryEntry.В данной структуре определяются два следующих свойства.
public object Key { get; set; } public object Value { get; set; }
Эти свойства служат для доступа к ключу или значению, связанному с элементом коллекции. Объект типаDictionaryEntryможет быть сконструирован с помощью конструктора:
public DictionaryEntry(objectkey,objectvalue)гдеkeyобозначает ключ, avalue— значение.
Классы необобщенных коллекций
А теперь, когда представлены интерфейсы необобщенных коллекций, можно перейти к рассмотрению стандартных классов, в которых они реализуются. Ниже приведены классы необобщенных коллекций, за исключением коллекции типаBitArray,рассматриваемой далее в этой главе.
Класс
Описание
ArrayList
Определяет динамический массив, т.е. такой массив, который может при
необходимости увеличивать свой размер
Hashtable
Определяет хеш-таблицу для пар “ключ-значение”
Queue
Определяет очередь, или список, действующий по принципу “первым при
шел — первым обслужен”
SortedList
Определяет отсортированный список пар “ключ-значение”
Stack
Определяет стек, или список, действующий по принципу “первым пришел —
последним обслужен”
Каждый из этих классов коллекций подробно рассматривается и демонстрируется далее на конкретных примерах.
Класс Ar г aylii s t
В классеArrayListподдерживаются динамические массивы, расширяющиеся и сокращающиеся по мере необходимости. В языке C# стандартные массивы имеют фиксированную длину, которая не может изменяться во время выполнения программы. Это означает, что количество элементов в массиве нужно знать заранее. Но иногда требуемая конкретная длина массива остается неизвестной до самого момента выполнения программы. Именно для таких ситуаций и предназначен классArrayList.В классеArrayListопределяется массив переменной длины, который состоит из ссылок на объекты и может динамически увеличивать и уменьшать свой размер. Массив типаArrayListсоздается с первоначальным размером. Если этот размер превышается, то массив автоматически расширяется. А при удалении объектов из такого массива он автоматически сокращается. Коллекции классаArrayListшироко применяются в практике программирования на С#. Именно поэтому они рассматриваются здесь подробно. Но многие способы применения коллекций классаArrayListраспространяются и на другие коллекции, в том числе и на обобщенные.
В классеArrayListреализуются интерфейсыICollection, IList, IEnumerableиICloneable.Ниже приведены конструкторы классаArrayList.
public ArrayList()
public ArrayList(ICollection с)
public ArrayList(intcapacity)
Первый конструктор создает пустую коллекцию классаArrayListс нулевой первоначальной емкостью. Второй конструктор создает коллекцию типаArrayListс количеством инициализируемых элементов, которое определяется параметромси равно первоначальной емкости массива. Третий конструктор создает коллекцию, имеющую указанную первоначальную емкость, определяемую параметромcapaci ty.В данном случае емкость обозначает размер базового массива, используемого для хранения элементов коллекции. Емкость коллекции типаArrayListможет увеличиваться автоматически по мере добавления в нее элементов.
В классеArrayListопределяется ряд собственных методов, помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются. Некоторые из наиболее часто используемых методов классаArrayListперечислены в табл. 25.4. Коллекцию классаArrayListможно отсортировать, вызвав методSort (). В этом случае поиск в отсортированной коллекции с помощью методаBinarySearch() становится еще более эффективным. Содержимое коллекции типаArrayListможно также обратить, вызвав методReverse ().
Таблица 25.4. Наиболее часто используемые методы, определенные в классе ArrayList
Метод
Описание
public virtual void AddRange(Icollection с) public virtual int BinarySearch(objectvalue)
Добавляет элементы из коллекции с в конец вызывающей коллекции типа ArrayList Выполняет поиск в вызывающей коллекции значения value. Возвращает индекс найденного элемента. Если искомое значение не найдено, возвращает отрицательное значение. Вызывающий список должен быть отсортирован
_Продолжение табл. 25.4
Метод
Описание
public virtual int
Выполняет поиск в вызывающей коллекции значения
BinarySearcii (object
value, используя для сравнения способ, определяемый
value,-Icomparer
параметром comparer. Возвращает индекс совпавше
comparer)
го элемента. Если искомое значение не найдено, возвращает отрицательное значение. Вызывающий список должен быть отсортирован
public virtual int
Выполняет поиск в вызывающей коллекции значения
BinarySearch(intindex,
value, используя для сравнения способ, определяемый
intcount,objectvalue,
параметром comparer. Поиск начинается с элемента,
IComparercomparer)
указываемого по индексу index, и включает количество элементов, определяемых параметром count. Метод возвращает индекс совпавшего элемента. Если искомое значение не найдено, метод возвращает отрицательное значение. Вызывающий список должен быть отсортирован
public virtual void
Копирует содержимое вызывающей коллекции в мас
CopyTo(Arrayarray)
сив array, который должен быть одномерным и совместимым по типу с элементами коллекции
public virtual void
Копирует содержимое вызывающей коллекции в массив
CopyTo(Arrayarray,int
array, начиная с элемента, указываемого по индексу
arraylndex)
arraylndex. Целевой массив должен быть одномерным и совместимым по типу с элементами коллекции
public virtual void
Копирует часть вызывающей коллекции, начиная с эле
CopyTo(intindex,Array
мента, указываемого по индексу index, и включая ко
array,intarraylndex,
личество элементов, определяемых параметром count,
intcount)
в массив array, начиная с элемента, указываемого по индексу arraylndex. Целевой массив должен быть одномерным и совместимым по типу с элементами коллекции
public static ArrayList
Заключает коллекцию list в оболочку типа ArrayList
FixedSize(ArrayListlist)
с фиксированным размером и возвращает результат
public virtual ArrayList
Возвращает часть вызывающей коллекции типа
GetRange(intindex,int
ArrayList. Часть возвращаемой коллекции начинает
count)
ся с элемента, указываемого по индексу index, и включает количество элементов, определяемое параметром count. Возвращаемый объект ссылается на те же элементы, что и вызывающий объект
public virtual int
Возвращает индекс первого вхождения объекта value
IndexOf(objectvalue)
в вызывающей коллекции. Если искомый объект не обнаружен, возвращает значение -1
public virtual void
Вставляет элементы коллекции с в вызывающую кол
InsertRange(intindex,
лекцию, начиная с элемента, указываемого по индексу
ICollection c)
index
public virtual int
Возвращает индекс последнего вхождения объекта
LastlndexOf(objectvalue)
value в вызывающей коллекции. Если искомый объект не обнаружен, метод возвращает значение -1
Метод
Описание
public static ArrayList
Заключает коллекцию list в оболочку типа
Readonly(ArrayListlist)
ArrayList, доступную только для чтения, и возвращает результат
public virtual void
Удаляет часть вызывающей коллекции, начиная с эле
RemoveRange(intindex,
мента, указываемого по индексу index, и включая
intcount)
количество элементов, определяемое параметром
count
public virtual void
Располагает элементы вызывающей коллекции в обрат
Reverse()
ном порядке
public virtual void
Располагает в обратном порядке часть вызывающей
Reverse(intindex,int
коллекции, начиная с элемента, указываемого по индек
count)
су index, и включая количество элементов, определяемое параметром count
public virtual void
Заменяет часть вызывающей коллекции, начиная с эле
SetRange(intindex,
мента, указываемого по индексу index, элементами
ICollection c)
коллекции с
public virtual void
Сортирует вызывающую коллекцию по нарастающей
Sort ()
public virtual void
Сортирует вызывающую коллекцию, используя для срав
Sort(Icomparercomparer)
нения способ, определяемый параметром comparer. Если параметр comparer имеет пустое значение, то для сравнения используется способ, выбираемый по умолчанию
public virtual void
Сортирует вызывающую коллекцию, используя для срав
Sort(intindex,int
нения способ, определяемый параметром comparer.
count,Icomparer
Сортировка начинается с элемента, указываемого по
comparer)
индексу index, и включает количество элементов, определяемых параметром count. Если параметр comparer имеет пустое значение, то для сравнения используется способ, выбираемый по умолчанию
public static ArrayList
Возвращает синхронизированный вариант коллекции
Synchronized(ArrayList
типа ArrayList, передаваемой в качестве параметра
list)
list
public virtual object[]
Возвращает массив, содержащий копии элементов вы
ToArray()
зывающего объекта
public virtual Array
Возвращает массив, содержащий копии элементов вы
ToArray(Typetype)
зывающего объекта. Тип элементов этого массива определяется параметром type
public virtual void
Устанавливает значение свойства Capacity равным
TrimToSize()
значению свойства Count
В классе ArrayList поддерживается также ряд методов, оперирующих элементами коллекции в заданных пределах. Так, в одну коллекцию типа ArrayList можно вставить другую коллекцию, вызвав метод InsertRange (). Для удаления из коллекции элементов в заданных пределах достаточно вызвать метод RemoveRange (). А для
перезаписи элементов коллекции типаArrayListв заданных пределах элементами из другой коллекции служит методSet Range (). И наконец, элементы коллекции можно сортировать или искать в заданных пределах, а не во всей коллекции.
По умолчанию коллекция типаArrayListне синхронизирована. Для получения синхронизированной оболочки, в которую заключается коллекция, вызывается методSynchronized().
В классеArrayListимеется также приведенное ниже свойствоCapacity,помимо свойств, определенных в интерфейсах, которые в нем реализуются.
public virtual int Capacity { get; set; }
СвойствоCapacityпозволяет получать и устанавливать емкость вызывающей коллекции типаArrayList.Емкость обозначает количество элементов, которые может содержать коллекция типаArrayListдо ее вынужденного расширения. Как упоминалось выше, коллекция типаArrayListрасширяется автоматически, и поэтому задавать ее емкость вручную необязательно. Но из соображений эффективности это иногда можно сделать, если количество элементов коллекции известно заранее. Благодаря этому исключаются издержки на выделение дополнительной памяти.
С другой стороны, если требуется сократить размер базового массива коллекции типаArrayList,то для этой цели достаточно установить меньшее значение свойстваCapacity.Но это значение не должно быть меньше значения свойстваCount.Напомним, что свойствоCountопределено в интерфейсеICollectionи содержит количество объектов, хранящихся в коллекции на данный момент. Всякая попытка установить значение свойстваCapacityменьше значения свойстваCountприводит к генерированию исключенияArgumentOutOfRangeException.Поэтому для получения такого количества элементов коллекции типаArrayList,которое содержится в ней на данный момент, следует установить значение свойстваCapacityравным значению свойстваCount.Для этой цели можно также вызвать методTrimToSize ().
В приведенном ниже примере программы демонстрируется применение классаArrayList.В ней сначала создается коллекция типаArrayList,а затем в эту коллекцию вводятся символы, после чего содержимое коллекции отображается. Некоторые элементы затем удаляются из коллекции, и ее содержимое отображается вновь. После этого в коллекцию вводятся дополнительные элементы, что вынуждает увеличить ее емкость. И наконец, содержимое элементов коллекции изменяется.
// Продемонстрировать применение класса ArrayList.
using System;
using System.Collections;
class ArrayListDemo { static void Main() {
// Создать коллекцию в виде динамического массива.
ArrayList al = new ArrayList ();
Console.WriteLine("Исходное количество элементов: " + al.Count);
Console.WriteLine();
Console.WriteLine("Добавить 6 элементов");
// Добавить элементы в динамический массив.
al.Add('С');
al.Add('А'); al.Add('E') ; al.Add(1В1) ; al.Add('D') ; al.Add(1F') ;
Console.WriteLine("Количество элементов: " + al.Count);
// Отобразить содержимое динамического массива,
// используя индексирование массива.
Console.Write("Текущее содержимое: "); for(int i=0; i < al.Count; i++)
Console.Write (al[i] + " ");
Console.WriteLine("\n");
Console.WriteLine("Удалить 2 элемента");
// Удалить элементы из динамического массива, al.Remove('F'); al.Remove('A');
Console.WriteLine("Количество элементов: " + al.Count);
// Отобразить содержимое динамического массива, используя цикл foreach. Console.Write("Содержимое: "); foreach(char с in al)
Console.Write(с + " ");
Console.WriteLine("\n");
Console.WriteLine("Добавить еще 20 элементов");
// Добавить количество элементов, достаточное для // принудительного расширения массива, for (int i=0; i < 20; i++) al.Add((char)('a' + i));
Console.WriteLine("Текущая емкость: " + al.Capacity);
Console.WriteLine("Количество элементов после добавления 20 новых: " + al.Count);
Console.Write("Содержимое: "); foreach(char с in al)
Console.Write(с + " ");
Console.WriteLine("\n");
// Изменить содержимое динамического массива,
// используя индексирование массива.
Console.WriteLine("Изменить три первых элемента"); al [0] = 1X1 ; al[1] = 'Y'; al[2] = 'Z';
Console.Write("Содержимое: "); foreach(char с in al)
Console.Write (c + " ");
Console.WriteLine ();
Вот к какому результату приводит выполнение этой программы.
Исходное количество элементов: О
Добавить 6 элементов Количество элементов: 6 Текущее содержимое: С А Е В D F
Удалить 2 элемента Количество элементов: 4 Содержимое: С Е В D
Добавить еще 20 элементов Текущая емкость: 32
Количество элементов после добавления 20 новых: 24 Содержимое: CEBDabcdefghij klmnopqrst
Изменить три первых элемента
Содержимое: XYZDabcdefghij klmnopqrst
Сортировка и поиск в коллекции типа ArrayList
Коллекцию типаArrayListможно отсортировать с помощью методаSort ().В этом случае поиск в отсортированной коллекции с помощью методаBinarySearch() становится еще более эффективным. Применение обоих методов демонстрируется в приведенном ниже примере программы.
// Отсортировать коллекцию типа ArrayList и осуществить в ней поиск.
using System;
using System.Collections;
class SortSearchDemo { static void Main() {
// Создать коллекцию в виде динамического массива.
ArrayList al = new ArrayList();
// Добавить элементы в динамический массив.
al.Add(55);
al.Add(43) ;
al.Add(-4);
al.Add(88);
al.Add(3);
al.Add(19) ;
Console.Write("Исходное содержимое: "); foreach(int i in al)
Console.Write (i + " ");
Console.WriteLine ("\n");
// Отсортировать динамический массив, al.Sort();
// Отобразить содержимое динамического массива, используя цикл foreach.
Console..Write ("Содержимое после сортировки: ") ; foreach (int i in al)
Console.Write (i + " ");
Console.WriteLine ("\n");
Console.WriteLine("Индекс элемента 43: " + al.BinarySearch (43));
}
}
Ниже приведен результат выполнения этой программы.
Исходное содержимое: 55 43 -488 3 19
Содержимое после сортировки: -4 3 19 43 55 88
Индекс элемента 43: 3
В одной и той же коллекции типаArrayListмогут храниться объекты любого типа. Тем не менее во время сортировки и поиска в ней эти объекты приходится сравнивать. Так, если бы список объектов в приведенном выше примере программы содержал символьную строку, то их сравнение привело бы к исключительной ситуации. Впрочем, для сравнения символьных строк и целых чисел можно создать специальные методы. О таких методах сравнения речь пойдет далее в этой главе.
Получение массива из коллекции типа ArrayList
В работе с коллекцией типаArrayListиногда требуется получить из ее содержимого обычный массив. Этой цели служит метод ТоАггау (). Для преобразования коллекции в массив имеется несколько причин. Две из них таковы: потребность в ускорении обработки при выполнении некоторых операций и необходимость передавать массив методу, который не перегружается, чтобы принять коллекцию. Но независимо от конкретной причины коллекция типаArrayListпреобразуется в обычный^лассив довольно просто, как показано в приведенном ниже примере программы.
// Преобразовать коллекцию типа ArrayList в обычный массив.
using System;
using System.Collections;
class ArrayListToArray { static void Main() {
ArrayList al = new ArrayList();
// Добавить элементы в динамический массив, al.Add(1); al.Add(2); al.Add(3) ; al.Add(4) ;
Console.Write("Содержимое: "); foreach(int i in al)
Console.Write(i + " ");
Console.WriteLine();
int[] ia = (int[]) al.ToArray(typeof(int)); int sum = 0;
// Просуммировать элементы массива, for(int i=0; icia.Length; i++) sum += ia[i];
Console.WriteLine("Сумма равна: " + sum);
}
}
Эта программа дает следующий результат.
Содержимое: 1 2 3 4 Сумма равна: 10
В начале этой программы создается коллекция целых чисел. Затем в ней вызывается методToArray() с указанием типаintполучаемого массива. В итоге создается целочисленный массив. Но посколькуArrayявляется типом, возвращаемым методомToArray (), то содержимое получаемого в итоге массива должно быть приведено к типуint[ ]. (Напомним, чтоArrayявляется базовым типом для всех массивов в С#.) И наконец, значения всех элементов массива суммируются.
Класс Hashtable
КлассHashtableпредназначен для создания коллекции, в которой для хранения ее элементов служит хеш-таблица. Как должно быть известно большинству читателей, информация сохраняется вхеш-таблицес помощью механизма, называемогохешированием.При хешировании для определения уникального значения, называемогохеш-кодом, используется информационное содержимое специального ключа. Полученный в итоге хеш-код служит в качестве индекса, по которому в таблице хранятся искомые данные, соответствующие заданному ключу. Преобразование ключа в хеш-код выполняется автоматически, и поэтому сам хеш-код вообще недоступен пользователю. Преимущество хеширования заключается в том, что оно обеспечивает постоянство времени выполнения операций поиска, извлечения и установки значений независимо от величины массивов данных. В классеHashtableреализуются интерфейсыI Dictionary, ICollection, IEnumerable, ISerializable, IDeserializationCallbackиICloneable.
В классеHashtableопределено немало конструкторов. Ниже приведены наиболее часто используемые конструкторы этого класса.
public Hashtable () public Hashtable(IDictionary d) public Hashtable(intcapacity)public Hashtable(intcapacity,
floatloadFactor)
В первой форме создается создаваемый по умолчанию объект классаHashtable.Во второй форме создаваемый объект типаHashtableинициализируется элементами из коллекцииd.В третьей форме создаваемый объект типаHashtableинициализируется, учитывая емкость коллекции, задаваемую параметромcapacity.И в четвертой форме создаваемый объект типаHashtableинициализируется, учитывая заданную емкостьcapacityи коэффициент заполненияloadFactor.Коэффициент заполнения, иногда еще называемыйкоэффициентом загрузки,должен находиться в пределах
от ОД до 1,0. Он определяет степень заполнения хеш-таблицы до увеличения ее размера. В частности, таблица расширяется, если количество элементов оказывается больше емкости таблицы, умноженной на коэффициент заполнения. В тех конструкторах, которые не принимают коэффициент заполнения в качестве параметра, этот коэффициент по умолчанию выбирается равным 1,0.
В классеHashtableопределяется ряд собственных методов, помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются. Некоторые из наиболее часто используемых методов этого класса приведены в табл. 25.5. В частности, для того чтобы определить, содержится ли ключ в коллекции типаHashtable,вызывается методContains Key(). А для того чтобы выяснить, хранится ли в такой коллекции конкретное значение, вызывается методContainsValue(). Для перечисления содержимого коллекции типаHashtableслужит методGetEnumerator(), возвращающий объект типаIDictionaryEnumerator.Напомним, чтоIDictionaryEnumerator— это перечислитель, используемый для перечисления содержимого коллекции, в которой хранятся пары "ключ-значение".
Таблица 25.5. Наиболее часто используемые методы, определенные в классе Hashtable
Метод
Описание
public virtual bool ContainsKey(objectkey)
public virtual bool ContainsValue(objectvalue)
public virtual IDictionaryEnumerator GetEnumerator () public static Hashtable Synchronized(Hashtabletable)
Возвращает логическое значение true, если в вызывающей коллекции типа Hashtable содержится ключ key, а иначе — логическое значение
false
Возвращает логическое значение true, если в вызывающей коллекции типа Hashtable содержится значение value, а иначе — логическое значение false
Возвращает для вызывающей коллекции типа Hashtable перечислитель типа IDictionaryEnumerator Возвращает синхронизированный вариант коллекции типа Hashtable, передаваемой в качестве параметра table
В классеHashtableдоступны также открытые свойства, определенные в тех интерфейсах, которые в нем реализуются. Особая роль принадлежит двум свойствам,KeysиValues,поскольку с их помощью можно получить ключи или значения из коллекции типаHashtable.Эти свойства определяются в интерфейсеIDictionaryследующим образом.
public virtual ICollection Keys { get; } public virtual ICollection Values { get; }
В классеHashtableне поддерживаются упорядоченные коллекции, и поэтому ключи или значения получаются из коллекции в произвольном порядке. Кроме того, в классеHashtableимеется защищенное свойствоEqualityComparer.А два других свойства,hepиcomparer,считаются устаревшими.
Пары "ключ-значение" сохраняются в коллекции типаHashtableв форме структуры типаDictionaryEntry,но чаще всего это делается без прямого вмешательства со стороны пользователя, поскольку свойства и методы оперируют ключами и значениями по отдельности. Если, например, в коллекцию типаHashtableдобавляется элемент, то для этой цели вызывается методAdd (), принимающий два аргумента: ключ и значение.
Нужно, однако, иметь в виду, что сохранение порядка следования элементов в коллекции типаHashtableне гарантируется. Дело в том, что процесс хеширования оказывается, как правило, непригодным для создания отсортированных таблиц.
Ниже приведен пример программы, в которой демонстрируется применение классаHashtable.
// Продемонстрировать применение класса Hashtable.
using System;
using System.Collections;
class HashtableDemo { static void Main() {
// Создать хеш-таблицу.
Hashtable ht = new Hashtable ();
// Добавить элементы в таблицу.
ht.Add("здание", "жилое помещение");
ht.Add("автомашина", "транспортное средство");
ht.Add("книга", "набор печатных слов");
ht.Add("яблоко", "съедобный плод");
// Добавить элементы с помощью индексатора, ht ["трактор"] = "сельскохозяйственная машина";
// Получить коллекцию ключей.
ICollection с = ht.Keys;
// Использовать ключи для получения значений, foreach(string str in с)
Console.WriteLine(str + ": " + ht[str]);
}
}
Выполнение этой программы приводит к следующему результату.
здание: жилое помещение книга: набор печатных слов трактор: сельскохозяйственная машина автомашина: транспортное средство яблоко: съедобный плод
Как следует из приведенного выше результата, пары "ключ-значение" сохраняются в произвольном порядке. Обратите внимание на то, как получено и отображено содержимое хеш-таблицыht.Сначала была получена коллекция ключей с помощью свойстваKeys.Затем каждый ключ был использован для индексирования хеш-таблицыhtс целью извлечь из нее значение, соответствующее заданному ключу. Напомним, что в качестве индекса в данном случае использовался индексатор, определенный в интерфейсеIDictionaryи реализованный в классеHashtable.
Класс SortedList
КлассSortedListпредназначен для создания коллекции, в которой пары "ключ-значение" хранятся в порядке, отсортированном по значению ключей. В классеSortedListреализуются интерфейсыIDictionary, ICollection, IEnumerableиICloneable.
В классеSortedListопределено несколько конструкторов, включая следующие.
public SortedList() public SortedList(IDictionary d) public SortedList(intinitialCapacity)public SortedList(IComparercomparer)
В первом конструкторе создается пустая коллекция, первоначальная емкость которой равна нулю. Во втором конструкторе создается пустая коллекция типаSortedList,которая инициализируется элементами из коллекции d. Ее первоначальная емкость равна количеству указанных элементов. В третьем конструкторе создается пустая коллекция типаSortedList,первоначальный размер которой определяет емкость, задаваемая параметромinitialCapacity.Эта емкость соответствует размеру базового массива, используемого для хранения элементов коллекции. И в четвертой форме конструктора с помощью параметрасотпрагегуказывается способ, используемый для сравнения объектов по списку. В этой форме создается пустая коллекция, первоначальная емкость которой равна нулю.
При добавлении новых элементов в список емкость коллекции типаSortedListувеличивается автоматически по мере надобности. Так, если текущая емкость коллекции превышается, то она соответственно увеличивается. Преимущество указания емкости коллекции типаSortedListпри ее создании заключается в снижении или полном исключении издержек на изменение размера коллекции. Разумеется, указывать емкость коллекции целесообразно лишь в том случае, если заранее известно, сколько элементов требуется хранить в ней.
В классеSortedListопределяется ряд собственных методов, помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются. Некоторые из наиболее часто используемых методов этого класса перечислены в табл. 25.6. Так, если требуется определить, содержится ли ключ в коллекции типаSortedList,вызывается методContains Key(). А если требуется выяснить, хранится ли конкретное значение в коллекции типаSortedList,вызывается методContainsValue(). Для перечисления содержимого коллекции типаSortedListслужит методGetEnumerator (),возвращающий объект типаIDict ionar yEnumerator.Напомним, чтоIDictionaryEnumerator— это перечислитель, используемый для перечисления содержимого коллекции, в которой хранятся пары "ключ-значение". И наконец, для получения синхронизированной оболочки, в которую заключается коллекция типаSortedList,вызывается методSynchronized().
Таблица 25.6. Наиболее часто используемые методы, определенные в классе SortedList
Метод
Описание
public virtual bool
Возвращает логическое значение true, если в
ContainsKey(objectkey)
вызывающей коллекции типа SortedList содер
жится ключ key, а иначе — логическое значение
false
Окончание табл. 25.6
Метод
Описание
public virtual bool
Возвращает логическое значение true, если в
ContainsValue(objectvalue)
вызывающей коллекции типа SortedList со
держится значение value, а иначе — логическое значение false
public virtual object
Возвращает значение, указываемое по индексу
GetBylndex(intindex)
index
public virtual
Возвращает для вызывающей коллек
IDictionaryEnumerator
ции типа SortedList перечислитель типа
GetEnumerator()
IDictionaryEnumerator
public virtual object
Возвращает значение ключа, указываемое по ин
GetKey(intindex)
дексу index
public virtual IList
Возвращает коллекцию типа SortedList с клю
GetKeyList()
чами, хранящимися в вызывающей коллекции типа SortedList
public virtual IList
Возвращает коллекцию типа SortedList со зна
GetValueList()
чениями, хранящимися в вызывающей коллекции типа SortedList
public virtual int
Возвращает индекс ключа key. Если искомый
IndexOfKey(objectkey)
ключ не обнаружен, возвращается значение -1
public virtual int
Возвращает индекс первого вхождения значения
IndexOfValue(objectvalue)
value в вызывающей коллекции. Если искомое значение не обнаружено, возвращается значение -1
public virtual void
Устанавливает значение по индексу index рав
SetBylndex(intindex,object
ным значению value
value)
public static SortedList
Возвращает синхронизированный вариант коллек
Synchronized(SortedListlist)
ции типа SortedList, передаваемой в качестве параметра list
public virtual void
Устанавливает значение свойства Capacity рав
TrimToSize()
ным значению свойства Count
Ключ илизначение можно получить разными способами. В частности, для получения значения по указанному индексу служит методGetBylndex(), а для установки значения по указанному индексу — методSetBylndex(). Для извлечения ключа по указанному индексу вызывается методGet Key(), а для получения списка ключей по указанному индексу — методGetKeyList(). Кроме того, для получения списка всех значений из коллекции служит методGetValueList().Для получения индекса ключа вызывается методIndexOfKey(), а для получения индекса значения — методIndexOfValue(). Безусловно, в классеSortedListтакже поддерживается индексатор, определяемый в интерфейсеIDictionaryи позволяющий устанавливать и получать значение по заданному ключу.
В классеSortedListдоступны также открытые свойства, определенные в тех интерфейсах, которые в нем реализуются. Как и в классеHashtable,в данном классе особая роль принадлежит двум свойствам,KeysиValues,поскольку с их помощью можно получить доступную только для чтения коллекцию ключей или значений из
коллекции типаSortedList.Эти свойства определяются в интерфейсеIDictionaryследующим образом.
public virtual ICollection Keys { get; } public virtual ICollection Values { get; }
Порядок следования ключей и значений отражает порядок их расположения в коллекции типаSortedList.
Аналогично коллекции типаHashtable,пары "ключ-значение" сохраняются в коллекции типаSortedListв форме структуры типаDictionaryEntry,но, как правило, доступ к ключам и значениям осуществляется по отдельности с помощью методов и свойств, определенных в классеSortedList.
В приведенном ниже примере программы демонстрируется применение классаSortedList.Это переработанный и расширенный вариант предыдущего примера, демонстрировавшего применение классаHashtable,вместо которого теперь используется классSortedList.Глядя на результат выполнения этой программы, вы можете сами убедиться, что теперь список полученных значений оказывается отсортированным по заданному ключу.
// Продемонстрировать применение класса SortedList.
using System;
using System.Collections;
class SLDemo { static void Main() {
// Создать отсортированный список.
SortedList si = new SortedList();
// Добавить элементы в список.
si.Add("здание", "жилое помещение");
si.Add("автомашина", "транспортное средство");
si.Add("книга", "набор печатных слов");
si.Add("яблоко", "съедобный плод");
// Добавить элементы с помощью индексатора, si["трактор"] = "сельскохозяйственная машина";
// Получить коллекцию ключей.
ICollection с = si.Keys;
// Использовать ключи для получения значений.
Console.WriteLine("Содержимое списка по индексатору."); foreach(string str in с)
Console.WriteLine(str + ": " + si[str]);
Console.WriteLine();
// Отобразить список, используя целочисленные индексы.
Console.WriteLine("Содержимое списка по целочисленным индексам."); for(int i=0; i < si.Count; i++)
Console.WriteLine(si.GetBylndex(i)) ;
Console.WriteLine() ;
// Показать целочисленные индексы элементов списка.
Console.WriteLine("Целочисленные индексы элементов списка."); foreach(string str in с)
Console.WriteLine(str + ": " + si.IndexOfKey(str));
}
}
Ниже приведен результат выполнения этой программы.
Содержимое списка по индексатору, автомашина: транспортное средство здание: жилое помещение книга: набор печатных слов трактор: сельскохозяйственная Машина яблоко: съедобный плод
Содержимое списка по целочисленным индексам.
транспортное средство
жилое помещение
набор печатных слов
сельскохозяйственная машина
съедобный плод
Целочисленные индексы элементов списка.
автомашина: О
здание: 1
книга: 2
трактор: 3
яблоко: 4
Класс Stack
Как должно быть известно большинству читателей,стекпредставляет собой список, действующий по принципу "первым пришел — последним обслужен". Этот принцип действия стека можно наглядно представить на примере горки тарелок, стоящих на столе. Первая тарелка, поставленная в эту горку, извлекается из нее последней. Стек относится к одним из самых важных структур данных в вычислительной технике. Он нередко применяется, среди прочего, в системном программном обеспечении, компиляторах, а также в программах отслеживания в обратном порядке на основе искусственного интеллекта
Класс коллекции, поддерживающий стек, носит названиеStack.В нем реализуются интерфейсыICollection, IEnumerableиICloneable.Этот класс создает динамическую коллекцию, которая расширяется по мере потребности хранить в ней вводимые элементы. Всякий раз, когда требуется расширить такую коллекцию, ее емкость увеличивается вдвое.
В классеStackопределяются следующие конструкторы.
public Stack()
public Stack(intinitialCapacity)public Stack(ICollectioncol)
В первой форме конструктора создается пустой стек, во второй форме — пустой стек, первоначальный размер которого определяет первоначальная емкость, задаваемая параметромinitialCapacity,ив третьей форме — стек, содержащий элементы указываемой коллекцииcol.Его первоначальная емкость равна количеству указанных элементов.
В классеStackопределяется ряд собственных методов, помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются. Некоторые из наиболее часто используемых методов этого класса приведены в табл. 25.7. Эти методы обычно применяются следующим образом. Для того чтобы поместить объект на вершине стека, вызывается методPush(). А для того чтобы извлечь и удалить объект из вершины стека, вызывается методPop(). Если же объект требуется только извлечь, но не удалить из вершины стека, то вызывается методРеек(). А если вызвать методPop() илиРеек(), когда вызывающий стек пуст, то сгенерируется исключениеInvalidOperationException.
Таблица 25.7. Наиболее часто используемые методы, определенные в классе Stack
Метод
Описание
public virtual void Clear ()
public virtual bool Contains (objectobj)
public virtual object Peek()
public virtual object Pop()
public virtual void Push (objectobj)public static Stack Synchronized(Stackstack)
public virtual object[] ToArray()
Устанавливает свойство Count равным нулю, очищая, по существу, стек
Возвращает логическое значение true, если объект obj содержится в вызывающем стеке, а иначе — логическое значение false Возвращает элемент, находящийся на вершине стека, но не удаляет его
Возвращает элемент, находящийся на вершине стека, удаляя его по ходу дела Помещает объект obj в стек
Возвращает синхронизированный вариант коллекции типа Stack, передаваемой в качестве параметра stack
Возвращает массив, содержащий копии элементов вызывающего стека
В приведенном ниже примере программы создается стек, в который помещается несколько целых значений, а затем они извлекаются обратно из стека.
// Продемонстрировать применение класса Stack.
using System;
using System.Collections;
class StackDemo {
static void ShowPush(Stack st, int a) { st.Push(a);
Console.WriteLine("Поместить в стек: Push(" + a + ")"); Console.Write("Содержимое стека: "); foreach(int i in st)
Console.Write(i + " ");
Console.WriteLine();
}
static void ShowPop(Stack st) {
Console.Write("Извлечь из стека: Pop -> "); int a = (int) st.PopO;
Console.WriteLine(а);
Console.Write("Содержимое стека: "); foreach(int i in st)
Console.Write(i + " ");
Console.WriteLine();
}
static void Main() {
Stack st = new Stack ();
foreach(int i in st)
Console.Write(i + " ");
Console.WriteLine();
ShowPush(st, 22);
ShowPush(st, 65);
ShowPush(st, 91);
ShowPop(st);
ShowPop(st);
ShowPop(st) ;
try {
ShowPop(st) ;
} catch (InvalidOperationException) { Console.WriteLine("Стек пуст.");
}
} .
}
Ниже приведен результат выполнения этой программы. Обратите внимание на то, как обрабатывается исключениеInvalidOperationException,генерируемое при попытке извлечь элемент из пустого стека.
Поместить в стек: Push(22)
Содержимое стека: 22 Поместить в стек: Push(65)
Содержимое стека: 65 22 Поместить в стек: Push (91)
Содержимое стека: 91 65 22 Извлечь из стека: Pop -> 91 Содержимое стека: 65 22 Извлечь из стека: Pop -> 65 Содержимое стека: 22 Извлечь из стека: Pop -> 22 Содержимое стека:
Извлечь из стека: Pop -> Стек пуст.
Класс Queue
Еще одной распространейной структурой данных являетсяочередь,действующая по принципу: первым пришел — первым обслужен. Это означает, что первым из очереди извлекается элемент, помещенный в нее первым. Очереди часто встречаются в
реальной жизни. Многим из нас нередко приходилось стоять в очередях к кассе в банке, магазине или столовой. В программировании очереди применяются для хранения таких элементов, как процессы, выполняющиеся в данный момент в системе, списки приостановленных транзакций в базе данных или пакеты данных, полученные по Интернету. Кроме того, очереди нередко применяются в области имитационного моделирования.
Класс коллекции, поддерживающий очередь, носит названиеQueue.В нем реализуются интерфейсыICollection, IEnumerableиICloneable.Этот класс создает динамическую коллекцию, которая расширяется, если в ней необходимо хранить вводимые элементы. Так, если в очереди требуется свободное место, ее размер увеличивается на коэффициент роста, который по умолчанию равен 2,0.
В классеQueueопределяются приведенные ниже конструкторы.
public Queue()
public Queue (intcapacity)
public Queue (intcapacity,floatgrowFactor)public Queue (ICollectioncol)
В первой форме конструктора создается пустая очередь с выбираемыми по умолчанию емкостью и коэффициентом роста 2,0. Во второй форме создается пустая очередь, первоначальный размер которой определяет емкость, задаваемая параметромcapaci ty,а коэффициент роста по умолчанию выбирается для нее равным 2,0. В третьей форме допускается указывать не только емкость (в качестве параметраcapacity), но и коэффициент роста создаваемой очереди (в качестве параметраgrowFactorв пределах от 1,0 до 10,0). И в четвертой форме создается очередь, состоящая из элементов указываемой коллекцииcol.Ее первоначальная емкость равна количеству указанных элементов, а коэффициент роста по умолчанию выбирается для нее равным 2,0.
В классеQueueопределяется ряд собственных методов, помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются. Некоторые из наиболее часто используемых методов этого класса перечислены в табл. 25.8. Эти методы обычно применяются следующим образом. Для того чтобы поместить объект в очередь, вызывается методEnqueue (). Если требуется извлечь' и удалить первый объект из начала очереди, то вызывается методDequeue(). Если же требуется извлечь, но не удалять следующий объект из очереди, то вызывается метод Реек (). А если методыDequeueOиРеек() вызываются, когда очередь пуста, то генерируется исключениеInvalidOperationException.
Таблица 25.8. Наиболее часто используемые методы, определенные в классе Queue
Метод
Описание
public virtual void Clear()
public virtual bool Contains(object obj)
public virtual object Dequeue()
public virtual void Enqueue(object obj)
Устанавливает свойство Count равным нулю, очищая, по существу, очередь
Возвращает логическое значение true, если объект obj содержится в вызывающей очереди, а иначе — логическое значение false Возвращает объект из начала вызывающей очереди. Возвращаемый объект удаляется из очереди Добавляет объект obj в конец очереди
Окончание табл. 25.8
Метод
Описание
public virtual object Peek()
public static Queue Synchronized(Queuequeue)
public virtual object[] ToArray()
public virtual void TrimToSize()
Возвращает объект из начала вызывающей очереди, но не удаляет его
Возвращает синхронизированный вариант коллекции типа Queue, передаваемой в качестве параметра queue
Возвращает массив, который содержит копии элементов из вызывающей очереди Устанавливает значение свойства Capacity равным значению свойства Count
В приведенном ниже примере программы демонстрируется применение класса
Queue.
// Продемонстрировать применение класса Queue.
using System;
using System.Collections;
class QueueDemo {
static void ShowEnq(Queue q, int a) { q.Enqueue(a) ;
Console.WriteLine("Поместить в очередь: Enqueue(" + a + ")");
Console.Write("Содержимое очереди: "); foreach(int i in q)
Console.Write(i + " ");
Console.WriteLine() ;
}
static void ShowDeq(Queue q) {
Console.Write("Извлечь из очереди: Dequeue -> "); int a = (int) q.Dequeue();
Console.WriteLine(a);
Console.Write("Содержимое очереди: "); foreach(int i in q)
Console.Write(i + " ") ;
Console.WriteLine();
}
static void Main() {
Queue q = new Queue();
foreach(int i in q)
Console.Write(i + " ");
ShowEnq(q, 22);
ShowEnq(q, 65);
ShowEnq(q, 91);
ShowDeq(q);
ShowDeq(q);
ShowDeq(q);
try {
ShowDeq (q);
} catch (InvalidOperationException) { Console.WriteLine("Очередь пуста.");
}
}
}
Эта программа дает следующий результат.
Поместить в очередь: Enqueue(22)
Содержимое очереди: 22 Поместить в очередь: Enqueue(65)
Содержимое очереди: 22 65 Поместить в очередь: Enqueue(91)
Содержимое очереди: 22 65 91 Извлечь из очереди: Dequeue -> 22 Содержимое очереди: 65 91 Извлечь из очереди: Dequeue -> 65 Содержимое очереди: 91 Извлечь из очереди: Dequeue -> 91 Содержимое очереди:
Извлечь из очереди: Dequeue -> Очередь пуста.
Хранение отдельных битов в классе коллекции BitArray
КлассBitArrayслужит для хранения отдельных битов в коллекции. А поскольку в коллекции этого класса хранятся биты, а не объекты, то своими возможностями он отличается от классов других коллекций. Тем не менее в классеBitArrayреализуются интерфейсыICollectionиIEnumerableкак основополагающие элементы поддержки всех типов коллекций. Кроме того, в классеBitArrayреализуется интерфейсICloneable.
В классеBitArrayопределено несколько конструкторов. Так, с помощью приведенного ниже конструктора можно сконструировать объект типаBitArrayиз массива логических значений.
public BitArray(bool[]values)
В данном случае каждый элемент массиваvaluesстановится отдельным битом в коллекции. Это означает, что каждому элементу массиваvaluesсоответствует отдельный бит в коллекции. Более того, порядок расположения элементов в массивеvaluesсохраняется и в коллекции соответствующих им битов.
Коллекцию типаBitArrayможно также составить из массива байтов, используя следующий конструктор.
Здесь битами в коллекции становится уже целый их набор из массиваbytes,причем элементbytes [ 0 ] обозначает первые 8 битов, элементbytes[ 1 ] — вторые 8 битов и т.д. Аналогично, коллекцию типаBit Arrayможно составить из массива целочисленных значений, используя приведенный ниже конструктор.
public BitArray(int[ ]values)
В данном случае элементvalues [0 ] обозначает первые 32 бита, элементvalues [ 1 ] — вторые 32 бита и т.д.
С помощью следующего конструктора можно составить коллекцию типаBitArray,указав ее конкретный размер:
public BitArray(intlength)
гдеlengthобозначает количество битов в коллекции, которые инициализируются логическим значениемfalse.В приведенном ниже конструкторе можно указать не только размер коллекции, но и первоначальное значение составляющих ее битов.
public BitArray(intlength,booldefaultValue)
В данном случае все биты в коллекции инициализируются значениемdefaultValue,передаваемым конструктору в качестве параметра.
И наконец, новую коллекцию типаBitArrayможно создать из уже существующей, используя следующий конструктор.
public BitArray(BitArraybits)
Вновь сконструированный объект будет содержать такое же количество битов, как и в указываемой коллекцииbits,а в остальном это будут две совершенно разные коллекции.
Коллекции типаBitArrayподлежат индексированию. По каждому индексу указывается отдельный бит в коллекции, причем нулевой индекс обозначает младший бит.
В классеBitArrayопределяется ряд собственных методов, помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются. Методы этого класса приведены в табл. 25.9. Обратите внимание на то, что в классеBitArrayне поддерживается методSynchronized(). Это означает, что для коллекций данного класса синхронизированная оболочка недоступна, а свойствоIsSynchronizedвсегда имеет логическое значениеfalse.Тем не менее для управления доступом к коллекции типаBitArrayее можно синхронизировать для объекта, предоставляемого упоминавшимся ранее свойствомSyncRoot.
Таблица 25.9. Методы, определенные в классе BitArray
Метод
Описание
public
value)
BitArray And(BitArray
Выполняет операцию логического умножения И битов вызывающего объекта и коллекции value. Возвращает коллекцию типа BitArray, содержащую результат
public
bool Get(intindex)
Возвращает значение бита, указываемого по индексу index
public
BitArray Not()
Выполняет операцию поразрядного логического отрицания НЕ битов вызывающей коллекции и возвращает коллекцию типа BitArray, содержащую результат
Метод
Описание
public BitArray Or(BitArrayvalue)
public void Set (intindex,boolvalue)
public void SetAll(boolvalue)
public BitArray Xor(BitArrayvalue)
Выполняет операцию логического сложения ИЛИ битов вызывающего объекта и коллекции value. Возвращает коллекцию типа BitArray, содержащую результат
Устанавливает бит, указываемый по индексу index, равным значению value
Устанавливает все биты равными значению value
Выполняет логическую операцию исключающее ИЛИ над битами вызывающего объекта и коллекции value. Возвращает коллекцию типа BitArray, со-
В классеBitArrayопределяется также собственное свойство, помимо тех, что указаны в интерфейсах, которые в нем реализуются.
public int Length { get; set; }
СвойствоLengthпозволяет установить или получить количество битов в коллекции. Следовательно, оно возвращает такое же значение, как и стандартное свойствоCount,определяемое для всех коллекций. В отличие от свойстваCount,свойствоLengthдоступно не только для чтения, но и для записи, а значит, с его помощью можно изменить размер коллекции типаBitArray.Так, при сокращении коллекции типаBitArrayлишние биты усекаются, начиная со старшего разряда. А при расширении коллекции типаBitArrayдополнительные биты, имеющие логическое значениеfalse,вводятся в коллекцию, начиная с того же старшего разряда.
Кроме того, в классеBitArrayопределяется следующий индексатор.
public bool this[intindex]{ get; set; }
С помощью этого индексатора можно получать или устанавливать значение элемента. В приведенном ниже примере демонстрируется применение классаBitArray.
// Продемонстрировать применение класса BitArray.
using System;
using System.Collections;
class BADemo {
public static void ShowBits(string rem,
BitArray bits) {
Console.WriteLine(rem);
for(int i=0; i < bits.Count; i++)
Console.Write("{0, -6} ", bits[i]);
Console.WriteLine ("\n");
}
static void Main() {
BitArray ba = new BitArray(8); byte[] b = { 67 };
BitArray ba2 = new BitArray(b);
ShowBits("Исходное содержимое коллекции Ьа:", Ьа) ; ba = Ьа.Not ();
ShowBits("Содержимое коллекции Ьа после логической операции NOT:", Ьа);
ShowBits("Содержимое коллекции Ьа2:", Ьа2);
BitArray ЬаЗ = Ьа.Хог(Ьа2);
ShowBits("Результат логической операции ba XOR Ьа2:", ЬаЗ);
}
}
Эта программа дает следующий результат.
Исходное содержимое коллекции Ьа:
False False False False False False False False Содержимое коллекции ba после логической операции NOT:
True True True True True True True True Содержимое коллекции ba2:
True True False False False False ?True False Результат логической операции ba XOR ba2:
False False True True True True False True
Специальные коллекции
В среде .NET Framework предусмотрен ряд специальных коллекций, оптимизированных для работы с данными конкретного типа или для их обработки особым образом. Классы этих необобщенных коллекций определены в пространстве имен System. Collections . Specialized и перечислены ниже.
Класс специальной коллекции
Описание
CollectionsUtil
Содержит фабричные методы для создания коллекций
HybridDictionary
Предназначен для коллекций, в которых для хранения небольшого количества пар “ключ-значение” используется класс ListDictionary. При превышении коллекцией определенного размера автоматически используется класс Hashtable для хранения ее элементов
ListDictionary
Предназначен для коллекций, в которых для хранения пар “ключ-значение" используется связный список. Такие коллекции рекомендуются только для хранения небольшого количества элементов
NameValueCollection
Предназначен для отсортированных коллекций, в которых хранятся пары “ключ-значение”, причем и ключ, и значение относятся к типу string
OrderedDictionary
Предназначен для коллекций, в которых хранятся индексируемые пары “ключ-значение”
StringCollection
Предназначен для коллекций, оптимизированных для хранения символьных строк
StringDictionary
Предназначен для хеш-таблиц, в которых хранятся пары “ключ-значение”, причем и ключ, и значение относятся к типу
string
Кроме того, в пространстве именSystem. Collectionsопределены три базовых абстрактных класса:CollectionBase, ReadOnlyCollectionBaseиDictionaryBase.Эти классы могут наследоваться и служить в качестве отправной точки для разработки собственных специальных коллекций.
Обобщенные коллекции
Благодаря внедрению обобщений прикладной интерфейс Collections API значительно расширился, в результате чего количество классов коллекций и интерфейсов удвоилось. Обобщенные коллекции объявляются в пространстве именSystem. Collections . Generic.Как правило, классы обобщенных коллекций являются не более чем обобщенными эквивалентами рассматривавшихся ранее классов необобщенных коллекций, хотя это соответствие не является взаимно однозначным. Например, в классе обобщенной коллекцииLinkedListреализуется двунаправленный список, тогда как в необобщенном эквиваленте его не существует. В некоторых случаях одни и те же функции существуют параллельно в классах обобщенных и необобщенных коллекций, хотя и под разными именами. Так, обобщенный вариант классаArrayListназываетсяList,а обобщенный вариант классаHashTable — Dictionary.Кроме того, конкретное содержимое различных интерфейсов и классов реорганизуется с минимальными изменениями для переноса некоторых функций из одного интерфейса в другой. Но в целом, имея ясное представление о необобщенных коллекциях, можно без особого труда научиться применять и обобщенные коллекции.
Как правило, обобщенные коллекции действуют по тому же принципу, что и-необобщенные, за исключением того, что обобщенные коллекции типизированы. Это означает, что в обобщенной коллекции можно хранить только те элементы, которые совместимы по типу с ее аргументом. Так, если требуется коллекция для хранения несвязанных друг с другом разнотипных данных, то для этой цели следует использовать классы необобщенных коллекций. А во всех остальных случаях, когда в коллекции должны храниться объекты только одного типа, выбор рекомендуется останавливать на классах обобщенных коллекций.
Обобщенные коллекции определяются в ряде интерфейсов и классов, реализующих эти интерфейсы. Все они описываются далее по порядку.
Интерфейсы обобщенных коллекций
В пространстве именSystem. Collections. Genericопределен целый ряд интерфейсов обобщенных коллекций, имеющих соответствующие аналоги среди интерфейсов необобщенных коллекций. Все эти интерфейсы сведены в табл. 25.10.
Таблица 25.10. Интерфейсы обобщенных коллекций
Интерфейс Описание
lCollection<T> Определяет основополагающие свойства обобщенных
коллекций
1Сотрагег<т> Определяет обобщенный метод Compare () для сравнения объектов, хранящихся в коллекции lDictionary<Tkey, TValue> Определяет обобщенную коллекцию, состоящую из пар
“ключ-значение"
Окончание табл. 25.10
Интерфейс
Описание
IEnumerable<T>
Определяет обобщенный метод GetEnumerator (),
-
предоставляющий перечислитель для любого класса
коллекции
Enumerator<T>
Предоставляет методы, позволяющие получать содержи
мое коллекции по очереди
IEqualityComparer<T>
Сравнивает два объекта на предмет равенства
IList<T>
Определяет обобщенную коллекцию, доступ к которой
можно получить с помощью индексатора
Интерфейс ICollection<T>
В интерфейсеICollection<T>определен ряд свойств, которые являются общими для всех обобщенных коллекций. ИнтерфейсICollection<T>является обобщенным вариантом необобщенного интерфейсаicollection,хотя между ними имеются некоторые отличия.
Итак, в интерфейсеICollection<T>определены следующие свойства.
int Count { get; } bool IsReadOnly { get; }
СвойствоCountсодержит ряд элементов, хранящихся в данный момент в коллекции. А свойствоIsReadOnlyимеет логическое значениеtrue,если коллекция доступна только для чтения. Если же коллекция доступна как для чтения, так и для записи, то данное свойство имеет логическое значение false.
Кроме того, в интерфейсеICollection<T>определены перечисленные ниже методы. Обратите внимание на то, что в этом обобщенном интерфейсе определено несколько большее количество методов, чем в его необобщенном аналоге.
Метод
Описание
void Add(Titem)
void Clear()
bool Contains(Titem)
void CopyTo(T[]array,intarraylndex)
void Remove(Titem)
Добавляет элемент item в вызывающую коллекцию. Генерирует исключение NotSupportedException, если коллекция доступна только для чтения Удаляет все элементы из вызывающей коллекции Возвращает логическое значение true, если вызывающая коллекция содержит элемент item, а иначе — логическое значение false
Копирует содержимое вызывающей коллекции в массив array, начиная с элемента, указываемого по индексу
arraylndex
Удаляет первое вхождение элемента item в вызывающей коллекции. Возвращает логическое значение true, если элемент i tem удален. А если этот элемент не найден в вызывающей коллекции, то возвращается логическое значение false
Некоторые из перечисленных выше методов генерируют исключениеNotSupportedException,если коллекция доступна только для чтения.
А поскольку интерфейсICollection<T>наследует от интерфейсовIEnumerableиIEnumerable<T>,то он включает в себя также обобщенную и необобщенную формы методаGetEnumerator ().
Благодаря тому что в интерфейсеICollection<T>реализуется интерфейсIEnumerable<T>,в нем поддерживаются также методы расширения, определенные в классеEnumerable.Несмотря на то что методы расширения предназначены главным образом для поддержки LINQ, им можно найти и другое применение, в том числе и в коллекциях.
Интерфейс IList<T>
В интерфейсеIList<T>определяется такое поведение обобщенной коллекции, которое позволяет осуществлять доступ к ее элементам по индексу с отсчетом от нуля. Этот интерфейс наследует от интерфейсовIEnumerable, IEnumerable<T>иICollection<T>и поэтому является обобщенным вариантом необобщенного интерфейсаIList.Методы, определенные в интерфейсеIList<T>,перечислены в табл. 25.11. В двух из этих методов предусматривается модификация коллекции. Если же коллекция доступна только для чтения или имеет фиксированный размер, то методыInsert () иRemoveAt ()генерируют исключениеNotSupportedException.
Таблица 25.11. Методы, определенные в интерфейсе IList<T>
Метод
Описание
int IndexOf(Тitem)
void Insert(intindex,
Titem)
void RemoveAt(intindex)
Возвращает индекс первого вхождения элемента item в вызывающей коллекции. Если элемент item не обнаружен, то метод возвращает значение -1 Вставляет в вызывающую коллекцию элемент item по индексу index
Удаляет из вызывающей коллекции элемент, расположенный по указанному индексу index
Кроме того, в интерфейсеIList<T>определяется индексатор
Т this[intindex]{ get; set; }
который устанавливает или возвращает значение элемента коллекции по указанному индексуindex.
Интерфейс IDictionary<TKey, TValue>
В интерфейсеIDictionary<TKey, TValue>определяется такое поведение обобщенной коллекции, которое позволяет преобразовать уникальные ключи в соответствующие значения. Это означает, что в данном интерфейсе определяется коллекция, в которой хранятся пары "ключ-значение". ИнтерфейсIDictionary<TKey, TValue>наследует от интерфейсовIEnumerable, IEnumerable<KeyValuePair<TKey, TValue>>иICollection<KeyValuePair<TKey, TValue>>и поэтому является обобщенным вариантом необобщенного интерфейсаIDictionary.Методы, объявленные в интерфейсеIDictionary<TKey, TValue>,приведены в табл. 25.12. Все эти методы генерируют исключениеArgumentNullExceptionпри попытке указать пустой ключ.
Таблица 25.12. Методы, определенные в интерфейсе IDictionaryCTKey, TValue>
Метод
Описание
void Add(TKeykey, TValuevalue\
bool Contains(TKeykey)
bool Remove(TKeykey)
bool TryGetValue(TKeykey, out TValuevalue)
Добавляет в вызывающую коллекцию пару “ключ-значение”, определяемую параметрами key и value. Генерирует исключение ArgumentException, если ключ key уже находится в коллекции Возвращает логическое значение true, если вызывающая коллекция содержит элемент key в качестве ключа, а иначе — логическое значение false Удаляет из коллекции элемент, ключ которого равен значению key
Предпринимает попытку извлечь значение из коллекции по указанному ключу key и присвоить это значение переменной value. При удачном исходе операции возвращается логическое значение true, а иначе — логическое значение false. Если ключ key не найден, переменной value присваивается значение, выбираемое по умолчанию
Кроме того, в интерфейсеIDictionary<TKey, TValue>определены перечисленные ниже свойства.
Свойство
Описание
ICollection Keys<TKey> { get; } Подучает коллекцию ключей ICollection Values<TValue> { get; } Получает коллекцию значений
Следует иметь в виду, что ключи и значения, содержащиеся в коллекции, доступны отдельными списками с помощью свойствKeysиValues.
И наконец, в интерфейсеIDictionary<TKey, TValue>определяется следующий индексатор.
TValue this[TKeykey] { get; set; }
Этот индексатор служит для получения и установки значения элемента коллекции, а также для добавления в коллекцию нового элемента. Следует, однако, иметь в виду, что в качестве индекса в данном случае служит ключ элемента, а не сам индекс.
Интерфейсы IEnumerable<T> и IEnumerator<T>
ИнтерфейсыIEnumerable<T>иIEnumerator<T>являются обобщенными эквивалентами рассмотренных ранее необобщенных интерфейсовIEnumerableиIEnumerator.В них объявляются аналогичные методы и свойства, да и действуют они по тому же принципу. Разумеется, обобщенные интерфейсы оперируют данными только того типа, который указывается в аргументе типа.
В интерфейсеIEnumerable<T>методGetEnumerator() объявляется следующим образом.
IEnumerator<T> GetEnumerator()
Этот метод возвращает перечислитель типаТдля коллекции. А это означает, что он возвращает типизированный перечислитель.
Кроме того, в интерфейсеIEnumerable<T>определяются два таких же метода, как и в необобщенном его варианте:MoveNext () иReset (). В этом интерфейсе объявляется также обобщенный вариант свойстваCurrent.
Т Current { get; }
Это свойство возвращает ссылку типаТна следующий объект. А это означает, что обобщенный вариант свойстваCurrentявляется типизированным.
Но между интерфейсамиIEnumeratorиIEnumerator<T>имеется одно важное различие: интерфейсIEnumerator<T>наследует от интерфейсаIDisposable,тогда как интерфейсIEnumeratorне наследует от него. В интерфейсеIDisposableопределяется методDispose (), который служит для освобождения неуправляемых ресурсов.
ПРИМЕЧАНИЕ
В интерфейсе lEnumerable<T> реализуется также необобщенный интерфейс IEnumerable. Это означает, что в нем поддерживается необобщенный вариант метода GetEnumerator (). Кроме того, в интерфейсе lEnumerable<T> реализуется необобщенный интерфейс IEnumerator, а следовательно, в нем поддерживаются необобщенные варианты свойства Current.
Интерфейс IComparer<T>
ИнтерфейсIComparer<Т>является обобщенным вариантом рассмотренного ранее интерфейсаIComparer.Главное отличие между ними заключается в том, что интерфейсIComparer<T>обеспечивает типовую безопасность. В нем обобщенный вариант методаCompare() объявляется следующим образом.
int Compare(Т х, Т у)
В этом методе сравниваются объекты х и у. Он возвращает положительное значение, если значение объекта х больше, чем у объекта у; отрицательное — если значение объекта х меньше, чем у объекта у; и нулевое значение — если сравниваемые значения равны.
Интерфейс IEqualityComparer<T>
ИнтерфейсIEqualityComparer<T>полностью соответствует своему необобщенному аналогуEqualityComparer.В нем определяются два следующих метода.
bool Equals(Тх,Т у) int GetHashCode(Тobj)
МетодEquals() должен возвратить логическое значениеtrue,если значения объектов х и у равны. А методGetHashCode() возвращает хеш-код для объектаobj.Если два сравниваемых объекта равны, то их хеш-коды также должны быть одинаковы.
Интерфейс ISet<T>
ИнтерфейсISet<T>был добавлен в версию 4.0 среды .NET Framework. Он определяет поведение обобщенной коллекции, реализующей ряд уникальных элементов. Этот интерфейс наследует от интерфейсовIEnumerable, IEnumerable<T>,а такжеICollection<T>.В интерфейсеISet<T>определен ряд методов, перечисленных в табл. 25.13. Обратите внимание на то, что параметры этих методов указываются как относящиеся к типу IEnumerable<T>. Это означает, что в качестве второго аргумента методу можно передать нечто, отличающееся от объектов типа ISet<T>. Но чаще всего оба аргумента оказываются экземплярами объектов типа ISet<T>.
Таблица 25.13. Методы, определенные в интерфейсе ISet<T>
Метод
Описание
void ExceptWith(Ienumerable<T>
Удаляет из вызывающего множества те элементы,
other)
которые содержатся в другом множестве other
void
После вызова этого метода вызывающее множе
IntersectWith(IEnumerable<T>
ство содержит пересечение своих элементов с эле
other)
ментами другого множества other
bool
Возвращает логическое значение true, если вы
IsProperSubsetOf(IEnumerable<T>
зывающее множество является правильным под
other)
множеством другого множества other, а иначе — логическое значение false
bool IsProperSupersetOf(lEnumera
возвращает логическое значение true, если вы
ble<T>other)
зывающее множество является правильным надмножеством другого множества other, а иначе — логическое значение false
bool IsSubsetOf(IEnumerable<T>
Возвращает логическое значение true, если вы
other)
зывающее множество является подмножеством другого множества other, а иначе — логическое значение false
bool
Возвращает логическое значение true, если вы
IsSupersetOf(IEnumerable<T>
зывающее множество является надмножеством
other)
другого множества other, а иначе — логическое значение false
bool Overlaps(IEnumerable<T>
Возвращает логическое значение true, если вы
other)
зывающее множество и другое множество other содержат хотя бы один общий элемент, а иначе — логическое значение false
bool SetEquals(IEnumerable<T>
Возвращает логическое значение true, если все
other)
элементы вызывающего множества и другого множества other оказываются общими, а иначе —логическое значение false. Порядок расположения элементов не имеет значения, а дублирующиеся элементы во другом множестве other игнорируются
void SymmetricExceptWith
После вызова этого метода вызывающее множе
(IEnumerable<T>other)
ство будет содержать симметрическую разность своих элементов и элементов другого множества
other
void UnionWith(IEnumerable<T>
После вызова этого метода вызывающее множе
other)
ство будет содержать объединение своих элементов и элементов другого множества other
Структура KeyValuePair<TKey, TValue>
В пространстве именSystem.Collections. Genericопределена структураKeyValuePair<TKey, TValue>.Она служит для хранения ключа и его значения и применяется в классах обобщенных коллекций, в которых хранятся пары "ключ-значение", как, например, в классеDictionary<TKey, TValueXВ этой структуре определяются два следующих свойства.
public TKey Key { get; }; public TValue Value { get; };
В этих свойствах хранятся ключ и значение соответствующего элемента коллекции. Для построения объекта типаKeyValuePair<TKey, TValue>служит конструктор:
public KeyValuePair(TKeykey,TValuevalue)гдеkeyобозначает ключ, avalue —значение.
Классы обобщенных коллекций
Как упоминалось ранее, классы обобщенных коллекций по большей части соответствуют своим необобщенным аналогам, хотя в некоторых случаях они носят другие имена. Отличаются они также своей организацией и функциональными возможностями. Классы обобщенных коллекций определяются в пространстве именSystem. Collections . Generic.В табл. 25.14 приведены классы, рассматриваемые в этой главе. Эти классы составляют основу обобщенных коллекций.
Таблица 25.14. Основные классы обобщенных коллекций
Класс
Описание
Dictionary<Tkey,
TValue>
Сохраняет пары “ключ-значение". Обеспечивает такие же функциональные возможности, как и необобщенный класс Hashtable
HashSet<T>
Сохраняет ряд уникальных значений, используя хеш-таблицу
LinkedList<T>
Сохраняет элементы в двунаправленном списке
List<T>
Создает динамический массив. Обеспечивает такие же функциональные возможности, как и необобщенный класс ArrayList
Queue<T>
Создает очередь. Обеспечивает такие же функциональные возможности, как и необобщенный класс Queue
SortedDictionary<TKey,
Создает отсортированный список из пар “ключ-
TValue>
значение"
SortedList<TKey,
TValue>
Создает отсортированный список из пар “ключ-значение”. Обеспечивает такие же функциональные возможности, как и необобщенный класс SortedList
SortedSet<T>
Создает отсортированное множество
Stack<T>
Создает стек. Обеспечивает такие же функциональные возможности, как и необобщенный класс Stack
ПРИМЕЧАНИЕ
В пространстве имен System. Collections. Generic находятся также следующие классы: класс SynchronizedCollection<T> синхронизированной коллекции на основе класса IList<T>; класс SynchronizedReadOnlyCollection<T>, доступной только для чтения синхронизированной коллекции на основе класса lList<T>; абстрактный класс SynchronizedKeyCollectioncK, т>, служащий в качестве базового для класса коллекции System. ServiceModel. UriSchemeKeyedCollection; а также класс KeyedByTypeCollection<T> коллекции, в которой в качестве ключей используются отдельные типы данных.
Класс List<T>
В классеList<T>реализуется обобщенный динамический массив. Он ничем принципиально не отличается от класса необобщенной коллекцииArrayList.В этом классе реализуются интерфейсыICollection, ICollection<T>, IList, IList<T>, IEnumerableиIEnumerable<T>.У классаList<T>имеются следующие конструкторы.
public List()
public List(IEnumerable<T>collection)public List(intcapacity)
Первый конструктор создает пустую коллекцию классаListс выбираемой по умолчанию первоначальной емкостью. Второй конструктор создает коллекцию типаListс количеством инициализируемых элементов, которое определяется параметромcollectionи равно первоначальной емкости массива. Третий конструктор создает коллекцию типаList,имеющую первоначальную емкость, задаваемую параметромcapacity. В данном случае емкость обозначает размер базового массива, используемого для хранения элементов коллекции. Емкость коллекции, создаваемой в виде динамического массива, может увеличиваться автоматически по мере добавления в нее элементов.
В классеList<T>определяется ряд собственных методов, помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются. Некоторые из наиболее часто используемых методов этого класса перечислены в табл. 25.15.
Таблица 25.15. Наиболее часто используемые методы, определенные в классе List<T>
Метод
Описание
public virtual void AddRange(Icollection -collection)public virtual int BinarySearch(Titem)
Добавляет элементы из коллекции collection в конец вызывающей коллекции типа ArrayList
Выполняет поиск в вызывающей коллекции значения, задаваемого параметром item. Возвращает индекс совпавшего элемента. Если искомое значение не найдено, возвращается отрицательное значение. Вызывающий список должен быть отсортирован
Метод
Описание
public
int BinarySearch(T
Выполняет поиск в вызывающей коллекции значе
item,
IComparer<T>comparer)
ния, задаваемого параметром item, используя для сравнения указанный способ, определяемый параметром comparer. Возвращает индекс совпавшего элемента. Если искомое значение не найдено, возвращается отрицательное значение. Вызывающий список должен быть отсортирован
public
int BinarySearch(int
Выполняет поиск в вызывающей коллекции значе
index,
intcount,Titem,
ния, задаваемого параметром item, используя для
IComparer<T>comparer)
сравнения указанный способ, определяемый параметром comparer. Поиск начинается с элемента, указываемого по индексу index, и включает количество элементов, определяемых параметром count. Метод возвращает индекс совпавшего элемента. Если искомое значение не найдено, возвращается отрицательное значение. Вызывающий список должен быть отсортирован
public
List<T> GetRange(int
Возвращает часть вызывающей коллекции. Часть
index,
intcount)
возвращаемой коллекции начинается с элемента, указываемого по индексу index, и включает количество элементов, задаваемое параметром count. Возвращаемый объект ссылается на те же элементы, что и вызывающий объект
public
int IndexOf(Titem)
Возвращает индекс первого вхождения элемента item в вызывающей коллекции. Если искомый элемент не обнаружен, возвращается значение -1
public
void InsertRange(int
Вставляет элементы коллекции collection в вы
index,
IEnumerable<T>
зывающую коллекцию, начиная с элемента, указы
collection)
ваемого по индексу index
publici tern)
int LastlndexOf(T
Возвращает индекс последнего вхождения элемента item в вызывающей коллекции. Если искомый элемент не обнаружен, возвращается значение -1
public
void RemoveRange(int
Удаляет часть вызывающей коллекции, начиная с
index,
intcount)
элемента, указываемого по индексу index, и включая количество элементов, определяемое параметром count
public
void Reverse()
Располагает элементы вызывающей коллекции в обратном порядке
public
void Reverse(int
Располагает в обратном порядке часть вызываю
index,
intcount)
щей коллекции, начиная с элемента, указываемого по индексу index, и включая количество элементов, определяемое параметром count
public
void Sort()
Сортирует вызывающую коллекцию по нарастающей
Окончание табл. 25.15
Метод
Описание
public void
Сортирует вызывающую коллекцию, используя
Sort(IComparer<T>comparer)
для сравнения способ, задаваемый параметром comparer. Если параметр comparer имеет пустое значение, то для сравнения используется способ, выбираемый по умолчанию
public void
Сортирует вызывающую коллекцию, используя для
Sort(Comparison<T>comparison)
сравнения указанный делегат
public void Sort(intindex,
Сортирует вызывающую коллекцию, используя
intcount,IComparer<T>
для сравнения способ, задаваемый параметром
comparer)
comparer. Сортировка начинается с элемента, указываемого по индексу index, и включает количество элементов, определяемых параметром count. Если параметр comparer имеет пустое значение, то для сравнения используется способ, выбираемый по умолчанию
public T [ ] ToArrayO
Возвращает массив, содержащий копии элементов вызывающего объекта
public void TrimExcess()
Сокращает емкость вызывающей коллекции таким образом, чтобы она не превышала 10% от количества элементов, хранящихся в ней на данный момент
В классеList<T>определяется также собственное свойствоCapacity,помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются. Это свойство объявляется следующим образом.
public int Capacity { get; set; }
СвойствоCapacityпозволяет установить и получить емкость вызывающей коллекции в качестве динамического массива. Эта емкость равна количеству элементов, которые может содержать коллекция до ее вынужденного расширения. Такая коллекция расширяется автоматически, и поэтому задавать ее емкость вручную необязательно. Но из соображений эффективности это иногда можно сделать, если заранее известно количество элементов коллекции. Благодаря этому исключаются издержки на выделение дополнительной памяти.
В классеList<T>реализуется также приведенный ниже индексатор, определенный в интерфейсеIList<T>.
public Т this[intindex]{ get; set; }
С помощью этого индексатора устанавливается и получается значение элемента коллекции, указываемое по индексуindex.
В приведенном ниже примере программы демонстрируется применение классаList<T>.Это измененный вариант примера, демонстрировавшего ранее классArrayList.Единственное изменение, которое потребовалось для этого, заключалось в замене классаArrayListклассомList,а также в использовании параметров обобщенного типа.
// Продемонстрировать применение класса List<T>. using System;
using System.Collections.Generic;
class GenListDemo { static void Main() {
// Создать коллекцию в виде динамического массива.
List<char> 1st = new List<char>();
Console.WriteLine("Исходное количество элементов: " + lst.Count);
Console.WriteLine();
Console.WriteLine("Добавить 6 элементов");
// Добавить элементы в динамический массив.
1st.Add('С');
1st.Add(1А *);
1st.Add('Е');
1st.Add(1В1);
1st.Add('D');
1st.Add('F');
Console.WriteLine("Количество элементов: " + lst.Count);
// Отобразить содержимое динамического массива,
// используя индексирование массива.
Console.Write("Текущее содержимое: "); for (int i=0; i < lst.Count;. i++)
Console.Write(1st[i] + " ");
Console.WriteLine("\n");
Console.WriteLine("Удалить 2 элемента ");
// Удалить элементы из динамического массива.
1st.Remove('F');
1st.Remove('А1);
Console.WriteLine("Количество элементов: " + lst.Count);
// Отобразить содержимое динамического массива, используя цикл foreach. Console.Write("Содержимое: "); foreach(char с in 1st)
Console.Write(с + " ");
Console.WriteLine("\n");
Console.WriteLine("Добавить еще 20 элементов");
// Добавить количество элементов, достаточное для // принудительного расширения массива, for(int i=0; i < 20; i++)
1st.Add((char) ('a1 + i));
Console.WriteLine("Текущая емкость: " + 1st.Capacity);
Console.WriteLine("Количество элементов после добавления 20 новых: " + 1st.Count);
Console.Write("Содержимое: ");
foreach(char с in 1st)
Console.Write(с + " ") ;
Console.WriteLine("\n");
// Изменить содержимое динамического массива,
//’ используя индексирование массива.
Console.WriteLine("Изменить три первых элемента"); 1st [0] = 1X'; lst[l] = ' Y' ;
1st[2] = 1Z1;
Console.Write("Содержимое: "); foreach(char с in 1st)
Console.Write(с + " ");
Console.WriteLine ();
// Следующая строка кода недопустима из-за // нарушения безопасности обобщенного типа.
// lst.Add(99); // Ошибка, поскольку это не тип char!
}
}
Эта версия программы дает такой же результат, как и предыдущая.
Исходное количество элементов: О
Добавить 6 элементов Количество элементов: 6 Текущее содержимое: С А Е В D F
Удалить 2 элемента Количество элементов: 4 Содержимое: С Е В D
Добавить еще 20 элементов Текущая емкость: 32
Количество элементов после добавления 20 новых: 24 Содержимое: CEBDabcdefghij klmnopqrst
Изменить три первых элемента
Содержимое: XYZDabcdefghij klmnopqrst
Класс LinkedList<T>
В классеLinkedList<T>создается коллекция в виде обобщенного двунаправленного списка. В этом классе реализуются интерфейсыICollection, ICollection<T>, IEnumerable, IEnumerable<T>, ISerializableиIDeserializationCallback.В двух последних интерфейсах поддерживается сериализация списка. В классеLinkedList<T>определяются два приведенных ниже открытых конструктора.
public LinkedListO
public LinkedList(IEnumerable<T>collection)
В первом конструкторе создается пустой связный список, а во втором конструкторе — список, инициализируемый элементами из коллекцииcollection.
Как и в большинстве других реализаций связных списков, в классеLinkedList<T>инкапсулируются значения, хранящиеся вузлахсписка, где находятся также ссылки на предыдущие и последующие элементы списка. Эти узлы представляют собой объекты классаLinkedListNode<T>.В классеLinkedListNode<T>предоставляются четыре следующих свойства.
public LinkedListNode<T> Next { get; } public LinkedListNode<T> Previous { get; } public LinkedList<T> List { get; } public T Value { get; set; }
С помощью свойствNextиPreviousполучаются ссылки на предыдущий и последующий узлы списка соответственно, что дает возможность обходить список в обоих направлениях. Если же предыдущий или последующий узел отсутствует, то возвращается пустая ссылка. Для получения ссылки на сам список служит свойствоList.А с помощью свойстваValueможно устанавливать и получать значение, находящееся в узле списка.
В классеLinkedList<T>определяется немало методов. В табл. 25.16 приведены наиболее часто используемые методы данного класса. Кроме того, в классеLinkedList<T>определяются собственные свойства, помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются. Эти свойства приведены ниже.
public LinkedListNode<T> First { get; } public LinkedListNode<T> Last { get; }
С помощью свойстваFirstполучается первый узел в списке, а с помощью свойстваLast— последний узел в списке.
Таблица 25.16. Наиболее часто используемые методы, определенные в классе LinkedList<T>
Метод
Описание
public LinkedListNode<T>
Добавляет в список узел со значением value не
AddAfter(LinkedListNode<T>
посредственно после указанного узла node. Указы
node, Tvalue)
ваемый узел node не должен быть пустым (null). Метод возвращает ссылку на узел, содержащий значение value
public void
Добавляет в список новый узел newNode непо
AddAfter(LinkedListNode<T>
средственно после указанного узла node. Ука
node,LinkedListNode<T>
зываемый узел node не должен быть пустым
newNode)
(null). Если узел node отсутствует в списке или если новый узел newNode является частью другого списка, то* генерируется исключение
InvalidOperationException
public LinkedListNode<T>
Добавляет в список узел со значением value непо
AddBefore(LinkedListNode<T>
средственно перед указанным узлом node. Указы
node,Tvalue)
ваемый узел node не должен быть пустым (null). Метод возвращает ссылку на узел, содержащий значение value
Окончание табл. 25.16
Метод
Описание
public void
Добавляет в список новый узел newNode не
AddBefore(LinkedListNode<T>
посредственно перед указанным узлом node.
node, LinkedListNode<T>
Указываемый узел node не должен быть пу
newNode)
стым (null). Если узел node отсутствует в списке или если новый узел newNode является частью другого списка, то генерируется исключение
InvalidOperationException
public LinkedList<T>
Добавляет узел со значением value в начало спи
AddFirst(Tvalue)
ска. Метод возвращает ссылку на узел, содержащий значение value
public void
Добавляет узел node в начало списка. Если узел
AddFirst(LinkedListNode
node является частью другого списка, то генериру
node)
ется исключение InvalidOperationException
public LinkedList<T>
Добавляет узел со значением value в конец спи
AddLast(Tvalue)
ска. Метод возвращает ссылку на узел, содержащий значение value
public void
Добавляет узел node в конец списка. Если узел
AddLast(LinkedListNodenode)
node является частью другого списка, то генериру
ется исключение InvalidOperationException
public LinkedList<T>
Find(T
Возвращает ссылку на первый узел в списке, име
value)
ющий значение value. Если искомое значение value отсутствует в списке, то возвращается пустое значение
public LinkedList<T>
Возвращает ссылку на последний узел в списке,
FindLast(Tvalue)
имеющий значение value. Если искомое значение value отсутствует в списке, то возвращается пустое значение
public bool Remove(T
value)
Удаляет из списка первый узел, содержащий значение value. Возвращает логическое значение true, если узел удален, т.е. если узел со значением value обнаружен в списке и удален; в противном случае возвращает логическое значение false
public void
Удаляет из списка узел, соответствующий ука
Remove(LinkedList<T>
node)
занному узлу node. Если узел node отсутствует в списке, то генерируется исключение
InvalidOperationException
public void RemoveFirst()
Удаляет из списка первый узел
public void RemoveLast()
Удаляет из списка последний узел
В приведенном ниже примере программы демонстрируется применение класса
LinkedList<T>.
// Продемонстрировать применение класса LinkedList<T>. using System;
using System.Collections.Generic;
class GenLinkedListDemo { static void Main() {
// Создать связный список.
LinkedList<char> 11 = new LinkedList<char>();
Console.WriteLine("Исходное количество элементов в списке: " + 11.Count) Console.WriteLine ();
Console.WriteLine("Добавить в список 5 элементов");
// Добавить элементы в связный список.
11.AddFirst('А');
11.AddFirst('В');
11.AddFirst('С') ;
11.AddFirst(' D') ;
11.AddFirst('Е *);
Console.WriteLine("Количество элементов в списке: " + 11.Count);
// Отобразить связный список, обойдя его вручную.
LinkedListNode<char> node;
Console.Write("Отобразить содержимое списка по ссылкам: "); for(node = 11.First; node != null; node = node.Next)
Console.Write(node.Value + " ") ;
Console.WriteLine("\n") ;
// Отобразить связный список, обойдя его в цикле foreach.
Console.Write("Отобразить содержимое списка в цикле foreach: "); foreach(char ch in 11)
Console.Write(ch + " ");
Console.WriteLine("\n");
// Отобразить связный список, обойдя его вручную в обратном направлении. Console.Write("Следовать по ссылкам в обратном направлении: "); for(node = 11.Last; node != null; node = node.Previous)
Console.Write(node.Value + " ");
Console.WriteLine ("\n");
// Удалить из списка два элемента.
Console.WriteLine("Удалить 2 элемента из списка");
// Удалить элементы из связного списка.
11.Remove(1С1);
11.Remove('А');
Console.WriteLine("Количество элементов в списке: " + 11.Count);
// Отобразить содержимое видоизмененного списка в цикле foreach.
Console.Write("Содержимое списка после удаления элементов: "); foreach(char ch in 11)
Console.Write(ch + " ");
Console.WriteLine ("\n");
// Добавить три элемента в конец списка.
11.AddLast('X');
11.AddLast('Y');
11.AddLast('Z');
Console.Write("Содержимое списка после ввода элементов: "); foreach(char ch in 11)
Console.Write(ch + " ");
Console.WriteLine("\n");
}
}
Ниже приведен результат выполнения этой программы.
. Исходное количество элементов в списке: О
Добавить в список 5 элементов Количество элементов в списке: 5
Отобразить содержимое списка по ссылкам: Е D С В А
Отобразить содержимое списка в цикле foreach: Е D С В А
Следовать по ссылкам в обратном направлении: А В С D Е
Удалить 2 элемента из списка Количество элементов в списке: 3
Содержимое списка после удаления элементов: Е D В
Содержимое списка после ввода элементов: Е D В X Y Z
Самое примечательное в этой программе — это обход списка в прямом и обратном направлении, следуя по ссылкам, предоставляемым свойствамиNextиPrevious.Двунаправленный характер подобных связных списков имеет особое значение для приложений, управляющих базами данных, где нередко требуется перемещаться по списку в обоих направлениях.
Класс DictionaryCTKey, TValue>
КлассDictionary<TKey, TValue>позволяет хранить пары "ключ-значение" в коллекции как в словаре. Значения доступны в словаре по соответствующим ключам. В этом отношении данный класс аналогичен необобщенному классуHashtable.В классеDictionary<TKey, TValue>реализуются интерфейсыIDictionary, IDictionary<TKey, TValue>, ICollection, ICollection<KeyValuePair<TKey, TValue>>, IEnumerable, IEnumerable<KeyValuePair<TKey, TValue>>, ISerializableиIDeserializationCallback.В двух последних интерфейсах поддерживается сериализация списка. Словари имеют динамический характер, расширяясь по мере необходимости.
В классеDictionary<TKey, TValue>предоставляется немало конструкторов. Ниже перечислены наиболее часто используемые из них.
public Dictionary()
public Dictionary(IDictionaryCTKey, TValue>dictionary)public Dictionary(intcapacity)
В первом конструкторе создается пустой словарь с выбираемой по умолчанию первоначальной емкостью. Во втором конструкторе создается словарь с указанным количеством элементовdictionary.А в третьем конструкторе с помощью параметраcapaci tyуказывается емкость коллекции, создаваемой в виде словаря. Если размер словаря заранее известен, то, указав емкость создаваемой коллекции, можно исключить изменение размера словаря во время выполнения, что, как правило, требует дополнительных затрат вычислительных ресурсов.
В классеDictionary<TKey, TValue>определяется также ряд методов. Некоторые наиболее часто используемые методы этого класса сведены в табл. 25.17.
Таблица 25.17. Наиболее часто используемые методы, определенные в классе Die tionaryCTKey, TValue>
Метод
Описание
public
value)
void Add(TKeykey, TValue
Добавляет в словарь пару “ключ-значение", определяемую параметрами key и value. Если ключ key уже находится в словаре, то его значение не изменяется, и генерируется исключение ArgumentException
public
key)
bool
ContainsKey(TKey
Возвращает логическое значение true, если вызывающий словарь содержит объект key в качестве ключа; а иначе — логическое значение false
public
value)
bool
ContainsValue(TValue
Возвращает логическое значение true, если вызывающий словарь содержит значение value; в противном случае — логическое значение false
public
bool
Remove(TKeykey)
Удаляет ключ key из словаря. При удачном исходе операции возвращается логическое значение true, а если ключ key отсутствует в словаре — логическое значение false
Кроме того, в классеDictionary<TKey, TValue>определяются собственные свойства, помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются. Эти свойства приведены ниже.
Свойство
Описание
public IEqualityComparer<TKey> Comparer { get; } public Dictionary<TKey, TValue>. KeyCollection Keys { get; } public Dictionary<TKey, TValue>. ValueCollection Values { get; }
Получает метод сравнения для вызывающего словаря
Получает коллекцию ключей -Получает коллекцию значений
Следует иметь в виду, что ключи и значения, содержащиеся в коллекции, доступны отдельными списками с помощью свойствKeysиValues.В коллекциях типаDictionaryCTKey, TValue>.KeyCollectionи Dictionary<TKey, TValue>. ValueCollectionреализуются как обобщенные, так и необобщенные формы интерфейсовICollectionиIEnumerable.
И наконец, в классеDictionaryCTKey, TValue>реализуется приведенный ниже индексатор, определенный в интерфейсеIDictionary<TKey, TValueX
public TValue this[TKey key] { get; set; }
Этот индексатор служит для получения и установки значения элемента коллекции, а также для добавления в коллекцию нового элемента. Но в качестве индекса в данном случае служит ключ элемента, а не сам индекс.
При перечислении коллекции типаDictionaryCTKey, TValue>из нее возвращаются пары "ключ-значение7' в форме структурыKeyValuePairCTKey, TValueXНапомним, что в этой структуре определяются два поля.
public TKey Key; public TValue Value;
В этих полях содержится ключ или значение соответствующего элемента коллекции. Как правило, структураKeyValuePairCTKey, TValue>не используется непосредственно, поскольку средства классаDictionaryCTKey, TValue>позволяют работать с ключами и значениями по отдельности. Но при перечислении коллекции типаDictionaryCTKey, TValue>,например, в циклеforeachперечисляемыми объектами являются пары типаKeyValuePair.
Все ключи в коллекции типаDictionaryCTKey, TValue>должны быть уникальными, причем ключ не должен изменяться до тех пор, пока он служит в качестве ключа. В то же время значения не обязательно должны быть уникальными. К тому же объекты не хранятся в коллекции типаDictionaryCTKey, TValue>в отсортированном порядке.
В приведенном ниже примере демонстрируется применение класса
DictionaryCTKey, TValueX
// Продемонстрировать применение класса обобщенной // коллекции DictionaryCTKey, TValueX
using System;
using System.Collections.Generic;
class GenDictionaryDemo { static void Main() {
// Создать словарь для хранения имен и фамилий // работников и их зарплаты.
Dictionarycstring, double> diet =
new Dictionarycstring, double>();
// Добавить элементы в коллекцию, diet.Add("Батлер, Джон", 73000); diet.Add("Шварц, Capa", 59000); diet.Add("Пайк, Томас", 45000); diet.Add("Фрэнк, Эд", 99000);
// Получить коллекцию ключей, т.е. фамилий и имен.
ICollection<string> с = diet.Keys;
// Использовать ключи для получения значений, т.е. зарплаты, foreach(string str in с)
Console.WriteLine ("{0}, зарплата: {1:C}", str, diet[str]);
}
}
Ниже приведен результат выполнения этой программы.
Батлер, Джон, зарплата: $73,000.00 Шварц, Сара, зарплата: $59,000.00 Пайк, Томас, зарплата: $45,000.00 Фрэнк, Эд, зарплата: $99,000.00
Класс SortedDictionary<TKey, TValue>
В коллекции классаSortedDictionary<TKey, TValue>пары "ключ-значение" хранятся таким же образом, как и в коллекции классаDictionaryCTKey, TValue>,за исключением того, что они отсортированы по соответствующему ключу. В классеSortedDictionary<TKey, TValue>реализуются интерфейсыIDictionary, IDictionary<TKey, TValue>, ICollection, ICollection<KeyValuePair<TKey, TValue>>, IEnumerableиIEnumerable<KeyValuePair<TKey, TValue>>.В классеSortedDictionary<TKey, TValue>предоставляются также следующие конструкторы.
public SortedDictionary()
public SortedDictionary(IDictionary<TKey, TValue>dictionary)
public SortedDictionary(IComparer<TKey>comparer)
public SortedDictionary(IDictionaryCTKey, TValue>dictionary,
IComparer<TKey>comparer)
В первом конструкторе создается пустой словарь, во втором конструкторе — словарь с указанным количеством элементовdictionary.В третьем конструкторе допускается указывать с помощью параметраcomparerтипаIComparerспособ сравнения, используемый для сортировки, а в четвертом конструкторе — инициализировать словарь, помимо указания способа сравнения.
В классеSortedDictionary<TKey, TValue>определен ряд методов. Некоторые наиболее часто используемые методы этого класса сведены в табл. 25.18.
Таблица 25.18. Наиболее часто используемые методы, определенные в классе SortedDictionaryCTKey, TValue>
Метод Описание
public void Add (TKeykey, Добавляет в словарь пару “ключ-значение", TValuevalue) определяемую параметрами key и value. Если
ключ key уже находится в словаре, то его значение не изменяется, и генерируется исключение ArgumentException public bool ContainsKey (TKey Возвращает логическое значение true, если вызыва-кеу) ющий словарь содержит объект key в качестве клю-
_ча; в противном случае — логическое значение false
Метод
Описание
public bool
ContainsValue(TValuevalue)public bool Remove(TKeykey)
Возвращает логическое значение true, если вызывающий словарь содержит значение value; в противном случае — логическое значение false Удаляет ключ key из словаря. При удачном исходе операции возвращается логическое значение true, а если ключ key отсутствует в словаре — логическое значение false
Кроме того, в классеSortedDictionary<TKey, TValue>определяются собственные свойства, помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются. Эти свойства приведены ниже.
Свойство
Описание
public Icomparer<TKey> Comparer { get; ]
public SortedDictionaryCTKey, TValue>. KeyCollection Keys { get; } public SortedDictionary<TKey, TValue>. ValueCollection Values { get; }
\ Получает метод сравнения для вызывающего словаря Получает коллекцию ключей
Получает коллекцию значений
Следует иметь в виду, что ключи и значения, содержащиеся в коллекции, доступны отдельными списками с помощью свойствKeysиValues.В коллекциях типаSortedDictionary<TKey, TValue>.KeyCollectionи SortedDictionary<TKey, TValu*e>. ValueCollectionреализуются как обобщенные, так и необобщенные формы интерфейсовICollectionиIEnumerable.
И наконец, в классеSortedDictionary<TKey, TValue>реализуется приведенный ниже индексатор, определенный в интерфейсеIDictionary<TKey, TValueX
public TValue this[TKeykey] {get; set; }
Этот индексатор служит для получения и установки значения элемента коллекции, а также для добавления в коллекцию нового элемента. Но в данном случае в качестве индекса служит ключ элемента, а не сам индекс.
При перечислении коллекции типаSortedDictionary<TKey, TValue>из нее возвращаются пары "ключ-значение" в форме структурыKeyValuePair<TKey, TValueXНапомним, что в этой структуре определяются два следующих поля.
public TKey Key; public TValue Value;
В этих полях содержится ключ или значение соответствующего элемента коллекции. Как правило, структураKeyValuePair<TKey, TValue>не используется непосредственно, поскольку средства классаSortedDictionary<TKey, TValue>позволяют работать с ключами и значениями по отдельности. Но при перечислении коллекции типаSortedDictionary<TKey, TValue>,например в циклеforeach,перечисляемыми объектами являются пары типаKeyValuePair.
Все ключи в коллекции типаSortedDictionary<TKey, TValue>должны быть уникальными, причем ключ не должен изменяться до тех пор, пока он служит в качестве ключа. В то же время значения не обязательно должны быть уникальными.
В приведенном ниже примере демонстрируется применение классаSortedDictionary<TKey, TValueXЭто измененный вариант предыдущего примера, демонстрировавшего применение классаDictionary<TKey, TValueXВ данном варианте база данных работников отсортирована по фамилии и имени работника, которые служат в качестве ключа.
// Продемонстрировать применение класса обобщенной // коллекции SortedDictionary<TKey, TValueX
using System;
using System.Collections.Generic;
class GenSortedDictionaryDemo { static void Main() {
// Создать словарь для хранения имен и фамилий // работников и их зарплаты.
SortedDictionary<string, double> diet =
new SortedDictionary<string, double>();
// Добавить элементы в коллекцию, diet.Add("Батлер, Джон", 73000); diet.Add("Шварц, Capa", 59000); diet.Add("Пайк, Томас", 45000); diet.Add("Фрэнк, Эд", 99000);
// Получить коллекцию ключей, т.е. фамилий и имен.
ICollection<string> с = diet.Keys;
// Использовать ключи для получения значений, т.е. зарплаты, foreach(string str in с)
Console.WriteLine("{0}, зарплата: {1:C}", str, diet[str]);
}
}
Эта программа дает следующий результат.
Батлер, Джон, зарплата: $73,000.00 Пайк, Томас, зарплата: $45,000.00 Фрэнк, Эд, зарплата: $99,000.00 Шварц, Сара, зарплата: $59,000.00
Как видите, список работников и их зарплаты отсортированы по ключу, в качестве которого в данном случае служит фамилия и имя работника.
Класс SortedListCTKey, TValue>
В коллекции классаSortedList<TKey, TValue>хранится отсортированный список пар "ключ-значение". Это обобщенный эквивалент класса необобщенной коллекцииSortedList.В классеSortedList<TKey, TValue>реализуются интерфейсыIDictionary, IDictionary<TKey, TValue>, ICollection, ICollection<KeyValuePair<TKey, TValue>>, IEnumerableиIEnumerable<KeyValuePair<TKey, TValue».Размер коллекции типаSortedList<TKey, TValue>изменяется динамически, автоматически увеличиваясь по мере необходимости. КлассSortedList<TKey, TValue>подобен классуSortedDictionary<TKey, TValue>,но у него другие рабочие характеристики. В частности, классSortedListcTKey, TValue>использует меньше памяти, тогда как классSortedDicti-onary<TKey, TValue>позволяет быстрее вставлять неупорядоченные элементы в коллекцию.
В классеSortedListcTKey, TValue>предоставляется немало конструкторов. Ниже перечислены наиболее часто используемые конструкторы этого класса.
public SortedList ()
public SortedList(IDictionaryCTKey, TValue>dictionary)
public SortedList(intcapacity)
public SortedList(IComparer<TK>comparer)
В первой форме конструктора создается пустой список с выбираемой по умолчанию первоначальной емкостью. Во второй форме конструктора создается отсортированный список с указанным количеством элементовdictionary.В третьей форме конструктора с помощью параметраcapacityзадается емкость коллекции, создаваемой в виде отсортированного списка. Если размер списка заранее известен, то, указав емкость создаваемой коллекции, можно исключить изменение размера списка во время выполнения, что, как правило, требует дополнительных затрат вычислительных ресурсов. И в четвертой форме конструктора допускается указывать с помощью параметраcomparerспособ сравнения объектов, содержащихся в списке.
Емкость коллекции типаSortedListcTKey, TValue>увеличивается автоматически по мере необходимости, когда в список добавляются новые элементы. Если текущая емкость коллекции превышается, то она увеличивается. Преимущество указания емкости коллекции типаSortedListcTKey, TValue>при ее создании заключается в снижении или полном исключении издержек на изменение размера коллекции. Разумеется, указывать емкость коллекции целесообразно лишь в том случае, если заранее известно, сколько элементов требуется хранить в ней.
В классеSortedListcTKey, TValue>определяется ряд собственных методов, помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются. Некоторые из наиболее часто используемых методов этого класса перечислены в табл. 25.19. Следует иметь в виду, что перечислитель, возвращаемый методомGetEnumerator (), служит для перечисления пар "ключ-значение", хранящихся в отсортированном списке в виде объектов типаKeyValuePair.
Таблица 25.19. Наиболее часто используемые методы, определенные в классе SortedListCTKey, TValue>
Метод Описание
public void Add (TKey key, Добавляет в список пару “ключ-значение",
TValuevalue) определяемую параметрами key и value.
Если ключ key уже находится в списке, то его значение не изменяется, и генерируется исключение ArgumentException public bool ContainsKey (ТК key) Возвращает логическое значение true, если вы
зывающий список содержит объект key в каче-_стве ключа; а иначе —логическое значение false
Метод
Описание
public bool
ContainsValue(TValuevalue)
public IEnumerator<KeyValuePair CTKey, TValue>> GetEnumerator() public int IndexOfKey(TKeykey)
public int IndexOfValue(TValuevalue)
public bool Remove(TKeykey)
public void RemoveAt(intindex)public void TrimExcessO
Возвращает логическое значение true, если вызывающий список содержит значение value; в противном случае — логическое значение false
Возвращает перечислитель для вызывающего словаря
Возвращает индекс ключа key. Если искомый ключ не обнаружен в списке, возвращается значение -1
Возвращает индекс первого вхождения значения value в вызывающем списке. Если искомое значение не обнаружено в списке, возвращается значение -1 Удаляет из списка пару “ключ-значение” по указанному ключу key. При удачном исходе операции возвращается логическое значение true, а если ключ key отсутствует в списке — логическое значение false Удаляет из списка пару “ключ-значение” по указанному индексу index Сокращает избыточную емкость вызывающей коллекции в виде отсортированного списка
Кроме того, в классеSortedList<TK, TV>определяются собственные свойства, помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются. Эти свойства приведены ниже.
Свойство
Описание
public int Capacity { get; set; }
Получает или устанавливает емкость
вызывающей коллекции в виде отсо
ртированного списка
public IComparer<TK> Comparer { get;
■ } Получает метод сравнения для вызы
вающего списка
public IList<TK> Keys { get; }
Получает коллекцию ключей
public IList<TV> Values { get; }
Получает коллекцию значений
И наконец, в классеSortedList<TKey,
TValue>реализуется приведенный ниже
индексатор, определенный в интерфейсеIDictionaryCTKey, TValueX
public TValue this[TKeykey] { get; set;
} .
Этот индексатор служит для получения и
установки значения элемента коллекции,
а также для добавления в коллекцию нового элемента. Но в данном случае в качестве
индекса служит ключ элемента, а не сам индекс.
В приведенном ниже примере демонстрируется применение класса
SortedList<TKey, TValueXЭто еще один измененный вариант представленного
ранее примера базы данных работников. В данном варианте база данных хранится в коллекции типаSortedList.
// Продемонстрировать применение класса обобщенной // коллекции SortedList<TKey, TValue>.
using System;
using System.Collections.Generic;
class GenSLDemo {
static void Main() {
// Создать коллекцию в виде отсортированного списка // для хранения имен и фамилий работников и их зарплаты.
SortedList<string, double> si =
new SortedList<string, double>();
// Добавить элементы в коллекцию, si.Add("Батлер, Джон", 73000); si.Add("Шварц, Capa", 59000); si.Add("Пайк, Томас", 45000); si.Add("Фрэнк, Эд", 99000);
// Получить коллекцию ключей, т.е. фамилий и имен.
ICollection<string> с = si.Keys;
// Использовать ключи для получения значений, т.е. зарплаты, foreach(string str in с)
Console.WriteLine("{0}, зарплата: {1:C}", str, si[str]);
Console.WriteLine();
}
}
Ниже приведен результат выполнения этой программы.
Батлер, Джон, зарплата: $73,000.00 Пайк, Томас, зарплата: $45,000.00 Фрэнк, Эд, зарплата: $99,000.00 Шварц, Сара, зарплата: $59,000.00
Как видите, список работников и их зарплаты отсортированы по ключу, в качестве которого в данном случае служит фамилия и имя работника.
Класс Stack<T>
КлассStack<T>является обобщенным эквивалентом класса необобщенной коллекцииStack.В нем поддерживается стек в виде списка, действующего по принципу "первым пришел — последним обслужен". В этом классе реализуются интерфейсыCollection, IEnumerableиIEnumerable<T>.Кроме того, в классеStack<T>непосредственно реализуются методыClear(),Contains() иСоруТоО,определенные в интерфейсеICollection<T>.А методыAdd() иRemove() в этом классе не поддерживаются, как, впрочем, и свойствоIsReadOnly.Коллекция классаStack<T>имеет динамический характер, расширяясь по мере необходимости, чтобы вместить все элементы, которые должны в ней храниться. В классеStack<T>определяются следующие конструкторы.
public Stack()
public Stack(intcapacity)
public Stack(IEnumerable<T>collection)
В первой форме конструктора создается пустой стек с выбираемой по умолчанию первоначальной емкостью, а во второй форме — пустой стек, первоначальный размер которого определяет параметрcapaci ty.И в третьей форме создается стек, содержащий элементы коллекции, определяемой параметромcollection.Его первоначальная емкость равна количеству указанных элементов.
В классеStack<T>определяется ряд собственных методов, помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются, а также в интерфейсеICollection<T>.Некоторые из наиболее часто используемых методов этого класса перечислены в табл. 25.20. Как и в классеStack,эти методы обычно применяются следующим образом.Длятого чтобы поместить объект на вершине стека, вызывается методPush(). А для того чтобы извлечь и удалить объект из вершины стека, вызывается методPop (). Если же объект требуется только извлечь, но не удалить из вершины стека, то вызывается методРеек(). А если вызвать методPop() илиРеек(), когда вызывающий стек пуст, то сгенерируется исключениеInvalidOperationException.
Таблица 25.20. Методы, определенные в классе Stack<T>
Метод
Описание
public
T Peek()
Возвращает элемент, находящийся на вершине стека, но не удаляет его
public
T Pop()
Возвращает элемент, находящийся на вершине стека, удаляя его в процессе работы
public
void Push(Titem)
Помещает элемент i tern в стек
public
T[] ToArrayO
Возвращает массив, содержащий копии элементов вызывающего стека
public
void TrimExcessO
Сокращает избыточную емкость вызывающей коллекции в виде стека
В приведенном ниже примере программы демонстрируется применение классаStack<T>.
// Продемонстрировать применение класса Stack<T>. using System;
using System.Collections.Generic;
class GenStackDemo { static void Main() {
Stack<string> st = new Stack<string>();
st.Push("один"); st.Push("два"); st.Push("три"); st.Push("четыре"); st.Push("пять");
while(st.Count > 0) { string str = st.Pop();
Console.Write(str + " ");
}
Console.WriteLine (); '
}
}
При выполнении этой программы получается следующий результат.
пять четыре три два один
Класс Queue<T>
КлассQueue<T>является обобщенным эквивалентом класса необобщенной коллекцииQueue.В нем поддерживается очередь в виде списка, действующего по принципу "первым пришел — первым обслужен". В этом классе реализуются интерфейсыICollection, IEnumerableиIEnumerable<T>.Кроме того, в классеQueue<T>непосредственно реализуются методыClear(), Contains() иCopyTo(), определенные в интерфейсеICollection<T>.А методыAdd() иRemove() в этом классе не поддерживаются, как, впрочем, и свойствоIsReadOnly.Коллекция классаQueue<T>имеет динамический характер, расширяясь по мере необходимости, чтобы вместить все элементы, которые должны храниться в ней. В классеQueue<T>определяются следующие конструкторы.
public Queue()
public Queue(intcapacity)
public Queue(IEnumerable<T>collection)
В первой форме конструктора создается пустая очередь с выбираемой по умолчанию первоначальной емкостью, а во второй форме — пустая очередь, первоначальный размер которой определяет параметрcapaci ty.И в третьей форме создается очередь, содержащая элементы коллекции, определяемой параметромcollection.Ее первоначальная емкость равна количеству указанных элементов.
В классеQueue<T>определяется ряд собственных методов, помимо тех, что уже объявлены в интерфейсах, которые в нем реализуются, а также в интерфейсеICollection<T>.Некоторые из наиболее часто используемых методов этого класса перечислены в табл. 25.21. Как и в классеQueue,эти методы обычно применяются следующим образом. Для того чтобы поместить объект в очередь, вызывается методEnqueue (). Если требуется извлечь и удалить первый объект из начала очереди, то вызывается методDequeue (). Если же требуется извлечь, но йе удалять следующий объект из очереди, то вызывается методРеек(). А если методыDequeue() иРеек() вызываются, когда очередь пуста, то генерируется исключениеInvalidOperationException.
Таблица 25.21. Методы, определенные в классе Queue<T>
Метод Описание
public т Dequeue () Возвращает объект из начала вызывающей очереди.
Возвращаемый объект удаляется из очереди public void Enqueue (Т item) Добавляет элемент i tern в конец очереди
public т Реек () Возвращает элемент из начала вызывающей очере-
_ди, но не удаляет его_
Метод
Описание
public virtual Т[] ToArray()
public void TrimExcess()
Возвращает массив, который содержит копии элементов из вызывающей очереди Сокращает избыточную емкость вызывающей коллекции в виде очереди
В приведенном ниже примере демонстрируется применение классаQueue<T>.
// Продемонстрировать применение класса Queue<T>. using System;
using System.Collections.Generic;
class GenQueueDemo { static void Main() {
Queue<double> q = new Queue<double>();
q.Enqueue(98.6); q.Enqueue(212.0); q.Enqueue(32.0); q.Enqueue(3.1416);
double sum = 0.0;
Console.Write("Очередь содержит: "); while(q.Count > 0) {
double val = q. Dequeue ();
Console.Write(val + " "); sum += val;.
}
Console.WriteLine("ХпИтоговая сумма равна " +• sum);
}
}
Вот к какому результату приводит выполнение этой программы.
Очередь содержит: 98.6 212 32 3.1416 Итоговая сумма равна 345.7416
Класс HashSet<T>
В классеHashSet<T>поддерживается коллекция, реализующая множество. Для хранения элементов этого множества в нем используется хеш-таблица. В классеHashSet<T>реализуются интерфейсыICollection<T>, ISet<T>, IEnumerable, IEnumerable<T>, ISerializable,а такжеIDeserializationCallback.В коллекции типаHashSet<T>реализуется множество, все элементы которого являются уникальными. Иными словами, дубликаты в таком множестве не допускаются. Порядок следования элементов во множестве не указывается. В классеHashSet<T>определяется полный набор операций с множеством, определенных в интерфейсеI$et<T>,включая пересечение, объединение и разноименность. Благодаря этому классHashSet<T>оказывается идеальным средством для работы с множествами объектов, когда порядок расположения элементов во множестве особого значения не имеет. Коллекция типа
HashSet<T>имеет динамический характер и расширяется по мере необходимости, чтобы вместить все элементы, которые должны в ней храниться.
Ниже перечислены наиболее употребительные конструкторы, определенные в классеHashSet<T>.
public -HashSetO
public HashSet(IEnumerable<T>collection)public HashSet(IEqualityComparecomparer)
public HashSet(IEnumerable<T>collection,IEqualityComparecomparer)
В первой форме конструктора создается пустое множество, а во второй форме — множество, состоящее из элементов указываемой коллекцииcollection.В третьей форме конструктора допускается указывать способ сравнения с помощью параметраcomparer.А в четвертой форме создается множество, состоящее из элементов указываемой коллекцииcollection, и используется заданный способ сравненияcomparer.Имеется также пятая форма конструктора данного класса, в которой допускается инициализировать множество последовательно упорядоченными данными.
В классеHashSet<T>реализуется интерфейсISet<T>,а следовательно, в нем предоставляется полный набор операций со множествами. В этом классе предоставляется также методRemoveWhere (), удаляющий из множества элементы, не удовлетворяющие заданному условию, или предикату.
Помимо свойств, определенных в интерфейсах, которые реализуются в классеHashSet<T>,в него введено дополнительное свойствоComparer,приведенное ниже.
public IEqualityComparer<T> Comparer { get; }
Оно позволяет получать метод сравнения для вызывающего хеш-множества.
Ниже приведен конкретный пример применения классаHashSet<T>.
// Продемонстрировать применение класса HashSet<T>. using System;
using System.Collections.Generic;
class HashSetDemo {
static void Show(string msg, HashSet<char> set) {
Console.Write(msg); foreach(char ch in set)
Console.Write(ch + " ");
Console.WriteLine ();
}
static void Main() {
HashSet<char> setA = new HashSet<char> ();
HashSet<char> setB = new HashSet<char> ();
setA.Add('A'); setA.Add(' В'); setA.Add('C') ;
setB.Add('С') ; setB.Add(' D1 );
setB.Add('Е');
Show("Исходное содержимое множества setA: ", setA);
Show("Исходное содержимое множества setB: ", setB);
setA.SymmetricExceptWith(setB);
Show("Содержимое множества setA после " +
"разноименности со множеством SetB: ", setA);
setA.UnionWith(setB);
Show("Содержимое множества setA после " +
"объединения со множеством SetB: ", setA);
setA.ExceptWith(setB);
Show("Содержимое множества setA после " +
"вычитания из множества setB: ", setA);
Console.WriteLine();
}
}
Ниже приведен результат выполнения программы из данного примера.
Исходное содержимое множества setA: ABC Исходное содержимое множества setB: С D Е
Содержимое множества setA после разноименности со множеством SetB: А В D Е Содержимое множества setA после объединения со множеством SetB: А В D Е С Содержимое множества setA после вычитания из множества setB: А В
Класс SortedSet<T>
КлассSortedSet<T>представляет собой новую разновидность коллекции, введенную в версию 4.0 среды .NET Framework. В нем поддерживается коллекция, реализующая отсортированное множество. В классеSortedSet<T>реализуются интерфейсыISet<T>, ICollection, ICollection<T>, IEnumerable, IEnumerable<T>, ISerializable,а такжеIDeserializationCallback.В коллекции типаSortedSet<T>реализуется множество, все элементы которого являются уникальными. Иными словами, дубликаты в таком множестве не допускаются. В классеSortedSet<T>определяется полный набор операций с множеством, определенных в интерфейсеISet<T>,включая пересечение, объединение и разноименность. Благодаря тому что все элементы коллекции типаSortedSet<T>сохраняются в отсортированном порядке, классSortedSet<T>оказывается идеальным средством для работы с отсортированными множествами объектов. Коллекция типаSortedSet<T>имеет динамический характер и расширяется по мере необходимости, чтобы вместить все элементы, которые должны в ней храниться.
Ниже перечислены четыре наиболее часто используемые конструктора, определенных в классеSortedSet<T>.
public SortedSetO
public SortedSet(IEnumerable<T>collection)public SortedSet(IComparercomparer)
public SortedSet(IEnumerable<T>collection,IComparercomparer)
В первой форме конструктора создается пустое множество, а во второй форме — множество, состоящее из элементов указываемой коллекцииcollection.В третьей форме конструктора допускается указывать способ сравнения с помощью параметраcomparer.А в четвертой форме создается множество, состоящее из элементов указываемой коллекцииcollection, и используется заданный способ сравненияcomparer.Имеется также пятая форма конструктора данного класса, в которой допускается инициализировать множество последовательно упорядоченными данными.
В классеSortedSet<T>реализуется интерфейсISet<T>,а следовательно, в нем предоставляется полный набор операций со множествами. В этом классе предоставляется также методGetViewBetween(), возвращающий часть множества в форме объекта типаSortedSet<T>,методRemoveWhere(), удаляющий из множества элементы, не удовлетворяющие заданному условию, или предикату, а также методReverse (),возвращающий объект типаIEnumerable<T>,который циклически проходит множество в обратном порядке.
Помимо свойств, определенных в интерфейсах, которые реализуются в классеSortedSet<T>,в него введены дополнительные свойства, приведенные ниже.
public IComparer<T> Comparer { get; } public T Max { get; } public T Min { get; }
СвойствоComparerполучает способ сравнения для вызывающего множества. СвойствоМахполучает наибольшее значение во множестве, а свойствоMin— наименьшее значение во множестве.
В качестве примера применения классаSortedSet<T>на практике просто замените обозначениеHashSetнаSortedSetв исходном коде программы из предыдущего подраздела, посвященного коллекциям типаHashSet<T>.
Параллельные коллекции
В версию 4.0 среды .NET Framework добавлено новое пространство именSystem. Collections . Concurrent.Оно содержит коллекции, которые являются потокобезопасными и специально предназначены для параллельного программирования. Это означает, что они могут безопасно использоваться в многопоточной программе, где возможен одновременный доступ к коллекции со стороны двух или больше параллельно исполняемых потоков. Ниже перечислены классы параллельных коллекций.
Параллельная коллекция
Описание
BlockingCollection<T>
Предоставляет оболочку для блокирующей реализации интерфейса IProducerConsumerCollection<T>
ConcurrentBag<T>
Обеспечивает неупорядоченную реализацию интерфейса
IProducerConsumerCollection<T>, которая оказыва
ется наиболее пригодной в том случае, когда информация вырабатывается и потребляется в одном потоке
ConcurrentDictionary
Сохраняет пары “ключ-значение", а значит, реализует парал
<TKey, TValue>
лельный словарь
ConcurrentQueue<T>
Реализует параллельную очередь и соответствующий вариант интерфейса IProducerConsumerCollection<T>
ConcurrentStack<T>
Реализует параллельный стек и соответствующий вариант интерфейса IproducerConsumerCollection<T>
Как видите, в нескольких классах параллельных коллекций реализуется интерфейсIProducerConsumerCollection.Этот интерфейс также определен в пространстве именSystem. Collections . Concurrent.Он служит в качестве расширения интерфейсовIEnumerable, IEnumerable<T>иICollection.Кроме того, в нем определены методыTryAdd () иTryTake (), поддерживающие шаблон "поставщик-потребитель". (Классический шаблон "поставщик-потребитель" отличается решением двух задач. Первая задача производит элементы коллекции, а другая потребляет их.) МетодTryAdd() пытается добавить элемент в коллекцию, а методTryTake() — удалить элемент из коллекции. Ниже приведены формы объявления обоих методов.
bool TryAdd(Тitem)bool TryTake(out Titem)
МетодTryAdd() возвращает логическое значениеtrue,если в коллекцию добавлен элементitem.А методTryTake() возвращает логическое значениеtrue,если элементitemудален из коллекции. Если методTryAdd() выполнен успешно, то элементiternбудет содержать объект. (Кроме того, в интерфейсеIProducerConsumerCollectionуказывается перегружаемый вариант методаCopyTo(), определяемого в интерфейсеICollection,а также метода ТоАггау (), копирующего коллекцию в массив.)
Параллельные коллекции зачастую применяются в комбинации с библиотекой распараллеливания задач (TPL) или языком PLINQ. В силу особого характера этих коллекций все их классы не будут рассматриваться далее подробно. Вместо этого на конкретных примерах будет дан краткий обзор классаBlockingCollection<T>.Усвоив основы построения классаBlockingCollection<T>,вы сможете без особого труда разобраться и в остальных классах параллельных коллекций.
В классеBlockingCollection<T>,по существу, реализуется блокирующая очередь. Это означает, что в такой очереди автоматически устанавливается ожидание любых попыток вставить элемент в коллекцию, когда она заполнена, а также попыток удалить элемент из коллекции, когда она пуста. Это идеальное решение для тех ситуаций, которые связаны с применением шаблона "поставщик-потребитель". В классеBlockingCollection<T>реализуются интерфейсыICollection, IEnumerable, IEnumerable<T>,а такжеIDisposable.
В классеBlockingCollection<T>определяются следующие конструкторы.
public BlockingCollection()
public BlockingCollection(intboundedCapacity)
public BlockingCollection(IProducerConsumerCollection<T>collection)public BlockingCollection(IProducerConsumerCollection<T>collection,
intboundedCapacity)
В двух первых конструкторах в оболочку классаBlockingCollection<T>заключается коллекция, являющаяся экземпляром объекта типаConcurrentQueue<T>.А в двух других конструкторах можно указать коллекцию, которая должна быть положена в основу коллекции типаBlockingCollection<T>.Если указывается параметрboundedCapaci ty,то он должен содержать максимальное количество объектов, которые коллекция должна содержать перед тем, как она окажется заблокированной. Если же параметрboundedCapaci tyне указан, то коллекция оказывается неограниченной.
Помимо методовTryAdd () иTryTake (), определяемых параллельно с теми, что указываются в интерфейсеIProducerConsumerCollection<T>,в классеBlockingCollection<T>определяется также ряд собственных методов. Ниже представлены методы, которые будут использоваться в приведенных далее примерах.
public void Add(Titem)public T Take()
Когда методAdd() вызывается для неограниченной коллекции, он добавляет элементitemв коллекцию и затем возвращает управление вызывающей части программы. А когда методAdd() вызывается для ограниченной коллекции, он блокирует доступ к ней, если она заполнена. После того как из коллекции будет удален один элемент или больше, указанный элементitemбудет добавлен в коллекцию, и затем произойдет возврат из данного метода. МетодТаке() удаляет элемент из коллекции и возвращает управление вызывающей части программы. (Имеются также варианты обоих методов, принимающие в качестве параметра признак задачи как экземпляр объекта типаCancellationToken.)
Применяя методыAdd() иТаке(),можно реализовать простой шаблон "поставщик-потребитель", как показано в приведенном ниже примере программы. В этой программе создается поставщик, формирующий символы от А до Z, а также потребитель, получающий эти символы. При этом создается коллекция типаBlockingCollection<T>,ограниченная 4 элементами.
// Простой пример коллекции типа BlockingCollection. using System;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Concurrent;
class BlockingDemo {
static BlockingCollection<char> be;
// Произвести и поставить символы от А до Z. static void Producer () {
for(char ch = 'A'; ch <= 'Z'; ch++) { be.Add(ch);
Console.WriteLine ("Производится символ " + ch) ;
}
}
// Потребить 26 символов, static void Consumer() {
for(int i=0; i < 26; i++)
Console .WriteLine ("Потребляется символ " + bc.TakeO);
}
static void Main() {
// Использовать блокирующую коллекцию, ограниченную 4 элементами, be = new BlockingCollection<char>(4);
// Создать задачи поставщика и потребителя.
Task Prod = new Task(Producer);
Task Con = new Task(Consumer);
// Запустить задачи.
Con.Start ();
Prod:Start();
// Ожидать завершения обеих задач, try {
Task.WaitAll(Con, Prod);
} catch(AggregateException exc) {
Console.WriteLine (exc);
} finally {
Con.Dispose ();
Prod.Dispose (); be.Dispose();
}
}
}
Еслизапустить эту программу на выполнение, то на экране появится смешанный результат, выводимый поставщиком и потребителем. Отчасти это объясняется тем, что коллекция Ьс ограничена 4 элементами, а это означает, что в нее может быть добавлено только четыре элемента, прежде чем ее придется сократить. В качестве эксперимента попробуйте сделать коллекцию Ьс неограниченной и понаблюдайте за полученными результатами. В некоторых средах выполнения это приведет к тому, что все элементы коллекции будут сформированы до того, как начнется какое-либо их потребление. Кроме того, попробуйте ограничить коллекцию одним элементом. В этом случае одновременно может быть сформирован лишь один элемент.
Для работы с коллекцией типаBlockingCollection<T>может оказаться полезным и методCompleteAdding (). Ниже приведена форма его объявления.
public void CompleteAdding()
Вызов этого метода означает, что в коллекцию не будет больше добавлено ни одного элемента. Это приводит к тому, что свойствоIsAddingCompleteпринимает логическое значениеtrue.Если же коллекция пуста, то свойствоIsCompletedпринимает логическое значениеtrue,и в этом случае вызовы методаТаке() не блокируются. Ниже приведены формы объявления свойствIsAddingCompleteиIsCompleted.
public bool IsCompleted { get; } public bool IsAddingComplete { get; }
Когда коллекция типаBlockingCollection<T>только начинает формироваться, эти свойства содержат логическое значениеfalse.А после вызова методаCompleteAdding() они принимают логическое значениеtrue.
Ниже приведен вариант предыдущего примера программы, измененный с целью продемонстрировать применение методаCompleteAdding (), свойстваIsCompletedи методаTryTake ().
// Применение методов CompleteAdding(), TryTake() и свойства IsCompleted. using System;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Concurrent;
class BlockingDemo {
static BlockingCollection<char> be;
// Произвести и поставить символы от А до Z. static void Producer() {
for (char ch = 'A'; ch <= 'Z'; ch++) { be.Add(ch);
Console.WriteLine("Производится символ " + ch);
}
be.CompleteAdding();
}
// Потреблять символы до тех пор, пока их будет производить поставщик.
static void Consumer() {
char ch;
while(!be.IsCompleted) { if(be.TryTake(out ch))
Console.WriteLine("Потребляется символ " + ch);
}
}
static void Main() {
// Использовать блокирующую коллекцию, ограниченную 4 элементами, be = new BlockingCollection<char>(4);
// Создать задачи поставщика и потребителя.
Task Prod = new Task(Producer);
Task Con = new Task(Consumer);
// Запустить задачи.
Con.Start();
Prod.Start();
// Ожидать завершения обеих задач, try {
Task.WaitAll(Con, Prod);
} catch(AggregateException exc) {
Console.WriteLine (exc);
} finally {
Con.Dispose();
Prod.Dispose(); be.Dispose();
}
}
}
Этот вариант программы дает такой же результат, как и предыдущий. Главное его отличие заключается в том, что теперь методProducer() может производить и поставлять сколько угодно элементов. С этой целью он просто вызывает методCompleteAdding(), когда завершает создание элементов. А методConsumer() лишь "потребляет" произведенные элементы до тех пор, пока свойствоIsCompletedне примет логическое значениеtrue.
Несмотря на специфический до некоторой степени характер параллельных коллекций, предназначенных в основном для параллельного программирования, у них, тем не менее, имеется немало общего с обычными, непараллельными коллекциями, описанными в предыдущих разделах. Если же вам приходится работать в среде параллельного программирования, то для организации одновременного доступа к данным из нескольких потоков вам, скорее всего, придется воспользоваться параллельными коллекциями.
Сохранение объектов, определяемых пользователем классов, в коллекции
Ради простоты приведенных выше примеров в коллекции, как правило, сохранялись объекты встроенных типов, в том числе int, string и char. Но ведь в коллекции можно хранить не только объекты встроенных типов. Достоинство коллекций в том и состоит, что в них допускается хранить объекты любого типа, включая объекты определяемых пользователем классов.
Рассмотрим сначала простой пример применения класса необобщенной коллекции ArrayList для хранения информации о товарных запасах. В этом классе инкапсулируется класс Inventory.
// Простой пример коллекции товарных запасов.
using System;
using System.Collections;
class Inventory { string name; double cost; int onhand;
public Inventory(string n, double c, int h) { name = n; cost = c; onhand = h;
}
public override string ToStringO { return
String.Format("{0,-10}Стоимость: {1,6:С} Наличие: {2}",
name, cost, onhand);
}
}
class InventoryList { static void Main() {
ArrayList inv = new ArrayList(); \
// Добавить элементы в список. inv.Add(new Inventory("Кусачки", 5.95, 3)); inv.Add(new Inventory("Отвертки", 8.29, 2)); inv.Add(new Inventory("Молотки", 3.50, 4)); inv.Add(new Inventory("Дрели", 19.88, 8));
Console.WriteLine("Перечень товарных запасов:"); foreach(Inventory i in inv) {
Console.WriteLine(" " + i);
При выполнении программы из данного примера получается следующий результат.
Перечень товарных запасов:
Обратите внимание на то, что в данном примере программы не потребовалось никаких специальных действий для сохранения в коллекции объектов типаInventory.Благодаря тому что все типы наследуют от классаob j ect,в необобщенной коллекции можно хранить объекты любого типа. Именно поэтому в необобщенной коллекции нетрудно сохранить объекты определяемых пользователем классов. Безусловно, это также означает, что такая коллекция не типизирована.
Для того чтобы сохранить объекты определяемых пользователем классов в типизированной коллекции, придется воспользоваться классами обобщенных коллекций. В качестве примера ниже приведен измененный вариант программы из предыдущего примера. В этом варианте используется класс обобщенной коллекцииList<T>,а результат получается таким же, как и прежде.
// Пример сохранения объектов класса Inventory в // обобщенной коллекции класса List<T>.
using System;
using System.Collections.Generic;
class Inventory { string name; double cost; int onhand;
public Inventory(string n, double c, int h) { name = n; cost = c; onhand = h;
}
public override string ToString() { return
String.Format ("{0,-10}Стоимость: {1,6:С} Наличие: {2}", name, cost, onhand);
}
}
class TypeSafelnventoryList { static void Main() {
List<Inventory> inv = new List<Inventory> ();
// Добавить элементы в список. inv.Add(new Inventory("Кусачки", 5.95, 3)); inv.Add(new Inventory("Отвертки", 8.29, 2)); inv.Add(new Inventory("Молотки", 3.50, 4)); inv.Add(new Inventory("Дрели", 19.88, 8));
Console.WriteLine("Перечень товарных запасов:"); foreach(Inventory i in inv) {
Console.WriteLine(" " + i);
}
}
}
Данный пример отличается' от предыдущего лишь передачей типаInventoryв качестве аргумента типа конструктору классаList<T>.Ав остальном оба примера рассматриваемой здесь программы практически одинаковы. Это, по существу, означает, что для применения обобщенной коллекции не требуется никаких особых усилий, но при сохранении в такой коллекции объекта конкретного типа строго соблюдается типовая безопасность.
Тем не менее для обоих примеров рассматриваемой здесь программы характерна еще одна особенность: они довольно кратки. Если учесть, что для организации динамического массива, где можно хранить, извлекать и обрабатывать данные товарных запасов, потребуется не менее 40 строк кода, то преимущества коллекций сразу же становятся очевидными. Нетрудно догадаться, что рассматриваемая здесь программа получится длиннее в несколько раз, если попытаться закодировать все эти функции коллекции вручную. Коллекции предлагают готовые решения самых разных задач программирования, и поэтому их следует использовать при всяком удобном случае.
У рассматриваемой здесь программы имеется все же один не совсем очевидный недостаток: коллекция не подлежит сортировке. Дело в том, что в классахArrayListиList<T>отсутствуют средства для сравнения двух объектов типаInventory.Но из этого положения имеются два выхода. Во-первых, в классеInventoryможно реализовать интерфейсIComparable,в котором определяется метод сравнения объектов данного класса. И во-вторых, для целей сравнения можно указать объект типаIComparer.Оба подхода рассматриваются далее по очереди.
Реализация интерфейса IComparable
Еслитребуется отсортировать коллекцию, состоящую из объектов определяемого пользователем класса, при условии, что они не сохраняются в коллекции классаSortedList,где элементы располагаются в отсортированном порядке, то в такой коллекции должен быть известен способ сортировки содержащихся в ней объектов. С этой целью можно, в частности, реализовать интерфейсIComparableдля объектов сохраняемого типа. ИнтерфейсIComparableдоступен в двух формах: обобщенной и необобщенной. Несмотря на сходство применения обеих форм данного интерфейса, между ними имеются некоторые, хотя и небольшие, отличия, рассматриваемые ниже.
Реализация интерфейса IComparable для необобщенных коллекций
Еслитребуется отсортировать объекты, хранящиеся в необобщенной коллекции, то для этой цели придется реализовать необобщенный вариант интерфейсаIComparable.В этом варианте данного интерфейса определяется только один метод,CompareTo(), который определяет порядок выполнения самого сравнения. Ниже приведена общая форма объявления методаCompareTo ().
int CompareTo(objectobj)
В методеCompareTo() вызывающий объект сравнивается с объектомobj. Длясортировки объектов по нарастающей конкретная реализация данного метода должна возвращать нулевое значение, если значения сравниваемых объектов равны; положительное — еслц значение вызывающего объекта больше, чем у объектаobj; и отрицательное — если значение вызывающего объекта меньше, чем у объектаobj.А для сортировки по убывающей можно обратить результат сравнения объектов. Если же тип объектаobjне подходит для сравнения с вызывающим объектом, то в методеCompareTo() может быть сгенерировано исключениеArgumentException.
В приведенном ниже примере программы демонстрируется конкретная реализация интерфейсаIComparable.В этой программе интерфейсIComparableвводится в классInventory,разработанный в двух последних примерах из предыдущего раздела. В классеInventoryреализуется методCompareTo() для сравнения полейnameобъектов данного класса, что дает возможность отсортировать товарные запасы по наименованию. Как показано в данном примере программы, коллекция объектов классаInventoryподлежит сортировке благодаря реализации интерфейсаIComparableв этом классе.
// Реализовать интерфейс IComparable.
using System;
using System.Collections;
// Реализовать необобщенный вариант интерфейса IComparable. class Inventory : IComparable { string name; double cost; int onhand;
public Inventory(string n, double c, int h) { name = n; cost = c; onhand = h;
}
public override string ToStringO { return
String.Format ("{0,-10}Стоимость: {1,6:С} Наличие: {2}", name, cost, onhand);
}
// Реализовать интерфейс IComparable. public int CompareTo(object obj) {
Inventory b;
b = (Inventory) obj;
return name.CompareTo(b.name);
}
}
class IComparableDemo { static void Main() {
ArrayList inv = new ArrayList();
inv.Add(new Inventory("Кусачки", 5.95, 3)); inv.Add(new Inventory("Отвертки", 8.29, 2)); inv.Add(new Inventory("Молотки", 3.50, 4)); inv.Add(new Inventory("Дрели", 19.88, 8));
Console.WriteLine("Перечень товарных запасов до сортировки:"); foreach(Inventory i in inv) {
^Console.WriteLine(" " + i);
} '
Console.WriteLine();
// Отсортировать список, inv.Sort();
Console.WriteLine("Перечень товарных запасов после сортировки:"), foreach(Inventory i in inv) {
Console.WriteLine(" " + i);
}
}
}
Ниже приведен результат выполнения данной программы. Обратите внимание на то, что после вызова метода Sort () товарные запасы оказываются отсортированными по наименованию.
Перечень товарных запасов до сортировки:
Реализация интерфейса IComparable для обобщенных коллекций
Если требуется отсортировать объекты, хранящиеся в обобщенной коллекции, то для этой цели придется реализовать обобщенный вариант интерфейса IComparable<T>. В этом варианте интерфейса IComparable определяется приведенная ниже обобщенная форма метода CompareTo ().
int CompareTo(Тother)
В методе CompareTo () вызывающий объект сравнивается с другим объектомother.Для сортировки объектов по нарастающей конкретная реализация данного метода должна возвращать нулевое значение, если значения сравниваемых объектов равны; положительное — если значение вызывающего объекта болыце, чем у объекта другогоother; и отрицательное — если значение вызывающего объекта меньше, чем у другого объектаother.А для сортировки по убывающей можно обратить результат сравнения объектов. При реализации обобщенного интерфейса IComparable<T> имя типа реализующего класса обычно передается в качестве аргумента типа.
Приведенный ниже пример программы является вариантом предыдущего примера, измененным с целью реализовать и использовать обобщенный интерфейсIComparable<T>.Обратите внимание на применение класса обобщенной коллекцииList<T>вместо класса необобщенной коллекцииArrayList.
// Реализовать интерфейс IComparable<T>. using System;
using System.Collections.Generic;
// Реализовать обобщенный вариант интерфейса IComparable<T>. class Inventory : IComparable<Inventory> { string name; double cost; int onhand;
public Inventory(string n, double c, int h) { name = n; cost = c; onhand = h;
}
public override string ToString() { return
String.Format("{0,-10}Стоимость: {1,6:C} Наличие: {2}", name, cost, onhand);
}
// Реализовать интерфейс IComparable<T>. public int CompareTo(Inventory obj) { return name.CompareTo(obj.name);
}
}
class GenericIComparableDemo { static void Main() {
List<Inventory> inv = new List<Inventory>();
// Добавить элементы в список. inv.Add(new Inventory("Кусачки", 5.95, 3)); inv.Add(new Inventory("Отвертки", 8.29, 2)); inv.Add(new’Inventory("Молотки", 3.50, 4)); inv.Add(new Inventory("Дрели", 19.88, 8У);
Console.WriteLine("Перечень товарных запасов до сортировки:"); foreach(Inventory i in inv) {
Console.WriteLine (" " + i);
}
Console.WriteLine ();
// Отсортировать список, inv.Sort ();
Console.WriteLine("Перечень товарных запасов после сортировки:"); foreach(Inventory i in inv) {
Console.WriteLine (" " + i);
}
}
}
Эта версия программы дает такой же результат, как и предыдущая, необобщенная версия.
Применение интерфейса IComparer
Для сортировки объектов определяемых пользователем классов зачастую проще всего реализовать в этих классах интерфейсIComparable.Тем не менее данную задачу можно решить и с помощью интерфейсаIComparer.Для этой цели необходимо сначала создать класс, реализующий интерфейсIComparer,а затем указать объект этого класса, когда потребуется сравнение.
ИнтерфейсIComparerсуществует в двух формах: обобщенной и необобщенной. Несмотря на сходство применения обеих форм данного интерфейса, между ними имеются некоторые, хотя и небольшие, отличия, рассматриваемые ниже.
Применение необобщенного интерфейса icomparer
В необобщенном интерфейсеIComparerопределяется только один метод,
Compare().
int Compare(object x, object y)
В методеCompare() сравниваются объектыxиу.Для сортировки объектов по нарастающей конкретная реализация данного метода должна возвращать нулевое значение, если значения сравниваемых объектов равны; положительное — если значение объектахбольше, чем у объекта у; и отрицательное — если значение объектахменьше, чем у объекта у. А для сортировки по убывающей можно обратить результат сравнения объектов. Если же тип объекта х не подходит для сравнения с объектом у, то в методеCompareTo() может быть сгенерировано исключениеArgumentException.
Объект типаIComparerможет быть указан при конструировании объекта классаSortedList,при вызове методаArrayList. Sort (IComparer),а также в ряде других мест в классах коллекций. Главное преимущество применения интерфейсаIComparerзаключается в том, что сортировке подлежат объекты тех классов, в которых интерфейсIComparableне реализуется.
Приведенный ниже пример программы является вариантом рассматривавшегося ранее необобщенного примера программы учета товарных запасов, переделанного с целью воспользоваться интерфейсомIComparerдля сортировки перечня товарных запасов. В этом варианте программы сначала создается классComp Inv,в котором реализуется интерфейсIComparerи сравниваются два объекта классаInventory.А затем объект классаComplnvуказывается в вызове методаSort() для сортировки перечня товарных запасов.
using System;
using System.Collections;
// Создать объект типа IComparer для объектов класса Inventory, class CompInv : IComparer {
// Реализовать интерфейс IComparer. public int Compare(object x, object y) {
Inventory, a, b; a = (Inventory) x; b = (Inventory) y;
return string.Compare(a.name, b.name, StringComparison.Ordinal);
}
}
class Inventory { public string name; double cost; int onhand;
public Inventory(string n, double c, int h) { name = n; cost = c; onhand = h;
}
public override string ToStringO { return
String.Format("{0,-10} Цена: {1,6:С} В наличии: {2}",
name, cost, onhand);
}
}
class IComparerDemo { static void Main() {
Complnv comp = new CompInv();
ArrayList inv = new ArrayList();
// Добавить элементы в список. inv.Add(new Inventory("Кусачки", 5.95, 3)); inv.Add(new Inventory("Отвертки", 8.29, 2)); inv.Add(new Inventory("Молотки", 3.50, 4)); inv.Add(new Inventory("Дрели", 19.88, 8));
Console.WriteLine("Перечень товарных запасов до сортировки:"); foreach(Inventory i in inv) {
Console.WriteLine (" " + i);
}
Console.WriteLine();
// Отсортировать список, используя интерфейс IComparer. inv.Sort(comp);
Console.WriteLine("Перечень товарных запасов после сортировки:"); foreach(Inventory i in inv) {
Console.WriteLine(" " + i);
}
}
}
Эта версия программы дает такой же результат, как и предыдущая.
Применение обобщенного интерфейса 1Сошрагег<т>
ИнтерфейсIComparer<T>является обобщенным вариантом интерфейсаIComparer.В нем определяется приведенный ниже обобщенный вариант методаCompare ().
int Compare(Т х, Т у)
В этом методе сравниваются объекты х и у и возвращается нулевое значение, если значения сравниваемых объектов равны; положительное — если значение объекта х больше, чем у объекта у; и отрицательное — если значение объекта х меньше, чем у объекта у.
Ниже приведена обобщенная версия предыдущей программы учета товарных запасов, в которой теперь используется интерфейсI Comparer <Т>.Она дает такой же результат, как и необобщенная версия этой же программы.
// Использовать обобщенный вариант интерфейса IComparer<T>. using System;
using System.Collections.Generic;
// Создать объект типа IComparer<T> для объектов класса Inventory, class CompInv<T> : IComparer<T> where T : Inventory {
// Реализовать интерфейс IComparer<T>. public int Compare(T x, T y) {
return string.Compare(x.name, y.name, StringComparison.Ordinal) ;
}
}
class Inventory { public string name; double cost; int onhand;
public Inventory(string n, double c, int h) { name = n; cost = c; onhand = h;
}
public override string ToString() { return
String.Format("{0,-10} Цена: {1,6:С} В наличии: {2}", name, cost, onhand);
}
}
class GenericIComparerDemo { static void Main() {
CompInv<Inventory> comp = new CompInv<Inventory>();
List<Inventory> inv = new List<Inventory>();
// Добавить элементы в список. inv.Add(new Inventory("Кусачки", 5.95, 3));
inv.Add(new Inventory("Отвертки", 8.29, 2)); inv.Add(new Inventory("Молотки", 3.50, 4)); inv.Add(new Inventory("Дрели", 19.88, 8));
Console.WriteLine("Перечень товарных запасов до сортировки:"); foreach(Inventory i in inv) {
Console.WriteLine (" " + i);
}
Console.WriteLine ();
// Отсортировать список, используя интерфейс IComparer. inv.Sort(comp);
Console.WriteLine("Перечень товарных запасов после сортировки:"); foreach(Inventory i in inv) {
Console.WriteLine (" " + i);
}
}
}
Применение класса StringComparer
В простых примерах из этой главы указывать явно способ сравнения символьных строк совсем не обязательно. Но это может потребоваться в тех случаях, когда строки сохраняются в отсортированной коллекции или когда строки ищутся либо сортируются в коллекции. Так, если строки должны быть отсортированы с учетом настроек одной культурной среды, а затем их приходится искать с учетом настроек другой культурной среды, то во избежание ошибок, вероятнее всего, потребуется указать способ сравнения символьных строк. Аналогичная ситуация возникает и при хешировании коллекции. Для подобных (и других) случаев в конструкторах классов некоторых коллекций предусмотрена поддержка параметра типаIComparer.С целью явно указать способ сравнения символьных строк этому параметру передается в качестве аргумента экземпляр объекта классаStringComparer.
КлассStringComparerбыл подробно описан в главе 21 при рассмотрении вопросов сортировки и поиска в массивах. В этом классе реализуются интерфейсыIComparer, IComparer<String>, IEqualityComparer,а такжеIEqualityComparer<String>.Следовательно, экземпляр объекта типаStringComparerможет быть передан параметру типаIComparerв качестве аргумента. В классеStringComparerопределяется несколько доступных только для чтения свойств, возвращающих экземпляр объекта типаStringComparer,который поддерживает различные способы сравнения символьных строк. Как пояснялось в главе 21, к числу этих свойств относятся следующие:CurrentCulture, CurrentCulturelgnoreCase, InvariantCulture, InvariantCulturelgnoreCase, Ordinal,а такжеOrdinallgnoreCase.Все эти свойства можно использовать для явного указания способа сравнения символьных строк.
В качестве примера ниже показано, как коллекция типаSortedList<TKey, TValue>конструируется для хранения символьных строк, ключи которых сравниваются порядковым способом.
SortedList<string, int> users =
new SortedList<string, int>(StringComparer.Ordinal);
Доступ к коллекции с помощью перечислителя
К элементам коллекции нередко приходится обращаться циклически, например, для отображения каждого элемента коллекции. С этой целью можно, с одной стороны, организовать циклforeach,как было показано в приведенных выше примерах, а с другой — воспользоваться перечислителем.Перечислитель —это объект, который реализует необобщенный интерфейсIEnumeratorили обобщенный интерфейсIEnumerator<T>.
В интерфейсеIEnumeratorопределяется одно свойство,Current,необобщенная форма которого приведена ниже.
object Current { get; }
А в интерфейсеIEnumerator<T>объявляется следующая обобщенная форма свойстваCurrent.
Т Current { get; }
В обеих формах свойстваCurrentполучается текущий перечисляемый элемент коллекции. Но поскольку свойствоCurrentдоступно только для чтения, то перечислитель может служить только для извлечения, но не видоизменения объектов в коллекции.
В интерфейсеIEnumeratorопределяются два метода. Первым из них является методMoveNext (), объявляемый следующим образом.
bool MoveNext()
При каждом вызове методаMoveNext() текущее положение перечислителя смещается к следующему элементу коллекции. Этот метод возвращает логическое значениеtrue,если следующий элемент коллекции доступен, и логическое значениеfalse,если достигнут конец коллекции. Перед первым вызовом методаMoveNext() значение свойстваCurrentоказывается неопределенным. (В принципе до первого вызова методаMoveNext() перечислитель обращается к несуществующему элементу, который должен находиться перед первым элементом коллекции. Именно поэтому приходится вызывать методMoveNext (), чтобы перейти к первому элементу коллекции.)
Для установки перечислителя в исходное положение, соответствующее началу коллекции, вызывается приведенный ниже методReset ().
void Reset()
После вызова методаReset() перечисление вновь начинается с самого начала коллекции. Поэтому, прежде чем получить первый элемент коллекции, следует вызвать методMoveNext().
В интерфейсеIEnumerator<T>методыMoveNext() иReset() действуют по тому же самому принципу.
Необходимо также обратить внимание на два следующих момента. Во-первых, перечислитель нельзя использовать для изменения содержимого перечисляемой с его помощью коллекции. Следовательно, перечислители действуют по отношению к коллекции как к доступной только для чтения. И во-вторых, любое изменение в перечисляемой коллекции делает перечислитель недействительным.
Применение обычного перечислителя
Прежде чем получить доступ к коллекции с помощью перечислителя, необходимо получить его. В каждом классе коллекции для этой цели предоставляется методGetEnumerator (), возвращающий перечислитель в начало коллекции. Используя этот перечислитель, можно получить доступ к любому элементу коллекции по очереди. В целом, для циклического обращения к содержимому коллекции с помощью перечислителя рекомендуется придерживаться приведенной ниже процедуры.
1. Получить перечислитель, устанавливаемый в начало коллекции, вызвав для этой коллекции методGetEnumerator ().
2. Организовать цикл, в котором вызывается методMoveNext(). Повторять цикл до тех пор, пока методMoveNext() возвращает логическое значениеtrue.
3. Получить в цикле каждый элемент коллекции с помощью свойстваCurrent.
Ниже приведен пример программы, в которой реализуется данная процедура. В этой программе используется классArrayList,но общие принципы циклического обращения к элементам коллекции с помощью перечислителя остаются неизменными для коллекций любого типа, в том числе и обобщенных.
// Продемонстрировать применение перечислителя.
using System;
using System.Collections;
class EnumeratorDemo { static void Main() {
ArrayList list = new ArrayList(1);
for(int i=0; i < 10; i++) list.Add(i);
// Использовать перечислитель для доступа к списку.
IEnumerator etr = list.GetEnumerator(); while(etr.MoveNext ())
Console.Write(etr.Current + " ") ;
Console.WriteLine() ;
// Повторить перечисление списка.
etr .Reset () ;
while(etr.MoveNext())
Console.Write(etr.Current + " ") ;
Console.WriteLine() ;
}
}
Вот к какому результату приводит выполнение этой программы.
0123456789
0123456789
Вообще говоря, для циклического обращения к элементам коллекции циклforeachоказывается более удобным, чем перечислитель. Тем не менее перечислитель предоставляет больше возможностей для управления, поскольку его можно при желании всегда установить в исходное положение.
Применение перечислителя типа IDictionaryEnumerator
Если дляорганизации коллекции в виде словаря, например типаHashtable,реализуется необобщенный интерфейсIDictionary,то для циклического обращения к элементам такой коллекции следует использовать перечислитель типаIDictionaryEnumeratorвместо перечислителя типаIEnumerator.ИнтерфейсIDictionaryEnumeratorнаследует от интерфейсаIEnumeratorи имеет три дополнительных свойства. Первым из них является следующее свойство.
DictionaryEntry Entry { get; }
СвойствоEntryпозволяет получить пару "ключ-значение7' из перечислителя в форме структурыDictionaryEntry.Напомним, что в структуреDictionaryEntryопределяются два свойства,KeyиValue,с помощью которых можно получать доступ к ключу или значению, связанному с элементом коллекции. Ниже приведены два других свойства, определяемых в интерфейсеIDictionaryEnumerator.
object Key { get; } object Value { get; }
С помощью этих свойств осуществляется непосредственный доступ к ключу или значению.
Перечислитель типаIDictionaryEnumeratorиспользуется аналогично обычному перечислителю, за исключением того, что текущее значение в данном случае получается с помощью свойствEntry, KeyилиValue,а не свойстваCurrent.Следовательно, приобретя перечислитель типаIDictionaryEnumerator,необходимо вызвать методMoveNext(), чтобы получить первый элемент коллекции. А для получения остальных ее элементов следует продолжить вызовы методаMoveNext (). Этот метод возвращает логическое значениеfalse,когда в коллекции больше нет ни одного элемента.
В приведенном ниже примере программы элементы коллекции типаHashtableперечисляются с помощью перечислителя типаIDictionaryEnumerator.
// Продемонстрировать применение перечислителя типа IDictionaryEnumerator.
using System;
using System.Collections;
class IDicEnumDemo { static void Main() {
// Создать хеш-таблицу.
Hashtable ht = new Hashtable();
// Добавить элементы в таблицу, ht.Add("Кен", "555-7756"); ht.Add("Мэри", "555-9876"); ht.Add("Том", "555-3456"); ht.Add("Тодд", "555-3452");
// Продемонстрировать применение перечислителя.
IDictionaryEnumerator etr = ht.GetEnumerator();
Console.WriteLine("Отобразить информацию с помощью свойства Entry."); while(etF.MoveNext())
Console.WriteLine(etr.Entry.Key + ": " + etr.Entry.Value);
Console.WriteLine();
Console.WriteLine("Отобразить информацию " +
"с помощью свойств Key и Value.");
etr .Reset () ;
while(etr.MoveNext ())
Console.WriteLine(etr.Key + ": " + etr.Value);
}
}
Ниже приведен результат выполнения этой программы.
Отобразить информацию с помощью свойства Entry.
Мэри: 555-9876 Том: 555-3456 Тодд: 555-3452 Кен: 555-7756
Отобразить информацию с помощью свойств Key и Value.
Мэри: 555-9876 Том: 555-3456 Тодд: 555-3452 Кен: 555-7756
Реализация интерфейсов IEnumerable и IEnumerator
Как упоминалось выше, для циклического обращения к элементам коллекции зачастую проще (да и лучше) организовать циклforeach,чем пользоваться непосредственно методами интерфейсаIEnumerator.Тем не менее ясное представление о принципе действия подобных интерфейсов важно иметь по еще одной причине: если требуется создать класс, содержащий объекты, перечисляемые в циклеforeach,то в этом классе следует реализовать интерфейсыIEnumeratorиIEnumerable.Иными словами, для того чтобы обратиться к объекту определяемого пользователем класса в циклеforeach,необходимо реализовать интерфейсыIEnumeratorиIEnumerableв их обобщенной или необобщенной форме. Правда, сделать это будет нетрудно, поскольку оба интерфейса не очень велики.
В приведенном ниже примере программы интерфейсыIEnumeratorиIEnumerableреализуются в необобщенной форме, с тем чтобы перечислить содержимое массива, инкапсулированного в классеMyClass.
using System;
using System.Collections;
class MyClass : IEnumerator, IEnumerable {
char[] chrs = { 'А', 'В', 'C', 'D' };
int idx = -1;
// Реализовать интерфейс IEnumerable. public IEnumerator GetEnumerator() {
return this;
}
// В следующих методах реализуется интерфейс IEnumerator
// Возвратить текущий объект, public object Current { get {
return chrs[idx];
}
}
// Перейти к следующему объекту, public bool MoveNext() { if(idx == chrs.Length-1) {
Reset(); // установить перечислитель в конец return false;
}
idx++;
f
return true;
}
// Установить перечислитель в начало, public void Reset() { idx = -1; }
}
class EnumeratorlmplDemo { static void Main() {
MyClass me = new MyClass();
// Отобразить содержимое объекта me. foreach(char ch in me)
Console .Write (ch + 11 11);
Console.WriteLine();
// Вновь отобразить содержимое объекта me. foreach(char ch in me)
Console .Write (ch + 11 ");
Console.WriteLine();
}
}
Эта программа дает следующий результат.
А В С D А В С D
В данной программе сначала создается классMyClass,в котором инкапсулируется небольшой массив типаchar,состоящий из символовА-D.Индекс этого массива хранится в переменнойidx,инициализируемой значением -1. Затем в классеMyClassреализуются оба интерфейса,IEnumeratorиIEnumerable.МетодGetEnumerator() возвращает ссылку на перечислитель, которым в данном случае оказывается текущий объект. СвойствоCurrentвозвращает следующий символ в массиве, т.е. объект, указываемый по индексуidx.МетодMoveNext() перемещает индексidxв следующее положение. Этот метод возвращает логическое значениеfalse,если достигнут конец коллекции, в противном случае — логическое значениеtrue.Напомним, что перечислитель оказывается неопределенным вплоть до первого вызова методаMoveNext ().Следовательно, методMoveNext() автоматически вызывается в циклеforeachперед обращением к свойствуCurrent.Именно поэтому первоначальное значение переменнойidxустанавливается равным -1. Оно становится равным нулю на первом шаге циклаforeach.Обобщенная реализация рассматриваемых здесь интерфейсов будет действовать по тому же самому принципу.
Далее в методеMain () создается объекттстипаMyClass,и содержимое этого объекта дважды отображается в циклеforeach.
Применение итераторов
Как следует из предыдущих примеров, реализовать интерфейсыIEnumeratorиIEnumerableнетрудно. Но еще проще воспользоватьсяитератором, который представляет собой метод, оператор или аксессор, возвращающий по очереди члены совокупности объектов от ее начала и до конца. Так, если некоторый массив состоит из пяти элементов, то итератор данного массива возвратит все эти элементы по очереди. Реализовав итератор, можно обращаться к объектам определяемого пользователем класса в циклеforeach.
Обратимся сначала к простому примеру итератора. Приведенная ниже программа является измененной версией предыдущей программы, в которой вместо явной реализации интерфейсовIEnumeratorиIEnumerableприменяется итератор.
// Простой пример применения итератора.
using System;
using System.Collections;
class MyClass {
char[] chrs = { fAf, fBf, 'C1, 'D' };
// Этот итератор возвращает символы из массива chrs. public IEnumerator GetEnumerator() {
foreach(char ch in chrs) yield return ch;
}
}
class ItrDemo {
static void Main() {
MyClass me = new MyClassO;
foreach(char ch in me)
Console .Write (ch + 11 ");
Console.WriteLine();
}
}
При выполнении этой программы получается следующий результат.
А В С D
Как видите, содержимое массиваme. chrsперечислено.
Рассмотрим эту программу более подробно. Во-первых, обратите внимание на то, что в классеMyClassне указываетсяIEnumeratorв качестве реализуемого интерфейса. При создании итератора компилятор реализует этот интерфейс автоматически. И во-вторых, обратите особое внимание на методGetEnumerator (), который ради удобства приводится ниже еще раз.
// Этот итератор возвращает символы из массива chrs. public IEnumerator GetEnumerator() {
foreach(char ch in chrs) yield return ch;
}
Это и есть итератор для объектов классаMyClass.Как видите, в нем явно реализуется методGetEnumerator (), определенный в интерфейсеIEnumerable.А теперь перейдем непосредственно к телу данного метода. Оно состоит из циклаforeach,в котором возвращаются элементы из массиваchrs.И делается это с помощью оператораyield return.Этот оператор возвращает следующий объект в коллекции, которым в данном случае оказывается очередной символ в массивеchrs.Благодаря этому средству обращение к объектутстипаMyClassорганизуется в циклеforeachвнутри методаMain ().
Обозначениеyieldслужит в языке C# в качествеконтекстного ключевого слова.Это означает, что оно имеет специальное назначение только в блоке итератора. А вне этого блока оно может быть использовано аналогично любому другому идентификатору.
Следует особо подчеркнуть, что итератор не обязательно должен опираться на массив или коллекцию другого типа. Он должен просто возвращать следующий элемент из совокупности элементов. Это означает, что элементы могут быть построены динамически с помощью соответствующего алгоритма. В качестве примера ниже приведена версия предыдущей программы, в которой возвращаются все буквы английского алфавита, набранные в верхнем регистре. Вместо массива буквы формируются в циклеfor.
// Пример динамического построения значений,
// возвращаемых по очереди с помощью итератора.
using System;
using System.Collections;
class MyClass { char ch = fAf;
// Этот итератор возвращает буквы английского // алфавита, набранные в верхнем регистре.
public IEnumerator GetEnumerator() {
for(int i=0; i < 26; i++)
yield return (char) (ch + i) ;
}
}
class ItrDemo2 {
static void Main() {
MyClass me = new MyClass();
foreach(char ch in me)
Console.Write(ch + " ");
Console.WriteLine();
}
}
Вот к какому результату приводит выполнение этой программы.
ABCDEFGHI JKLMNOPQRSTUVWXYZ
Прерывание итератора
Для преждевременного прерывания итератора служит следующая форма оператора yield.
yield break;
Когда этот оператор выполняется, итератор уведомляет о том, что достигнут конец коллекции. А это, по существу, останавливает сам итератор.
Приведенная ниже программа является версией предыдущей программы, измененной с целью отобразить только первые десять букв английского алфавита.
// Пример прерывания итератора.
using System;
using System.Collections;
class MyClass { char ch = 'A';
// Этот итератор возвращает первые 10 букв английского алфавита, public IEnumerator GetEnumerator() {
for(int i=0; i < 26; i++) {
if(i == 10) yield break; // прервать итератор преждевременно yield return (char) (ch + i);
}
}
}
class ItrDemo3 {
static void Main() {
MyClass me = new MyClass();
foreach(char ch in me)
Console.Write(ch + " ");
Console.WriteLine();
}
}
Эта программа дает следующий результат.
ABCDEFGHIJ
Применение нескольких операторов yield
В итераторе допускается применение нескольких операторов yield. Но каждый такой оператор должен возвращать следующий элемент в коллекции. В качестве примера рассмотрим следующую программу.
// Пример применения нескольких операторов yield.
using System;
using System.Collections;
class MyClass {
// Этот итератор возвращает буквы А, В, С, D и Е. public IEnumerator GetEnumerator() {
yield return 'A'; yield return 'B'; yield return 'C'; yield return 'D'; yield return 'E';
}
}
class ItrDemo5 {
static void Main() {
MyClass me = new MyClass();
foreach(char ch in me)
Console.Write(ch + " ");
Console.WriteLine();
}
}
Ниже приведен результата выполнения этой программы.
А В С D Е
В данной программе внутри методаGetEnumerator() выполняются пять операторовyield.Следует особо подчеркнуть, что они выполняются по очереди и каждый раз, когда из коллекции получается очередной элемент. Таким образом, на каждом шаге циклаforeachв методеMain() возвращается только один символ.
Создание именованного итератора
В приведенных выше примерах был продемонстрирован простейший способ реализации итератора. Но ему имеется альтернатива в виде именованного итератора. В данном случае создается метод, оператор или аксессор, возвращающий ссылку на
объект типа I Enumerable. Именно этот объект используется в коде для предоставления итератора. Именованный итератор представляет собой метод, общая форма которого приведена ниже:
public IEnumerableимя_итератора (список_параметров) {
// ...
yield returnobj;
}
гдеимя_итератораобозначает конкретное имя метода;список_параметров —от нуля до нескольких параметров, передаваемых методу итератора;obj —следующий объект, возвращаемый итератором. Как только именованный итератор будет создан, его можно использовать везде, где он требуется, например для управления циклом
foreach.
Именованные итераторы оказываются весьма полезными в некоторых ситуациях, поскольку они позволяют передавать аргументы итератору, управляющему процессом получения конкретных элементов из коллекции. Например, итератору можно передать начальный и конечный пределы совокупности элементов, возвращаемых из коллекции итератором. Эту форму итератора можно перегрузить, расширив ее функциональные возможности. В приведенном ниже примере программы демонстрируются два способа применения именованного итератора для получения элементов коллекции. В одном случае элементы перечисляются в заданных начальном и конечном пределах, а в другом — элементы перечисляются с начала последовательности и до указанного конечного предела.
// Использовать именованные итераторы.
using System;
using System.Collections;
class MyClass { char ch = 'A';
// Этот итератор возвращает буквы английского алфавита,
}
}
class ItrDemo4 {
static void Main() {
MyClass me = new MyClass ();
Console.WriteLine("Возвратить по очереди первые 7 букв:"); foreach(char ch in mc.MyItr(7))
Console.Write(ch + " ");
Console .WriteLine (lf\nlf) ;
Console.WriteLine("Возвратить по очереди буквы от F до L:"); foreach(char ch in mc.Myltr(5, 12))
Console.Write(ch + " ");
Console.WriteLine();
}
}
Эта программа дает следующий результат.
Возвратить по очереди первые 7 букв:
А В С D Е F G
Возвратить по очереди буквы от F до L:
F G Н I J К L
Создание обобщенного итератора
В приведенных выше примерах применялись необобщенные итераторы, но, конечно, ничто не мешает создать обобщенные итераторы. Для этого достаточно возвратить объект обобщенного типаIEnumerator<T>илиIEnumerable<T>.Ниже приведен пример создания обобщенного итератора.
// Простой пример обобщенного итератора, using System;
using System.Collections.Generic;
class MyClass<T> {
T [ ] array;
public MyClass(T[] a) { array = a;
}
// Этот итератор возвращает символы из массива chrs. public IEnumetator<T> GetEnumerator() {
foreach(T obj in array) yield return obj;
}
}
class GenericItrDemo { static void Main() {
int [ ] nums ={4, 3, 6,4,7, 9 };
MyClass<int> me = new MyClass<int>(nums);
foreach(int x in me)
Console.Write(x + " ");
Console.WriteLine();
bool[] bVals = { true, true, false, true };
MyClass<bool> mc2 = new MyClass<bool>(bVals);
foreach(bool b in mc2)
Console.Write(b + " ");
Console.WriteLine ();
}
}
Вот к какому результату приводит выполнение этой программы.
4 3 6 4 7 9
True True False True
В данном примере массив, состоящий из возвращаемых по очереди объектов, передается конструктору классаMyClass.Тип этого массива указывает в качестве аргумента типа в конструкторе классаMyClass.
МетодGetEnumerator() оперирует данными обобщенного типаТи возвращает перечислитель типаIEnumerator<T>.Следовательно, итератор, определенный в классеMyClass,способен перечислять данные любого типа.
Инициализаторы коллекций
В С# имеется специальное средство, называемоеинициализатором коллекциии упрощающее инициализацию некоторых коллекций. Вместо того чтобы явно вызывать методAdd (), при создании коллекции можно указать список инициализаторов. После этого компилятор организует автоматические вызовы методаAdd (), используя значения из этого списка. Синтаксис в данном случае ничем не отличается от инициализации массива. Обратимся к следующему примеру, в котором создается коллекция типаList<char>,инициализируемая символами С, А, Е, В, D и F.
List<char> 1st = new List<char>() { 'С1, 'А1, 'Е1, 'В1, 1D1, 1F1 };
После выполнения этого оператора значение свойства1st. Countбудет равно 6, поскольку именно таково число инициализаторов. А после выполнения следующего циклаforeach:
foreach(ch in 1st)
Console.Write(ch + " ");
получится такой результат:
С A E В D F
Для инициализации коллекции типаLinkedListcTKey, TValue>,в которой хранятся пары "ключ-значение", инициализаторы приходится предоставлять парами, как показано ниже.
SortedListcint, string> 1st =
new SortedListcint, string>() { {1, "один11}, {2, "два" }, {3, "три"} };
Компилятор передаст каждую группу значений в качестве аргументов методуAdd (). Следовательно, первая пара инициализаторов преобразуется компилятором в вызовAdd(1, "один").
Компилятор вызывает методAdd() автоматически для ввода инициализаторов в коллекцию, и поэтому инициализаторы коллекций можно использовать только в коллекциях, поддерживающих открытую реализацию методаAdd (). Это означает, что инициализаторы коллекций нельзя использовать в коллекциях типаStack, Stack<T>, QueueилиQueue<T>,поскольку в них методAdd() не поддерживается. Их нельзя применять также в тех коллекциях типаLinkedList<T>,где методAdd() предоставляется как результат явной реализации соответствующего интерфейса.
ГЛАВА 26 Сетевые средства подключения к Интернету
Язык C# предназначен для программирования в современной вычислительной среде, где Интернету, естественно, принадлежит весьма важная роль. Одной из главных целей разработки C# было внедрение в этот язык программирования средств, необходимых для доступа к Интернету. Такой доступ можно было осуществить и в предыдущих версиях языков программирования, включая С и C++, но поддержка операций на стороне сервера, загрузка файлов и получение сетевых ресурсов в этих языках не вполне отвечали потребностям большинства программистов. Эта ситуация коренным образом изменилась в С#. Используя стандартные средства C# и среды .NET Framework, можно довольно легко сделать приложения совместимыми с Интернетом и написать другие виды программ, ориентированных на подключение к Интернету.
Поддержка сетевого подключения осуществляется через несколько пространств имен, определенных в среде .NET Framework, и главным среди них является пространство именSystem.Net.В нем определяется целый ряд высокоуровневых, но простых в использовании классов, поддерживающих различные виды операций, характерных для работы с Интернетом. Для этих целей доступен также ряд пространств, вложенных в пространство именSystem. Net.Например, средства низкоуровневого сетевого управления через сокеты находятся в пространстве именSystem.Net. Sockets,поддержка электронной почты — в пространстве именSystem.Net.Mail,а поддержка защищенных сетевых потоков — в пространстве именSystem.Net. Security.Дополнительные функциональные возможности предоставляются в ряде других вложенных пространств имен. К числу других не менее важных пространств имен,
связанных с сетевым подключением к Интернету, относится пространствоSystem. Web.Это и вложенные в него пространства имен поддерживают сетевые приложения на основе технологии ASP.NET.
В среде .NET Framework имеется достаточно гибких средств и возможностей для сетевого подключения к Интернету. Тем не менее для разработки многих приложений более предпочтительными оказываются функциональные возможности, доступные в пространстве именSystem.Net.Они и удобны, и просты в использовании. Именно поэтому пространству именSystem. Netбудет уделено основное внимание в этой главе.
Члены пространства имен System.Net
Пространство именSystem.Netдовольно обширно и состоит из многих членов. Полное их описание и обсуждение всех аспектов программирования для Интернета выходит далеко за рамки этой главы. (На самом деле для подробного рассмотрения всех вопросов, связанных с сетевым подключением к Интернету и его поддержкой в С#, потребуется отдельная книга.) Однако целесообразно хотя бы перечислить члены пространства именSystem. Net,чтобы дать какое-то представление о том, что именно доступно для использования в этом пространстве.
Ниже перечислены классы, определенные в пространстве именSystem.Net.
AuthenticationManager
Authorization
Cookie
CookieCollection
CookieContainer
CookieException
CredentialCache
Dns
DnsEndPoint
DnsPermission
DnsPermissionAttribute
DownloadDataCompletedEventArgs
DownloadProgressChangedEventArgs
DownloadstringCompletedEventArgs
EndPoint
EndpointPermission
FileWebRequest
FileWebResponse
FtpWebRequest
FtpWebResponse
HttpListener
HttpListenerBasicIdentity
HttpListenerContext
HttpListenerException
HttpListenerPrefixCol lection
HttpListenerRequest
HttpListenerResponse
HttpVersion
HttpWebRequest
HttpWebResponse
IPAddress
IPEndPoint
IPEndPointCollection
IPHostEntry
IrDAEndPoint
NetworkCredential
OpenReadCompletedEventArgs
OpenWriteCompletedEventArgs
ProtocolViolationException
ServicePoint
ServicePointManager
SocketAddress
SocketPermission
SocketPermissionAttribute
TransportContext
UploadDataCompletedEventArgs
UploadFileCompletedEventArgs
UploadProgressChangedEventArgs
UploadstringCompletedEventArgs
UploadValuesCompletedEventArgs
WebClient
WebException
WebHeaderCollection
WebPermission
WebPermissionAttribute
WebProxy
WebRequest
WebRequestMethods
WebRequestMethods.File
WebRequestMethods.Ftp
WebRequestMethods.Http
WebResponse
WebUtility
Кроме того, в пространстве именSystem.Netопределены перечисленные ниже
интерфейсы.
AuthenticationModule
IcertificatePolicy I Credential Pol icy
ICredentials
IcredentialsByHost IWebProxy
IWebProxyScript
IWebRequestCreate
В этом пространстве имен определяются также приведенные ниже перечисления.
AuthenticationSchemes
DecompressionMethods FtpStatusCode
HttpRequestHeader
HttpResponseHeader HttpStatusCode
NetworkAccess
SecurityProtocolType TransportType
WebExceptionStatus
Помимо этого, в пространстве именSystem.Netопределен ряд делегатов.
Несмотря на то что в пространстве именSystem.Netопределено немало членов, лишь немногие из них на самом деле требуются при решении наиболее типичных задач программирования для Интернета. Основу сетевых программных средств составляют абстрактные классыWebRequestиWebResponse.От этих классов наследуют все классы, поддерживающие конкретные сетевые протоколы. (Протоколопределяет правила передачи данных по сети.) Например, к производным классам, поддерживающим стандартный сетевой протокол HTTP, относятся классыHttpWebRequestиHttpWebResponse.
КлассыHttpWebRequestиHttpWebResponseдовольно просты в использовании. Тем не менее решение некоторых задач можно еще больше упростить, применяя подход, основанный на классеWebClient.Так, если требуется только загрузить или выгрузить файл, то для этой цели лучше всего подойдет классWebClient.
Универсальные идентификаторы ресурсов
В основу программирования для Интернета положено понятиеуниверсального иден-тификйтора ресурса(URI), иногда еще называемогоунифицированным указателем информационного ресурса(URL). Этот идентификатор описывает местоположение ресурса в сети. В корпорации Microsoft принято пользоваться сокращением URI при описании членов пространства именSystem.Net,и поэтому в данной книге выбрано именно это сокращение для обозначения универсального идентификатора ресурса. Идентификаторы URI, без сомнения, известны каждому, кто хотя бы раз пользовался браузером для поиска информации в Интернете. По существу, это адрес информационного ресурса, который указывается в соответствующем поле окна браузера.
Ниже приведена общая форма идентификатора URI:
Протокол://Идентмфмкационный_номер_сервера/Путь_к_файлу1 Запрос
гдеПротокол—это применяемый протокол, например HTTP;Идентификацион-ный_номер_сервера— конкретный сервер, напримерmhprof essional. comили
HerbSchildt. com;Путь_к_файлу—путь к конкретному файлу. Если жеПуть_к_ файлуне указан, то получается страница, доступная на указанном сервере по умолчанию. И наконец,Запрособозначает информацию, отправляемую на сервер. УказыватьЗапроснеобязательно. В C# идентификаторы URI инкапсулированы в классUri,рассматриваемый далее в этой главе.
Основы организации доступа к Интернету
В классах, находящихся в пространстве именSystem.Net,поддерживается модель взаимодействия с Интернетом по принципу запроса и ответа. При таком подходе пользовательская программа, являющаяся клиентом, запрашивает информацию у сервера, а затем переходит в состояние ожидания ответа. Например, в качестве запроса программа может отправить на сервер идентификатор URI некоторого веб-сайта. В ответ она получит гипертекстовую страницу, соответствующую указанному идентификатору URL Такой принцип запроса и ответа удобен и прост в применении, поскольку большинство деталей сетевого взаимодействия реализуются автоматически.
На вершине иерархии сетевых классов находятся классыWebRequestиWebResponse,реализующие так называемыеподключаемые протоколы.Как должно быть известно большинству читателей, для передачи данных в сети применяется несколько разнотипных протоколов. К числу наиболее распространенных в Интернете относятся протокол передачи гипертекстовых файлов (HTTP), а также протокол передачи файлов (FTP). При создании идентификатора URI его префикс обозначает применяемый сетевой протокол. Например, в идентификатореhttp: //www. HerbSchildt. comиспользуется префиксhttp,обозначающий протокол передачи гипертекстовых файлов (HTTP).
Как упоминалось выше, классыWebRequestиWebResponseявляются абстрактными, а следовательно, в них определенны в самом общем виде операции запроса и ответа, типичные для всех протоколов. От этих классов наследуют более конкретные производные классы, в которых реализуются отдельные протоколы. Эти производные классы регистрируются самостоятельно, используя для этой цели статический методRegisterPrefix (), определенный в классеWebRequest.При создании объекта типаWebRequestавтоматически используется протокол, указываемый в префиксе URI, если, конечно, он доступен. Преимущество такого принципа "подключения" протоколов заключается в том, что большая часть кода пользовательской программы остается без изменения независимо от типа применяемого протокола.
В среде выполнения .NET Runtime протоколы HTTP, HTTPS и FTP определяются автоматически. Так, если указать идентификатор URI с префиксом HTTP, то будет автоматически получен HTTP-совместимый класс, который поддерживает протокол HTTP. А если указать идентификатор URI с префиксом FTP, то будет автоматически получен FTP-совместимый класс, поддерживающий протокол FTP.
При сетевом подключении к Интернету чаще всего применяется протокол HTTP, поэтому именно он и рассматривается главным образом в этой главе. (Тем не менее аналогичные приемы распространяются и на все остальные поддерживаемые протоколы.) Протокол HTTP поддерживается в классахHttpWebRequestиHttpWebResponse.Эти классы наследуют от классовWebRequestиWebResponse,а кроме того, имеют собственные дополнительные члены, применимые непосредственно к протоколу HTTP.
В пространстве именSystem.Netподдерживается как синхронная, так и асинхронная передача данных. В Интернете предпочтение чаще всего отдается синхронным транзакциям, поскольку ими легче пользоваться. При синхронной передаче данных пользовательская программа посылает запрос и затем ожидает ответа от сервера. Но для некоторых разновидностей высокопроизводительных приложений более подходящей оказывается асинхронная передача данных. При таком способе передачи данных пользовательская программа продолжает обработку данных, ожидая ответа на переданный запрос. Но организовать асинхронную передачу данных труднее. Кроме того, не во всех программах можно извлечь выгоды из асинхронной передачи данных. Например, когда требуется получить информацию из Интернета, то зачастую ничего другого не остается, как ожидать ее. В подобных случаях потенциал асинхронной передачи данных используется не полностью. Вследствие того что синхронный доступ к Интернету реализуется проще и намного чаще, именно он и будет рассматриваться в этой главе.
Далее речь пойдет прежде всего о классахWebRequestиWebResponse,поскольку именно они положены в основу сетевых программных средств, доступных в пространстве именSystem.Net.
Класс WebReques t
КлассWebRequestуправляет сетевым запросом. Он является абстрактным, поскольку в нем не реализуется конкретный протокол. Тем не менее в нем определяются те методы и свойства, которые являются общими для всех сетевых запросов. В табл. 26.1 сведены методы, определенные в классеWebRequestи поддерживающие синхронную передачу данных, а в табл. 26.2 — свойства, объявляемые в классеWebRequest.Устанавливаемые по умолчанию значения свойств задаются в производных классах. Открытые конструкторы в классеWebRequestне определены.
Для того чтобы отправить запрос по адресу URI, необходимо сначала создать объект класса, производного от классаWebRequestи реализующего требуемый протокол. С этой целью вызывается статический методCreate(), определенный в классеWebRequest.МетодCreate() возвращает объект класса, наследующего от классаWebRequestи реализующего конкретный протокол.
Таблица 26.1. Методы, определенные в классе WebRequest
Метод
Описание
public static WebRequest
Создает объект типа WebRequest для иден
Create(string
тификатора URI, указываемого в строке
requestUriString)
requestUriString. Возвращаемый объект реализует протокол, заданный префиксом идентификатора URI. Следовательно, возвращаемый объект будет экземпляром класса, призводного от класса WebRequest. Если затребованный протокол недоступен, то генерируется исключение NotSupportedException. А если недействителен указанный формат идентификатора URI, то генерируется исключение UriFormatException
Метод
Описание
public static WebRequest Create(UrirequestUri)
public virtual Stream GetRequestStream() public virtual WebResponse GetResponse()
Создает объект типа WebRequest для идентификатора URI, указываемого с помощью параметра reques tUri. Возвращаемый объект реализует протокол, заданный префиксом идентификатора URI. Следовательно, возвращаемый объект будет экземпляром класса, призводного от класса WebRequest. Если затребованный протокол недоступен, то генерируется исключение NotSupportedException Возвращает поток вывода, связанный с запрошенным ранее идентификатором URI Отправляет предварительно сформированный запрос и джидает ответа. Получив ответ, возвращает его в виде объекта класса WebReponse. Этот объект используется затем в программе для получения информации по указанному адресу URI
Таблица 26.2. Свойства, определенные в классе WebRequest
Свойство
Описание
public AuthenticationLevel
Получает или устанавливает уровень аутентифи
AuthenticationLevel( get; set; }
кации
public virtual
Получает или устанавливает правила использо
RequestCachePolicy CachePolicy
вания кеша, определяющие момент получения
{ get; set; }
ответа из кеша
public virtual string
Получает или устанавливает имя группы подклю
ConnectionGroupName { get;
чения. Группы подключения представляют собой
set; }
способ создания ряда запросов. Они не нужны для простых транзакций в Интернете
public virtual long
Получает или устанавливает длину передаваемо
ContentLength { get; set; }
го содержимого
public virtual string
Получает или устанавливает описание переда
ContentType { get; set; }
ваемого содержимого
public virtual Icredentials
Получает или устанавливает мандат, т.е. учетные
Credentials { get; set; }
данные пользователя
public static
Получает или устанавливает правила использо
RequestCachePolicy
вания кеша по умолчанию, определяющие мо
DefaultCachePolicy { get; set; }
мент получения ответа из кеша
public static IWebProxy
Получает или устанавливает используемый по
DefaultWebProxy { get; set; }
умолчанию прокси-сервер
public virtual
Получает или устанавливает коллекцию заголовков
WebHeaderCollection Headers{
get; set; }
public TokenlmpersonationLevel
Получает или устанавливает уровень анонимно
ImpersonationLevel { get; set; }
го воплощения
Свойство
Описание
public virtual string Method { get; set; } public virtual bool PreAuthenticate { get; set; }
public virtual IWebProxy Proxy { get; set; }
public virtual Uri RequestUri { get; }
public virtual int Timeout { get; set; }
public virtual bool UseDefaultCredential { get; set; }
Получает или устанавливает протокол
Если принимает логическое значение true, то в отправляемый запрос включается информация для аутентификации. А если принимает логическое значение false, то информация для аутентификации предоставляется только по требованию адресата URI
Получает или устанавливает прокси-сервер. Применимо только в тех средах, где используется прокси-сервер
Получает идентификатор URI конкретного запроса
Получает или устанавливает количество миллисекунд, в течение которых будет ожидаться ответ на запрос. Для установки бесконечного ожидания используется значение Timeout. Infinite Получает или устанавливает значение, которое определяет, используется ли для аутентификации устанавливаемый по умолчанию мандат. Если имеет логическое значение true, то используется устанавливаемый по умолчанию мандат, т.е. учетные данные пользователя, в противном случае этот мандат не используется
Класс WebResponse
В классеWebResponseинкапсулируется ответ, получаемый по запросу. Этот класс является абстрактным. В наследующих от него классах создаются отдельные его версии, поддерживающие конкретный протокол. Объект классаWebResponseобычно получается в результате вызова методаGetResponse(), определенного в классеWebRequest.Этот объект будет экземпляром отдельного класса, производного от классаWebResponseи реализующего конкретный протокол. Методы, определенные в классеWebResponse,сведены в табл. 26.3, а свойства, объявляемые в этом классе, — в табл. 26.4. Значения этих свойств устанавливаются на основании каждого запроса в отдельности. Открытые конструкторы в классеWebResponseне определяются.
Таблица 26.3. Наиболее часто используемые методы, определенные в классе WebResponse
Метод
Описание
public virtual void Close()*
public virtual Stream GetResponseStream()
Закрывает ответный поток. Закрывает также поток ввода ответа, возвращаемый методом
GetResponseStream()
Возвращает поток ввода, связанный с запрашиваемым URI. Из этого потока могут быть введены данные из запрашиваемого URI
Свойство
Описание
public virtual long
Получает или устанавливает длину принимаемого со
ContentLength { get; set; }
держимого. Устанавливается равным -1, если данные о длине содержимого недоступны
public virtual string
Получает или устанавливает описание принимаемого
ContentType { get; set; }
содержимого
public virtual
Получает или устанавливает коллекцию заголовков,
WebHeaderCollection Headers
связанных с URI
{ get; }
public virtual bool
Принимает логическое значение true, если запрос
IsFromCache { get; }
получен из кэша. А если запрос доставлен по сети, то принимает логическое значение false
public virtual bool
Принимает логическое значение true, если клиент
IsMutuallyAuthenticated {
и сервер опознают друг друга, а иначе — принимает
get; }
логическое значение false
public virtual Uri
Получает URI, по которому был сформирован ответ.
ResponseUri { get; }
Этот идентификатор может отличаться от запрашиваемого, если ответ был переадресован по другому URI
Классы HttpWebRequest и HttpWebResponse
Оба класса,HttpWebRequestиHttpWebResponse,наследуют от классовWebRequestиWebResponseи реализуют протокол HTTP. В ходе этого процесса в обоих классах вводится ряд дополнительных свойств, предоставляющих подробные сведения о транзакции по протоколу HTTP. О некоторых из этих свойств речь пойдет далее в настоящей главе. Но для выполнения простых операций в Интернете эти дополнительные свойства, как правило, не требуются.
Первый простой пример
Доступ к Интернету организуется на основе классовWebRequestиWebResponse.Поэтому, прежде чем рассматривать этот процесс более подробно, было бы полезно обратиться к прострму примеру, демонстрирующему порядок доступа к Интернету по принципу запроса и ответа. Глядя на то, как эти классы применяются на практике, легче понять, почему они организованы именно так, а не как-то иначе.
В приведенном ниже примере программы демонстрируется простая, но весьма типичная для Интернета операция получения гипертекстового содержимого из конкретного веб-сайта. В данном случае содержимое получается из веб-сайта издательства McGraw-Hill по адресуwww. McGraw-Hill. com,но вместо него можно подставить адрес любого другого веб-сайта. В этой программе гипертекстовое содержимое выводится на экран монитора отдельными порциями по 400 символов, чтобы полученную информацию можно было просматривать, не прибегая к прокрутке экрана.
// Пример доступа к веб-сайту.
using System.Net; using System.10;
class NetDemo {
static void Main() { int ch;
// Сначала создать объект запроса типа WebRequest по указанному URI. HttpWebRequest req = (HttpWebRequest)
WebRequest.Create("http://www.McGraw-Hill.com");
// Затем отправить сформированный запрос и получить на него ответ. HttpWebResponse resp = (HttpWebResponse) req.GetResponse();
// Получить из ответа поток ввода.
Stream istrm = resp.GetResponseStream();
/* А теперь прочитать и отобразить гипертекстовое содержимое,
полученное по указанному URI. Это содержимое выводится на экран отдельными порциями по 400 символов. После каждой такой порции следует нажать клавишу <ENTER>, чтобы вывести на экран следующую порцию из 400 символов. */ for (int i=l; ; i++) { ch = istrm.ReadByte(); if(ch == -1) break;
Console.Write ( (char) ch); if((i % 4 0 0)==0) {
Console.Write ("ХпНажмите клавишу <Enter>.");
Console.ReadLine();
}
}
// Закрыть ответный поток. При этом закрывается также поток ввода istrm. resp.Close(); •
}
}
Ниже приведена первая часть получаемого результата. (Разумеется, это содержимое может со временем измениться в связи с обновлением запрашиваемого веб-сайта, и поэтому у вас оно может оказаться несколько иным.)
<html>
<head>
<title>Home - The McGraw-Hill Companies</title>
<meta name="keywords" content="McGraw-Hill Companies,McGraw-Hill, McGraw Hill, Aviation Week, BusinessWeek, Standard and Poor's, Standard & Poor1s,CTB/McGraw-Hill,Glencoe/McGraw-Hill, The Grow Network/McGraw-Hill,Macmillan/McGraw-Hill, McGraw-Hill Contemporary,McGraw-Hill Digital Learning,McGraw-Hill Professional Development,SRA/McGraw
Нажмите клавишу <Enter>.
-Hill,Wright «Group/McGraw-Hill,McGraw-Hill Higher Education,McGraw-Hill/Irwin, McGraw-Hill/Primis Custom Publishing,McGraw-Hill/Ryerson,Tata/McGraw-Hill,
McGraw-Hill Interamericana,Open University Press, Healthcare Information Group, Platts, McGraw-Hill Construction, Information & Media Services" />
<meta name="description" content="The McGraw-Hill Companies Corporate Website." /> <meta http-equiv
Нажмите клавишу <Enter>. 1
Итак, выше приведена часть гипертекстового содержимого, полученного из вебсайта издательства McGraw-Hill по адресуwww.McGraw-Hill. com.В рассматриваемом здесь примере программы это содержимое просто выводится в исходном виде на экран посимвольно и не форматируется в удобочитаемом виде, как это обычно делается в окне браузера.
Проанализируем данную программу построчно. Прежде всего обратите внимание на использование в ней пространства именSystem. Net.Как пояснялось ранее, в этом пространстве имен находятся классы сетевого подключения к Интернету. Обратите также внимание на то, что в данную программу включено пространство именSystem. 10, которое требуется для того, чтобы прочитать полученную на веб-сайте информацию, используя объект типаStream.
В начале программы создается объект типаWebRequest,содержащий требуемый URL Как видите, для этой цели используется методCreate(), а не конструктор. Это статический член классаWebRequest.Несмотря на то что классWebRequestявляется абстрактным, это обстоятельство не мешает вызывать статический метод данного класса. МетодCreate() возвращает объект типаHttpWebRequest.Разумеется, его значение требуется привести к типуHttpWebRequest,прежде чем присвоить его переменнойreqссылки на объект типаHttpWebRequest.На этом формирование запроса завершается, но его еще нужно отправить по указанному URL
Для того чтобы отправить запрос, в рассматриваемой здесь программе вызывается методGetResponse() для объекта типаWebRequest.Отправив запрос, методGetResponse() переходит в состояние ожидания ответа. Как только ответ будет получен, методGetResponse() возвратит объект типаWebResponse,в котором инкапсулирован ответ. Этот объект присваивается переменнойresp.Но в данном случае ответ принимается по протоколу HTTP, и поэтому полученный результат приводится к типуHttpWebResponse.Среди прочего в ответе содержится поток, предназначаемый для чтения данных из источника по указанному URL
Далее поток ввода получается в результате вызова методаGetResponseStream() для объектаresp.Это стандартный объект классаStreamсо всеми атрибутами и средствами, необходимыми для организации потока ввода. Ссылка на этот поток присваивается переменнойistrm,с помощью которой данные могут быть прочитаны из источника по указанному URI, как из обычного файла.
После этого в программе выполняется чтение данных из веб-сайта издательства McGraw-Hill по адресуwww .McGraw-Hill. comи последующий их вывод на экран. А поскольку этих данных много, то они выводятся на экран отдельными порциями по 400 символов, после чего в программе ожидается нажатие клавиши <Enter>, чтобы продолжить вывод. Благодаря этому выводимые данные можно просматривать без прокрутки экрана. Обратите внимание на то, что данные читаются посимвольно с помощью методаReadByte (). Напомним, что этот метод возвращает очередной байт из потока ввода в виде значения типаint,которое требуется привести к типуchar.По достижении конца потока этот метод возвращает значение -1.
И наконец, ответный поток закрывается при вызове методаClose() для объектаresp.Вместе с ответным потоком автоматически закрывается и поток ввода. Ответный поток следует закрывать в промежутках между последовательными запросами. В противном случае сетевые ресурсы могут быть исчерпаны, препятствуя очередному подключению к Интернету.
И в заключение анализа рассматриваемого здесь примера следует обратить особое внимание на следующее: для отображения гипертекстового содержимого, получаемого от сервера, совсем не обязательно использовать объект типаHttpWebRequestилиHttpWebResponse.Ведь для решения этой задачи в данной программе оказалось достаточно стандартных методов, определенных в классахWebRequestиWebResponse,и не потребовалось прибегать к специальным средствам протокола HTTP. Следовательно, вызовы методовCreate() иGetResponse() можно было бы написать следующим образом.
// Сначала создать объект запроса типа WebRequest по указанному URI.
WebRequest req = WebRequest.Create("http://www.McGraw-Hill.com");
// Затем отправить сформированный запрос и получить на него ответ.
WebResponse resp = req.GetResponse() ;
В тех случаях, когда не требуется приведение к конкретному типу реализации протокола, лучше пользоваться классамиWebRequestиWebResponse,так как это дает возможность менять протокол, не оказывая никакого влияния на код программы. Но поскольку во всех примерах, приведенных в этой главе, используется протокол HTTP, то в ряде примеров демонстрируются специальные средства этого протокола из классовHttpWebRequestиHttpWebResponse.
Обработка сетевых ошибок
Программа из предыдущего примера составлена верно, но она совсем не защищена от простейших сетевых ошибок, которые способны преждевременно прервать ее выполнение. Конечно, для программы, служащей в качестве примера, это не так важно, как для реальных приложений. Для полноценной обработки сетевых исключений, которые могут быть сгенерированы программой, необходимо организовать контроль вызовов методовCreate (),GetResponse () иGetResponseStream (). Следует особо подчеркнуть, что генерирование конкретных исключений зависит от используемого протокола. И ниже речь пойдет об ошибках, которые могут возникнуть при использовании протокола HTTP, поскольку средства сетевого подключения к Интернету, доступные в С#, рассматриваются в настоящей главе на примере именно этого протокола.
Исключения, генерируемые методом Create ()
МетодCreate(), определенный в классеWebRequest,может генерировать четыре исключения. Так, если протокол, указываемый в префиксе URI, не поддерживается, то генерируется исключениеNotSupportedException.Если формат URI оказывается недействительным, то генерируется исключениеUriFormatException.А если у пользователя нет соответствующих полномочий для доступа к запрашиваемому сетевому ресурсу, то генерируется исключениеSystem. Security. SecurityException.Кроме того, методCreate() генерирует исключениеArgumentNullException,если он вызывается с пустой ссылкой, хотя этот вид ошибки не имеет непосредственного отношения к сетевому подключению.
Исключения, генерируемые методом GetResponse ()
При вызове методаGetResponse() для получения ответа по протоколу HTTP может произойти целый ряд ошибок. Эти ошибки представлены следующими исключениями:InvalidOperationException, ProtocolViolationException, NotSupportedExceptionиWebException.Наибольший интерес среди них вызывает исключениеWebException.
У исключенияWebExceptionимеются два свойства, связанных с сетевыми ошибками:ResponseиStatus.С помощью свойстваResponseможно получить ссылку на объект типаWebResponseв обработчике исключений. Для соединения по протоколу HTTP этот объект описывает характер возникшей ошибки. СвойствоResponseобъявляется следующим образом.
public WebResponse Response { get; }
Когда возникает ошибка, то с помощью свойстваStatusтипаWebExceptionможно выяснить, что именно произошло. Это свойство объявляется следующим образом:
public WebExceptionStatus Status {get; }
гдеWebExceptionStatus— это перечисление, которое содержит приведенные ниже значения.
CacheEntryNotFound
ConnectFailure
ConnectionClosed
KeepAliveFailure
MessageLengthLimitExceeded
NameResolutionFailure
Pending
PipelineFailure
ProtocolError
ProxyNameResolutionFailure
ReceiveFailure
RequestCanceled
RequestProhibitedByCachePolicy
RequestProhibitedByProxy
SecureChannelFailure
SendFailure
ServerProtocolViolation
Success
Timeout
TrustFailure
UnknownError
Как только будет выяснена причина ошибки, в программе могут быть предприняты соответствующие действия.
Исключения, генерируемые методом GetResponseStream ()
Длясоединения по протоколу HTTP методGetResponseStream() из классаWebResponseможет сгенерировать исключениеProtocolViolationException,которое в целом означает, что в работе по указанному протоколу произошла ошибка. Что же касается методаGetResponseStream(), то это означает, что ни один из действительных ответных потоков недоступен. ИсключениеObjectDisposedExceptionгенерируется в том случае, если ответ уже утилизирован. А исключениеIOException,конечно, генерируется при ошибке чтения из потока, в зависимости от того, как организован ввод данных.
Обработка исключений
В приведенном ниже примере программы демонстрируется обработка всевозможных сетевых исключений, которые могут возникнуть в связи с выполнением программы из предыдущего примера, в которую теперь добавлены соответствующие обработчики исключений.
// Пример обработки сетевых исключений.
using System; using System.Net; using System.10;
class NetExcDemo { static void Main() { int ch;
try {
// Сначала создать объект запроса типа WebRequest по указанному URI. HttpWebRequest req = (HttpWebRequest)
WebRequest.Create ("http://www.McGraw-Hill.com");
// Затем отправить сформированный запрос и получить на него ответ. HttpWebResponse resp = (HttpWebResponse) req.GetResponse ();
// Получить из ответа поток ввода.
Stream istrm = resp.GetResponseStream();
/* А теперь прочитать и отобразить гипертекстовое содержимое,
полученное по указанному URI. Это содержимое выводился на экран отдельными порциями по 400 символов. После каждой такой порции следует нажать клавишу <ENTER>, чтобы вывести на экран следующую порцию, состоящую из 400 символов. */ for(int i=l; ; i++) { ch = istrm.ReadByte(); if(ch == -1) break;
Console.Write ( (char) ch) ; if((i % 4 0 0)==0) {
Console.Write ("ХпНажмите клавишу <Enter>.");
Console.ReadLine() ;
}
}
// Закрыть ответный поток. При этом закрывается // также поток ввода istrm. resp.Close ();
} catch(WebException exc) {
Console.WriteLine ("Сетевая ошибка: " + exc.Message +
"\пКод состояния: " + exc.Status);
} catch(ProtocolViolationException exc) {
Console.WriteLine("Протокольная ошибка: " + exc.Message);
} catch(UriFormatException exc) {
Console.WriteLine("Ошибка формата URI: " + exc.Message);
} catch(NotSupportedException exc) {
Console.WriteLine("Неизвестный протокол: " + exc.Message);
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода: " + exc.Message);
} catch(System.Security.SecurityException exc) {
Console.WriteLine("Исключение в связи с нарушением безопасности: " + exc.Message);
} catch(InvalidOperationException exc) {
Console.WriteLine("Недопустимая операция: " + exc.Message);
}
}
}
Теперь перехватываются все исключения, которые могут быть сгенерированы сетевыми методами. Так, если изменить вызов методаCreate() следующим образом:
WebRequest.Create("http://www.McGraw-Hill.com/moonrocket");
а затем перекомпилировать и еще раз выполнить программу, то в результате может быть выдано приведенное ниже сообщение об ошибке.
Сетевая ошибка: Удаленный сервер возвратил ошибку: (404) Не найден.
Код состояния: ProtocolError
На веб-сайте по адресуwww.McGraw-Hill. comотсутствует разделmoonrocket,и поэтому он не найден по указанному URI, что и подтверждает приведенный выше результат.
Ради краткости и ясности в программах большинства примеров из этой главы отсутствует полноценная обработка исключений. Но в реальных приложениях она просто необходима.
Класс Uri
Как следует из табл. 26.1, методWebRequest. Create() существует в двух вариантах. В одном варианте он принимает идентификатор URI в виде строки. Именно этот вариант и был использован в предьтдугцих примерах программ. А во втором варианте этот метод принимает идентификатор URI в виде экземпляра объекта классаUri,определенного в пространстве именSystem.КлассUriинкапсулирует идентификатор URL Используя классUri,можно сформировать URI, чтобы затем передать этот идентификатор методуCreate (). Кроме того, идентификатор URI можно разделить на части. Для выполнения многих простых операций в Интернете классUriмалопригоден. Тем не менее он может оказаться весьма полезным в более сложных ситуациях сетевого подключения к Интернету.
В классеUriопределяется несколько конструкторов. Ниже приведены наиболее часто используемые конструкторы этого класса.
public Uri(stringuriString)
public Uri(UribaseUri,stringrelativeUri)
В первой форме конструктора объект классаUriсоздается по идентификатору URI, заданному в виде строкиuriString.А во второй форме конструктора он создается по относительному URI, заданному в виде строкиrelativeUriотносительно абсолютного URI, обозначаемого в виде объектаbaseUriтипаUri.Абсолютный URI определяет полный адрес URI, а относительный URI — только путь к искомому ресурсу.
В классеUriопределяются многие поля, свойства и методы, оказывающие помощь в управлении идентификаторами URI или в получении доступа к различным частям URI. Особый интерес представляют приведенные ниже свойства.
Свойство
Описание
public
string Host { get; }
Получает имя сервера
public
string LocalPath { get; }
Получает локальный путь к файлу
public
string. PathAndQuery { get; }
Получает абсолютный путь и строку запроса
public
int Port { get; }
Получает номер порта для указанного протокола. Так, для протокола HTTP номер порта равен 80
public
string Query { get; }
Получает строку запроса
public
string Scheme { get; }
Получает протокол
Перечисленные выше свойства полезны для разделения URI на составные части. Применение этих свойств демонстрируется в приведенном ниже примере программы.
// Пример применения свойств из класса Uri.
using System; using System.Net;
class UriDemo {
static void Main() {
Uri sample = new Uri("http://HerbSchildt.com/somefile.txt?SomeQuery");
Console.WriteLine("Хост: " + sample.Host);
Console.WriteLine("Порт: " + sample.Port);
Console.WriteLine("Протокол: " + sample.Scheme);
Console .WriteLine ("Локальный путь: 11 + sample. LocalPath) ;
Console.WriteLine("Запрос: " + sample.Query);
Console.WriteLine("Путь и запрос: " + sample.PathAndQuery);
}
}
Эта программа дает следующий результат.
Хост: HerbSchildt.com Порт: 80 Протокол: http
Локальный путь: /somefile.txt Запрос: ?SomeQuery
Путь и запрос: /somefile.txt?SomeQuery
Доступ к дополнительной информации, получаемой в ответ по протоколу HTTP
С помощью сетевых средств, имеющихся в классеHttpWebResponse,можно получить доступ к другой информации, помимо содержимого указываемого ресурса. К этой информации, в частности, относится время последней модификации ресурса, а также имя сервера. Она оказывается доступной с помощью различных свойств, связанных с получаемым ответом. Все эти свойства, включая и те что, определены в классеWebResponse,сведены в табл. 26.5. В приведенных далее примерах программ демонстрируется применение этих свойств на практике.
Свойство
Описание
public
string CharacterSet { get; }
Получает название используемого набора символов
public { get;
string ContentEncoding }
Получает название схемы кодирования
public
long ContentLength { get; }
Получает длину принимаемого содержимого. Если она недоступна, свойство имеет значение -1
public
string ContentType { get; }
Получает описание содержимого
public
CookieCollection Cookies
Получает или устанавливает список cookie-
{ get;
set; }
наборов, присоединяемых к ответу
public
WebHeaderCollection
Получает коллекцию заголовков, присоединяе
Headers! get; }
мых к ответу
public
bool IsFromCache { get; }
Принимает логическое значение true, если запрос получен из кеша. А если запрос доставлен по сети, то принимает логическое значение false
public
bool
Принимает логическое значение true, если
IsMutuallyAuthenticated { get; }
клиент и сервер опознают друг друга, а иначе — принимает логическое значение false
public DateTime LastModified { get; }
Получает время последней модификации ресурса
public
string Method { get; }
Получает строку, которая задает способ ответа
public
Version ProtocolVersion
Получает объект типа Version, описываю
{ get;
}
щий версию протокола HTTP, используемую в транзакции
public
Uri ReponseUri { get; }
Получает URI, по которому был сформирован ответ. Этот идентификатор может отличаться от запрашиваемого, если ответ был переадресован по другому URI
public
string Server { get; }
Получает строку, обозначающую имя сервера
public
HttpStatusCode StatusCode
Получает объект типа HttpStatusCode, опи
{ get;
}
сывающий состояние транзакции
public
string StatusDescription
Получает строку, обозначающую состояние
{ get;
}
транзакции в удобочитаемой форме
Доступ к заголовку
Длядоступа к заголовку с информацией, получаемой в ответ по протоколу HTTP, служит свойствоHeaders,определенное в классеHttpWebResponse.
public WebHeaderCollection Headers{ get; }
Заголовок протокола HTTP состоит из пар "имя-значение", представленных строками. Каждая пара "имя-значение" хранится в коллекции классаWebHeaderCollection.Эта коллекция специально предназначена для хранения пар "имя-значение" и применяется аналогично любой другой коллекции (подробнее об этом см. в главе 25). Строковый массив имен может быть получен из свойстваAllKeys,а отдельные значения — по соответствующему имени при вызове методаGet Values (). Этот метод возвращает массив строк, содержащий значения, связанные с заголовком, передаваемым в качестве аргумента. МетодGet Values() перегружается, чтобы принять числовой индекс или имя заголовка.
В приведенной ниже программе отображаются заголовки, связанные с сетевым ресурсом, доступным по адресу www.McGraw-Hill.com.
// Проверить заголовки.
using System; using System.Net;
class HeaderDemo { static void Main() {
// Создать объект запроса типа WebRequest по указанному URI.
HttpWebRequest req = (HttpWebRequest)
WebRequest.Create("http://www.McGraw-Hill.com");
// Отправить сформированный запрос и получить на него ответ.
HttpWebResponse resp = (HttpWebResponse) req.GetResponse ();
// Получить список имен.
string[] names = resp.Headers.AllKeys;
// Отобразить пары "имя-значение" из заголовка.
Console.WriteLine ("{0,-20}{1}\п", "Имя", "Значение"); foreach(string n in names) {
Console.Write ("{0,-20}", n);
foreach(string v in resp.Headers.GetValues(n))
Console.WriteLine(v);
}
// Закрыть ответный поток, resp.Close();
}
}
Ниже приведен полученный результат. Не следует забывать, что информация в заголовке периодически меняется, поэтому у вас результат может оказаться несколько иным.
Имя Значение
Transfer-encoding chunked
Content-Type text/html
Date Sun, 06 Dec 2009 20:32:06 GMT
Server Sun-ONE-Web-Server/6.1
Доступ к cookie-наборам
Для доступа к cookie-наборам, получаемым в ответ по протоколу HTTP, служит свойствоCookies,определенное в классеHttpWebResponse.В cookie-Ha6opax содержится информация, сохраняемая браузером. Они состоят из пар "имя-значение"
и упрощают некоторые виды доступа к веб-сайтам. Ниже показано, каким образом определяется свойствоCookies.
public CookieCollection Cookies { get; set; }
В классеCookieCollectionреализуются интерфейсыICollectionиIEnumerable,и поэтому его можно использовать аналогично классу любой другой коллекции (подробнее об этом см. в главе 25). У этого класса имеется также индексатор, позволяющий получать cookie-Ha6op по указанному индексу или имени.
В коллекции типаCookieCollectionхранятся объекты классаCookie.В классеCookieопределяется несколько свойств, предоставляющих доступ к различным фрагментам информации, связанной с cookie-набором. Ниже приведены два свойства,NameиValue,используемые в примерах программ из этой главы.
public string Name { get; set; } public string Value { get; set; }
Имя cookie-Ha6opa содержится в свойствеName,а его значение — в свойствеValue.
Для того чтобы получить список cookie-наборов из принятого ответа, необходимо предоставить соок1е-контейнер с запросом. И для этой цели в классеHttpWebRequestопределяется свойствоCookieContainer,приведенное ниже.
public CookieContainer CookieContainer { get; set; }
В классеCookieContainerпредоставляются различные поля, свойства и методы, позволяющие хранить сооЫе-наборы. По умолчанию свойствоCookieContainerсодержит пустое значение. Для того чтобы воспользоваться cookie-наборами, необходимо установить это свойство равным экземпляру классаCookieContainer.Во многих приложениях свойствоCookieContainerне применяется непосредственно, а вместо него из принятого ответа составляется и затем используется коллекция типаCookieCollection.СвойствоCookieContainerпросто обеспечивает внутренний механизм сохранения cookie-наборов.
В приведенном ниже примере программы отображаются имена и значения cookie-наборов, получаемых из источника по URI, указываемому в командной строке. Следует, однако, иметь в виду, что cookie-наборы используются не на всех веб-сайтах, поэтому нужно еще найти такой веб-сайт, который поддерживает cookie-наборы.
/* Пример проверки cookie-наборов.
Для того чтобы проверить, какие именно cookie-наборы используются на веб-сайте, укажите его имя в командной строке.
Так, если назвать эту программу CookieDemo, то по команде
CookieDemohttp://msn.com
отобразятся cookie-наборы с веб-сайта по адресуwww.msn.com. */
using System; using System.Net;
class CookieDemo {
static void Main(string[] args) {
Console.WriteLine("Применение: CookieDemo <uri>"); return;
}
11Создать объект запроса типа WebRequest по указанному URI.
HttpWebRequest req = (HttpWebRequest)
WebRequest.Create(args[0]);
// Получить пустой контейнер.
req.CookieContainer = new CookieContainer();
// Отправить сформированный запрос и получить на него ответ.
HttpWebResponse resp = (HttpWebResponse) req.GetResponse ();
// Отобразить cookie-наборы.
Console.WriteLine("Количество cookie-наборов: " + resp.Cookies.Count);
Console.WriteLine("{0,-20}{1}", "Имя", "Значение"); for(int i=0; i < resp.Cookies.Count; i++)
Console.WriteLine("{0, -20}{1}",
resp.Cookies[i].Name, resp.Cookies[i].Value);
// Закрыть ответный поток, resp.Close ();
}
}
Применение свойства LastModified
#Иногда требуется знать, когда именно сетевой ресурс был обновлен в последний раз. Это нетрудно сделать, пользуясь сетевыми средствами классаHttpWebResponse,среди которых определено свойствоLastModified,приведенное ниже.
public DateTime LastModified { get; }
С помощью свойстваLastModifiedполучается время обновления содержимого сетевого ресурса в последний раз.
В приведенном ниже примере программы отображаются дата и время, когда был в последний раз обновлен ресурс, указываемый по URI в командной строке.
/* Использовать свойство LastModified.
Для того чтобы проверить дату последнего обновления веб-сайта, введите его URI в командной строке. Так, если назвать эту программу LastModifiedDemo, то для проверки даты последней модификации веб-сайта по адресуwww.HerbSchildt.comвведите команду
LastModif iedDemo http: //HerbSchildt. com
*/
using System; using System.Net;
static void Main(string[] args) {
if(args.Length != 1) {
Console.WriteLine("Применение: LastModifiedDemo <uri>"); return;
}
HttpWebRequest req = (HttpWebRequest)
WebRequest.Create(args[0]) ;
HttpWebResponse resp = (HttpWebResponse) req.GetResponse();
Console.WriteLine("Последняя модификация: " + resp.LastModified);
resp.Close ();
}
}
Практический пример создания программы MiniCrawler
Длятого чтобы показать, насколько просто программировать для Интернета средствами классовWebRequestиWebReponse,обратимся к разработке скелетного вариантапоискового роботапод названием MiniCrawler. Поисковый робот представляет собой программу последовательного перехода от одной ссылки на сетевой ресурс к другой. Поисковые роботы применяются в поисковых механизмах для каталогизации содержимого. Разумеется, поисковый робот MiniCrawler не обладает такими развитыми возможностями, как те, что применяются в поисковых механизмах. Эта программа начинается с ввода пользователем конкретного адреса URI, по которому затем читается содержимое и осуществляется поиск в нем ссылки. Если ссылка найдена, то программа запрашивает пользователя, желает ли он перейти по этой ссылке к обнаруженному сетевому ресурсу, найти другую ссылку на имеющейся странице или выйти из программы. Несмотря на всю простоту такого алгоритма поиска сетевых ресурсов, он служит интересным и наглядным примером доступа к Интернету средствами С#.
Программе MiniCrawler присущ ряд ограничений. Во-первых, в ней обнаруживаются только абсолютные ссылки, указываемые по гипертекстовой командеhref="http.Относительные ссылки при этом не обнаруживаются. Во-вторых, возврат к предыдущей ссылке в программе не предусматривается. И в-третьих, в ней отображаются только ссылки, но не окружающее их содержимое. Несмотря на все указанные ограничения данного скелетного варианта поискового робота, он вполне работоспособен и может быть без особых хлопот усовершенствован для решения других задач. На самом деле добавление новых возможностей в программу MiniCrawler — это удобный случай освоить на практике сетевые классы и узнать больше о сетевом подключении к Интернету. Ниже приведен полностью исходный код программы MiniCrawler.
/* MiniCrawler: скелетный вариант поискового робота.
Применение: для запуска поискового робота укажите URI в командной строке. Например, для того чтобы начать поиск с адресаwww.McGraw-Hill.com, введите следующую команду:
MiniCrawlerhttp://McGraw-Hill.com
*/
using System; using System.Net; using System.10;
class MiniCrawler {
// Найти ссылку в строке содержимого, static string FindLink(string htmlstr,
ref int startloc) {
int i;
int start, end; string uri = null;
i = htmlstr.IndexOf("href=\"http", startloc,
StringComparison.OrdinallgnoreCase);
if(i != -1) {
start = htmlstr. IndexOf (1111, i) + 1; end = htmlstr. IndexOf (1111, start); uri = htmlstr.Substring(start, end-start); startloc = end;
}
return uri;
}
static void Main(string[] args) { string link = null; string str; string answer;
int curloc; // содержит текущее положение в ответе if(args.Length != 1) {
Console.WriteLine ("Применение: MiniCrawler <uri>"); return ;
}
string uristr = args[0]; // содержит текущий URI HttpWebResponse resp = null; try { do {
Console .WriteLine ("Переход по ссылке 11 + uristr);
// Создать объект запроса типа WebRequest по указанному URI. HttpWebRequest req = (HttpWebRequest)
WebRequest.Create(uristr);
uristr = null; // запретить дальнейшее использование этого URI
// Отправить сформированный запрос и получить на него ответ, resp = (HttpWebResponse) req.GetResponse();
Stream istrm = resp.GetResponseStream () ;
// Заключить поток ввода в оболочку класса StreamReader. StreamReader rdr = new StreamReader(istrm) ;
// Прочитать всю страницу, str = rdr.ReadToEndO ;
curloc = 0;
do {
// Найти следующий URI для перехода по ссылке, link = FindLink(str, ref curloc);
if(link != null) {
Console.WriteLine("Найдена ссылка: " + link);
Console.Write("Перейти по ссылке, Искать дальше, Выйти?"); answer = Console.ReadLine();
if(string.Equals(answer, "П",
StringComparison.OrdinallgnoreCase)) {
uristr = string.Copy(link); break;
} else if(string.Equals(answer, "B",
StringComparison.OrdinallgnoreCase)) { break;
} else if(string.Equals(answer, "И",
StringComparison.OrdinallgnoreCase)) {
Console.WriteLine("Поиск следующей ссылки.");
}
} else {
Console.WriteLine("Больше ссылок не найдено."); break;
}
} while(link.Length > 0);
// Закрыть ответный поток, if(resp != null) resp.Close();
} while(uristr != null);
} catch(WebException exc) {
Console.WriteLine("Сетевая ошибка: " + exc.Message +
"\пКод состояния: " + exc.Status);
} catch(ProtocolViolationException exc) {
Console.WriteLine("Протокольная ошибка: " + exc.Message);
} catch(UriFormatException exc) {
Console.WriteLine("Ошибка формата URI: " + exc.Message);
} catch(NotSupportedException exc) {
Console.WriteLine("Неизвестный протокол: " + exc.Message);
} catch(IOException exc) {
Console.WriteLine("Ошибка ввода-вывода: " + exc.Message);
} finally {
if(resp != null) resp.Close();
Console.WriteLine("Завершение программы MiniCrawler.");
}
}
Ниже приведен пример сеанса поиска, начиная с адресаwww .McGraw-Hill. com.Следует иметь в виду, что конкретный результат поиска зависит от состояния содержимого на момент поиска.
Переход по ссылкеhttp://mcgraw-hill.com
Найдена ссылка:http://sti.mcgraw-hill.com:9000/cgi-bin/query?mss=search&pg=aqПерейти по ссылке, Искать дальше, Выйти? И Поиск следующей ссылки.
Найдена ссылка: http: //investor .mcgraw-hill. com/phoenix. zhtml?c=96562&p=irol-irhome Перейти по ссылке,'Искать дальше, Выйти? П
Переход по ссылкеhttp://investor.mcgraw-hill.com/phoenix. zhtml?c=96562&p=irol-irhome
Найдена ссылка:http://www.mcgraw-hill.com/index.html
Перейти по ссылке, Искать дальше, Выйти? П
Переход по ссылкеhttp://www.mcgraw-hill.com/index.html
Найдена ссылка:http://sti.mcgraw-hill.com:9000/cgi-bin/query?mss=search&pg=aqПерейти по ссылке, Искать дальше, Выйти? В Завершение программы MiniCrawler.
Рассмотрим подробнее работу программы MiniCrawler. Она начинается с ввода пользователем конкретного URI в командной строке. В методеMain() этот URI сохраняется в строковой переменнойuristr.Затем по указанному URI формируется запрос, и переменнойuristrприсваивается пустое значение, указывающее на то, что данный URI уже использован. Далее отправляется запрос и получается ответ. После этого содержимое читается из потока ввода, возвращаемого методомGetResponseStream() и заключаемого в оболочку классаStreamReader.Для этой цели вызывается методReadToEnd (), возвращающий все содержимое в виде строки из потока ввода.
Далее программа осуществляет поиск ссылки в полученном содержимом. Для этого вызывается статический методFindLink (), определяемый в программе MiniCrawler. Этот метод вызывается со строкой содержимого и исходным положением, с которого начинается поиск в полученном содержимом. Эти значения передаются методуFindLink() в виде параметровhtmlstrиstartlocсоответственно. Обратите внимание на то, что параметрstartlocотносится к типуref.Сначала в методеFindLink() создается копия строки содержимого в нижнем регистре, а затем осуществляется поиск подстрокиhref="http,обозначающей ссылку. Если эта подстрока найдена, то URI копируется в строковую переменнуюuri,а значение параметраstartlocобновляется и становится равным концу ссылки. Но поскольку параметрstartlocотносится к типуref,то это приводит к обновлению соответствующего аргумента методаMain(), активизируя поиск с того места, где он был прерван. В конечном итоге возвращается значение переменнойuri.Эта переменная инициализирована пустым значением, и поэтому если ссылка не найдена, то возвращается пустая ссылка, обозначающая неудачный исход поиска.
Если ссылка, возвращаемая методомFindLink(), не является пустой, то она отображается в методеMain (), и далее программа запрашивает у пользователя очередные действия. Пользователю предоставляются одна из трех следующих возможностей: перейти по найденной ссылке, нажав клавишу <П>, искать следующую ссылку в имеющемся содержимом, нажав клавишу <И>, или же выйти из программы, нажав клавишу <В>. Если пользователь нажмет клавишу <П>, то программа осуществит переход по найденной ссылке и получит новое содержимое по этой ссылке. После этого поиск очередной ссылки будет начат уже в новом содержимом. Этот процесс продолжается до тех пор, пока не будут исчерпаны все возможные ссылки.
В качестве упражнения вы сами можете усовершенствовать программу MiniCrawler, дополнив ее, например, возможностью перехода по относительным ссылкам. Сделать это не так уж и трудно. Кроме того, вы можете полностью автоматизировать поисковый робот, чтобы он сам переходил по найденной ссылке без вмешательства со стороны пользователя, начиная со ссылки, обнаруженной на самой первой странице полученного содержимого, и продолжая переход по ссылкам на новых страницах. Как только будет достигнут тупик, поисковый робот должен вернуться на один уровень назад, найти следующую ссылку и продолжить переход по ссылке. Для организации именно такого алгоритма работы программы вам потребуется стек, в котором должны храниться идентификаторы URI и текущее состояние поиска в строке URL С этой целью можно, в частности, воспользоваться коллекцией классаStack.В качестве более сложной, но интересной задачи попробуйте организовать вывод ссылок в виде дерева.
Применение класса WebClient
В заключение этой главы уместно рассмотреть классWebClient.Как упоминалось в самом ее начале, классWebClientрекомендуется использовать вместо классовWebRequestиWebResponseв том случае, если в приложении требуется лишь выгружать или загружать данные из Интернета. Преимущество классаWebClientзаключается в том, что он автоматически выполняет многие операции, освобождая от их программирования вручную.
В классеWebClientопределяется единственный конструктор.
public WebClient()
Кроме того, в классеWebClientопределяются свойства, сведенные в табл. 26.6, а также целый ряд методов, поддерживающих как синхронную, так и асинхронную передачу данных. Но поскольку рассмотрение асинхронной передачи данных выходит за рамки этой главы, то в табл. 26.7 приведены только те методы, которые поддерживают синхронную передачу данных. Все методы классаWebClientгенерируют исключениеWebException,если во время передачи данных возникает ошибка.
Таблица 26.6. Свойства, определенные в классе WebClient
Свойство
Описание
public string BaseAddress { get; set; }
public RequestCachePolicy CachePolicy { get; set; } public ICredentials Credentials { get; set; }
public Encoding Encoding { get; set; }
Получает или устанавливает базовый адрес требуемого URI. Если это свойство установлено, то адреса, задаваемые в методах класса WebClient, должны определяться относительно этого базового адреса Получает или устанавливает правила, определяющие, когда именно используется кэш1 Получает или устанавливает мандат, т.е. учетные данные пользователя. По умолчанию это свойство имеет пустое значение
Получает или устанавливает схему кодирования символов при передаче строк
Свойство
Описание
public WebHeaderCollection Headers! get; set; } public bool IsBusy( get; }
public IWebProxy Proxy { get; set; }
,public NameValueCollection QueryString { get; set; }
public WebHeaderCollection ResponseHeaders{ get; } public bool
UseDefaultCredentials { get; set; }
Получает или устанавливает коллекцию заголовков запроса
Принимает логическое значение true, если данные по-прежнему передаются по запросу, а иначе — логическое значение false Получает или устанавливает прокси-сервер
Получает или устанавливает строку запроса, состоящую из пар “имя-значение”, которые могут быть присоединены к запросу. Строка запроса отделяется от URI символом ?. Если же таких пар несколько, то каждая из них отделяется символом 0 Получает коллекцию заголовков ответа
Получает или устанавливает значение, которое определяет, используется ли для аутентификации устанавливаемый по умолчанию мандат. Если принимает логическое значение true, то используется мандат, устанавливаемый по умолчанию, т.е. учетные данные пользователя, в противном случае этот мандат не используется
Таблица 26.7. Методы синхронной передачи, определенные в классе WebClient
Метод
Определение
public byte[]
Загружает информацию по адресу UR1, обозначае
DownloadData(stringaddress)
мому параметром address. Возвращает результат в виде массива байтов
public byte[]
Загружает информацию по адресу URI, обозначае
DownloadData(Uriaddress)
мому параметром address. Возвращает результат в виде массива байтов
public void
Загружает информацию по адресу URI, обозначае
DownloadFile(stringuri,
мому параметром fileName. Сохраняет результат
stringfileName)
в файле fileName
public void DownloadFile(Uri
Загружает информацию по адресу URI, обозначае
address,stringfileName)
мому параметром address. Сохраняет результат в файле fileName
public string
Загружает информацию по адресу URI, обозначае
DownloadString(string
мому параметром address. Возвращает результат
address)
в виде символьной строки типа string
public string
Загружает информацию по адресу URI, обозначае
DownloadString(Uriaddress)
мому параметром address. Возвращает результат в виде символьной строки типа string
public Stream
Возвращает поток ввода для чтения информации по
OpenRead(stringaddress)
адресу URI, обозначаемому параметром address. По окончании чтения информации этот поток необходимо закрыть
Метод
Определение
public Stream OpenRead(Uri
Возвращает поток ввода для чтения информации по
address)
адресу URI, обозначаемому параметром address. По окончании чтения информации этот поток необходимо закрыть
public Stream
Возвращает поток вывода для записи информа
OpenWrite(stringaddress)
ции по адресу URI, обозначаемому параметром address. По окончании записи информации этот поток необходимо закрыть
public Stream OpenWrite(Uri
Возвращает поток вывода для записи информа
address)
ции по адресу URI, обозначаемому параметром address. По окончании записи информации этот поток необходимо закрыть
public Stream
Возвращает поток вывода для записи информа
OpenWrite(stringaddress,
ции по адресу URI, обозначаемому параметром
stringmethod)
address. По окончании записи информации этот
поток необходимо закрыть. В строке, передаваемой в качестве параметра method, указывается, как именно следует записывать информацию
public Stream OpenWrite(Uri
Возвращает поток вывода для записи информа
address,stringmethod)
ции по адресу URI, обозначаемому параметром address. По окончании записи информации этот поток необходимо закрыть. В строке, передаваемой в качестве параметра method, указывается, как именно следует записывать информацию
public byte[]
Записывает информацию из массива data по
UploadData(stringaddress,
адресу URI, обозначаемому параметром address.
byte[]data)
В итоге возвращается ответ
public byte[] UploadData(Uri
Записывает информацию из массива data по
address,byte[]data)
адресу URI, 'обозначаемому параметром address. В итоге возвращается ответ
public byte[]
Записывает информацию из массива data по
UploadData(stringaddress,
адресу URI, обозначаемому параметром address.
stringmethod,byte[]data)
В итоге возвращается ответ. В строке, передаваемой в качестве параметра method, указывается, как именно следует записывать информацию
public byte[] UploadData(Uri
Записывает информацию из массива data по
address,stringmethod,
адресу URI, обозначаемому параметром address.
byte[]data)
В итоге возвращается ответ. В строке, передаваемой в качестве параметра method, указывается, как именно следует записывать информацию
public byte[]
Записывает информацию в файл fileName по
UploadFile(stringaddress,
адресу URI, обозначаемому параметром address.
stringfileName)
В итоге возвращается ответ
public byte[] UploadFile(Uri
Записывает информацию в файл fileName по
address,stringfileName)
адресу URI, обозначаемому параметром address. В итоге возвращается ответ
Окончание табл. 26.7
Метод
Определение
public byte[]
Записывает информацию в файл fileName по
UploadFile (-stringaddress,
адресу UR1, обозначаемому параметром address.
stringmethod,string
В итоге возвращается ответ. В строке, передавае
fileName)
мой в качестве параметра method, указывается, как именно следует записывать информацию
public byte[] UploadFile(Uri
Записывает информацию в файл fileName по
address,stringmethod,
адресу URI, обозначаемому параметром address.
stringfileName)
В итоге возвращается ответ. В строке, передаваемой в качестве параметра method, указывается, как именно следует записывать информацию
public string
Записывает строку data по адресу URI, обозначае
UploadString(stringaddress,
мому параметром address. В итоге возвращается
stringdata)
ответ
public string
Записывает строку data по адресу URI, обозначае
UploadString(Uriaddress,
мому параметром address. В итоге возвращается
stringdata)
ответ
public string
Записывает строку data по адресу URI, обозначае
UploadString(stringaddress,
мому параметром address. В итоге возвращается
stringmethod,stringdata)
ответ. В строке, передаваемой в качестве параметра method, указывается, как именно следует записывать информацию
public string
Записывает строку data по адресу URI, обозначае
UploadString(Uriaddress,
мому параметром address. В итоге возвращается
stringmethod,stringdata)
ответ. В строке, передаваемой в качестве параметра method, указывается, как именно следует записывать информацию
public byte[]
Записывает значения из коллекции data по адресу
UploadValues(stringaddress,
URI, обозначаемому параметром address. В итоге
NameValueCollectiondata)
возвращается ответ
public byte[]
Записывает значения из коллекции data по адресу
UploadValues(Uriaddress,
URI, обозначаемому параметром address. В итоге
NameValueCollectiondata)
возвращается ответ
public byte[]
Записывает значения из коллекции data по адресу
UploadValues(string
URI, обозначаемому параметром address. В итоге
address,stringmethod,
возвращается ответ. В строке, передаваемой в ка
NameValueCollectiondata)
честве параметра method, указывается, как именно следует записывать информацию
public byte[]
Записывает значения из коллекции data по адресу
UploadValues(Uri
URI, обозначаемому параметром address. В итоге
address,stringmethod,
возвращается ответ. В строке, передаваемой в ка
NameValueCollectiondata)
честве параметра method, указывается, как именно следует записывать информацию
В приведенном ниже примере программы демонстрируется применение классаWebClientдля загрузки данных в файл по указанному сетевому адресу.
// Использовать класс WebClient для загрузки данных // в файл по указанному сетевому адресу.
using System; using System.Net; using System.10;
class WebClientDemo { static void Main() {
WebClient user = new WebClient(); string uri = "http://www.McGraw-Hill.com"; string fname = "data.txt";
try {
Console.WriteLine("Загрузка данных по адресу " + uri + " в файл " + fname); user.DownloadFile(uri, fname);
} catch (WebException exc) {
Console.WriteLine(exc);
}
Console.WriteLine("Загрузка завершена.");
}
}
Эта программа загружает информацию по адресуwww .McGrawHill. comи помещает ее в файлdata. txt.Обратите внимание на строки кода этой программы, в которых осуществляется загрузка информации. Изменив символьную строкуuri,можно загрузить информацию по любому адресу URI, включая и конкретные файлы, доступные по указываемому URL
Несмотря на то что классыWebRequestиWebResponseпредоставляют больше возможностей для управления и доступа к более обширной информации, для многих приложений оказывается достаточно и средств классаWebClient.Этим классом особенно удобно пользоваться в тех случаях, когда требуется только загрузка информации из веб-ресурса. Так, с помощью средств классаWebClientможно получить из Интернета обновленную документацию на приложение.
ПРИЛОЖЕНИЕ Краткий справочник по составлению документирующих комментариев
В языке C# предусмотрено три вида комментариев. К двум первым относятся комментарии // и /* */, а третий основан на дескрипторах языка XML и называетсядокументирующим комментарием.(Иногда его еще называют XML-комментарием.) Однострочный документирующий комментарий начинается с символовIII,а многострочный начинается с символов / * * и оканчивается символами */. Строки после символов /** могут начинаться с одного символа *, хотя это и не обязательно. Если все последующие строки многострочного комментария начинаются с символа *, то этот символ игнорируется.
Документирующие комментарии вводятся перед объявлением таких элементов языка С#, как классы, пространства имен, методы, свойства и события. С помощью документирующих комментариев можно вводить в исходный текст программы сведения о самой программе. При компиляции программы документирующие комментарии к ней могут быть помещены в отдельный XML-файл. Кроме того, документирующие комментарии можно использовать в средстве IntelliSense интегрированной среды разработки Visual Studio.
Дескрипторы XML-комментариев
В С# поддерживаются дескрипторы документации в формате XML, сведенные в табл. 1. Большинство дескрипторов XML-комментариев не требует особых пояснений
и действуют подобно всем остальным дескрипторам XML, знакомым многим программистам. Тем не менее дескриптор <list> — сложнее других. Он состоит из двух частей: заголовка и элементов списка. Ниже приведена общая форма дескриптора
<list>:
<listheader>
<term> имя </term>
.<description> текст </description>
</listheader>
где текст описывает имя. Для описания таблиц текст не используется. Ниже приведена общая форма элемента списка:
<item>
<term> имя_элемента </term>
<description> текст </description>
</item>
где текст описывает имя_элемента. Для описания маркированных и нумерованных списков, а также таблиц имя элемента не используется. Допускается применение нескольких элементов списка <item>.
Таблица 1. Дескрипторы XML-комментариев
Дескриптор
Описание
<с> код </с>
Определяет текст, на который указывает код, как программный код
<code> код </code>
Определяет несколько строк текста, на который указывает код, как программный код
<example> пояснение </example>
Определяет текст, на который указывает пояснение, как описание примера кода
<exception cref = "имя">
Описывает исключительную ситуацию, на ко
пояснение </exception>
торую указывает имя
<include file = 1fname1 path =
Определяет файл, содержащий XML-kom-
'path[0tagName = "tagID 11 ] ' />
ментарии для текущего исходного файла. При
этом fname обозначает имя файла; path — путь к файлу; tagName — имя дескриптора; tagID — идентификатор дескриптора
<list type = "тип""> заголовок
Определяет список. При.этом тип обозначает
списка элементы списка </list>
тип списка, который может быть маркированным, нумерованным или таблицей
<рага> текст </para>
Определяет абзац текста в другом дескрипторе
<param name = 'имя параметра'>
Документирует параметр, на который указы
пояснение </param>
вает имя параметра. Текст, обозначаемый как пояснение, описывает параметр
<paramref name = "имя параметра" />
Обозначает имя параметра как имя конкретного параметра
<permission cref = "идентификатор">
Описывает параметр разрешения, связанный с
пояснение </permission>
членами класса, на которые указывает идентификатор. Текст, обозначаемый как пояснение, описывает параметры разрешения
Дескриптор
Описание
<remarks> пояснение </remarks>
Текст, обозначаемый как пояснение, представляет собой общие комментарии, которые часто используются для описания класса или структуры
<returns> пояснение </returns>
Текст, обозначаемый как пояснение, описывает значение, возвращаемое методом
<see cref = "идентификатор" />
Объявляет ссылку на другой элемент, обозначаемый как идентификатор
<seealso cref = "идентификатор" />
Объявляет ссылку типа “см. также" на идентификатор
<sumnjary> пояснение </summary>
Текст, обозначаемый как пояснение, представляет собой общие комментарии, которые часто используются для описания метода или другого члена класса
<typeparam name = "имя параме
Документирует параметр типа, на который
тра1^ пояснение </typeparam>
указывает имя параметра. Текст, обозначаемый как пояснение, описывает параметр типа
ctypeparamref name = "имя пара
Обозначает имя параметра как имя пара
метра" />
метра типа
Компилирование документирующих комментариев
Дляполучения XML-файла, содержащего документирующие комментарии, достаточно указать параметр /doc в командной строке компилятора. Например, для компилирования файла DocTest. cs, содержащего XML-комментарии, в командной строке необходимо ввести следующее.
csc DocTest.cs /doc:DocTest.xml
Длявывода результата в XML-файл из интегрированной среды разработки Visual Studio необходимо активизировать окно Свойства (Properties) для текущего проекта. Затем следует выбрать свойство Построение (Build), установить флажок XML-файл документации (XML Documentation File) и указать имя выходного XML-файла.
Пример составления документации в формате XML
В приведенном ниже примере демонстрируется применение нескольких документирующих комментариев: как однострочных, так и многострочных. Любопытно, что многие программисты пользуются последовательным рядом однострочных документирующих комментариев вместо многострочных, даже если комментарий занимает насколько строк. Такой подход применяется и в ряде комментариев из данного примера. Его преимущество заключается в том, что он позволяет ясно обозначить каждую строку как часть длинного документирующего комментария. Но это все же, скорее, дело стиля, чем общепринятая практика составления документирующих комментариев.
// Пример составления документирующих комментариев, using System;
/** <remark>
Это пример многострочного документирования в формате XML.
В классе Test демонстрируется ряд дескрипторов.
</remark>
*/
class Test {
III<summary>
IIIВыполнение программы начинается с метода Main().
Ill</summary> static void Main() { int sum;
sum = Summation(5) ;
Console.WriteLine("Сумма последовательных чисел " +
5 + " равна " + sum);
}
III<summary>
IIIМетод Summation() возвращает сумму его аргументов.
Ill<param name = "val">
IIIСуммируемое значение передается в качестве параметра val.
Ill</param>
III<see cref="int"> </see>
III<returns>
IIIСумма возвращается в виде значения типа int.
Ill</returns>
III</summary>
static int Summation(int val) { int result = 0;
for(int i=l; i <= val; i++) result += i;
return result;
}
}
Если текст приведенной выше программы содержится в файле XmlTest. cs,то по следующей команде будет скомпилирована программа и получен файл XmlTest.xml, содержащий комментарии к ней.
csc XmlTest.cs /doc:XmlTest.xml
После компилирования получается XML-файл, содержимое которого приведено ниже.
<?xml version="l.0"?>
<doc>
<assembly>
<name>DocTest</name>
</assembly>
<members>
cmember name=ffT:Testff>
<remark>
Это пример многострочного документирования в формате XML. В классе Test демонстрируется ряд дескрипторов.
</remark>
</member>
<member name=lfM: Test .Main11 >
<summary>
Выполнение программы начинается с метода Main(). </summary>
</member>
<member name="M:Test.Summation(System.Int32)">
<summary>
Метод Summation() возвращает сумму его аргументов.
<param name="val">
Суммируемое значение передается в качестве параметра val. </param>
<see cref=”T:System.Int32"> </see>
<returns>
Сумма возвращается в виде значения типа int.
</returns>
</summary>
</member>
</members>
</doc>
Следует заметить, что каждому документируемому элементу присваивается уникальный идентификатор. Такие идентификаторы применяются в других программах, которые документируются в формате XML.
Предметный указатель
А
Аксессоры вызов304
модификаторы доступа ограничения323применение320назначение304разновидности304событий500Анонимные функции назначение483преимущество483разновидности483Аргументы именованные назначение 252 применение 252 командной строки 255 метода162назначение 52 необязательные назначение247и неоднозначность250и перегрузка методов249порядок объявления249способы передачи методу220типа579Атрибуты AttributeUsage570Conditional571
MethodlmplAttribute, применение860
Obsolete 572
встроенные570
извлечение 564
именованные параметры 566
назначение562
позиционные параметры 566
присоединение564
создание 563
указание563
Б
Байт-код34Библиотека TPL возврат значения из задачи899задачи, создание и исполнение887идентификаторы задач, назначение и применение890классы
Parallel, назначение и применение906TaskFactory, назначение и применение895Task, назначение и применение887лямбда-выражения, в качестве задачи, применение896методы
Dispose(), назначение и применение895ForEach(), назначение и применение915ForQ, назначение и применение909Invoke(), назначение и применение906ожидания, назначение и применение892назначение886особенности885отмена задачи901признак отмены901продолжение задачи, создание897Библиотеки классов С#. 727
для среды .NET Framework 66 организация 727 пространство имен System члены 729
структуры встроенных типов данных 727 Буферы фиксированного размера назначение693создание694
В
Ввод-вывод в файл байтовый441символьный449последовательный 462 с произвольным доступом 462 данных в массив 463 двоичных данных 436,454 консольный 436 основанный на потоках 432 отдельными байтами 432 отдельными символами 432 переадресация 453 с запоминанием 465 Виртуальная машина Java 34 Возможность взаимодействия;
межъязыковая 35 Выводимость типов609Вызов
перегружаемого конструктора 245
по значению 220 по ссылке220
Г
Групповая адресация, определение478
А
Делегаты Action формы769применение769вызов
методов экземпляра477любых методов474главное преимущество474групповая адресация478ковариантность481контравариантность481назначение483обобщенные EventHandler<TEventArgs>, применение508вариантные633объявление610общая форма объявления474определение473применение474
типа EventHandler, применение508Деструкторы, назначение и применение172Десятичная система счисления80Динамическая диспетчеризация методов, принцип356идентификация типов назначение 537 причины полезности 537 Директивы #define529#else и #elif531#error 533 #if и #endif529#line534#pragma534#region и #endregion534#undef 533 #warning534using518
препроцессора528Доступ к Интернету сооЫе-наборы1027заголовки протокола HTTP1026обработка исключений1022сетевых ошибок1021
организация1018передача данных асинхронная1015синхронная1015получение дополнительной информации1025по принципу запроса и ответа1014пространство имен System.Net, члены1012протоколы определение1013подключаемые1014разработка поискового робота1030сетевой ресурс, последнее обновление1029универсальный идентификатор ресурса, определение1013
И
Идентификаторы URI1013
директив препроцессора529назначение 65 применение 65 Иерархии классов многоуровневые347обобщенных620
порядок вызова конструкторов350простые346
ссылки на объекты разных классов351Импликация103Индексаторы аксессоры get и set304без базового массива310интерфейсные385многомерные311назначение303 .ограничения на применение311одномерные304перегружаемые307преимущество304Индекс массива, назначение178Инициализаторы коллекций 2009 массивов180объектов 246,319проекции 666 Инкапсуляция как механизм программирования42классы и объекты43открытые и закрытые данные и код42Интегрированная среда разработки Visual Studio44, 46Интернет, определение34
Интерфейсы ICloneable, реализация779IComparable и IComparable<T>, реализация 627,778, 990-993Icomparer и IComparer<T>, реализация994-996Iconvertible, реализация779IEnumerable, реализация 2002 IEnumerator, реализация1001IEquatable<T>, реализация 626,778IFormatProvider, реализация781IFormattable, реализация781IObservable<T> и IObserver<T>, реализация и применение781индексаторы реализация385общая форма объявления385коллекций924наследование387обобщенные контравариантность, применение630объявление 622
ковариантность, применение 626 применение 622 определение и реализация 375 порядок и форма реализации 377 правило выбора391свойства реализация383общая форма объявления383стандартные для среды .NET Framework391форма объявления 376 явная реализация388Исключения базового класса, перехват426блоки try/catch, применение404блок finally, применение 426 вложение блоков try413внутренние420генерирование вручную414и перехват405повторное415классы404
обработчики403 ,
оператор throw, применение414последствия неперехвата408при вводе-выводе433, 442 *
производных классов, перехват426разнотипные, обработка411сетевые, при доступе к Интернету 2022 специальные, создание и применение422стандартные403, 420удаление после обработки411универсальный перехват и обработка412Исключительные ситуации обработка назначение403главное преимущество403для устранения программных ошибок410ключевые слова try и catch404организация обработки404подсистема обработки в C#404появление403Итераторы именованные применение 2007 создание 2006 назначение925несколько операторов yield, применение 2006 обобщенные, создание1008определение 2003 прерывание 2005 применение 2003
к
Классы Array назначение 750 методы 750 свойства 750 Assembly, члены 555 Attribute, назначение 563 BinaryReader методы456конструктор455
• BinaryWriter методы455конструктор454BitConverter назначение 772 методы 772 Console методы437
переадресация потоков, методы453Constructorlnfo, члены 552 Cookie, свойства1028CookieCollection, члены1028CookieContainer, члены1028Exception методы418конструкторы420свойства418File
назначение 467 методы 467
FileStream методы 444,446 конструкторы 441 средства копирования файлов 448 GC
назначение 774 методы 774 свойство 776 HttpWebRequest, назначение 1018 HttpWebResponse назначение 1018 свойства 1025 Interlocked назначение 873 методы 873 Math назначение 721 методы 721 поля 721 Memberlnfo методы 542 свойства 542 MemoryStream конструктор 463 применение 463 Methodlnfo, члены 544 Monitor назначение 855
методы управления синхронизацией 855 Mutex методы 863 конструкторы 863 object назначение 368
как универсальный тип данных 372 методы 368, 776 конструктор 777 Parameterlnfo, члены 544 Process назначение 883 методы 883 Random ,
назначение 773 методы 773 конструкторы 773 Semaphore методы 868 конструкторы 867 Stream назначение 433 методы 433 свойства 433 StreamReader конструкторы 451, 452
свойство EndOfStream 452 применение 451 StreamWriter конструкторы 449, 450 применение 449 String назначение 784 реализация интерфейсов 784 методы расширения 812 форматирования строк 816 конструкторы 784 перегружаемые операторы 786 поле, индексатор и свойство 785 StringComparer свойства 766 применение 997 StringReader конструктор 465 применение 465 StringWriter конструктор 465 применение 465 System.Delegate, члены 483 Thread назначение 835
методы управления потоками 835 конструкторы 836 свойства IsBackground 846 Priority 847 Tuple, назначение 777 Type назначение 542 методы 542 свойства 543 Uri
назначение 1024 конструкторы 1024 свойства 1024 WebClient назначение 1034 методы 1034 конструктор 1034 свойства 1034 WebRequest назначение 1015 методы 1015 свойства 1015 WebResponse назначение 1017 методы 1017 свойства 1017
абстрактные реализация364объявление364правило выбора391атрибутов члены563объявление563базовые329
инициализация объектов246исключений404как ссылочные типы154коллекций назначение925необобщенных931обобщенных960параллельных983конструкторы167назначение147наследование331обобщенные частичные701базовые620иерархии620
общая форма объявления585определение578переопределение виртуальных методов623с несколькими параметрами типа583применение579
получение экземпляров объектов 636 производные 622 оболочки символьных потоков TextReader, методы ввода434TextWriter, методы вывода435общая форма определения148объекты как экземпляры класса147оператор-точка150определение43порядок определения149потоков назначение432байтовых434двоичных436символьных436специальные434производные329
синхронизации, старые и новые874статические ,266 суперклассы и подклассы335функции-члены148члены данных148
доступ при наследовании333
методы и другие функции-члены43защищенные336закрытый и открытый доступ212открытые и закрытые209управление доступом209статические264
поля и переменные экземпляра43Ключевые слова base339,343,344checked и unchecked общие формы428применение428const и volatile710delegate474,484enum397event494extern712fixed693interface376lock708,850new343, 415partial700readonly709sealed367static260this174unsafe684using711virtual356
для обработки исключений404зарезервированные64контекстные64,1004Ключи, назначение929Ковариантность481, 626Кодовые блоки назначение 62 применение 62 создание61Коллекции
главное преимущество924назначение923необобщенные назначение925классы931-949интерфейсы926-930структура DictionaryEntry931обобщенные классы960-982интерфейсы954-959объявление954
структура KeyValuePaiKTKey TValue>960принцип действия954параллельные назначение983классы983методы984применение984специальные назначение953классы953с поразрядной организацией хранение отдельных битов950класс BitArray950сравнение строк, порядок997типы924хранение объектов встроенных типов988определяемых пользователем классов988Комментарии документирующие дескрипторы XML1039многострочные1039однострочные1039определение1039компилирование1041пример составления1041составление1039многострочные51назначение51однострочные 52 Компилирование и выполнение программ в среде Visual Studio46из командной строки45Компиляция многовариантная 532 условная529Конструкторы базового класса, вызов339вызываемые по умолчанию167и наследование 337 назначение167
общая форма определения167параметризированные168перегружаемые241статические265Контравариантность481, 626Копии объектов, разновидности779Критический раздел кода709
Л
Литералы буквальные, строковые82десятичные80определение79символьные79Сплавающей точкой79
строковые81типы, указание80целочисленные79шестнадцатеричные80Лямбда-выражения блочные492
как обработчики событий505лямбда-оператор =>488назначение488одиночные489разновидности489этапы применения489явное указание параметров491
м
Массивы главное преимущество 277 границы, соблюдение181двумерные182динамические назначение932в качестве коллекции932обобщенные961получение обычного массива938сортировка и поиск 937 доступ по индексу178инициализация180копирование767массивов185многомерные инициализация184объявление183определение182неявно типизированные 292 обращение содержимого766одномерные178определение 277 порядок применения178присваивание ссылок187прямоугольные185реализация в виде объектов 277 свойство Length, применение189сортировка 763 строк203ступенчатые185указателей 692 Методы Main() возврат значений254вызов 52
передача аргументов 255 абстрактные назначение364
реализация 364 общая форма364анонимные назначение484как обработчики событий505внешние переменные, применение487
• возврат значения485передача аргументов484виртуальные объявление356
предотвращение переопределения368переопределение 355 применение360внешние, применение712возврат массивов234значений159объектов231условия158групповое преобразование476запроса назначение669реализация669назначение43необязательные параметры и аргументы248обобщенные наложение ограничений 620 объявление609порядок вызова609создание607обращения со строками199общая форма определения 255 операторные назначение 270 формы270определение 255 параметризированные164параметры и аргументы 255, 262 перегружаемые 235 передача аргументов, способы 220 значений по ссылке 222 объектов по ссылке218переопределение 356,359 расширения назначение678объявление678рекурсивные 257 синтаксического анализа 472 сокрытие 345
с переменным числом аргументов 229 статические ограничения 262
применение261условные571частичные реализация701объявление 702 ограничения703Многозадачность
запуск отдельной задачи882разновидности834управление отдельным процессом883Многопоточная обработка блокировка850взаимоблокировка860главное преимущество834момент окончания потока, определение841новые средства .NET882определение состояния потока880основной поток назначение835применение880отмена прерывания потока878передача аргумента потоку844потоки определение834приоритеты847приоритетные и фоновые835состояния835прерывание потока875приостановка и возобновление потока880процессы, определение834рекомендации882синхронизация835, 849создание нескольких потоков839сообщение между потоками856состояние гонки860способы усовершенствования838Многоязыковое программирование34Множество в качестве коллекции980объектов980операции980, 982отсортированное982Модификаторы abstract364const710fixed685override356partial 700 volatile710доступа155,210internal536private155, 210
protected 336 protected internal 536 public 249,167,210 параметров out 225,227-params 229 ref 223,227 Мьютексы именованные 867 назначение 863 получение и освобождение 863 применение 863
н
Наследование главное преимущество 332 интерфейсов 387 как один из основных принципов ООП 329 классов 329
повторное использование кода 349 поддержка в C# 329 предотвращение 367 принцип иерархической классификации 44 сокрытие методов 345 имен 344 Небезопасный код выполнение 681 определение 681 Недоступный код, исключение 166 Непрямая адресация многоуровневая 691 одноуровневая 682 Неуправляемый код 39, 681
О
Области действия вложенные 87 определяемые классом 86 методом 86 соблюдение правил 88 Обнуляемые объекты в выражениях отношения 699 объявление 696 применение в выражениях 697 проверка на пустое значение 696 Обобщения аргументы типа 579 главное преимущество 583 контроль типов 579
обеспечение типовой безопасности 580 определение 576 основная польза 583 особая роль 575 параметры типа назначение и указание 578 сравнение экземпляров 615 присущие ограничения 636 Общая система типов CTS 39 Общеязыковая спецификация CLS 39 Объектно-ориентированное программирование инкапсуляция 42 метод 33 наследование 44 основные принципы 41 особенности 42 полиморфизм 43 Объекты, определение 42 Ограничения на базовый класс назначение 585 наложение, общая форма 586 применение 586 последствия 588 на интерфейс назначение 585 наложение, общая форма 594 применение 594 на конструктор new(), наложение 598 назначение 586 порядок наложения списком 603 ссылочного типа назначение 586 наложение 599 типа, неприкрытые назначение 585 наложение 602 типа значения назначение 586 наложение 599 Операторы as 539
break, применение 239 continue, применение 142 default 604 goto метки 243 применение 243 is 538
new 253,170 return 243, 158
sizeof692stackalloc692switch вложенные129обычные125
правило недопущения "провалов"128typeof540using711yield return1004арифметические 56,97выбора121вычисления остатка98декремента61, 98инкремента61, 98итерационные121логические обычные101укороченные104нулеобъединяющие698отношения59,101перегружаемые269перехода121поразрядные обычные 207
составные, присваивания 227 предшествование119преобразования назначение293явного, применение295неявного, применение 295 ограничения296формы293присваивания 55 обычные 206 укороченные107составные 207 сдвига114цикла do-while138for60,129foreach139,194while 237 Очередь в качестве коллекции948коэффициент роста948применение948принцип действия947
п
Параллелизм данных886задач886Перегрузка
индексаторов 307
конструкторов преимущества242причины242методов назначение 235 главное преимущество240операторных 277 с несколькими параметрами типа по принципу полиморфизма240операторов унарных 273 бинарных 270 главное преимущество 269 true и false283 \
ограничения 297 логических286укороченных, логических288отношения281определение 269 основной принцип 297 Переменные внешние486
динамическая инициализация84захваченные486инициализация83локальные83неявно типизированные85область действия86обнуляемые объявление 696 присваивание значений 696 проверка на пустое значение696объявление типа 55 определение54ссылочного типа назначение 253 интерфейсного381объявление 253 присваивание154статические 260 форма и порядок объявления83экземпляра, объявление149Переполнение, появление428Перечисления базовый тип 399 доступ к членам 397 инициализация 399 объявление 397 определение 397 применение 399 Перечислители доступ к коллекции998назначение924-925
обычные, применение 999 определение998применение в цикле foreach 925 типа IDictionaryEnumerator, применение 2000 установка в исходное положение998Полиморфизм динамический 356 основной принцип 43, 359 Последовательности случайных чисел, генерирование 773 Потоки байтовые 432 встроенные 432 запоминающие 465 исполнения 709,834определение 432 переадресация 442, 452 символьные 432 стандартные ввода 432 вывода 432
сообщений об ошибках 432 Предикаты назначение768применение768Преобразование типов в выражениях 93 неявное, условия 90,297 перечислимых 397 расширяющее 90 сужающее 92 явное 92 Препроцессор, назначение 529 Приведение типов в выражениях 95
как явное преобразование типов89назначение 92 Продвижение типов неожиданные результаты 94 правила 93 целочисленное 94 Проецирование 650 Пространства имен System54,719System.Collections 924 System.IO 432 System.Net 2022 System.Reflection 542 System.Threading835 .
System.Web 2022 аддитивный характер 522 вложенные 523 глобальные 524
назначение 52, 524 объявление 524 описатель псевдонима 525 определение 523
предотвращение конфликтов имен516псевдонимы 520 Пустая ссылка, определение 422
Р
Распаковка 370 Рекурсия главное преимущество 260 определение 257 принцип действия258Рефлексия вызов методов548
излечение типов данных из сборок 555 назначение 542
обнаружение типов, полностью автоматизированное 560 получение списка методов 544 конструкторов 552 применение 543 принцип действия 542
С
Сборки
автоматическое получение 535 декларация 535 дружественные708метаданные типов 535 назначение 535 программные ресурсы 535 программный код в формате MSIL 535 составные разделы 535 Свойства
автоматически реализуемые общая форма318
ограничение доступа к аксессорам 322 применение 329 поддерживающее поле 329 аксессоры get и set 323 главное преимущество 323 индексированные707инициализаторы объектов, применение 329 интерфейсные383назначение 323 общая форма 323 ограничения 320 Связный список в качестве коллекции 965 двунаправленный965
реализация 966 узлы 966 Семафоры именованные 870 назначение 867 применение 868 разрешение на доступ 867 создание 867 счетчики разрешений 867 Сигнатуры, назначение 242 Символы в коде ASCII 74 в уникоде 74, 742 форматы UTF-16 и UTF-32 742 кодовая точка 742 старший и младший суррогаты 742 заполнители специального формата 820 Синтаксические ошибки, обработка 53 Система
"сборки мусора" назначение 171
номера поколений оперативной памяти 776 применение 171 ввода-вывода 432 Скобки и пробелы, назначение 119 Словарь в качестве коллекции 969 динамический характер 969 создание 970 События аксессоры 500 групповая адресация 496 для синхронизации потоков, применение 870 методы экземпляра как обработчики 497 обработчики 494 объявление 494 порядок обработки 495 практический пример обработки 509 принцип действия 494 разнообразные возможности 504 рекомендации по обработке в .NET 506 статические методы как обработчики 499 управление списками вызовов обработчиков 500 устанавливаемые автоматически 870 вручную 870 хранение обработчиков 500 Совместимость типов, принцип 352 Сокрытие имен 89, 343, 388 Спецификаторы
доступа148,210формата назначение813даты и времени824числовых данных814номера аргументов815перечислений830применение813промежутков времени829Среда .NET Framework библиотека классов38назначение37
общеязыковая среда выполнения CLR 37 Среда CLR JIT-компилятор38метаданные38назначение38принцип действия38псевдокод MSIL38Стек
в качестве коллекции945классический пример ООП212основные операции212применение945принцип действия212, 945Стиль оформления исходного кода64Строки в операторе switch206вставка, удаление и замена810заполнение и обрезка808индексирование201обращение199операции201определение783поиск, методы796получение подстрок811постоянство205, 784построение198преобразование в лексемы806разделение и соединение804реализация в виде объектов198смена регистра, методы811сравнение методы787основные способы786с учетом и без учета регистра199с учетом культурной среды199порядковое199сцепление203, 793числовые, преобразование469Структурное программирование 32 Структуры Boolean, члены748
Char, члены 742 Decimal, члены 735 DictionaryEntry, члены931KeyValuePair<TKey, TValue>, члены960встроенных типов данных, в .NET 727 инициализация 393 назначение391,395обобщенные наложение ограничений606создание606объявление391применение 392,395 присваивание 393 псевдонимы469
типов данных с плавающей точкой, члены730целочисленных типов данных, члены728числовых типов данных470
т
Типы данных анонимные 663 десятичные 73 динамические703закрыто сконструированные 579 закрытые580значений68логические 75 наложение ограничений585обнуляемые601, 695, 697обобщенные580ограниченные585особенное значение67открыто сконструированные580параметризированные 576 перечислимые 397 полубайты, пример реализации298простые68символьные74сконструированные580соотносимые682с плавающей точкой 57,71ссылочные68,154строковые198, 783целочисленные55, 69частичные700
Точка с запятой, назначение 63
У
Указатели
арифметические операции686доступ к членам структуры686и массивы688
индексирование689и строки690на указатели691объявление682оператор-стрелка686операторы * и & 683определение682сравнение688файлов461Упаковка370Управляемый код38, 682Управляющие операторы, категории121Управляющие последовательности символов81Условные операторы ?117else121if58,121
вложенные, if122
многоступенчатая конструкция if-else-if124
Ф
Фабрики классов, назначение 233 Флаг знака 69 Форматирование ввода-вывода 76 даты и времени824команды 77,813образцы формата78перечислений830поставщики формата812промежутков времени829спецификаторы формата 77,812строковое представление значения, способы получения816форматирующие строки77, 813Форматы данных, специальные820даты и времени, специальные827изображения820с обратным порядком байтов 772 с прямым порядком байтов 772
X
Хеш-таблицы
в качестве коллекции 939 коэффициент заполнения 939 назначение 939 применение хеш-кода 939 Хэширование механизм 939 преимущество 939
ц
Цепочки вызовов480событий497
Циклы do-while138for129foreach194while137без тела135бесконечные135
ш
Шестнадцатеричная система счисления80
Я
Язык C#
генеалогическое дерево язык С32язык C++33язык Java33история развития36создания 35 как хороший язык программирования 25 нововведения в версии C# 4.0 37 происхождение 25 связь со средой .NET Framework 37 усовершенствование 26
Язык LINQ
сохранение данных во временной переменной659вложенные операторы from 653 группирование результатов запроса 655 групповое объединение, создание 666 деревья выражений676запросы связь между типами данных 642 выполнение638немедленное выполнение 675 неоднократное выполнение641обработка642общая форма 643 формирование638отложенное выполнение 675 определение638интерфейс IEnumerable, формы реализации639
ключевые слова 643 методы расширения 673
• запроса669назначение637
обращение к источнику данных639объединение данных из разных источников660операторы from640group655into 657 join661let659orderby 646 select 643,649where640, 644отбор запрашиваемых данных 644 переменные диапазона640запроса640предикаты640продолжение запроса 657 сортировка результатов запроса 646 средства формирования запросов 637 формирование запросов методы запроса670сравнение способов 673 синтаксис запросов670Язык PLINQ
вопросы эффективности922другие средства922классы ParallelEnumerable918ParallelQuery918методы AsOrdered(), назначение и применение919AsParallelQ, назначение и применение918WithCancellationQ, назначение и применение920назначение885параллельные запросы отмена920формирование918применение917
Спасибо, что скачали книгу в бесплатной электронной библиотеке BooksCafe.Net
Оставить отзыв о книге
Все книги автора