Текст
                    ПРОГРАММИСТ — ПРОГРАММИСТУ
для профессионалов
Том I
А
wrox
I PROGRAMMER TO PROGRAMMER™
Симон Робинсон, Олли Корнес, Джей Глинн, Бартон Харвей,
Крейг Макквин, Джерод Моемека, Кристиан Нагель,
Морган Скиннер, Карли Ватсон


Professional C# Simon Robinson Ollie Cornes Jay Glynn Burton Harvey Craig McQueen Jerod Moemeka Christian Nagel Morgan Skinner Karli Watson Wrox Press Ltd.
С# для профессионалов Том I Симон Робинсон Олли Корнес Джей Глинн Бартон Харвей Крейг Макквин Джерод Моемека Кристиан Нагель Морган Скиннер Карли Ватсон Издательство "ЛОРИ"
Professional C# Simon Robinson, OIlie Comes, Jay Glynn, Burton Harvey, Craig McQueen, Jerod Moemeka, Christian Nagel, Morgan Skinner, Karli Watson © 2001 Wrox Press All rights reserved C# для профессионалов Том I Симон Робинсон, Олли Корнес, Джей Глинн, Бартон Харвей, Крейг Макквин, Джерод Моемека, Кристиан Нагель, Морган Скиннер, Карли Ватсои Переводчики С. Коротыгин, О. Труфанов Научный редактор А. Головко Корректоры Т. Килимник, Л. Осиггова Верстка Л. Федерякиной Published by Wrox Press Ltd, Arden House, 1102 Warwick Road, Acocks Green, Birmingham, B27 6BH, UK ISBN 1861004990 © Издательство «Лори», 2003 Изд. N : OAI @3) ЛР N : 070612 30.09.97 г. ISBN 5-85582-187-0 Подписано в печать 28.01.2003 Формат 84x108/16 Гарнитура Нью-Баскервиль Печать офсетная Печ.л 32 Тираж 3200 Заказ N69 Цена договорная Изд-во "ЛОРИ". Москва, 123100 Шмитовский пр., д. 13/6, стр. 1 (пом. ТАРП ЦАО) Телефон для оптовых покупателей: @95) 256-02-83 Размещение рекламы: @95) 259-01-62 www.lory-press.ru Отпечатано в типографии ООО "Типография ИПО профсоюзов Профиздат" 109044, Москва, ул. Крутицкий вал, д. 18
Благодарности Я хотел бы поблагодарить редакторов Wrox Press, которые сделали мой текст доступным для чтения другим людям. Спасибо сотрудникам Microsoft, в частности, Алексу Холи (Alex Holy), Сабине Флейчман (Sabine Fleischmann) и разработчикам платформы .NET. Наконец, я хочу поблагодарить свою жену Элизабет за ее терпение в течение того времени, пока я круглосуточно трудился над книгой. Морган Скиннер (Morgan Skinner) Я начал заниматься компьютерами в раннем возрасте, начав с ZX80 в школе. Мне не по- понравился код, который продемонстрировал мой учитель на одном из уроков, и я решил, что могу сделать это лучше на ассемблере. После того как меня затянуло в паутину Z80 (он был намного лучше, чем всего три регистра в мире 6502!), я перерос школьный ZX80 и приобрел свой собственный ZX Spectrum. С тех пор я использовал почти все типы языков и платформ, включая VAX Macro Assembler, Pascal, Modula2, Smalltalk, ассемблер х86, PowerBuilder, C/C++, Visual Basic, а теперь и С#. В течение 12-ти лет я тружусь в одной и той же компании, во многом бла- благодаря разнообразию дел и хорошей рабочей обстановке. В свободное время я трачу большие суммы денег на велосипеды и борюсь с сорняками на своем участке. Со мной можно связаться по электронной почте: morgan.skinner@totalise.co.uk Карли Ватсон (Karli Watson) Карли Ватсон является штатным автором Wrox Press, имеющим склонность к пестрой одежде. Он хотел стать всемирно известным специалистом в области нанотехнологий, так что, возможно, однажды вы вспомните его имя, когда он получит Нобелевскую пре- премию. Сейчас, однако, интересы Карли включают в себя все мобильные и вновь появляю- появляющиеся технологии, такие как С#. Нередко он рассказывает об этих технологиях на конференциях так, как будто провел перед этим несколько часов в питейных заведениях. Карли обожает кататься на сноубордах и хотел бы иметь дома кошку. БлагоАорности Выражаю благодарность команде Wrox за то, что они предложили мне заняться этой книгой и помогали в процессе ее написания. Спасибо моей жене Донне за то, что она, несмотря ни на что, продолжает оставаться со мной.
Об авторах Симон Робинсон (Simon Robinson) Симон Робинсон живет в Ланкастере, Великобритания. Серьезно заниматься програм- программированием он начал в процессе подготовки докторской диссертации по физике, моде- моделируя сложные процессы квантовой механики применительно к сверхпроводникам. Полученный им опыт программирования был таковым, чтобы до конца жизни оттолк- оттолкнуть его от компьютеров, и в течение какого-то времени он даже пытался стать физиоте- физиотерапевтом по спортивному массажу. Однако Симон быстро понял, что компьютеры могут принести гораздо больше денег, чем спортивный массаж, и устроился на работу програм- программистом-исследователем на C++. Симон доброжелательный, глубоко духовный человек, понимающий истинный смысл жизни. Будучи программистом, он начал писать, и теперь основным его заработком является издание отличных книг для программистов. Симон большой энтузиаст С#. По его мне- мнению, этот язык призван совершить революцию в программировании. Свободное время Симон проводит либо в танцевальном классе (ему нравится искусство), либо в работе над своим любимым проектом — компьютерной игрой-стратегией. Помимо всего прочего, он является почетньш исследователем Университета Ланкастера, где он занимается исследо- исследованиями в области вычислительной гидродинамики совместно с кафедрой изучения окру- окружающей среды. Web-сайт Симона расположен по адресу: www.SimonRobinson.com. Благодарности Помимо редакторов, с которыми было приятно работать и которые трудились изо всех сил, способствуя выходу этой книги в свет, я хотел бы поблагодарить: 3 Джо Крампа (Joe Crump) из Microsoft за предоставление полезной технической информации и за ответы на мои вопросы. з Моргана Скиннера (Morgan Skinner) за некоторые полезные предложения, каса- касающиеся главы 7. 3 Ясона Сиклера (Jason Sickler), студента Массачусетского Технологического Ин- Института по прозвищу Darrius, за его согласие использовать часть нашей перепис- переписки в одном из примеров главы 14. Олли Корнес (Ollie Comes) Олли имеет дело с Интернетом и платформой Microsoft с начала 90-х гг. В 1999 г. он стал одним из организаторов Интернет-компании, работающей в сфере В2В ("бизнес для биз- бизнеса"), и до недавних пор занимал в этой компании пост технического директора. До этого он занимался программированием, сетевым администрированием, написани- написанием книг, возглавлял проекты по разработке программного обеспечения и оказывал кон- консалтинговые услуги. Он работал с такими компаниями, как Demon Internet, Microsoft, Saab, Tesco, Travelstore и Vodafone. Олли имеет степень в области вычислительной техники и является сертифицированным специалистом Microsoft. В свободное время он читает книги о человеческих возможностях, практикуется в китайских боевых искусствах, медитирует и играет с огнем. благодарности Я х< лоблагодарить моих друзей и семью за их поддержку и любовь. Жизнь поворачива- поворачивается к 1.) хорошей, то плохой стороной, и именно вы помогли мне преодолеть самые тяжелые ■ 1гы моей жизни. Глинн (-су Glynn) Джс i i лчлл разрабать е пъ программное обеспечение в конце 80-х гг., создавая приложе- приложения " ^(.рационной сие темы Pick и используя для этих целей язык программирования Pick BASiC Позже он писал программы на таких языках, как Paradox PAL и Object PAL, Delphi, Pdbcal. C/C++, Java. VBA и Visual Basic. Сейчас Джей является архитектором и ко- координатором проекта в большой страховой компании в Нешвиле, штат Теннеси. В тече- течение последних пяти лет он занимался разработкой программного обеспечения для киоманных компьютеров, ASP и серверных систем. В свободное время Джей восстанав- п: 'aei свой дом во Франклине, штат Теннеси, играет, где только возможно, в гольф и <..., Mir диснеевские мультфильмы со своей женой и трехлетним сыном.
Бартон Харвей (Burton Harvey) Бартон Харвей создает программное обеспечение для пользователей. Имея степень MCSD и обладая более чем пятнадцатилетним опытом применения инструментов разра- разработки Microsoft, Барт является специалистом во многих областях, включая VB, СОМ, ASP, SQL, С#, C++. ассемблер х86, UML, WML и Palm OS. В 1998 г. Барт выступал в качестве основателя и редактора онлайнового журнала Sci- entia, посвященного научным исследованиям. Его диплом на соискание степени магист- магистра, "Неправильный метод для решения многомодальных функций при помощи параллельных генетических алгоритмов", был представлен на международной конфе- конференции по эволюционным вычислениям. Барт выступал по теме С# на конференциях Wrox в Лас-Вегасе и Амстердаме. В качестве консультанта Бартон предоставляет услуги здравоохранительным служ- службам, музыкальным компаниям, финансовым институтам и спортивным организациям. Как СЕО компании Promethean Personal Software он разрабатывает мобильные приложе- приложения, обеспечивающие индивидуальность (www.propersonal.com). В настоящее время Барт живет в Нэшвиле, штат Теннеси, его адрес электронной почты: kbharvey@mindspring.com. Это его третья книга. Крейг Макквин (Craig McQueen) Ведущий архитектор по E-Business приложениям, Sage Information Consultants, Inc. Как советник по стратегическим технологиям в Sage Крейг играет ключевую роль в обучении клиентов современным направлениям в существующих и появляющихся про- промышленных стандартах информационных технологий (COM, .NET, Dynamic HTML, XML и т.п.). Крейг активно участвует во всевозможных публикациях, общается с торго- торговой средой и индустрией. Он является соавтором шести книг, связанных с созданием приложений для Web, а также автором периодических изданий, включая журналы Visual C++ Developer и ActiveWeb Developer. Крейг выступал с докладами на конференциях ASP Professional Web Developer в 1999 и 2000 г. Джерод Моемека (Jerod Moemeka) Джерод Моемека является архитектором в Fujitsu Transaction services. Он несет ответст- ответственность за поиск новых возможностей, а также за весь цикл разработки продукта, вклю- включающий в себя планирование, дизайн и реализацию. В частности, он занимается планированием проекта, разработкой UML, оценкой программного и аппаратного обес- обеспечения, приобретением ресурсов и руководством командой разработчиков. Джерод так- также был архитектором Java в Sun Mycrosystems, старшим специалистом по информационным технологиям (ИТ) в IBM и ведущим архитектором в JIS. Он участвовал в разработке таких широкоизвестных сайтов, как Priceline.com, MyFujiFilm.com, SonyStyle.com и Volume.com. Благодарности Я хотел бы поблагодарить Бога за то, что он постоянно руководил моими действиями, за- замечательных парней из Wrox Press за предоставленную мне возможность написать эту книгу, моих суперродителей за замечательное смешение генов, которое создало меня, и мою жену Келли за то, что она позволяла мне оставлять по вечерам лампу включенной. Кристиан Нагель (Christian Nagel) Кристиан Нагель работает в качестве инструктора и консультанта в компании Global Knowledge, являющейся крупнейшим независимым поставщиком услуг по обучению ин- информационным технологиям. Он работал с платформами PDP 11, VMS, Unix и имеет бо- более чем пятнадцатилетний опыт создания программного обеспечения. Обладая глубокими знаниями технологий Microsoft — он является Сертифицированным инструк- инструктором Microsoft (MCT), Разработчиком решений (MCSD) и Системным инженером (MCSE), — Кристиан с удовольствием обучает других программированию и разработке распределенных решений. Как основатель группы пользователей .NET Австрии и Регио- Региональный директор MSDN он выступает докладчиком на европейских конференциях раз- разработчиков. Многие обращаются к нему за помощью. Web-сайт Кристиана расположен по адресу: christian.nagel.net.
Введение Не будет преувеличением сказать, что язык С# и связанная с ним среда, платформа .NET, являются наиболее важной новой технологией для разработчиков за последние годы. Платформа .NET создана для того, чтобы предложить новую среду, в которой можно раз- разрабатывать практически любое приложение, действующее под управлением Windows, а в будущем, возможно, и под управлением других систем. В свою очередь С# — новый язык программирования, созданный специально для работы с платформой .NET. Используя С#, можно, например, создать динамическую web-страницу, компонент распределенного приложения, компонент доступа к базе данных или традиционное оконное приложение Windows. Не стоит заблуждаться по поводу аббревиатуры .NET. Слово NET в названии плат- платформы подчеркивает уверенность Microsoft в том, что будущее именно за распределен- распределенными приложениями, в которых обработка информации распределена между клиентом и сервером, а С# — это язык не только для написания сетевых или Интернет-приложе- Интернет-приложений. Он предоставляет средства для создания практически любых типов программного обеспечения и компонентов, которые могут потребоваться для платформы Windows. Вместе С# и .NET призваны революционизировать методику создания программ и об- облегчить процесс программирования для Windows. Это довольно серьезное утверждение, и его необходимо обосновать. В конце концов, все мы знаем, как быстро меняются компьютерные технологии. Каждый год Microsoft выпускает новые версии программного обеспечения, инструментов программирования или новые версии Windows, утверждая, что они будут более полезны и удобны для разра- разработчика. Так чем же отличаются платформа .NET и язык С#? Важность платформы .NET Для того чтобы осознать важность платформы .NET, полезно вспомнить о природе тех Windows^rexнoлoгий, которые появились в течение последних десяти лет. По части опе- операционных систем для домашнего компьютера развитие идет от Windows 3.1 (выпущен- (выпущенной в 1992 г.) через Windows95, 98 к наиболее поздней Windows ME (Millennium Edition). Офисные системы прошли через Windows NT 3.5, NT 4.0, и теперь Windows 2000. A Win- Windows ХР разработана для удовлетворения нужд обеих групп пользователей. Хотя внешне операционные системы Windows ХР, Windows 2000 и Windows ME заметно отличаются от Windows 3.1 (a Windows 2000 к тому же имеет совершенно иное ядро), процесс про- программирования всех этих систем чрезвычайно схож. Дело в том, что все они имеют в своей основе хорошо известный Windows API. По мере появления новых версий Windows к этому API добавлялось большое количество но- новых функций, т.е. в целом это был процесс расширения и развития, а не замены API. То же самое можно сказать и в отношении многих других технологий и платформ, которые на нынешний момент используются в процессе разработки приложений для Windows. Так, компонентная объектная модель СОМ произошла от OLE (Object Linking and Embedding). По большому счету это был просто метод, с помощью которого различ- различные типы офисных документов могли быть связаны друг с другом; например, с помощью OLE можно было поместить небольшую таблицу Excel в документ Word. На этой основе была создана СОМ, затем DCOM (распределенная СОМ) и, наконец, СОМ+ — сложная технология, обеспечивающая взаимодействие практически всех компонентов, а также реализующая обработку транзакций, службы передачи сообщений и функции работы с пулом объектов.
Введение Аналогично, Visual Basic начинался как способ размещения в приложении элементов управления ActiveX, а затем последовательно расширялся до тех пор, пока не стал пол- полноценным языком разработки, который можно использовать для написания элементов управления ActiveX и для полноценной компиляции исполняемого кода, каковым собст- собственно и является VB6. Напротив, расширение C++ заключалось в добавлении различных атрибутов, больших библиотек классов и шаблонов, таких как MFC (Microsoft Foundation Classes) и ATL (Active Template Library). Эволюция против революции Смысл этих примеров в том, что все они показывают эволюционное развитие. Новые тех- технологии, конечно же, появлялись, но основной акцент делался на развитие и расширение существующих технологий. Microsoft выбрала этот путь по одной простой причине — обес- обеспечение обратной совместимости с уже имеющимися продуктами. В течение многих лет для Windows было написано огромное число программ, и Windows не приобрела бы тот успех, который она имеет сейчас, если бы Microsoft каждый раз создавала новую техноло- технологию, которая не позволяла бы использовать существующие программистские наработки. Хотя обратная совместимость является ключевой особенностью технологий Windows и одним из ее наиболее сильных моментов, она обладает огромным недостатком. Каждый раз, когда какая-нибудь технология расширяется и приобретает новые возможности, она становится чуть более сложной. В результате этого спустя десятилетие появилось боль- большое число областей программирования, для которых написание кода сильно затрудняет- затрудняется именно тем, что в них предусматривается обратная совместимость с технологиями, появившимися 10 и более лет тому назад. Например, СОМ основывается на относительно сложных потоковых моделях, кото- которые являются таковыми лишь потому, что им необходимо сочетать текущую многопото- многопотоковую технологию с компонентами, написанными для Windows 3.1, в которой не существовало многопотоковых процессов. СОМ — хороший пример семейства техноло- технологий, чья полезность отрицательно сказалась на сложности, когда даже опытные разработ- разработчики сталкиваются с необходимостью изучения в течение 6 месяцев, а то и года, целого лабиринта интерфейсов, GUID (Globally Unique IDentifiers), сложных процессов перено- переноса данных (marshalling) и других низкоуровневых хитростей, которыми пользуется СОМ. Точно так же MFC основывается на объектной модели, которая должна быть совме- совместима с Windows 3.1, и не использует многих замечательных особенностей C++ только потому, что этих особенностей не было в то время, когда разрабатывалась MFC. Visual Basic сохраняет относительную простоту и успешно скрывает от пользователя сложности, к примеру, низкоуровневой СОМ, но за это приходится платить. Вызов API Windows из кода VB сопряжен со значительными трудностями, и разработчики на VB в основном имеют доступ лишь к небольшой части той функциональности и гибкости, которые предлагают Windows и Windows API. Все с начала Ясно, что что-то должно было измениться. Microsoft не могла больше расширять все те же инструменты и языки разработки, делая их все более и более сложными для удовлет- удовлетворения конфликтующих между собой требований поддержки современного оборудова- оборудования и обеспечения обратной совместимости с теми продуктами, которые были созданы в начале 1990-х гг. во время первого появления Windows. Наступает момент, когда необхо- необходимо начать с чистого листа для того, чтобы создать простой, но имеющий сложную структуру набор языков, сред и средств разработки, которые позволят разработчику легко создавать современные программные продукты. С# и .NET являются той самой отправной точкой. Если говорить упрощенно, то .NET представляет собой новую платформу, новый API для программирования в Win- Windows, а С# — новый язык, созданный с нуля, для работы с этой платформой, а также для извлечения всех выгод из прогресса сред разработки и нашего понимания принципов объектно-ориентированного программирования в течение последних 20 лет. Необходимо отметить, что обратная совместимость не потеряна. Существующие программы будут выполняться, а платформа .NET была спроектирована таким образом, чтобы она могла работать с имеющимся программным обеспечением. Связь между ком- компонентами в Windows сейчас почти целиком осуществляется при помощи СОМ. С уче- учетом этого .NET обладает способностью (а) создавать оболочки (wrappers) вокруг существующих компонентов СОМ, так что компоненты .NET могут общаться с ними, и (б) создавать оболочки вокруг компонентов .NET, что позволяет им выглядеть как обычные СОМ-компоненты.
Введение XI Чтобы писать код для платформы .NET, необязательно изучать С#. Microsoft расши- расширила язык C++ и произвела значительные изменения в VB, который превратился в еще более мощный язык VB.NET. Благодаря этому код, написанный с применением обоих этих языков, может работать на платформе .NET. Однако эти языки отягощены наслед- наследством, приобретенным в процессе длительного развития, так как они не были созданы с нуля с учетом использования современных технологий. Язык Visual/++, применение которого с самого начала сопровождалось трудностями и который на самом деле никогда не выходил на лидирующие позиции, не был доработан для использования в .NET. Вероятно, этот язык со временем исчезнет. С# похож naj++, и с некоторых позиций можно рассматривать С# как замену /++. Если вы (а) изучите язык С# и (б) поймете, как работает платформа .NET, то обнаружите, что во многих случаях писать программы на С# намного приятнее и эффективнее, чем на более ранних языках C++ и VB. Эта книга научит вас программировать на С# и даст необходимые общие представле- представления о работе архитектуры .NET. В ней раскрываются основы языка С#, а также приво- приводятся примеры приложений, которые используют целый ряд связанных технологий, включая доступ к базам данных, динамические web-страницы, сложную графику и доступ к каталогам. Единственным требованием является то, что вы должны быть опытным разработчиком, хорошо знакомым как минимум с одним из языков высокого уровня, используемых в Windows: C++, VB или J++. Знакомство с .NET Для того чтобы понять, что такое .NET, следует сравнить платформу .NET с Windows и выяснить, что представляет собой Windows для разработчиков. Здесь возможен двойст- двойственный ответ. С одной стороны, Windows является библиотекой: это набор вызовов всех функций Windows API, которые доступны для использования в программе. Эти функции реализуют общие возможности, такие как отображение диалоговых окон, окон много- многооконных и однооконных программ, доступ к основным функциям, например к функциям безопасности, компонентным службам и т.д. С другой стороны, Windows является средой, в которой исполняются приложения и сама операционная система. Аналогично. .NET представляет собой одновременно две вещи. Во-первых, это биб- библиотека, такая же большая, как и Windows API. Вы можете использовать ее для вызова тех же функций, которые традиционно выполнялись операционной системой Windows: ото- отображение диалоговых и обычных окон, проверка удостоверений безопасности, вызов основных служб операционной системы, создание потоков и т.д. Кроме того, предлагают- предлагаются новые возможности, например доступ к базам данных, соединение с Интернетом, пре- предоставление служб Web. Однако в отличие от предыдущего Windows API, который по сути дела представляет собой огромный набор функций языка С, библиотека .NET (известная также как библиотека базовых классов .NET) является полностью объектно-ориентиро- объектно-ориентированной. Она выглядит как набор объектов, каждый из которых реализует определенный набор методов. Скажем, объект Thread позволяет создавать потоки. Во-вторых, платформа .NET — это среда, в которой выполняется программа. В этом контексте речь идет о среде исполнения .NET (также известной как общая среда ис- исполнения (CLR)), которая представляет собой программное обеспечение, связанное с выполнением пользовательских программ. Когда исполняется код, написанный для .NET (обычно применяется термин "управляемый (managed) код"), платформа .NET запус- запускает код, управляет потоками, предоставляет различные вспомогательные службы и на са- самом деле является той средой, которую "видит" вокруг себя исполняемый код. Можно с таким же успехом рассматривать .NET как среду, обеспечивающую некоторый уровень абстракции от операционной системы. Однако необходимо отметить, что сама по ce6e.NET не является операционной сис- системой. Операционной системой остается Windows, по крайней мере до того, как .NET не будет портирована на другие платформы (если это вообще произойдет), a Windows API по-прежнему стоит на заднем плане. Среда исполнения .NET представляет собой прослойку между ОС Windows и другими приложениями, предлагая более современный, простой и объектно-ориентированный каркас для разработки и выполнения кода. В ряде случаев придется отказаться от преимуществ, предлагаемых .NET, и напрямую
XII Введение использовать Windows API, и это можно будет делать с помощью С# (или управляемого кода, созданного на C++ или VB.NET). Конечно, более ранние приложения, которые не были созданы специально для .NET (неуправляемые (unmanaged) приложения), будут напрямую работать с Windows и Windows API так же, как прежде. Описанная ситуация представлена на рисунке ниже: Неуправляемые и существующие приложения (обычно C++ или VB) \ \ \ Управляемые приложения (написанные на C++, VB.NET или С#) Среда исполнения .NET Операционная система Windows (Windows ME, 98, 2000, ХР и т.п.) г Windows API Базовые классы .NET Преимущества .NET Выше говорилось о том, насколько хороша платформа .NET, но ничего не было сказано по поводу того, каким образом .NET облегчает жизнь разработчика. Рассмотрим некото- некоторые улучшения, произведенные в .NET. Объектно-ориентированное программирование Объектно-ориентированное программирование (ООП) представляет собой способ на- написания программ, при котором код разбивается на единицы, известные как объекты или классы. Каждый объект обычно служит абстракцией некоторого объекта реального мира. Объект содержит информацию о своем состоянии, а также имеет некоторый на- набор доступных методов (функций). ООП позволяет создавать новые объекты на основе существующих при помощи процесса, именуемого наследованием. ООП можно противопоставить процедурном)' программированию, в котором код просто разбивается на большое число функций. За последние 20 лет ООП практически вытеснил процедурное программирование, так как выяснилось, что использование объ- объектно-ориентированного стиля обеспечивает более качественное программирование и увеличивает степень повторного использования кода. Из применяемых на данный мо- момент языков программирования C++ и J++ полностью похдерживают ООП, в то время как VB поддерживает ограниченное подмножество функций ООП. Старый Windows API был написан на С и, следовательно, является полностью процедурным. Это означает, что API состоит буквально из тысяч функций. Напротив, .NET и С# пол- полностью основаны на принципах ООП. В частности, библиотека .ХЬТ является библиоте- библиотекой классов, а не функций. Можно создавать экземпляры этих классов и во многих случаях производить наследование классов, а также вызывать их общие методы. Это по- позволяет создавать интуитивно понятный, хорошо структурированный и более надежный клиентский код, что невозможно сделать с помощью Windows API. Хороший дизайн Мнение о том, что является хорошим дизайном, субъективно, однако Microsoft в течение долгого времени обсуждала с разработчиками самые разные вопросы по поводу .NET и извлекла уроки из ошибок прошлого. В результате появилась библиотека базовых клас- классов, которая разработана с чистого листа и сделана интуитивно понятной. В подавляю- подавляющем большинстве случаев достаточно всего лишь взглянуть на определение метода, чтобы понять, что он делает и как его использовать. Безусловно, такого нельзя сказать о Windows API, функции которого не всегда понятны и зачастую требуют передачи пара- параметров, которые необходимы для запутанной внутренней работы API или обеспечения обратной совместимости, а не для выполнения цели данной функции.
Введение xiii Например, создание и отображение нового окна с помощью .NET означает создание экземпляра объекта Form, возможно, установку некоторых его свойств и вызов метода Show (). Использование Windows API потребовало бы сложного процесса регистрации оконного класса и установки цикла обработки сообщений. Элементы управления VB6 упрощали этот процесс почти так же, как это делает .NET, однако при этом многие функ- функции Windows становились недоступными. Платформа .NET облегчает работу с окнами, одновременно предоставляя доступ ко всей функциональности Windows. Языковая независимость Хотя раньше СОМ-компоненты могли общаться друг с другом независимо от того, на ка- каком языке они написаны, все же существовала большая пропасть между Visual Basic, Visu- Visual J++ и Visual C++. Типы данных различались, и создание СОМ-объекта, который должен быть доступен из других языков, означало наложение жестких ограничений на сигнату- сигнатуры его методов, часто за счет ограничения производительности. И, конечно же, невоз- невозможно было смешивать различные языки в одном и том же блоке кода. Теперь все изменилось. С применением платформы .NET любой язык, включая VB.NET, С# и управляемый C++, может компилироваться в общий промежуточный язык. Это означает, что языки совместимы на совершенно новом уровне. Например, в отлад- отладчике можно перешагивать с кода C++ прямо на код С#, а затем на код VB.NET без ка- каких-либо проблем. Более того, все языки используют теперь общую среду разработки. Важно понимать, что языковая независимость была достигнута не за счет приведения всех языков к наименьшему общему кратному по части функциональности. Напротив, Microsoft осуществила этот переход путем добавления в каждый язык тех свойств, которые превосходно показали себя в других языках. VB.NET теперь содержит классы и наследование, ранее являвшиеся прерогативой C++. Если для программирова- программирования используется C++, то среда разработки поддерживает элементы управления drag-and-drop точно так же, как это было реализовано в VB. Тем не менее некоторые особенности языка C++, такие как множественное наследование и шаблоны, не поддерживаются в .NET и, следовательно, не могут использоваться в управляемом коде. Улучшенная поддержка динамических web-страниц До сегодняшнего дня стандартным способом размещения динамических страниц на Windows-серверах являлось использование ASP. ASP — довольно гибкое средство, но оно не эффективно из-за применения интерпретирующих языков сценариев, а отсутст- отсутствие объектно-ориентированной разработки часто приводит к созданию нечитаемого кода. Платформа .NET предлагает встроенную поддержку web-страниц с использовани- использованием новой технологии — ASP.NET. При помощи ASP.NET код в web- страницах компили- компилируется и может быть написан на любом языке высокого уровня, предназначенном для .NET, например на С# или VB. Это повышает эффективность обработки запросов, по- поступающих от браузеров. Языки сценариев для работы на стороне сервера теперь счи- считаются устаревшими (хотя их применение допускается), но в то же время клиентский JScript для браузеров по-прежнему используется, он был преобразован в JScript.NET. Большое число других возможностей ASP.NET позволяет создавать страницы с более отчетливым разделением пользовательского интерфейса и фоновой обработки данных. Новые элементы управления сервера могут работать с определенными элементами по- пользовательского интерфейса и, помимо всего прочего, способны адаптировать результа- результаты своей деятельности к нуждам конкретных браузеров, так что по большому счету не надо даже беспокоиться об этой стороне дела, а можно сконцентрироваться на биз- бизнес-логике web-страниц. Доступ к данным Библиотека базовых классов .NET чрезвычайно обширна, она включает в себя практиче- практически полную поддержку основных служб Windows и библиотеки компонентов. В частно- частности, доступ к данным, который был добавлен в Windows позже, является основной составляющей библиотеки базовых классов. Набор компонентов .NET, известный как ADO.NET, обеспечивает эффективный доступ к реляционным базам данных и к большо- большому числу источников данных. Также имеются компоненты для доступа к файловой систе- системе и каталогам. Поддержка XML встроена в .NET, что позволяет работать с данными, которые могут быть импортированы с/на не-Windows платформы.
xiv Введение Отказ от применения DLL Когда было объявлено о создании DLL (dynamic linked libraries), это было большим до- достижением по части экономии дискового пространства и памяти и позволяло различным процессам совместно использовать один и тот же код. Однако по мере работы с DLL ста- стало понятно, что их применение приводит к появлению ряда проблем, в основном из-за отсутствия какой-либо формальной системы контроля над версиями и из-за того, что бо- более новая версия DLL, как правило, затирает собой предыдущую версию. Проблема в большинстве случаев заключается в том, что некоторое программное обеспечение перезаписывает разделяемую DLL новой версией. Обновленная версия оказывается не полностью совместимой с предыдущей, в результате чего обнаруживает- обнаруживается, что некоторые программы, использующие ту же DLL, больше не работают на данной машине. Такие ошибки трудно отыскать. Платформа .NET лишена этого недостатка. .NET полностью изменила способ, с помощью которого код совместно используется приложениями, введя концепцию сборки (assembly), заменившей традиционную DLL. Сборки имеют встроенные средства контроля версий, а разные версии сборок могут со- сосуществовать одновременно. Улучшенная безопасность От того, как разработаны сборки, зависит безопасность системы. Каждая сборка может содержать встроенную информацию о безопасности, которая четко указывает, какой по- пользователь, группа пользователей или процесс может вызывать определенные методы того или иного класса. Это позволяет управлять использованием создаваемых сборок. Простая установка (Zero Impact Installation) Начиная с Windows 95, СОМ-объекты должны регистрироваться в реестре. Эта практика является хорошей в плане обеспечения централизованной информации о том, какие программы установлены в системе. Однако в результате использования системой СОМ-объектов библиотек типов появляются дополнительные проблемы из- за того, что информация о приложении хранится в разных местах. Например, код, который пред- представляет собой компонент, может находиться в файле ЕХЕ или DLL, описания методов и интерфейсов будут храниться где-то еще в библиотеке типов, a GUID и т.п. будут описаны в реестре. Разброс информации может привести к ее рассинхронизации. Многим разработчикам СОМ-компонентов в определенный момент приходилось заниматься поиском и исправле- исправлением несоответствий, возникающих в реестре. С .NET это перестает быть проблемой, так как сборки полностью описывают себя в этом плане. В то же время различные эффективные и сложные алгоритмы хэширования обеспе- обеспечивают во время выполнения проверку того, что сборка не была модифицирована. Мож- Можно также различать общие сборки, доступные для других приложений, и частные сборки, которые могут использоваться только тем приложением, которое их установи- установило. Это решает еще одну проблему СОМ — проблему того, что реестр постепенно забива- забивается информацией о тысячах компонентов, которые на самом деле не предназначены для совместного использования. Полдержка web-служб Web-службы, по мнению многих людей из ИТ-индустрии, являются тем, что будет иметь большое значение в течение следующих нескольких лет. Идея состоит в том, что различ- различные методы объекта могут быть вызваны по Интернету при помощи стандартных web-протоколов. Это придает новый смысл технологии распределенных приложений: различные компании смогут предоставлять такие услуги, как прогнозы погоды, состоя- состояния счетов и проверки кредитных карт, с помощью этих методов, а клиенты смогут вы- вызывать их по Интернету с помощью собственного программного обеспечения. Вплоть до настоящего времени Microsoft обеспечивала ограниченную поддержку web-служб посред- посредством инструментария SOAP. .NET имеет полностью встроенную поддержку разработки web-служб, и создавать их теперь так же легко, как и любые другие типы приложений. с# Список преимуществ .NET не был бы полным без упоминания того факта, что использо- использование .NET означает, что вам нужно будет писать код на С#. С# — хорошо продуманный язык, учитывающий ряд ошибок, которые присутствуют в существующих языках про- программирования. Если вы действительно желаете выяснить, насколько хорош С#, вам придется прочесть эту книгу целиком.
Введение xv Сферы применения С# В каком-то смысле С# для программирования можно считать тем же, чем является .NET для окружения Windows. Точно так же, как Microsoft добавляла в течение последнего де- десятилетия все новые и новые возможности в Windows и Windows API, расширялись и языки VB и C++. В результате VB и C++ превратились в довольно мощные языки програм- программирования, но в то же время у них появились проблемы. Главным достоинством языка Visual Basic является то, что он прост в понимании и позволяет легко выполнять многие программистские задачи, большей частью скрывая от разработчика детали Windows API и структуру компонентов СОМ. Недостаток заклю- заключается в том, что Visual Basic никогда не был полностью объектно-ориентированным. Крупные проекты в этом языке быстро становятся неорганизованными и плохо управля- управляемыми. Из-за того что синтаксис VB унаследован от ранних версий BASIC (который, в свою очередь, был разработан для обучения новичков основам программирования, а не для создания серьезных проектов), он не позволяет писать хорошо структурированные или объектно- ориентированные программы. Корни C++ лежат в определении языка ANSI C++. Он не является полностью ANSI C++ совместимым, так как Microsoft создала свой компилятор до появления стандарта ANSI C++, но очень близок к нему. К сожалению, это привело к появлению двух проблем. Во-первых, ANSI C++ был разработан более 10 лет назад, отсюда отсутствие поддержки со- современных концепций (таких, как строки Unicode и создание XML-документации) и нали- наличие архаических синтаксических конструкций, предусмотренных для компиляторов вчерашнего дня (например, отделение определения от описания методов). Во-вторых, Microsoft пыталась приспособить C++ для решения высокопроизводительных задач в Win- Windows, и для этого пришлось добавить в язык большое количество ключевых слов и различ- различных библиотек. В результате язык стал представлять собой некое месиво. Достаточно спросить программиста о том, как много различных определений строки он может приду- придумать: char*, LPTSTR, string, CString (в версии MFC), CString (в версии WTL), wchar_t*, OLECHAR* и Т.д. Что же касается .NET, то это абсолютно новая среда, которая должна привнести но- новые расширения в оба языка. Microsoft решила эту проблему путем добавления еще боль- большего числа специфических ключевых слов к C++ и путем полной перестройки VB в VB.NET, язык, который сохраняет некоторый первоначальный синтаксис VB, но отли- отличается от него настолько, что его смело можно считать новым языком. Именно поэтому Microsoft решила предложить разработчикам альтернативу — язык, созданный с нуля специально для .NET. В результате появился С#. Официально Micro- Microsoft описывает С# как "простой, современный, объектно-ориентированный язык про- программирования с безопасными типами, производный от С и C++". Многие независимые обозреватели скорее всего изменили бы эту формулировку на "производный от С, C++ и Java". Подобные определения технически корректны, но не показывают легкости и эле- элегантности языка. Синтаксически С# очень похож на C++ и Java, и даже совпадают неко- некоторые ключевые слова. С# также использует блочную структуру со скобками ({}) для отметки блоков кода и точки с запятой для разделения выражений. Первое впечатление от кода на С# таково, что он выглядит как код C++ или Java. Однако если не брать в учет внешнее сходство, С# гораздо проще в изучении, чем C++, и сравним по сложности с Java. Его дизайн более созвучен с современными инструментами разработчика. Он был создан для того, чтобы предоставить программисту простоту использования VB и высо- высокопроизводительный, низкоуровневый доступ к памяти по типу C++ в случае необходи- необходимости. К особенностям С# относятся: О Полная поддержка классов и объектно-ориентированного программирования, включая наследование интерфейсов и реализаций, виртуальных функций и пере- перегрузки операторов. О Полный и хорошо определенный набор основных типов. О Встроенная поддержка автоматической генерации XML-документации. О Автоматическое освобождение динамически распределенной памяти. О Возможность отметки классов и методов атрибутами, определяемыми пользова- пользователем. Это может быть полезно при документировании и способно воздейство- воздействовать на процесс компиляции (например, можно пометить методы, которые должны компилироваться только в отладочном режиме). О Полный доступ к библиотеке базовых классов .NET, а также легкий доступ к Windows API (если это действительно необходимо).
XVi Введение О Указатели и прямой доступ к памяти, если они необходимы. Однако язык раз- разработан таким образом, что практически во всех случаях можно обойтись и без этого. О Поддержка свойств и событий в стиле VB. О Простое изменение ключей компиляции. Позволяет получать исполняемые файлы или библиотеки компонентов .NET, которые могут быть вызваны другим кодом так же, как элементы управления ActiveX (компоненты СОМ). О Возможность использования С# для написания динамических web-страниц ASP.NET. Надо отметить, что большинство из приведенного выше справедливо и для VB.NET, и для управляемого C++. Однако тот факг, что С# создан с нуля для работы с .NET, озна- означает, что он более полно поддерживает все особенности .NET и предлагает в этом кон- контексте более удобный синтаксис, чем остальные языки. Сам по себе язык С# похож на Java, однако есть некоторые улучшения, и, кроме того, Java не создан для работы в среде .NET. Подводя черту, можно сказать, что С# является не только мощным языком, который не сложен в изучении, но и, пожалуй, единственным языком на рынке, который был со- создан на основе современных технологий и инструментов разработки. Изучая опыт пре- предыдущих языков, Microsoft может гарантировать, что С# хорошо проработан и позволяет быстро получать высококачественный код. Нужно все же отметить несколько ограничений С#. Одной из областей, для которых не предназначен этот язык, являются критичные по времени и высокопроизводитель- высокопроизводительные программы, когда имеет значение, занимает исполнение цикла 1000 или 1050 ма- машинных циклов, и освобождать ресурсы требуется немедленно. C++ останется в этой области наилучшим из языков низкого уровня. В С# отсутствуют некоторые ключевые моменты, необходимые для создания высокопроизводительных приложений, в частно- частности подставляемые функции и деструкторы, выполнение которых гарантируется в опре- определенных точках кода. Однако число приложений, попадающих в эту категорию, невелико. Новая среда разработки Платформа .NET предлагает новую среду разработки — Visual Studio.NET. До сих пор каж- каждый язык имел свою собственную среду разработки в рамках Visual Studio 6. Программи- Программисты C++ применяли свою среду (ее нередко ошибочно называют просто Visual Studio 6), разработчики на VB использовали так называемую VB6 IDE, а соответствующим эквива- эквивалентом для J++ являлась среда Visual J++. И все эти среды, реализуя многие функции друг друга, выглядели совершенно разными. Самыми обделенными оказались разработчики ASP-страниц, они обычно применяли Visual Interdev, среду, которая предлагала лишь не- немногие возможности по генерации и отладке кода из тех, что требовались пользовате- пользователям компилируемых языков (однажды автор слышал, как Visual Interdev назвали Визуальным блокнотом). Каждая из этих сред (за исключением Interdev) обладала свои- своими преимуществами, но все же это были совершенно разные среды, с отличающимися интерфейсами пользователя. Каждая среда разработки имеет свои слабые и сильные стороны, которые в большой степени отражают достоинства и недостатки соответствующего языка. Например, Visual Studio (C++) обладает мощными возможностями отладки, не доступными в других сре- средах, в то время как среда разработки Visual Basic хороша в случае визуального програм- программирования, когда с помощью одного щелчка мыши можно поместить в форму огромное число элементов управления ActiveX, а соответствмощий VB-код среда сгенерирует авто- автоматически. Так как целью .NET является полная совместимость языков (включая воз- возможность перехода от кода на одном языке к коду на другом языке в отладчике), то наличие отдельных сред разработки совершенно недопустимо. Платформа .NET обладает новой средой разработки, Visual Studio.NET, которая мо- может одинаково работать с кодом C++, С#, VB.NET и ASP.NET. Visual Studio.NET сочетает в себе все лучшие свойства соответствующих сред Visual Studio 6. Когда вы начнете пи- писать в Visual Studio.NET, вам придется привыкать к другому расположению окон и к дру- другой системе меню, однако положительной стороной является то, что это более мощная среда разработки по сравнению со всеми предыдущими.
Введение xvii Как и в случае с языками, Microsoft совместила среды разработки не за счет нахожде- нахождения наименьшего общего кратного от их возможностей, а за счет того, что каждая особен- особенность, предлагаемая Visual Studio, теперь доступна для всех языков. Следовательно, каким бы языком вы ни пользовались, вы получите новые функции. Пользователи C++ найдут улучшенную поддержку визуального построения форм, в то время как разработчики VB смогут воспользоваться мощными возможностями отладки и различными вариантами сборки приложений. Однако наиболее заметные усовершенствования, безусловно, будут отмечены разработчиками ASP-страниц. В этой книге рассказывается об использовании Visual Studio.NET, однако из-за того, что книга предназначена для профессионалов, в ней не приводятся мелкие детали. Предполагается, что читатель является опытным разработчиком и способен установить Visual Studio.NET и изучить самостоятельно систему меню. В главе 8 описываются основные особенности среды разработки отчасти для того, чтобы познакомить пользо- пользователей других языков с новыми моментами, а отчасти для того, чтобы показать, куда переместились некоторые популярные инструменты и пункты меню. Необходимое программное обеспечение Платформа .NET работает под управлением Windows 98, 2000 и ХР. Для того чтобы про- программировать с использованием .NET, необходимо установить .NET SDK. Если вы не планируете применять для написания кода простой текстовый редактор или среду разра- разработки от сторонних производителей, вам понадобится также Visual Studio.NET. Для запу- запуска управляемого кода не требуется полный SDK, однако необходима среда исполнения .NET (.NET runtime). Возможно, вместе с готовым кодом придется распространять и среду исполнения .NET, если она еще не установлена у клиентов. Содержание книги В начале книги предлагается обзор архитектуры .NET, дающий некоторые общие сведе- сведения, необходимые для написания управляемого кода. В последующих частях описываются язык С# и его применение в ряде областей. Часть 1. Язык С# В этой части рассматриваются основы языка С#. Раздел не предполагает знание како- какого-либо определенного языка программирования, однако читатель должен быть опыт- опытным программистом. Сначала описываются синтаксис и типы данных С#. Дальше рассказ идет о принципах объектно-ориентированного программирования и о более сложных аспектах С#. Изучив предлагаемый здесь материал, читатель получит представ- представление о языке в целом. Часть 2. Программирование .NET Здесь рассматриваются принципы программирования среды .NET. В частности, обсуж- обсуждаются Visual Studio.NET, вопросы создания классических оконных приложений, ото- отображающих окна (или, выражаясь более современным языком .NET, формы), а также рассказывается о том, как создавать свои собственные библиотеки (сборки). Часть 3. Доступ к данным Рассматривается процесс доступа к базам данных с помощью ADO.NET, а также процесс взаимодействия с каталогами и службой Active Directory. Подробно рассказывается о под- поддержке XML в .NET и со стороны операционной системы Windows, о доступе к файлам и реестру. Часть 4. Программирование для Интернета В этом разделе рассказывается о создании компонентов, предназначенных для работы на web-сайтах, и об обслуживании web-страниц. Рассматриваются ASP.NET и процесс написания служб Web.
xviii Введение Часть 5. Компоненты Обратная совместимость с СОМ является важной частью .NET. Помимо этого СОМ+ не является полностью устаревшим — он все еще ответственен за транзакции, работу с пу- пулом объектов и с очередями сообщений. В этой части рассматриваются возможности, предоставляемые .NET для работы с СОМ и СОМ+, а также рассказывается о том, как на- написать код, который будет взаимодействовать с этими технологиями. Часть 6. Более сложное программирование .NET Эта заключительная часть затрагивает некоторые сложные темы: усовершенствованную графику с GDI+, службы Windows (ранее известные как службы NT), удаленную работу и вопросы безопасности. Соглашения Для выделения той или иной информации в книге используется несколько различных стилей текста. Строки списка печатаются с отступом, а каждая новая строка отмечается следующим образом: О Важная информация выделяется жирным шрифтом. О Слова, обозначающие пункты меню, например File и Window, приводятся таким же шрифтом, каким они отображаются на экране. О Клавиши, которые необходимо нажать на клавиатуре, такие как Ctrl и Enter, запи- записываются курсивом. Для обозначения кода используется несколько шрифтов. Слово, о котором идет речь в тексте, например, при обсуждении цикла if.. .else, записывается этим шрифтом. Фрагмент кода, который можно набрать как программу и запустить на исполнение, до- дополнительно располагается на сером фоне: public static void Haiti<) Ч к •- AFuncU,2,"abc"); ■ } Иногда код записывается в смешанном стиле, например: // Если мы не достигли конца, вернем true, иначе // отметим позицию как неправильную и вернем false _ pos++; ■*— ;if <pos < 4) ": v . .• . returo tjrue; •■?■■* else { pos = -1-; return false; } Это означает следующее: код, приводимый на белом фоне, уже рассматривался и здесь ему внимание не уделяется. Советы, указания, вспомогательная информация записываются курсивом с отступом. Важная информация размещается так. Г Для демонстрации синтаксиса методов, свойств и т.п. используется следующий формат: RegsvcsBookDistributor.dll [COM+AppName] [TypeLibrary.tbl] Здесь части, выделенные курсивом, описывают объекты, переменные или значения параметров, которые необходимо подставить; в квадратные скобки заключаются необя- необязательные параметры.
Содержание ТОМ I Глава 1 Архитектура .NET 1 Обзор .NET 1 Процессы, происходящие при компиляции и запуске программы • ■ • 2 Компиляция 3 Исполнение ... 4 Преимущества исполнения программы как управляемого кода • • • 4 Промежуточный язык -5 Традиционное объектно-ориентированное программирование -6 Типы по ссылке и по значению • • 7 Строгий контроль типов ■ • • . . . . . 7 Свойства IL: подведение итогов • • • 8 Совместимость языков 8 Общая система типов (CTS) .... -9 Важность для совместимости языков -9 Иерархия CTS • • • 10 Общая спецификация языка (CLS) 11 Библиотека базовых классов .NET ■ ■ • ■ ■ 12 Компоненты платформы .NET • 13 Сборки . . 13 Метаданные и манифесты 13 Совместно используемые и частные сборки •••.•■ • • 14 Пространства имен • • • ■ ■ -15 Области приложений • • 15 ЛТ-компиляторы- ■ • ■ • • • 17 Инструменты .NET ■ • 17 Сборщик мусора • • 18 Исключения • • • 20 Безопасность- • • 20 Атрибуты - ■ 21 Отражение • .... 21 Языки и технологии ■ • • • • • ... ■ • ■ ■ 21 С# • ■ ■ 21 C++ • • ■ 22 J++ • • • 22 VB 22 ASP 23 Языки сценариев ■ 23 ADO/OLE DB • • • 23 ADSI • • • • • 23 СОМ и СОМ+ 23 Возможности .NET: подведение итогов 23 Историческое примечание • • • 25 Заключение 26 Глава 2 Введение в С# 27 Разработка С# 27 Появление ассемблера 27
XX Содержание Первые компиляторы 28 Язык программирования С- 29 С в сравнении с C++ ■ • 29 Программирование на С и C++ в Windows- 30 Появление VB 30 Java 31 С# 33 С# в сравнении с другими языками 33 Сравнение С# с VB6 • • 33 Сравнение С# с VB.NET- - - ■ ■ -33 Сравнение С# с Visual C++ • ■ ...... 33 Сравнение С# с управляемым C++ 34 Сравнение С# с C++ Builder ■ ■ ■ 34 Сравнение С# с Delphi 35 Сравнение С# с Java 35 Сравнение С# с JavaScript • • ■ ■ ■ ■ • • 35 Сравнение С# с VBScript • 36 Использование С# • ■ • • 36 Приложения ASP.NET • ■ • ■ .36 Web Forms ■ • 37 Web Controls 37 Службы Web 37 Библиотеки классов- 38 Приложения Windows • • ■ 38 Элементы управления Windows • 38 Консольные приложения 39 Место, занимаемое С# в корпоративных архитектурах • • • 39 Обзор свойств С# • • ■ 40 Базовые классы .NET ■ • ■ ■ 40 Выражения, управляющие ходом выполнения программы 40 Обьектно-ориентированные свойства 40 Отражение и атрибуты • • • 41 Небезопасный код ...... 41 Использование компилятора С# 42 Обзор 42 Входные и выходные файлы ■ ■ • • • • • • -42 Компиляция различных типов проектов 42 Ответные файлы • • • ...... 42 Заключение- 42 Глава 3 Основы С# 43 Начало работы • • 43 Типы данных С# ■ ■ • 45 Новые специализированные типы в CTS ........ 45 Безопасность типов ... 45 Типы по значению и типы по ссылке • 45 Предопределенные типы С# ■ ... 48 Типы по значению • • 48 Целые типы- • ■ • 49 Числа с плавающей точкой . 49 Десятичный тип . . -50 Тип Boolean- ■ 50 Символьный тип • ■ . ■ 51 Типы по ссылке ■ 51 Тип Object • 52 Тип String 52 Сложные типы- • • • • -53 Типы по значению • 54 Структуры 54 Перечисления 55 Типы по ссылке -56 Интерфейсы . . . . 56 Делегаты- ■ 56 Массивы -56 Преобразование типов 60
Содержание xxi Неявные преобразования 60 Явные преобразования 61 Упаковка и распаковка • • 63 Переменные . . 63 Идентификаторы 64 Область видимости переменных 65 Поля и локальные переменные 66 Модификаторы переменных ..... 67 Модификаторы доступа ■ • 67 Статические переменные и переменные экземпляра • • • • 68 Переменные только для чтения • • • 69 Константы . . 69 Операции 70 Сокращенная запись операций 71 Тернарный оператор 72 Операторы checked и unchecked 72 Оператор is ■ • • 73 Оператор sizeof 73 Оператор typeof 73 Приоритет операций • • • • ■ ■ • 73 Управление ходом выполнения программы- • • • • ■ 74 Условные операторы 74 Оператор if • • ... 74 Оператор switch 76 Циклы 77 Цикл for • - • 77 Цикл while ■ 78 Цикл do...while- - • 78 Цикл foreach • • .... 78 Операторы перехода ■ .... 79 Оператор goto- • ...... 79 Оператор break • 79 Оператор continue 79 Оператор return • 79 Оператор using • • • • • • • • 80 Обработка исключений 80 Структура программы • • 81 Классы • 81 Члены класса • .... 81 Методы ■ ■ • • • . . . . . • • • 82 Пространства имен • • 85 Оператор using • • • • 87 Псевдонимы пространств имен ■ • 88 Метод Main 88 Множественные методы Main 89 Передача аргументов в Main ..... • 89 Компиляция файлов С# 90 Консольный ввод/вывод -91 Класс Console ■ • • • • 91 Комментарии • ...... -93 Документация XML -93 XML-документация в Visual Studio.NET 95 Заключение 96 Глава 4 Классы и наследование 97 Несколько слов для начала- ..... 97 Знакомство с объектами и классами 98 Объекты в повседневной жизни • • • 98 Объекты в программировании 89 Члены класса 99 Пример: класс Authenticated • -100 Реализация классов- ... 103 Экземпляр и статические поля ' ЮЗ
XXii Содержание Методы экземпляра и статические методы 105 Доступ к статическим методам и методам экземпляра 105 Реализация методов экземпляров и статических методов 106 Типы по ссылке и по значению ■•■■■■ 107 Свойства 109 Преимущества свойств НО Наследование • • • 112 Наследование в повседневной жизни 112 Наследование в С#: пример Mortimer Phones • • 113 Добавление наследования 115 Класс Object 119 Метод ToString() 120 Диаграммы иерархии классов ....-• 122 Виртуальные и невиртуальные методы ...... 122 Сокрытие методов ■ • ■ ■ 124 Конфликт версий • • 125 Абстрактные функции и базовые классы 126 Перестройка Mortimer Phones: добавление абстрактного класса • • • 127 Запечатанные классы и методы 130 Вызов базовых версий методов 130 Наследование: дополнительные аспекты • - -132 Архитектура ООП-программы 133 Заключение • 134 Глава 5 Объектно-ориентированный С# 135 Перегрузка методов • 135 Определение перегруженных методов 136 Вызов перегруженных методов . . . • • • 138 Требования к перегрузке методов 139 Применение перегрузки • • • • 139 Параметры по умолчанию • • • • • • 139 Различные входные типы • .... 140 Различные выходные типы- 140 Конструирование и освобождение .... 140 Конструкторы ... 140 Добавление конструкторов в класс Authenticator • 141 Конструкторы, принимающие параметры 142 Другие применения конструкторов ■ • • 143 Статические конструкторы .144 Константные поля и поля только для чтения- • 145 Вызов конструкторов из других конструкторов 147 Конструкторы производных классов 147 Добавление конструктора без параметров в иерархию 149 Добавление конструкторов с параметрами в иерархию • 150 Уборка: деструкторы • • 153 Finalize!) • 153 Dispose!) и Closef) 154 Отличие Closef) от Dispose!) • 154 IDisposable • • • 155 Пример: DestructorDemo .... -155 Переменные по ссылке и по значению: что происходит в памяти • • • 157 Типы данных по значению- ........ • • 158 Типы данных по ссылке- • • • • • 160 Структуры • 164 Структуры как типы по значению • . 165 Структуры и наследование ....... ... -166 Конструкторы для структур -•• 167 Перегрузка операций 167 Назначение перегрузки операций ■ ■ - • • 168 Выполнение операций • • 169 Пример перегрузки операции сложения: структура Vector 170 Перегрузка операторов сравнения 173
Содержание xxiii Операции, допускающие перегрузку .... 175 Индексаторы 176 Добавление индексатора в Vector 176 Другие примеры индексаторов 178 Интерфейсы 179 Реализация интерфейсов: пример с джемпером и рубашкой • • • ■ 180 Наследование интерфейсов ... 183 Заключение -183 Глава 6 Дополнительные возможности С# 185 Ошибки и обработка исключений 186 Программирование исключений 186 Пример SimpleExceptions 188 Обработка исключений из другого кода 192 Другие свойства и методы System.Exception 192 Действия, производимые при отсутствии обработки исключений 193 Исключения базовых классов • • • • • ■ 193 Вложенные блоки try • 195 Изменение типа исключения 196 Обработка различных исключений в разных участках кода 197 Пример использования исключений: MortimerColdCall 197 Определение собственных классов исключений 198 Метод Main<) 199 Класс ColdCallRleReader 200 Определенные пользователем приведения типов 203 Пример: структура Currency 203 Приведение типов между классами • • • ■ ■ ■ • • 206 Приведения типов между базовыми и производными классами -207 Упаковывающие и распаковывающие приведения типов 208 Множественное приведение типов -209 Делегаты 212 Использование делегатов в С# ... 214 Простой пример использования делегатов 216 Пример BubbleSorter 217 Многообъектные делегаты ■ • ■ ... 219 События 221 Событие с точки зрения клиента - - 222 Пример использования событий: консольные уведомления 223 Директивы препроцессора С# • • • • • 228 «define и #undef 228 #if, #elif, #else и #endif 228 «warning и #error -229 «region и «endregion 229 «line 230 Атрибуты • ... 230 Небезопасный код- • • • 232 Указатели 233 Синтаксис указателей • -234 Приведение указателей к целочисленным типам 236 Приведение типов для разных типов указателей • 236 Указатели void • - 237 Оператор sizeof 237 Пример PointerPlayaround • 237 Арифметика указателей • • 238 Указатели на структуры: оператор доступа к компоненту структурированного объекта- • • • 239 Указатели на члены класса 240 Добавление в пример классов и структур 241 Использование указателей для повышения производительности 243 Создание стековых массивов -243 Пример QuickArray -245 Заключение 246
xxiv Содержание Глава 7 С# и базовые классы 247 System.Object 247 Сравнение объектов по ссылке на равенство 249 ReferenceEquals() 249 Виртуальный метод Equals() 249 Статический метод EqualsQ • 249 Операция сравнения (==)• 249 Сравнение на равенство типов по значению 250 Работа со строками 250 System.String -251 Создание строк 252 Члены StringBuilder 254 Форматирование строк ■ ....... -254 Форматирование строки • • • • 256 Пример FormattableVector • . . . . 258 Регулярные выражения -259 Введение в регулярные выражения • 259 Пример RegularExpressionsPlayaround- • 260 Отображение результатов • 262 Совпадения, группы и выборки • 263 Группы объектов ...-.- 265 Списки массивов • .• 265 Коллекции • 266 Общее представление о коллекциях • 266 Добавление поддержки коллекций в структуру Vector • 268 Словари • 269 Словари в повседневной жизни -270 Словари в.NET 270 Принцип работы словаря • ...... 271 Пример MortimerPhonesEmployees • 273 Пользовательские атрибуты • . . 277 Написание пользовательских атрибутов 277 Пользовательские классы атрибутов • 278 Пример WhatsNewAttributes 280 Библиотечная сборка WhatsNewAttributes • • ■ • 280 Использование атрибутов: сборка VectorClass • • 282 Отражение- • • • • • • 282 Класс System.Type ... 283 Свойства Туре -283 Методы • • • • . . . 284 Пример TypeView ■ 285 Класс Assembly 286 Получение сведений о типах, определенных в сборке 287 Получение сведений о пользовательских атрибутах ■ • ■ ...... 287 Завершение примера WhatsNewAttributes . . 288 Потоки • • 290 Многопоточные приложения- • ■ 291 Принципы работы • . . . 292 Обработка потоков ■ ■ - - 292 Запуск потока ... 292 Пример ThreadPlayaround ... 294 Приоритеты потоков ... . . 297 Синхронизация - • . . 298 Понятие синхронизации • • . . . . 298 Проблемы синхронизации ... 299 Заключение ... 301 Глава 8 Программирование в среде. NET 303 Visual Studio.NET 303 Создание проекта 306 Выбор типа проекта • ■ - 307 Новый проект консольного приложения 308 Другие созданные файлы 309
Содержание xxv Решения и проекты • • 310 Добавление в решение другого проекта 311 Код приложения Windows 312 Чтение проектов Visual Studio 6 313 Просмотр и написание кода для проекта 314 Сворачивающий редактор • ■ ■ 314 Другие окна ■ 316 Прикрепляющие кнопки • 322 Сборка проекта • 323 Сборка, компиляция и создание программы • • 323 Отладочные и окончательные версии • • ■ ■ 323 Выбор конфигурации • • ■ • 325 Редактирование конфигураций 325 Отладка 326 Точки останова 326 Окно наблюдения ■ • ...... 327 Исключения . ........ 327 Другие инструменты .NET- • • • • 328 WinCV 328 Указания по использованию .NET • ■ ■ ■ ........ 329 Соглашения по именованию- 330 Регистры имен ... -330 Стили имен ...... ■ 331 Имена пространств имен 331 Имена и ключевые слова • 331 Использование свойств и методов ■ ■ '• 333 Использование полей ■ • • ■ 334 Заключение ■ • • -334 Глава 9 Приложения Windows 335 Архитектура . . ■ . ■ 336 Формы -337 Элементы управления • • • . . . . 339 События и делегаты- ■ • • . . 342 Обработчики событий .... 343 Группировка элементов управления ■ . . ... 345 Классы для рисования■ ■ ■ • ■ 346 Размеры . 346 Кисти- 347 Перья- • ■ 347 Шрифты- • • 347 Graphics 348 Пользовательские элементы управления • • 348 Пример: рендерер Безье 349 Использование • ■ ■ ■ • 352 Меню- • • 356 Диалоговые окна 357 Заключение • • ... ...... 358 Глава 10 Сборки 359 Понятие сборки .... 359 Сборки как ответ на ад DLL . -360 Особенности сборок 380 Необходимость в Microsoft Windows Installer (MSI) • • ■ jgf Области приложений и сборки • • - 301 Структура сборки • 363 Манифесты сборок ■ 364 Пространства имен, сборки и компоненты • • 364 Частные и разделяемые сборки • -365 Просмотр сборок • - - 365 Создание сборок 365 Создание модулей и сборок 365 Создание сборок с помощью Visual Studio.NET 367
xxvi Содержание Кросс-языковая поддержка 368 Языковая независимость в действии 369 Написание класса с использованием управляемого C++ 370 Написание класса с использованием VB.NET -373 Написание класса с использованием С# -375 Требования CLS 376 Атрибут CLSCompliant 377 Правила CLS 377 Добавление ресурсов к сборкам 378 Создание ресурсных файлов 378 ResGen 378 ResourceWriter 379 Использование ресурсных файлов 379 Интернационализация и ресурсы 382 Культура 383 Регион 383 Пространство имен System.Globalization 383 Ресурсы для локализации -386 Сборки-спутники -386 Пространство имен System.Resources • • 386 Пример локализации с использованием Visual Studio.NET • 386 Программное изменение культуры 390 Использование двоичных ресурсных файлов • 391 Использование ресурсных XML-файлов • - • 392 Изменение ресурсов для подъязыков • • • 393 Глобальный кэш сборок . 393 Генератор родного кода • • -394 Средство просмотра глобального кэша сборок . 394 Утилита глобального кэша сборок (gacutil.exe) • • • • 395 Создание совместных сборок 395 Имена совместных сборок - ■ ■ 396 Криптография с открытым ключом ....... 396 Целостность при использовании строгих имен • • • - 397 Замена ключа 397 Создание совместной сборки • • • -397 Создание строгого имени 398 Установка совместной сборки 399 Использование совместной сборки • • • 399 Конфигурация • • • • 400 Версии- ■ • 401 Номер версии • 401 Программное получение версии ■ • 401 Конфигурационные файлы приложения • • • 402 Конфигурирование каталогов ... -404 <codeBase> -405 <probing> - ■ • 405 Распространение • ....... 406 Распространение DLL .... -406 Создание Merge Module • • • -407 Заключение ■ • -408 Глава 11 Доступ к данным с помощью .NET 409 O63opADO.NET • • 409 Совместные классы 409 Классы, относящиеся к базам данных 410 Пространства имен • • 410 Соединения 410 Транзакции -411 Команды • • 412 Исполнение команд • • • 413 ExecuteNonQuery ■ • • : • 413 ExecuteReader 413 ExecuteScalar 413
Содержание xxvii ExecuteResultSet (только для провайдера Sql) • 414 ExecuteXmlReader (только для провайдера Sql) 414 Вызов хранимых процедур 414 Вызов хранимой процедуры, не возвращающей значение 415 Вызов хранимой процедуры, которая возвращает выходные параметры -416 Вставка записи 416 Считыватели данных • • • 417 Наборы данных 419 Таблицы данных 420 Столбцы данных 420 Строки данных • 421 Генерация схемы ■ • ■ 423 Отношения между данными • • • • -425 Ограничения для данных..... -426 Схема XML 428 Заполнение набора данных 436 Заполнение DalaSet с помощью адаптера данных 436 Заполнение набора данных из XML 437 Сохранение изменений, произведенных в наборе данных ■ • • • 438 Обновление с помощью адаптеров данных 438 Запись выходных данных XML • 440 Pa60TacAD0.NET • • 441 Многоуровневая разработка- ..... 441 Копирование и слияние данных- • ■ 441 Генерация ключа для SQL Server- • • • • 442 Соглашения по именованию ■ 444 Таблицы баз данных . . .... 444 Столбцы базы данных 444 Ограничения • • • ... ... . . ... . . 445 Производительность- • • 445 Заключение ... 446 Глава 12 Просмотр данных .NET 447 Элемент управления DataGrid 447 Отображение табличных данных ■ • • • 447 Источники данных . . 449 Массив 449 DataTable 450 DataView- 451 DalaSet 453 ILJstSource и IList • • ... • • • 455 Иерархия классов DataGrid • 455 DalaGridTableStyle и DataGridColumnStyle • 455 Привязка данных • • • • ... .... 458 Простая привязка- • • - • • • • 458 Объекты привязки данных . . . . 459 Visual Studio и доступ к данным ■ • 463 Создание соединения . . . 463 Выбор данных . 464 Генерация DataSet ■ • • • -466 Построение схемы • • • • 467 Добавление элемента ■ • • 468 Другие общие требования ■ ■ • ■ -471 Создаваемые таблицы и строки ■ ■ 474 Использование атрибутов . . 475 Методы диспетчеризации 476 Получение выбранной строки 477 Заключение 478
xxviii Содержание ТОМИ Глава 13 XML 479 Стандарты W3C 479 Пространство имен System.Xml •••••■ 480 XML 3.0 (MSXML3.DLL) в С# . . . . 481 System.Xml 484 Чтение и запись XML • • ■ 484 XmlTextReader 485 Проверка 489 Запись XML 491 Объектная модель документа в .NET 492 XPath и XslTransform 499 XML и ADO.NET 507 Данные ADO.NET в документе XML ■ • 507 Преобразование документа XML в данные ADO.NET 514 Запись и чтение DiffGram 516 Сериализация объектов в XML 519 Заключение • • • • • 529 Глава 14 Операции с файлами и реестром 531 Управление файловой системой ■ 531 Классы .NET, представляющие файлы и папки • • 532 Класс Path 535 Пример: файловый браузер • • 535 Перемещение, копирование и удаление файлов • 539 Пример FilePropertiesAndMovement 539 Чтение и запись файлов • 542 Потоки • 542 Чтение и запись двоичных файлов • 544 Класс RleStream ■ • • ... 544 Пример: объект чтения двоичного файла 546 Чтение и запись текстовых файлов 548 Класс StreamReader • • ■ 549 Класс StreamWriter 550 Пример: ReadWriteText • 551 Чтение и запись в реестр- • • 553 Реестр • 554 Классы реестра в .NET • • • . 555 Пример: SelfPlacingWindow • • 557 Заключение ... ... .... 562 Глава 15 Работа с активным каталогом 563 Архитектура активного каталога ■ ■ • • 564 Свойства • *' 564 Концепции активного каталога 565 Объекты 565 Схема 565 Конфигурация • ■ • 565 Домен активного каталога • • 565 Контроллер домена • • 566 Сайт 566 Дерево домена ■ .... . . . . 567 Лес ■ • ■ 567 Глобальный каталог ■ • 568 Репликация 568 Характеристики данных активного каталога ■ • • • 569 Схема • • • 570 Управление активным каталогом 571 Active Directory Users and Computers ■ 571 ADSI Edit 572 ADSI Viewer 573
Содержание xxix Интерфейсы службы активного каталога (ADSI) 574 Программирование активного каталога • • • • • 574 Классы в System.DirectoryServices 575 Связывание 575 Протокол 575 Имя сервера ■ 576 Номер порта ...... 576 Известное имя 576 Известное имя 576 Имя пользователя . 578 Аутентификация 578 Связывание с помощью класса DirectoryEntry -578 Получение записей каталога 578 Свойства объектов пользователей 578 Коллекции объектов- ... 580 Кэш • 581 Обновление записей каталога 581 Создание новых объектов • • • -582 Поиск в активном каталоге • 582 Пределы поиска 583 Поиск объектов пользователей 585 Интерфейс пользователя 585 Получение именующего контекста схемы 586 Получение имен свойств класса пользователя . . 587 Поиск объектов User • 588 Заключение -589 Глава 16 Страницы ASP.NET 591 Введение в ASP.NET -591 Управление состоянием в ASP.NET . . . . 592 Формы Web ASP.NET 593 Серверные элементы управления ASP.NET • ■ • • 596 Палитра элементов управления -.■■■•.. ■ • 599 Пример серверного элемента управления -603 ADO.NET и связывание данных -608 Модернизация приложения заказа помещения . . . . 608 База данных . . 608 Соединение с базой данных ■ ■ ■ 609 Модификация элемента управления календарем • 611 Запись мероприятий в базу данных • ■ ■ • 613 Еще о связывании данных ...•■•• 614 Вывод данных с помощью шаблонов- ...... 616 Конфигурация приложения • 619 Заключение ...... • -620 Глава 17 Службы Web 621 SOAP 621 WSDL -623 Службы Web • 624 Создание служб Web • ■ ■ . 624 Типы данных, доступные для служб Web 626 Использование служб Web • ■ ■ 626 Расширение примера заказа помещения для проведения мероприятий • • ■ • 628 Служба Web заказа помещения для проведения мероприятий • • ■ • • 628 Клиент приложения предварительного заказа помещения для проведения мероприятия • • • • 630 Заключение 632
XXX Содержание Глава 18 Специальные элементы управления 633 Элементы управления пользователя 634 Простой элемент управления пользователя 634 Преобразование приложения предварительного заказа мероприятия в элемент управления пользователя 638 Специальные элементы управления 639 Конфигурация проекта специального элемента управления .... 640 Базовые специальные элементы управления 642 Производный элемент управления RainbowLabel 644 Поддержание состояния в специальном элементе управления • • - ■ • 645 Создание композитного специального элемента управления ■ • • • 647 Элемент управления выборочным опросом 648 Элемент управления Option 649 Построитель элемента управления StrawPoll 650 Стиль StrawPoll - - 651 Элемент управления StrawPoll 651 Добавление обработчика событий 653 Заключение 655 Глава 19 Взаимодействие с СОМ 657 Сравнение СОМ и .NET 657 Принципы работы СОМ 658 Недостатки СОМ 658 Как работают компоненты .NET 658 СОМ или .NET? 659 Использование компонентов СОМ в .NET • 659 Диалоговое окно ссылок ■ • ■ • • • 659 Оболочки времени выполнения 663 Tlblmp.exe 663 Позднее связывание с компонентами СОМ -664 Использование элементов управления ActiveX в .NET ■ 666 Axlmp.exe 666 Ссылка на сборку прокси ActiveX 666 Размещение элемента управления ActiveX в WinForm 667 Использование компонентов .NET в СОМ 667 RegAsm.exe - • • 668 TlbExp.exe 669 Службы вызова платформы 670 Неуправляемый код и ненадежный код • 670 Доступ к неуправляемому коду.... 670 Недостатки Plnvoke -671 Заключение -671 Глава 20 Службы СОМ+ 673 Введение • ■ ........ 673 Службы СОМ+ в ретроспективе ••.■■-■ • • • 673 Состав служб СОМ+ • 673 Snap-in служб компонентов .... 674 Транзакции СОМ+ 675 Назначение транзакций 675 Принципы транзакций 675 Транзакции в N-звенной архитектуре • . 676 Службы СОМ+ и время жизни объекта ... 676 Создание пулов объектов ■ 676 Оперативная активизация (ЛТ)- ....... • -677 Безопасность . 677 Новые службы СОМ+ • • 678 События ....... . . . 678 Очереди сообщений • • 678 Выравнивание нагрузки компонентов 679 Использование служб СОМ+ со сборками .NET 679
Содержание xxxi Подготовка сборок .NET для служб СОМ+ • 680 Предоставление атрибутов сборок ■ • 680 Развертывание сборки для служб СОМ+ 681 Предварительные итоги 682 Использование транзакций со сборками .NET 682 Определение транзакционной поддержки • ■ 682 Кодирование транзакций с помощью ContextUtil • • 684 Другие полезные методы ContextUtil 687 Использование пулов объектов со сборками .NET 688 Атрибут ObjectPooling 688 Интерфейс ServicedComponent • • • ■ 689 Использование активизации ЛТ со сборками .NET • 689 Заключение • • 690 Глава 21 Графические возможности GDI+ 691 Основные принципы рисования 693 GDI и GDI+ 693 Контексты устройств и объект Graphics- • • 694 Пример: рисование контуров 695 Рисование фигур с помощью OnPaint 697 Использование области вырезания 698 Измерение координат и областей- 700 Point и PointF 700 Size и SizeF 701 Rectangle и RectangleF- • • . . 702 Region 703 Замечание об отладке 704 Изображение прокручиваемых окон ■ • • -705 Координаты мировые, страницы и устройства ■ • • • 709 Цвета 710 Значения красный-зеленый-синий (RGB) ■ • .... 710 Именованные цвета 711 Режимы вывода графики и палитра безопасности 711 Палитра безопасности .... 712 Перья и клети • ■ • • 712 Кисти -713 Перья 713 Рисование фигур и линий- • • 714 Вывод изображений 716 Вопросы, возникающие при манипуляциях с изображениями 717 Рисование текста • • ... 718 Простой пример с текстом • • • • • 719 Шрифты и семейства шрифтов • • 720 Пример: перечисление семейств шрифтов 721 Редактирование текстового документа: пример CapsEditor 723 Метод Invalidate!) 727 Вычисление размеров объектов и размера документа 728 OnPaint() 729 Преобразования координат 730 Ответ на ввод пользователя .... 731 Печать -734 Заключение 735 Глава 22 Доступ в Интернет 737 Класс WebClient 737 Загрузка файлов ... • 738 Пример: базовый клиент Web • ■ • 738 Выгрузка файлов 739
xxxil Содержание Классы WebRequest 739 Другие свойства WebRequest и WebResponse 740 Отображение выходных данных в виде страницы HTML ..■•... . . 740 Иерархия классов WebRequest и WebResponse -742 Служебные классы . • • • 743 URI -743 Пример вывода страницы 744 Адреса IP и имена DNS • • • -745 Классы .NET для адресов IP • 745 Пример: DnsLookup • 746 Протоколы нижнего уровня 747 Классы нижнего уровня • • • • 748 Заключение 748 Глава 23 Создание распределенных приложений с помощью .NET Remoting 749 Что такое .NET Remoting 749 Web Services Anywhere -750 CLR Object Remoting -750 Обзор .NET Remoting • 750 Контексты 752 Активизация- ■ • • • 753 Атрибуты и свойства .... 753 Коммуникация между контекстами • 754 Удаленные объекты, клиенты и серверы ■ 754 Удаленные объекты • • • • • ■ 754 Простой сервер • • • 755 Простой клиент • • • • 756 Архитектура .NET Remoting • ■ 757 Каналы 757 Задание свойств канала • • • 759 Подключаемость канала- • 759 Форматтеры- • • • ■ ■ ■ • ■ • • 759 ChannelServices и RemotingConfiguration . . 760 Сервер для активизированных клиентом объектов -761 Активизация объектов ■ • 761 URL-приложения - • -761 Активация хорошо известных объектов • • ■ 761 Активизация объектов, акшвизированных клиентом 762 Объекты прокси 763 Сообщения 763 Приемники сообщений- • ■ • 764 Уполномоченный приемник ■ ■ • 764 Приемник серверного контекста . . 764 Объектный приемник- 764 Передача объектов в удаленные методы 764 Направляющие атрибуты ■ ■ • ■ . 767 Управление временем жизни 767 Обновление аренды ■ ■ ..... 768 Классы, используемые для управления временем жизни ■ • • .... 768 Пример: получение информации об аренде • • 768 Изменение используемых по умолчанию конфигураций аренды • ■ ■ . . 769 Конфигурационные файлы ■ ... . . 769 Конфигурация сервера для хорошо известных объектов • ... ... 771 Конфигурация клиента для хорошо известных объектов ■■■•••■ 771 Серверная конфигурация для активизированных клиентом объектов • • • • 772 Клиентская конфигурация для активизированных клиентом объектов • 772 Серверный код, использующий конфигурационные файлы 772 Клиентский код, использующий конфигурационные файлы ■ 773 Службы времени жизни в конфигурационных файлах • • 773 Инструменты для файлов удаленной конфигурации ••.-•■ 774 Приложения хостинга • 774 Хостинг удаленных серверов в ASP.NET 774 Классы, интерфейсы и SOAPSuds 775
Содержание xxxiii Интерфейсы . . . . . 775 SOAPSuds 775 Отслеживание служб • • • 776 Асинхронная удаленная работа 777 Атрибут OneWay • 778 Удаленное выполнение и события 778 Удаленный объект 779 Аргументы событий 780 Сервер • • • 780 Приемник событий- 781 Клиент 781 Выполнение программы- • • 782 Контексты вызова 783 Заключение 784 Глава 24 Службы Windows 785 Понятие службы • ■ • ■ • . . 785 Архитектура • ... ... 786 Служебная программа . 786 Управляющий менеджер служб ■ -787 Служебная управляющая программа 787 Конфигурационная программа службы ■ 788 Пространство имен System.ServiceProcess • ■ 788 Создание службы • • 788 Библиотека классов, использующая сокеты ■ 789 Пример TcpClient 791 Проект Windows Service 793 Класс ServiceBase • • - ■ -794 Потоки выполнения и службы ■ 797 Установка службы • 797 Программы установки 797 Класс Installer 798 Классы ServiceProcesslnstaller и Servicelnstaller • • • • • 799 ServicelnstallerDialog -800 InstallUtil 801 Клиент ■ • • 801 Мониторинг и управление службой ■ -801 Консоль управления Microsoft (MMC) ■ • 802 net.exe- • 802 scexe -803 Server Explorer 804 Класс ServiceController • 804 Управление службой 807 Поиск неисправностей • • -808 Интерактивные службы- • 808 Регистрация событий • • ■ • 809 Архитектура регистрации событий- • 810 Классы регистрации событий ■ ■ ■ 810 Добавление регистрации событий -811 Трассировка 811 Создание приемника событий • • 812 Мониторинг производительности • 813 Классы мониторинга производительности 813 Построитель счетчиков производительности 813 Добавление счетчиков производительности 814 perfmon.exe ■ • 816 Служба счетчика производительности ■ 817 Свойства служб Windows 2000 817 Изменения сетевого соединения и события электропитания 817 Восстановление 818 Приложения СОМ+ в роли служб -819 Заключение '• • • 819 1 Зак. 69
xxxiv Содержание Глава 25 Система безопасности .NET 821 Система безопасности доступа к коду 821 Группы кода ... 822 CaspoLexe — утилита политики системы безопасности доступа к коду 823 Полномочия доступа к коду и множества полномочий 827 Просмотр полномочий сборки • 829 Уровни политики: машина, пользователь и предприятие •■•-••• 831 Поддержка безопасности в .NET . -832 Требуемые полномочия 833 Запрашиваемые полномочия 835 Неявное полномочие 837 Отказ от полномочий 837 Заявляемые полномочия 838 Создание полномочий доступа к коду • • 839 Декларативная безопасность ... 839 Система безопасности на основе ролей 840 Принципал • 840 Принципал Windows • • • 841 Роли 842 Система безопасности на основе декларативной роли • • • 842 Управление политикой системы безопасности 843 Конфигурационный файл системы безопасности 844 Управление группами кода и полномочиями 847 Включение и выключение системы безопасности 847 Восстановление политики системы безопасности ■ - - • ■ 847 Создание группы кода . 847 Удаление группы кода... 848 Изменение полномочий группы кода 848 Создание и применение множеств полномочий • • 849 Распространение кода с помощью строгого имени • 850 Распространение кода с помощью сертификатов 851 Управление зонами ... -856 Заключение ■ 858 Приложение А С# для разработчиков C++ 859 Приложение В С# для разработчиков Java 901 Приложение С С# для разработчиков VB6 947 Приложение D Параметры компиляции С# 989 С# Сегодня 993
г Л '/ ^ а в <\ С- а ^ ^ Архитектура .NET На протяжении всей книги говорится о том, что язык С# должен рассматриваться совме- совместно с платформой .NET. Компилятор С* специально предназначен для .NET, т.е. код, написанный на С#, будет всегда исполняться на платформе .NET. Этим определяются два важных свойства языка С#: О Архитектура и методология С# отражают архитектуру и методологию .NET. На- Например, С# основывается на одиночном наследовании классов и имеет систему типов, которая базируется на различии между типами по ссылке и по значению, так же как и в случае .NET. О Во многих случаях специфические особенности языка С# зависят от особенно- особенностей .NET или ее базовых классоп. Так, все основные типы данных в С# (int, float, jtring и т.д.) на самом деле являются основными типами классов .NET, которые С# лишь представляет с использованием более удобного синтаксиса. Так что прежде чем начать программировать на С#, следует получить некоторое представление об архитектуре и методологии .NET. Это и является целью настоящей главы. Начнем с обзора компонентов .NET и выясним, как .NET работает с кодом, рассмот- рассмотрев, что происходит с предназначенным для .NET кодом в процессе компиляции и испол- исполнения. После этого подробно опишем промежуточный язык Microsoft (MSIL, часто сокращается до 1L — Intermediate Language), в который компилируется весь код. В част- частности, обсудим, как IL совместно с общей системой типов (CTS) и общей специфика- спецификацией языков (CLS) обеспечивает совместимость языков. Наконец, изучив подробно некоторые компоненты .NET, сделаем заключение о том, как различные языки, включая С# и существующие языки VB и C++, сочетаются со всем тем, о чем мы уже говорили. Обзор .NET Для упрощения рассуждений будем полагать, что платформа .NET — это среда, которую видит код в процессе исполнения. Это означает, что .NET занимается исполнением кода: запускает его, дает ему соответствующие права, выделяет память для хранения данных, помогает с освобождением памяти и ресурсов, которые больше не требуются, и т.д. По- Помимо этого .NET предоставляет обширную библиотеку классов, так называемую библио- библиотек)- базовых классов .NET, для выполнения большого числа задач Windows. В этом плане .NET можно рассматривать с двух сторон: как управляющую выполнением кода и предоставляющую коду различные службы. Как упоминалось ранее, .NET располагается между кодом и Windows, позволяя ей предоставлять требуемые службы. Однако прежде чем углубиться в тему, дадим некоторые определения и термины, связанные с .NET. □ Среда исполнения .NET (.NET runtime), также известная как общая среда ис- исполнения или CLR,— это то, что собственно управляет кодом. Можно рассмат- рассматривать ее как код, который обеспечивает загрузку программы, ее выполнение и предоставление всех необходимых служб.
Глава 1 О Управляемый код (managed code). Любой код, который разработан для исполне- исполнения в .NET, называется управляемым. Код, который работает под управлением Windows, называется неуправляемым. О Промежуточный язык (IL). Это тот самый язык, на котором должен быть напи- написан код, загружаемый и запускаемый средой исполнения .NET. При компиляции управляемого кода компилятор генерирует код на промежуточном языке, a CLR выполняет заключительную стадию компиляции непосредственно перед испол- исполнением кода. Язык IL разработан таким образом, чтобы обеспечить быструю ком- компиляцию в машинный код, но в то же время он поддерживает все особенности .NET. О Общая система типов (Common Type System, CTS). Для обеспечения совмести- совместимости языков необходимо иметь согласованный набор основных типов данных, чтобы все языки могли быть стандартизированы. Этим целям и служит CTS. Кро- Кроме того, CTS предоставляет правила для определения пользовательских классов. О Базовые классы .NET. Это обширная библиотека классов, которая содержит код для выполнения большого числа задач в Windows, начиная от отображения окон и форм и заканчивая базовыми службами Windows, чтением и записью в файлы, доступом к сети и Интернету и доступом к источникам данных. □ Сборка. Это модуль, в котором хранится компилированный управляемый код. Она похожа на классический исполняемый файл или DLL, но имеет важное свой- свойство — она полностью себя описывает. Сборки содержат метаданные, которые включают в себя сведения о сборке и обо всех определенных внутри нее типах, методах и т.п. Сборка может быть частной (доступной только одному приложению) и разделяемой (доступной любому приложению Windows). а Кэш сборок. Место на диске, где хранятся разделяемые сборки. О Общая спецификация языка (Common Language Specification, CLS). Это мини- минимальный набор стандартов, который гарантирует, что доступ к коду может быть осуществлен из любого языка. Все компиляторы, предназначенные для .NET, дол- должны поддерживать CLS. CLS формирует подмножество функций, доступных в .NET и IL, и полезна для кода, использующего особенности вне компетенции CLS. Если в сборке присутствуют какие-либо не CLS особенности, они могут быть недоступны в некоторых языках. О Отражение. То, что сборки полностью себя описывают, открывает теоретиче- теоретическую возможность программного доступа к метаданным сборки. На самом деле су- существует несколько базовых классов, которые разработаны с этой целью. Технология известна как отражение (возможно, из-за того интригующего факта, чти программа может использовать эту технологию для доступа к собственным метаданным). О Компиляция Just-In-Time (JIT). Этот термин обозначает процесс выполнения за- заключительной стадии компиляции с промежуточного языка в машинный код. На- Название определяется тем, что части кода компилируются по мере необходимости. О Манифест. Область сборки, содержащая метаданные. О Область приложения. Это способ, с помощью которого CLR позволяет различ- различным программам исполняться в одном и том же пространстве процессов. Изоля- Изоляция модулей кода достигается путем использования безопасности типов IL для проверки того, что каждый сегмент кода ведет себя правильно. О Сборка мусора. CLR применяет этот способ для очистки памяти, которая больше не требуется приложению. Таким образом, приложение не несет ответственно- ответственности за освобождение памяти. Процессы, происходящие при компиляции и запуске программы Для получения представления о том, как работает .NET и какие службы она предлагает, рассмотрим, что происходит при запуске программы, разработанной для среды исполне- исполнения .NET. Предполагается, что приложение состоит из основного кода, написанного на С#, и библиотеки, созданной с помощью VB.NET. Приложению необходимо общаться с существующим СОМ-компонентом, и подразумевается, что оно будет использовать
Архитектура .NET некоторые из базовых классов .NET (практически невозможно создать приложение, ко- которое будет делать что- нибудь полезное, но при этом не будет использовать базовые классы .NET): Исходный код С# Исходный код VB.NET КОМПИЛЯЦИЯ Сборка, содержащая IL-код Языковая совместимость за счет CTS и CLS ИСПОЛНЕНИЕ ПРОЦЕСС Область приложений -КОД ИСПОЛНЯЕТСЯ ЗДЕСЬ Сборка, содержащая IL-код CLR организует: JIT-компиляция Выданы разрешения безопасности Проверена безопасность по типу памяти Создает область приложения Сборщик мусора очищает ресурсы Службы совместимости с СОМ Базовые классы .NET Загруженные сборки Существующий компонент СОМ На диаграмме прямоугольники показывают основные компоненты, связанные с компиляцией и исполнением программы, а стрелки — исполняемые задачи. В верх- верхней части рисунка представлен процесс раздельной компиляции каждого проекта в сборку. Две сборки способны взаимодействовать друг с другом благодаря свойствам со- совместимости языков .NET. Нижняя часть диаграммы демонстрирует процесс JIT-компи- ляции из IL в сборках в машинный код, который выполняется в области приложения внутри процесса. Показаны некоторые действия, которые выполняет код внутри CLR для достижения этой цели. Компиляция Перед запуском программа должна быть откомпилирована. Однако, в отличие от преж- прежних исполняемых файлов и DLL, теперь компилированный код программы не содержит инструкций ассемблера. Вместо этого он содержит инструкции на промежуточном языке Microsoft (MSIL или IL). Промежуточный язык в чем-то похож на байт-код Java. Это низ- низкоуровневый код, который может быть быстро преобразован (откомпилирован JIT) в родной машинный код.
Глава 1 Пакет, внутри которого будет содержаться откомпилированная программа, состоит из некоторого числа сборок. Каждая сборка содержит код на промежуточном языке и метадан- метаданные, описывающие типы данных и методы внутри сборки. Метаданные содержат также: простой хэш, который строится на основе содержимого сборки и может быть использован для проверки ее целостности; информацию о версиях; сведения о том, какие сборки будут вызываться данной сборкой; и, возможно, информацию о том, какие привилегии потребуются для выполнения кода сборки. Прежний программный продукт состоял бы из исполняемого файла, содержащего точку входа основной программы, и одной или нескольких библиотек или компонентов СОМ. Продукт для .NET состоит из некоторого числа сборок, одна из которых является исполняемой и содержит точку входа основной программы, а другие представляют со- собой библиотеки. В нашем случае имеются всего две сборки: исполняемая, содержащая код на С#, и библиотека, содержащая компилированный код VB.NET. Исполнение Во время выполнения программы среда исполнения .NET загружает первую сборку, ту, что содержит точку входа основной программы. Среда использует хэш для проверки це- целостности сборки и метаданные для того, чтобы просмотреть определенные типы и убе- убедиться, что среда сможет выполнить сборку. Правильно разработанные коммерческие приложения должны явно указывать, какие привилегии .NET им могут потребоваться (например, понадобится ли приложению доступ к файловой системе, реестру и т.д.). В этом случае CLR обратится к политике безопасности системы и к учетной записи, под ко- которой выполняется программа, и проверит, может ли она предоставить необходимые привилегии. Если код не запрашивает права явно, они будут предоставлены ему по пер- первому требованию, если, конечно, это возможно. На этом этапе CLR выполнит еще одно действие для проверки так называемой безо- безопасности кода по типу памяти (memory type safety). Код считается безопасным по типу памяти только в том случае, если он обращается к памяти способами, которые может контролировать CLR. Безопасный по типу памяти код гарантированно не будет пытать- пытаться прочесть или записать в память, не принадлежащую ему. Это важно, так как .NET име- имеет механизм (так называемые области приложений), позволяющий нескольким приложениям выполняться в одном процессе. При этом необходимо гарантировать, что никакое приложение не будет пытаться обратиться к памяти другого приложения. Если CLR не будет уверена в том, что код безопасен по типу памяти, то в зависимости от мест- местной политики безопасности она может даже отказать в исполнении кода. Затем CLR выполняет код. Она создает процесс, в котором будет исполняться код, и отмечает область приложения, в которой размещается главный поток приложения. В не- некоторых случаях программа может потребовать поместить ее в уже имеющийся процесс запущенного ранее кода, тогда CLR создаст для нее только новую область приложения. CLR берет первую часть кода, которая требуется для исполнения, и компилирует ее с промежуточного языка на язык ассемблера, после чего выполняет ее из соответствующего потока программы. Каждый раз, когда в процессе исполнения встречается новый метод, не исполнявшийся ранее, он компилируется в исполняемый код. Процесс компиляции проис- происходит только один раз. Как только метод откомпилирован, его адрес заменяется адресом компилированного кода. Таким образом, производительность не ухудшается, так как ком- компилируются только те участки кода, которые действительно используются. Этот процесс носит название компиляции just-in-time. Отметим, что JIT-компилятор может в зависимости от параметров компиляции, указанных в сборке, оптимизировать код в процессе компиля- компиляции, например, путем подстановки некоторых методов (inline) вместо их вызовов. Во время выполнения кода CLR следит за использованием памяти. На основе этих наблюдений она в определенные моменты будет останавливать программу на короткий промежуток времени (обычно миллисекунды) и запускать сборщика мусора, который проверит переменные программы и выяснит, какие из областей памяти активно исполь- используются программой, для того, чтобы освободить неиспользуемые участки. CLR также производит загрузку сборок по мере необходимости, в том числе СОМ- компонентов, используя службы .i'JET COM interop. Преимущества исполнения программы как управляемого кода Из приведенного описания уже очевидны некоторые преимущества исполнения управ- управляемого кода:
Архитектура .NET О Прежде всего улучшается безопасность. Благодаря тому, что сборка написана на промежуточном языке, среда исполнения .NET может проверить, что собирается делать код. Реальное преимущество здесь в том, что становится более безопасно выполнять код, полученный, например, по Интернету. Политика безопасности .NET для этого кода устанавливается таким образом, чтобы он получал права на выполнение лишь тех задач, которые он предположительно должен выполнять. .NET поддерживает как функциональную безопасность (базирующуюся на сущ- сущности процесса, исполняющего код), так и безопасность на основе кода (базиру- (базирующуюся на степени доверия к самому коду). О Присутствие сборщика мусора освобождает программиста от необходимости на- написания кода для освобождения используемой памяти. Это также означает, что отсутствует риск утечки памяти для всех переменных, обслуживаемых сборщи- сборщиком мусора. О Новая концепция областей приложений означает, что различные приложения, которые необходимо изолировать друг от друга, но которые в то же время дол- должны общаться друг с другом, могут быть размещены в одном процессе. Это дает огромный выигрыш в производительности. Необходимо отметить также следующие преимущества: О В приведенном примере есть участки кода, исходный текст которых был написан как на С#, так и HaVB.NET. Это означает, что имеет место совместимость языков, которая позволяет легко использовать код, написанный на разных языках. О Самоописываемая структура сборок практически исключает ошибки, которые возникают из-за проблем версий или проблем, связанных с перезаписыванием другими приложениями совместно используемых сборок. Тем самым значительно экономятся время и стоимость разработки. □ Базовые классы .NET предлагают интуитивную, простую объектную модель, кото- которая заметно упрощает вызовы функциональных возможностей Windows (в смысле упрощения исходного кода) по сравнению с тем, что было в прошлом. На первый взгляд кажется, что будет иметь место потеря производительности, так как часть процесса компиляции осуществляется во время исполнения программы. Microsoft возражает тем, что в долгосрочном периоде это не будет являться проблемой, a JIT-kom- лиляторы не ухудшают, а даже улучшают производительность. Этот вопрос подробно обсуждается ниже, в общих же словах, улучшение происходит потому, что JIT-компиля- тор знает, на каком процессоре будет исполняться код. Эта информация недоступна для традиционных компиляторов. Промежуточный язык Промежуточный язык и Java в своей основе имеют одну и ту же идею: это языки низкого уровня с простым синтаксисом (основанным на числовых кодах, а не на тексте), кото- который может быть быстро оттранслирован в родной машинный код. Напомним, что целью байт-кода Java является обеспечение платформенной независимости. Обладание четко определенным, универсальным синтаксисом байт- кода означало бы, что файл, содержа- содержащий инструкции байт-кода, мог бы быть размещен на любой платформе, например UNIX, Linux или Windows, а во время выполнения могла бы быть легко выполнена за- заключительная стадия компиляции, и код мог бы быть запущен на этой платформе. Следовательно, при написании исходного кода его можно было бы компилировать в байт-код Java, который может быть выполнен где угодно. Промежуточный язык использует ту же концепцию, но более амбициозно. Важно то, что промежуточный язык компилируется в процессе выполнения программы, в то время как байт-код Java интерпретируется. Это означает, что большая часть потерь производи- производительности, связанных с интерпретацией байт-кода Java, не затрагивает IL. Код на промежуточном языке компилируется в родной машинный код, а не интерпретируется. I Целью промежуточного языка является не просто платформенная независимость, а языковая независимость в объектно-ориентированной среде. Идея заключается в том, что должна существовать возможность компиляции кода с любого языка, и компилиро- компилированный код должен быть совместим с кодом, откомпилированным с других языков.
Глава 1 Эта совместимость достигается в .NET, так как с ее помощью можно писать код, ко- который компилируется в промежуточный язык, на C++, VB.NET или С#. Компиляторы для других языков, включая COBOL, Eiffel и Perl, должны быть вскоре выпущены сто- сторонними производителями. В настоящее время платформенная независимость являет- является только возможной, так как во время создания среда .NET была доступна лишь для Windows, хотя сейчас ведутся активные разговоры о том, чтобы портировать .NET на другие платформы. Из-за требований языковой независимости и совместимости про- промежуточный язык гораздо сложнее байт-кода Java. Конечно, языковая независимость имеет некоторые практические ограничения. В ча- частности, промежуточный язык неизбежно реализует некоторую определенную методоло- методологию программирования, а это означает, что языки, целью которых является IL, должны быть совместимы с этой методологией. Конкретный путь, который был выбран Microsoft, заключается в следовании классическому объектно-ориентированному программирова- программированию с классами и наследованием. Поэтому классы и наследование определены внутри промежуточного языка. Ниже кратко рассматриваются характеристики промежуточного языка. Традиционное объектно-ориентированное программирование Для того чтобы понять принципы промежуточного языка, необходимо знать принципы классического объектно-ориентированного программирования (ООП). Эти принципы известны программистам на Visual C++ иJava/J++ (так как оба языка основаны на концеп- концепциях ООП), но будут в новинку программистам на Visual Basic. Классическое объектно-ориентированное программирование представляет собой тему отдельного большого разговора и подробно рассматривается в главах 4 и 5. Ниже дается лишь информация, необходимая для понимания материала этой главы. Идея классического объектно-ориентированного программирования состоит в том, что код организуется в виде классов. Для разработчиков VB: представьте себе, что класс это нечто типа модуля класса, или легковесного компонента СОМ, или элемента управле- управления ActiveX, который в большинстве случаев не имеет пользовательского интерфейса. Каждый класс определяет некоторый отдельный модуль, с которым можно выполнять какие-то действия. Например, в GUI Windows классы могут включать в себя ListBox, TextBox или Form. Классы в некотором смысле могут рассматриваться как типы данных, например, целое или число с плавающей точкой. Различие в том, что класс является более сложным, он со- состоит из нескольких простых типов данных, хранящихся вместе, а также функций (или методов), которые могут быть вызваны для этого класса. Класс является "типом данных пользователя" еще и потому, что его можно определить в исходном коде. Объектно-ори- Объектно-ориентированное программирование основано на идее, что приложение будет создавать эк- экземпляры объектов, а затем вызывать некоторые из их методов, так что в конце концов объекты начнут взаимодействовать друг с другом. Классическое объектно-ориентированное программирование, однако, включает в себя большее. Помимо всего прочего, оно зависит от концепции наследования реализации. Наследование реализации — это методика, которая позволяет повторно использо- использовать в классе особенности другого класса. При определении класса можно объявить, что он унаследован от другого класса, называемого базовым. Определяемый класс автома- автоматически приобретает все данные и методы, описанные в базовом классе. После этого в производный класс можно добавлять методы или заменять их новыми реализациями. В приложении С приводится пример, в котором участвует класс Employee, содержащий информацию о работниках компании. Класс может использоваться для расчета суммы денег, получаемой каждым работником в конце месяца. Допустим, что метод расчета зарплаты менеджеров немного отличается, так как они имеют дополнительные доходы. Для учета этого создается дополнительный класс Manager, который является производ- производным от Employee, но в котором заменен метод, ответственный за расчет ежемесячных выплат. Программы, которые пишутся с применением объектно-ориентированных язы- языков, таких как C++ или Java, обычно разрабатываются с использованием наследования, и включают в себя определения целых иерархий классов, наследуемых друг от друга. Промежуточный язык поддерживает так называемое одиночное наследование, т.е. класс может быть напрямую унаследован только от одного класса, но число производ- производных классов от данного не ограничено (базовый класс, от которого производится насле- наследование, в свою очередь может быть производным от другого класса). В конечном итоге
Архитектура .NET образуется древовидная структура классов. В случае промежуточного языка все классы являются производными от класса, известного как Obj ect. Некоторые из базовых клас- классов .NET показаны на рисунке. Заметим, что чем ниже мы спускаемся по иерархии клас- классов, тем более узко определены конкретные классы. ["Object | | MarshallByRefObject | ScrollBar |- ►JMarshallByRefComponentU f ■ \ / Control \ [StatusBar] ' | ButtonBase | | RichControl 'l t | Button | | ScrollableControl i ContainerControl | Form | FormatControl TextBoxBase \ | TextBox RichTextBox Помимо классического объектно-ориентированного программирования, промежуточ- промежуточный язык использует идею интерфейсов, которые были впервые реализованы в Windows с применением СОМ. Интерфейсы .NET — это не то же самое, что интерфейсы СОМ: им не требуется поддерживать инфраструктуру СОМ, в частности, они не наследуются от Iunknown и не имеют связанных GUID. Однако в их основе лежит общая с СОМ идея, за- заключающаяся в том, что классы, реализующие данный интерфейс, обязаны содержать реализации методов и свойств, которые определены этим интерфейсом. Типы по ссылке и по значению Как и любой другой язык программирования, промежуточный язык имеет некоторое число заранее определенных простых типов данных. Одним из свойств промежуточного языка является то, что он различает типы по ссылке и по значению. Типы по значению — это типы, для которых переменная хранит свои данные непосредственно в памяти, в то время как переменная ссылочного типа содержит адрес участка памяти, в котором хра- хранятся данные. Эта модель в значительной степени повторяет модель типов по ссылке и значению, используемую в Java. В терминологии C++, ссылочные типы могут рассматриваться как способ доступа к пе- переменной посредством указателя, а наилучшей аналогией для ссылочных типов Visual Ba- Basic будут Objects (объекты), доступ к которым в VB всегда осуществляется по ссылке. Промежуточный язык содержит также спецификации, касающиеся хранения данных: эк- экземпляры типов по ссылке всегда хранятся в области памяти, известной как куча, в то время как типы по значению обычно хранятся в стеке, хотя в случае, если типы по значе- значению объявлены как поля внутри типов по ссылке, они будут храниться в куче (см. главу 5). Строгий контроль типов Одним из аспектов промежуточного языка является то, что он основан на строгом контроле типов. Это означает, что каждая переменная имеет один определенный тип данных (в про- промежуточном языке нет, например, типа данных Variant, как в Visual Basic и языках сцена- сценариев). В частности, промежуточный язык обычно не допускает выполнения операций, которые могут привести к различной трактовке того, на какой тип данных ссылается та или иная ссылка. Разработчикам на C++ приходится постоянно преобразовывать тип указателя. Это имеет большое значение для производительности, но нарушает безопасность типов. Поэ- Поэтому такая операция допускается только при определенных обстоятельствах лишь в неко- некоторых языках, предназначенных для создания управляемого кода. В частности, указатели
8 Глава 1 (в противоположность ссылкам) разрешены только в некоторых фрагментах кода в С#, но никак не в VB. Использование указателей в коде немедленно нарушит проверку безо- безопасности типов памяти, выполняемую CLR. И хотя требование безопасности типов ухуд- ухудшает производительность, все же в большинстве случаев выгоды, получаемые от использования служб, предоставляемых .NET (например, области приложений), и осно- основывающиеся как раз на безопасности типов, будут перекрывать это ухудшение. Точно так же разработчикам на VB не придется думать о типах переменных, посколь- поскольку VB автоматически выполнит все необходимые преобразования. Если в каком-то фраг- фрагменте кода вместо типа String будет передан тип Integer, VB преобразует Integer в String. Философия IL и .NET состоит в том, что удобство неявных преобразований пе- перевешивается связанными с этим проблемами безопасности типов и, в частности, по- потенциальными трудноуловимыми ошибками времени выполнения, возникающими из-за неверных типов данных. Поэтому при написании кода, предназначенного для .NET, эти преобразования придется целенаправленно выполнять вручную. Существует несколько серьезных причин, по которым важен строгий контроль типов,— на самом деле некоторые части .NET не будут без него работать. D В первую очередь среда исполнения общего языка полагается на способность проверки кода для определения того, какие операции необходимо выполнить пе- перед непосредственным запуском кода. Это важно как с точки зрения предоставле- предоставления привилегий доступа, так и с точки зрения проверки того, что код не может повредить другой код, который выполняется в другой области приложений, но в том же адресном пространстве. Промежуточный язык был разработан для облег- облегчения проверок, и совершенно очевидно, что если бы присутствовали какие-то сомнения насчет типов данных в коде, такие проверки не были бы возможны. □ Для того чтобы выяснить, какую память надо освободить, сборщик мусора дол- должен уметь определять тип данных, содержащийся в каждой ячейке памяти (иначе он не сможет узнать, какой объем памяти занимает переменная). Опять же, лю- любые неоднозначности в типах данных вызовут проблемы, и среда исполнения .NET начнет работать неправильно. D Совместимость языков, одна из основ платформы .NET, базируется на хорошо определенном и цельном наборе типов данных. Система типов, используемая промежуточным языком, известна как общая система типов (CTS) (см. ниже). Свойства IL: подведение итогов К основным характеристикам промежуточного языка относятся: О Объектно-ориентированный подход с одиночным наследованием классов D Интерфейсы D Типы по ссылке и значению D Обработка ошибок с помощью исключений D Система строгого контроля типов Любой язык, который рассчитан на платформу .NET, должен поддерживать эти кон- концепции. Что касается существующих языков, то это не является проблемой для C++, од- однако означает, что определение Visual Basic должно быть улучшено для поддержки этих требований. Новая версия Visual Basic, VB.NET, существенно отличается от предыдущей версии (VB6). С# изначально разрабатывался с учетом всех требований промежуточного языка, поэтому он полностью поддерживает указанные концепции. Совместимость языков Работа с .NET означает, что код компилируется в промежуточный язык и что необходи- необходимо программировать с использованием традиционного объектно- ориентированного подхода. Однако этого недостаточно для обеспечения совместимости языков. В конце концов, C++ и Java используют одинаковые объектно-ориентированные парадигмы, од- однако не считаются совместимыми. В этом разделе рассматриваются межъязыковые возможности, предоставляемые .NET. В значительной степени они основываются на использовании общей системы типов и общей спецификации языка.
Архитектура .NET Прежде всего возникает вопрос: что понимается под совместимостью языков? В кон- конце концов, СОМ позволяет компонентам, написанным на разных языках, работать вмес- вместе, вызывая методы друг друга. Чем нас не устраивает этот вариант? СОМ, будучи по сути двоичным стандартом, разрешает компонентам создавать экземпляры других ком- компонентов и вызывать их методы и свойства, не беспокоясь о том, на каком языке они на- написаны. Чтобы добиться этого, экземпляр каждого объекта должен создаваться посредством среды исполнения СОМ, а доступ к нему должен осуществляться при помо- помощи интерфейса. Этот факт, а также зависимость от потоковой модели соответствующе- соответствующего компонента могли вызывать значительные потери производительности, связанные с переносом данных (marshalling) между моделями и с выполнением компонентов в раз- разных потоках. В предельном случае, когда компоненты размещаются в исполняемых фай- файлах, а не в DLL, необходимо создавать отдельные процессы для их запуска. Ударение сделано на том, что компоненты могут общаться друг с другом, но только в рамках среды исполнения СОМ. Не важно, общаются ли напрямую СОМ- компоненты, написанные на разных языках, или создают экземпляры друг друга — это всегда происходит при участии СОМ в качестве посредника. Кроме того, архитектура СОМ не позволяет применять наследование реализаций, что означает потерю многих преимуществ объектно-ориен- объектно-ориентированного программирования. Связанной с этим проблемой является то, что компоненты, написанные на разных язы- языках, приходится отлаживать по отдельности. В отладчике невозможно переключаться между языками. Таким образом, под совместимостью языков понимается то, что классы, созданные на одном из языков, могут напрямую общаться с классами, созданными в другом языке. В частности: D Класс, написанный на одном языке, должен иметь возможность наследования от класса, созданного в другом языке. D Класс должен иметь возможность содержать экземпляр другого класса, не важно, на каком языке он написан. Объект должен иметь возможность напрямую вызы- вызывать методы другого объекта независимо от того, на каких языках они написаны. О Должна существовать возможность передачи объектов (или ссылок на объекты) между методами. О При вызове методов между языками должно быть позволено перешагивать через методы в отладчике, даже если это означаег переход между кодами, написанны- написанными на разных языках. Все это является крайне амбициозной целью, но .NET и промежуточный язык до- достигли этого. Возможность перешагивания между методами в отладчике предоставляет- предоставляется Visual Studio.NET, а не самой средой исполнения .NET. В дальнейшем этот факт не будет комментироваться, потому что Visual Studio.NET теперь поддерживает единую среду разработки для всех языков .NET, в том числе для C++. Остальные аспекты совмес- совместимости языков достигаются путем использования общей системы типов. Общая система типов (CTS) Общая спецификация типов означает, что промежуточный язык содержит богатый на- набор предопределенных типов данных. Они организованы в иерархию типов, характер- характерную для сред объектно-ориентированного программирования. Важность для совместимости языков Причина, по которой важна общая спецификация типов, заключается в том, что если класс наследуется от другого класса или содержит экземпляры других классов, то он дол- должен знать, какие типы данных используются в других классах. На самом деле именно от- отсутствие в прошлом какой-либо общей системы для определения этой информации и являлось тем барьером, который не позволял осуществлять наследование между языка- языками. Этот тип информации не представлялся в стандартном исполняемом файле или DLL. В некотором смысле проблема получения доступа к информации о типах решается по- посредством метаданных в сборках. Предположим, что пишется класс на С# и он должен быть унаследован от класса, написанного на VB.NET. Для этого необходимо указать ком- компилятору, в какой сборке определен этот класс VB.NET. Компилятор может использовать метаданные из этой сборки для выяснения того, какие методы, свойства, поля и т.п. со- содержит класс VB.NET. Очевидно, что компилятору необходима эта информация для компиляции исходного кода. Для разработчиков на C++ это является аналогом включе- включения заголовочного файла. Одной из целей заголовочных файлов в C++ можно считать предоставление информации о доступных типах исходным файлам.
10 Глава 1 Однако компилятору требуется больше информации, нежели могут предоставить ме- метаданные. Предположим, что один из методов класса VB.NET возвращает тип Integer — один из стандартных типов VB.NET. С# не имеет типа данных с таким именем. Очевид- Очевидно, что создавать производный класс, использовать этот метод и возвращаемое им зна- значение можно только в том случае, если компилятор знает, как привести значение типа Integer VB.NET к некоторому известному типу, определенному в С#. Это возможно, поскольку общая спецификация типов описывает предопределенные типы данных, доступные в промежуточном языке, так что все языки, предназначенные для .NET, будут генерировать код, основанный на этом наборе типов. В нашем примере тип Integer VB на самом деле представляет собой 32- разрядное целое со знаком, кото- которому соответствует тип Int32 промежуточного языка. Таким образом, в промежуточном коде переменная будет иметь именно этот тип данных. Так как компилятор С# осведом- осведомлен об этом типе, проблемы не возникает. На уровне исходного кода С# обращается к Int32 с помощью ключевого слова int, и компилятор будет трактовать метод VB.NET как возвращающий значение типа int. Иерархия CTS Общая система типов не только специфицирует простые типы данных, но и описывает об- обширную иерархию типов с четким определением позиций, в которых код имеет право определять свои собственные типы. Иерархическая структура общей системы типов отра- отражает объектную методологию промежуточного языка и выглядит примерно следующим образом: Тип Тип по ссылке Тип по значению Встроенные типы по значению Интерфейсные типы Типы-указатели Самоописываемые типы Перечисления Определенные пользователем типы по значению Типы с классами Делегаты Массивы Упакованные типы по значению Определенные пользователем типы по ссылке В этом дереве типы представляют собой: Тип Массивы Упакованные типы по значению Встроенные типы по значению Типы с классами Значение Любой тип, который содержит массив объектов. Тип по значению, который временно приведен к типу по ссылке так, что он может храниться в куче. Большинство стандартных базовых типов, которые представляют собой числа, значения Boolean или символы. Типы, описывающие сами себя, но не являющиеся массивами.
Архитектура .NET 11 Тип Делегаты Перечисления Интерфейсные типы Типы-указатели Тип по ссылке Самоописываемые типы Тип Определенные пользователем типы по значению Определенные пользователем типы по ссылке Тип по значению Значение Типы, разработанные для хранения ссылок на методы. Наборы перечисляемых значений, в которых каждое значение представляется меткой, но хранится как числовой тип. Например, если необходимо представить таким образом цвета, то перечисления позволят записать {Red, Green, Yellow} вместо {0, 1, 2}. Интерфейсы. Указатели. Любые типы данных, доступ к которым осуществ- осуществляется по ссылке и которые хранятся в куче. Типы данных, которые предоставляют информа- информацию о себе для использования сборщиком мусора. Базовый класс, который представляет любой тип. Типы, которые определены в исходном коде и хранятся как типы по значению. В терминах С# это означает любую структуру. Типы, которые определены в исходном коде и хранятся как типы по ссылке. В терминах С# это означает любой класс. Базовый класс, который представляет собой лю- любой тип по значению. В этой таблице не приведен список встроенных типов по значению, более подробно они рассматриваются в главе 3. В С# каждый предопределенный тип преобразуется ком- компилятором только в один из встроенных типов IL. То же самое справедливо для VB. Единственный пункт, который требует пояснения, это приведенные типы по значе- значению. Существует ряд обстоятельств, при которых типы по значению необходимо вре- временно преобразовывать в типы по ссылке и сохранять в куче (сюда относятся случаи, когда типы по значению передаются по ссылке в метод и когда они специально согласу- согласуются с объектом). Этот процесс известен как упаковка и требует, чтобы для каждого типа по значению существовал соответствующий тип по ссылке, который представлял бы собой упакованную версию этого типа в куче. Другими словами, всякий раз, когда в коде С# создается тип по значению, .NET формирует для него соответствующий упако- упаковочный тип, который применяется для представления переменных этого типа, если их необходимо использовать как типы по ссылке. Общая спецификация языка (CLS) Общая спецификация языка работает с общей системой типов для достижения совмести- совместимости языков. CLS является минимальным набором стандартов, которые должны под- поддерживаться всеми компиляторами, предназначенными для .NET. CLS необходима по той причине, что IL очень богатый язык, и поэтому возможен случай, когда производи- производители некоторых компиляторов предпочтут ограничить возможности конкретного ком- компилятора поддержкой лишь некоторого подмножества функций, предлагаемых IL и CTS. Никаких проблем не будет возникать, пока компилятор поддерживает то, что определено в CLS. Например, CTS определяет два типа данных для 32-битовых целых чисел: Int32 (це- (целое со знаком) и UInt32 (целое без знака). С# определяет эти типы как int и uint соот- соответственно. С другой стороны, VB.NET определяет только тип Int32, для которого используется ключевое слово Integer. Другой пример — чувствительность к регистру символов. IL чувствителен к регистру символов, что позволяет чувствительным к регистру языкам C++ и С# определять пере- переменные, отличающиеся только регистром символов: EmployeeName и employeeName. Разработчики постоянно пользуются этой возможностью при выборе имен перемен- переменных. VB.NET, однако, не чувствителен к регистру символов. CLS решает эту проблему.
12 Глава 1 указывая, что совместимый с CLS код не должен содержать двух имен, отличающихся только регистром символов. Таким образом, код VB:NET может работать с совместимым с CLS кодом. Этот пример показывает два важных аспекта CLS. Во-первых, конкретные компилято- компиляторы не обязаны быть настолько мощными, чтобы поддерживать все особенности .NET,— это должно стимулировать разработку компиляторов с других языков программирования для .NET. Во-вторых, предоставляется гарантия того, что если класс ограничен только CLS-совместимыми функциями, он сможет использовать код, написанный на любом язы- языке. Например, если требуется, чтобы код был CLS-совместимым, нельзя возвращать зна- значения UInt32, так как этот тип не является частью CLS. Разумеется, можно возвращать и значения типа UInt32, однако в этом случае не гарантируется, что код будет работать со всеми языками. Подчеркнем, что не совместимый с CLS код допустим. Однако в этом случае не гарантируется, что откомпилированный код будет полностью независим от языка. Красота этой идеи заключается в том, что ограничение на использование CLS- совме- совместимых особенностей действительно только для тех элементов, которые могут быть вид- видны извне данной сборки: для открытых и защищенных членов классов и открытых классов. Внутри закрытых определений классов можно писать какой угодно не совмес- совместимый с CLS код — это не является проблемой, так как код других сборок в любом случае не сможет получить доступ к этой части кода. CLS практически не затрагивает код на С#, поскольку С# имеет лишь несколько не совместимых с CLS особенностей. Ниже приводится ряд примеров, показывающих, ка- какие ограничения накладывает CLS на С#: Требование CLS Действие на код С# Не разрешаются глобальные методы и переменные Запрещается использовать определенные типы данных Имена должны быть различимы нечувствительными к регистру языками Исключения (см. Ниже) должны наследоваться от базового класса Exception Типы-указатели не допустимы Переменные списки параметров не допустимы Не действует — С# не позволяет объявлять глобальные методы и переменные. Для того чтобы код был CLS-совместимым, не должно быть общих или частных методов типа sbyte, ushort, uint и ulong. Для того чтобы код был CLS-совместимым, не следует применять открытые или защищенные методы, чьи имена отличаются только регистром. Не действует — С# требует этого автоматически. Для того чтобы код был CLS-совместимым, не следует использовать небезопасный код и указатели нигде, кроме закрытых методов. Не действует — С# поддерживает списки параметров переменной длины, но представляет их в выходном IL-коде в виде массивов фиксированного размера (которые CLS-совместимы). Библиотека базовых классов .NET Вероятно, одним из самых больших достоинств управляемого кода, помимо упрощения процесса написания кода, является возможность использования библиотеки базовых классов .NET. Базовые классы .NET представляют собой большую коллекцию классов управляемого кода. Они были созданы Microsoft и позволяют выполнять практически любые задачи, которые ранее были доступны благодаря Windows API. Эти классы следуют той же объ- объектной модели, основанной на одиночном наследовании, что используется промежуточ- промежуточным языком. Это означает, что можно как создать экземпляр класса, определенного в библиотеке базовых классов .NET, так и определить класс, производный от данного.
Архитектура .NET 13 Замечательной особенностью базовых классов .NET является то, что они просты в использовании и самодокументированны. Например, для запуска потока необходимо вызвать метод Start () класса Thread. Для открытия файла нужно вызвать метод Open () класса File. Для того чтобы сделать неактивным TextBox, необходимо присво- присвоить значение false свойству Enabled объекта TextBox. Идея самодокументированных классов знакома разработчикам Visual Basic и Java, чьи библиотеки так же просты в применении. Возможно, это будет большим облегчением для программистов на C++, которые вы- вынуждены иметь дело с такими функциями API, как GetDIBits (), RegisterWndClassEx () и isEqualllD (), а также с целой плеядой функций для обработки дескрипторов Windows. С другой стороны, разработчики на C++ всегда могут получить доступ к Windows API, в то время как разработчики на Visual Basic и Java ограничены в применении функционально- функциональности Windows на низком уровне. Новым в базовых классах .NET является то, что они соче- сочетают в себе легкость использования библиотек Visual Basic и Java с полным описанием функций API. В книге дается немало примеров практического применения различных классов из библиотеки базовых классов .NET. К областям, покрываемым базовыми классами .NET, относятся: О Основные возможности, предоставляемые IL, например, простые типы данных в общей системе типов □ Поддержка Windows GUI, элементов управления и т.п. О Формы Web (ASP.NET) О Доступ к данным (ADO.NET) D Доступ к каталогам □ Доступ к файловой системе и реестру О Работа с сетью и просмотр Web D Атрибуты .NET и отражение О Доступ к некоторым объектам операционной системы Windows, переменным окружения и т.п. О Доступ к исходному коду и компиляторам различных языков D Совместимость с СОМ □ Графика (GDI+) Кстати, согласно источникам в Microsoft, большая часть базовых классов .NET была написана на С#! Компоненты платформы .NET В этом разделе рассматриваются компоненты, которые составляют основу платформы .NET. Сборки Детально сборки рассматриваются в главе 10, здесь же представлены только основные моменты. Сборка — это логическая единица, которая содержит компилированный код, пред- предназначенный для .NET. С этой точки зрения сборка аналогична DLL и исполняемому файлу или файлу, содержащему компоненты СОМ. Сборка полностью описывает себя и является логической, а не физической единицей, так как может располагаться более чем в одном файле. Если сборка хранится в нескольких файлах, то существует один главный файл, содержащий точку входа и информацию о других файлах сборки. Заметим, что для исполняемого кода и кода библиотеки используется одинаковая структура сборки. Единственное отличие заключается в том, что исполняемая сборка со- содержит точку входа основной программы, а библиотечная сборка — нет. Метаданные и манифесты Важной характеристикой сборок является то, что они содержат метаданные, описываю- описывающие типы и методы, определенные в соответствующем коде. Сборка содержит также ме- метаданные, которые описывают саму сборку. Метаданные сборки, хранящиеся в области,
14 Глава 1 известной как манифест, позволяют проверить версию сборки, ее целостность и ряд других сведений. Тот факт, что сборка содержит метаданные о программе, означает, что программы и сборки, которые вызывают код этой сборки, не должны обращаться к реестру или ино- иному источнику данных для выяснения того, как использовать сборку. Это значительный прорыв по сравнению со старой технологией СОМ, когда из реестра необходимо было получать GUID компонентов и их интерфейсы, а в некоторых случаях детали методов и свойств приходилось читать из библиотек типов. То, что данные были разбросаны по двум, а то и трем различным местам, могло при- привести к рассинхронизации данных и соответственно к невозможности использования библиотеки другим кодом. При применении сборок отсутствует риск нарушения синхро- синхронизации, так как все метаданные хранятся вместе с исполняемыми инструкциями про- программы. Отметим, что хотя сборки могут храниться в нескольких файлах, это все равно не создает проблемы рассинхронизации данных. Дело в том, что файл, содержащий точ- точку входа основной программы, имеет также информацию и хэш, сформированный на основе содержимого других файлов. Если один из файлов будет удален, заменен или ка- каким-либо образом модифицирован, это будет сразу же обнаружено, и сборка не станет загружаться. Совместно используемые и частные сборки Сборки бывают двух типов: совместные и частные. Частные сборки Частные сборки являются самым простым видом. Как правило, они поставляются в комп- комплекте с программным обеспечением и будут использоваться исключительно этим програм- программным обеспечением. Обычный способ поставки частных сборок — клиенту предоставляется приложение в виде исполняемого файла и набора библиотек, содержащих используемый приложением код. Система гарантирует, что частные сборки не будут применяться другим програм- программным обеспечением, так как приложение может загружать только те частные сборки, которые расположены вместе с ним в одном каталоге или подкаталогах. Обычно предполагается, что коммерческий программный продукт устанавливается в свой собственный каталог, тем самым исключается всякий риск удаления, модификации сборок или загрузки частной сборки, предназначенной для использования другим при- приложением. Поскольку частные сборки могут применяться только тем приложением, для которого они предназначены, у пользователя есть возможность контролировать исполь- использование сборок тем или иным программным обеспечением. Следовательно, требуется предпринимать меньше усилий по обеспечению безопасности, так как не существует ри- риска, например, того, что какое-то программное средство перепишет одну из сборок бо- более новой версией (не считая, конечно, программ, написанных с целью умышленного нанесения ущерба). Также исключаются проблемы совпадающих имен. Если классы в ча- частной библиотеке имеют те же названия, что и классы в другой частной библиотеке, это уже неважно, поскольку приложение будет видеть только один набор частных сборок. Так как частная сборка полностью самоописываема, процесс инсталляции чрезвы- чрезвычайно прост. Необходимые файлы помещаются в указанный каталог на диске. Не требу- требуется производить запись в реестре и другие подобные действия. Этот процесс называют установкой с нулевым усилием. Совместные сборки Совместные сборки — это общие библиотеки, которые могут использоваться любыми другими приложениями. Совместное использование требует учета следующих возможных опасностей: О Коллизии имен, затрагивающие совместную сборку от другой компании, реализу- реализующую типы, которые используют те же имена, что и ваша совместная сборка. Так как клиентский код теоретически может иметь доступ к обеим библиотекам, не исключено возникновение серьезных проблем. О Опасность перезаписи сборки другой версией той же сборки, причем новая версия может оказаться несовместимой с уже имеющимся клиентским кодом. Решение этих проблем заключается в размещении совместных сборок в специальном поддереве каталогов файловой системы, известном как кэш сборок. В отличие от част- частных сборок, общую сборку нельзя просто скопировать в соответствующий каталог, ее необходимо установить в кэш. Этот процесс может быть осуществлен с использованием
Архитектура .NET 15 нескольких утилит .NET; при этом проводятся определенные проверки сборки, а также создание небольшой структуры каталогов внутри кэша сборок, которая используется для обеспечения целостности сборки. Во избежание риска коллизий имен совместные сборки именуются на основе крип- криптографии с закрытым ключом (частные сборки имеют то же имя, что и основная про- программа). Это имя называется строгим именем (strong name), оно является гарантиро- гарантированно уникальным и должно применяться приложениями, которые желают обращаться к совместной сборке. Проблемы, связанные с риском перезаписи сборки, решаются указанием инфор- информации о версиях в манифесте сборки и обеспечением возможности сосуществования нескольких одноименных библиотек разных версий. Пространства имен Используя пространства имен, .NET избегает коллизий имен между классами. Простран- Пространства имен разработаны для исключения таких ситуаций, когда определяется представля- представляющий потребителя класс под названием Customer, и то же самое делает кто-то другой. (Весьма вероятный сценарий: процент бизнес- приложений, имеющих дело с потребите- потребителями, очень высок.) Пространства имен — это нечто большее, чем простая группировка типов данных. Имена всех типов данных в определенном пространстве имен автомати- автоматически расширяются префиксом, образованным от названия пространства имен. Также можно создавать вложенные пространства имен. Например, большинство базовых клас- классов .NET общего назначения расположено в пространстве имен System. Базовый класс Array находится в этом пространстве имен, поэтому его полное имя — System. Array. .NET требует, чтобы все типы были определены в пространстве имен, поэтому в при- приведенном примере можно было бы разместить класс Customer в пространстве имен YourCompanyName. Тогда класс имел бы полное имя YourCompanyName. Customer. Если пространство имен не объявлено, то тип будет помещен в безымянное глобальное пространство имен. Microsoft рекомендует использовать в большинстве случаев как минимум два вложенных пространства имен, одно из которых должно быть названием компании, а другое — названи- названием технологии или программного пакета, к которому принадлежит данный класс, напри- например YourCompanyName.SalesServices.Customer. Выполнение этого условия защитит класс от возможной коллизии имен с классами, написанными в других организациях. Во многих языках пространства имен могут быть объявлены в исходном коде. Напри- Например, синтаксис в С# таков: namespace ■YourConpanyName.SalesServices i class Customer /•/ и т.д. Области приложений Области приложений являются важным нововведением .NET. Они разработаны для уме- уменьшения накладных расходов по изоляции друг от друга запущенных приложений, кото- которым требуется общаться друг с другом. Классический пример такого случая — приложение web-сервера, способное одновременно обрабатывать большое число запросов от брау- браузеров. Оно будет иметь несколько одновременно запущенных экземпляров компонента, ответственного за обработку этих запросов. Раньше можно было бы позволить этим экземплярам использовать общий процесс, что способно привести к остановке всего web-сайта в случае зависания хотя бы одного процесса, или изолировать эти экземпляры в разных процессах, что вызывает соответст- соответствующее увеличение накладных расходов. До сих пор единственным способом изоляции кода являлись процессы. Когда запуска- запускается новое приложение, оно работает в контексте процесса. Windows изолирует процессы друг от друга с помощью адресных пространств. Идея заключается в том, что для каждого процесса доступно 4 Гбайт виртуальной памяти, в которой он может хранить данные и ис- исполняемый код (ограничение в 4 Гбайт действует для 32-разрядных систем, дня 64-разряд- 64-разрядных систем эта величина намного больше). Доступ к этой памяти осуществляется каждым приложением путем использования адресов в диапазоне от 0 до 4 Гбайт. Однако эта
16 Глава 1 память остается виртуальной: для достижения этого Windows использует дополнитель- дополнительный уровень абстракции, с помощью которого виртуальная память размещается в опре- определенной области физической памяти или области на диске. Память разных процессов размещается в разных участках физической памяти, не перекрывающихся друг с другом. Описанный способ показан на рисунке: Физическая память Физическая память или дисковое пространство Физическая память или дисковое пространство ПРОЦЕСС 1 4 Гбайт виртуальной памяти ПРОЦЕСС 2 4 Гбайт виртуальной памяти Вообще, любой процесс может обращаться к памяти только путем указания адреса в виртуальной памяти — процесс не имеет прямого доступа к физической памяти. Следо- Следовательно,- один процесс ни в каком случае не может получить доступ к памяти другого процесса. Это дает гарантию того, что плохой код не сможет повредить ничего за пре- пределами своего адресного пространства. (Отметим, что в Windows 9x это реализовано не так хорошо, как в NT/2000, и там существует теоретическая возможность нарушения работы Windows путем доступа к чужой памяти.) Процессы служат не только для отделения друг от друга различных экземпляров ис- исполняемого кода. В системах Windows NT/2000 они также образуют блок, с которым связаны привилегии доступа. Каждый процесс имеет свои собственные привилегии, которые показывают Windows, какие операции может выполнять данный процесс. Процессы хорошо подходят для обеспечения безопасности (для исключения доступа к чужим участкам памяти и для разграничения по привилегиям), но их недостатком яв- является производительность. Нередко большое число процессов должно работать сооб- сообща и иметь возможность связи друг с другом. Типичный пример, когда процесс вызывает СОМ-компонент, который является исполняемым файлом и, следовательно, должен быть запущен в отдельном процессе. То же самое происходит в СОМ, где приме- применяются замещения. Так как процесс не может использовать память совместно еще с кем-то, то для передачи данных между процессами должны применяться сложные функ- функции переноса данных (marshalling). Это серьезно ухудшает производительность. Если необходимо, чтобы компоненты работали совместно и при этом не было потерь произ- производительности, единственный способ — использование DLL и работа всего кода в еди- едином адресном пространстве с риском того, что плохое поведение единственного компонента приведет к краху всей системы. Области приложений разработаны как способ разделения компонентов с исключени- исключением проблем производительности, связанных с передачей данных между процессами. Идея состоит в том, что каждый процесс делится на некоторое число областей приложения. В общем случае каждая область приложения соответствует одному приложению, а каж- каждый исполняемый поток запускается в определенной области приложения: ПРОЦЕСС: 4 Гбайт виртуальной памяти Область приложений: приложение использует часть этой виртуальной памяти Область приложений: приложение использует часть этой виртуальной памяти
Архитектура .NET 17 Если различные исполняемые файлы запущены в одном адресном пространстве, они могут свободно разделять данные, так как теоретически они способны напрямую видеть данные друг друга. Однако среда исполнения .NET не допускает этого на практике, про- проверяя код каждого запущенного приложения и убеждаясь в том, что код не может обра- обращаться к чужим областям данных. На первый взгляд это кажется невозможным: как узнать, что собирается делать программа, не запуская ее? На самом деле это возможно благодаря строгому контролю типов в промежуточном языке. В большинстве случаев, если только код не использует небезопасные средства, например указатели, применяемые им типы данных будут гарантировать, что доступ к памяти осуществляется корректно. Скажем, массивы в .NET выполняют проверку гра- границ для того, чтобы исключить выход за свои границы. Если приложению все же требу- требуется общаться или обмениваться данными с приложениями, работающими в других областях приложений, то тогда должны использоваться удаленные службы .NET, пред- представляющие собой различные базовые классы в пространстве имен System.Remoting. Код, в отношении которого в ходе проверки было установлено, что он не способен получить доступ к данным вне области приложения (кроме как посредством механизма удаленного сообщения), считается безопасным по типу (или безопасным по типу памя- памяти). Такой код может безопасно исполняться вместе с другим безопасным по типу кодом в различных областях приложений внутри одного процесса. J 1Т-компиляторы Компилятор JIT является ключевым средством платформы .NET и жизненно важной со- составляющей в реализации попытки Microsoft сделать так, чтобы управляемый код работал с большей производительностью, чем неуправляемый код. Проблема производительности возникает из-за того, что код компилируется в промежу- промежуточный язык, и это может стать неожиданностью для некоторых разработчиков. В конце концов, одним из недостатков fava является то, что процесс трансляции с байт-кода Java в исполняемый код в процессе выполнения программы означает потерю производительно- производительности. Однако существует все же большая разница, заключающаяся в том, что байт-код Java интерпретируется, a IL компилируется. Более того, JIT-компилятор компилирует не всю программу сразу (что может привести к излишне долгому запуску программы), а лишь кусок кода и именно тогда, когда он вызывается (отсюда название JIT-компилятора: just-in-time — вовремя). После того как код единожды откомпилирован, получившийся в результате машинный код сохраняется в памяти до тех пор, пока не будет осуществлен выход из программы, поэтом)" при следующем запуске того же самого кода его уже не потребуется компилировать. Microsoft считает, что это более эффективный процесс, чем компиля- компиляция всей сборки сразу, так как велика вероятность того, что большая часть кода сборки не будет выполняться во время одного конкретного запуска программы. При использо- использовании JIT- компилятора такой код вообще не придется компилировать. Этим объясняется, почему исполнение управляемого кода на IL будет почти таким же быстрым, как и исполнение родного машинного «ода, но не объясняется, почему Microsoft ожидает получить еще и улучшение по производительности. А дело в следую- следующем: так как окончательная стадия компиляции происходит уже в процессе исполнения, JIT-компилятор точно знает, на каком процессоре будет работать программа. Это означа- означает, что код может быть оптимизирован под использование тех особенностей, которые предлагаются каждым конкретным процессором. Традиционные компиляторы оптимизируют код, но они могут выполнять только оп- оптимизацию, не зависящую от конкретного процессора. Это происходит из-за того, что современные компиляторы осуществляют компиляцию сразу в машинный код перед тем, как программа будет отправлена потребителю. Следовательно, компилятор не зна- знает, на каком процессоре будет исполняться код. Ему могут быть известны лишь общие сведения, что это будет процессор семейства х86 или процессор Alpha. Visual Studio 6, например, оптимизирует код для процессора Pentium, поэтому такой код не сможет вос- воспользоваться преимуществами процессора Pentium III. Напротив, JIT-компилятор мо- может выполнить ту же оптимизацию, что и Visual Studio 6, но вдобавок оптимизировать код для того процессора, на котором он будет исполняться. Инструменты .NET Помимо служб исполнения программы, .NET предоставляет ряд инструментов, которые призваны помочь в разработке приложений .NET:
18 Глава 1 О Visual Studio.NET — интегрированная среда разработки, с помощью которой мож- можно писать, компилировать и отлаживать код на всех языках .NET, включая С#, VB.NET, управляемый C++, страницы ASP.NET и неуправляемый код C++. Visual Studio.NET обсуждается в главе 8. О Компиляторы командной строки для С#, VB.NET и C++. О ILDASM — утилита с оконным интерфейсом, которую можно использовать для просмотра содержимого сборки, включая манифест и метаданные. ILDASM опи- описывается в главе 10. Сборщик мусора Сборщик мусора — это ответ .NET на проблемы управления памятью, в частности, на во- вопрос, связанный с перераспределением памяти, требующейся приложениям. До сих пор в Windows использовались две методики перераспределения памяти, динамически запра- запрашиваемой процессами у системы: методика перераспределения памяти самим приложе- приложением и применение в объектах счетчиков ссылок. В дополнение Java использует сборщик мусора, аналогичный работающему в .NET. Методика применения программного кода для перераспределения памяти использует- используется низкоуровневыми высокопроизводительными языками, например C++. Эта методика эффективна и имеет то преимущество, что ресурсы не используются сверх положенного срока. Большим недостатком, однако, является значительное число ошибок. Код, кото- который запрашивает память, должен в конце информировать систему о том, что память ему больше не требуется. В C++ для этого предусмотрено ключевое слово delete, кроме того, существуют различные функции API, предназначенные для той же цели. Программисты должны быть очень осторожны и внимательно следить за тем, чтобы освобождалась вся используемая память. Об этом нередко забывают, что приводит к утечкам памяти. Со- Современные среды разработчика предоставляют инструменты для обнаружения утечек памяти, но эти ошибки все же очень сложно выявить, так как проявляются они лишь тогда, когда происходит утечка большого количества памяти и Windows в определенный момент просто отказывается выделить память процессу из-за ее отсутствия. К этому мо- моменту работа всего компьютера может сильно замедлиться из-за потребления большого объема памяти. Использование счетчиков ссылок применяется СОМ-объектами. Идея заключается в том, что каждый СОМ-объект поддерживает информацию о том, как много клиентов в данный момент используют ссылки на него. Когда число ссылок становится равным нулю, компонент уничтожает себя и освобождает память и ресурсы. Проблема здесь заключает- заключается в том, что СОМ-объект рассчитывает на корректное поведение клиентов, которые дол- должны сообщать ему об окончании своей работы с объектом (что осуществляется путем вызова метода iunknown. Release ()). Стоит только одному из клиентов не сделать этого, и объект останется в памяти. В некоторых случаях это является потенциально еще более серьезной проблемой, чем простая утечка памяти в C++, так как СОМ-объект может суще- существовать в своем собственном процессе, а это означает, что он никогда не будет удален си- системой (в случае утечек памяти система, по крайней мере, может освободить всю память по завершении процесса). Какое же решение применяется в .NET? Среда исполнения .NET полагается на так называемый сборщик мусора, который представляет собой программу, чьей целью является освобождение памяти. Смысл заклю- заключается в том, что вся динамически запрашиваемая память распределяется в куче (что справедливо для всех языков). По мере того как .NET выясняет, что для данного процесса куча полностью заполняется и, следовательно, требует очистки, она вызывает сборщик мусора. Сборщик мусора просматривает переменные кода, находящиеся в данный момент в области видимости, исследуя ссылки на объекты, хранящиеся в куче, для определения того, какие из них доступны в коде, или иными словами, какие объекты содержат ссылки на себя. Все объекты, на которые нет ссылок, считаются более недоступными из кода и должны быть уничтожены. Посмотрим, как это работает на практике, воспользовавшись фрагментом кода на С#: {" TextBox UserlnputArea; UserlnputArea = new TextBox(); TextBox >txtBoxCopy = OserXriputArea; // Допустим, что сборщик мусора вызван здесь // Обработка данных
Архитектура .NET 19 /*/ Теперь UserlnputArea и txtBoxCopy находятся вне зоны видимости кода // Допустим, что здесь снова был вызван сборщик мусора Код начинается с объявления переменной типа TextBox. Эта переменная имеет имя UserlnputArea. TextBox является типом по ссылке. Это означает, что UserlnputArea содержит адрес, и нам необходимо отдельно создать экземпляры объектов TextBox в куче, что делается в следующей строке. Затем мы объявляем еще одну переменную, txtBoxCopy, и присваиваем ей значение UserlnputArea, т.е. она тоже ссылается на TextBox. Теперь допустим, что это происходит в тот момент, когда вызывается сборщик мусо- мусора. Просматривая ссылки по коду, он обнаружит, что обе переменные ссылаются на TextBox. Очевидно, что объект TextBox до сих пор используется и должен оставаться в куче. Сборщик мусора не только удаляет объекты — он может упорядочивать кучу для по- повышения производительности. Поэтому существует вероятность того, что он изменит местоположение данных для объекта TextBox. Если это будет сделано, то он обновит адреса, содержащиеся в UserlnputArea и txtBoxCopy. Затем переменные UserlnputArea и txtBoxCopy выходят из области видимости. Допустим, что больше никакие переменные не ссылаются на TextBox и что в этот мо- момент снова вызывается сборщик мусора. Теперь он обнаружит, что существует область в куче, используемая для хранения TextBox, и что на эту область не ссылается ни одна из видимых переменных. Исходя из этого, сборщик мусора решит, что TextBox больше не требуется, и удалит его. Отметим, что данный механизм совершенно безопасен: если на TextBox нет ссылок, то не существует законного пути, посредством которого код сможет снова получить до- доступ к TextBox. Промежуточный язык не обеспечивает каких- либо безопасных спосо- способов получения ссылки на объект в куче, кроме как его создание или копирование уже существующей ссылки. Если нет ссылки, которую можно скопировать, он не способен законно получить ссылку на существующий объект. Здесь мы говорим "законно", так как теоретически можно написать небезопасный код на С# или C++, который будет использовать указатели, арифметику указателей и преобразование типов для повторного получения ссылки па TextBox. Однако здесь не видно никаких причин, по которым кто-то захотел бы сделать это, кроме как для того, чтобы продемонстрировать саму возможность подобных действий,- определенно, это не в духе методологии .NET. Ссылку можно получить более простым способом - написав код, который никогда не потеряет ее! Сборка мусора работает потому, что промежуточный язык был спроектирован для облегчения этого процесса. Требуется, первое, чтобы ссылки на существующие объекты нельзя было получить иначе, кроме как копированием имеющихся ссылок на объекты, и второе, чтобы промежуточный язык был безопасным по типам. В этом контексте под бе- безопасностью по типам понимается то, что если существует ссылка на объект, то в этой ссылке присутствует достаточно информации для точного определения типа объекта. Механизм сборки мусора невозможно было бы применять с таким языком, как неуп- неуправляемый C++, поскольку C++ допускает свободное преобразование типов указателей. Это означает, что ни одна из программ, просматривающих код, не сможет определить по значениям указателей, какие участки кучи используются на данный момент. Как уже отмечалось ранее, IL допускает применение указателей в небезопасном коде, а также позволяет преобразовывать тип указателя. Однако в IL существуют жесткие ограниче- ограничения на использование указателей, которые были разработаны таким образом, чтобы их применение в коде не конфликтовало с требованиями сборщика мусора. В частности, указатели не могут указывать на ссылочные объекты. Важной особенностью сборки мусора является то, что это недетерминированный процесс. Нельзя сказать, когда будет вызван сборщик мусора: он вызывается тогда, когда среда исполнения .NET решает, что его необходимо вызвать. Очевидно, что чем больше требований программа предъявляет по части памяти, тем чаще будет вызываться сбор- сборщик мусора. Сборщик мусора можно вызвать из программы вручную с помощью базово- базового класса .NET System.CC. Это можно сделать, например, в точке, где код заканчивает работу с большим числом переменных. В большинстве ситуаций, однако, придется дове- довериться среде исполнения .NET, которая сама будет вызывать сборщик мусора при необ- необходимости.
20 Глава Л Исключения .NET призвана упростить обработку ошибочных состояний. В ней используется тот же ме- механизм, основанный на исключениях, что реализован в Java и C++. Разработчики на C++, однако, заметят, что благодаря более строгой системе типов в IL нет такого ухудшения производительности, вызываемого применением исключений, какое наблюдается в C++. Исключения детально рассматриваются в главе 6. Идея заключается в том, что функ- функции обработчиков исключений выполняются отдельными фрагментами кода и каждый из них имеет дело с конкретным ошибочным состоянием (например, файл не найден или не было получено разрешения на выполнение некоторой операции). Эти состояния могут быть определены настолько узко или широко, насколько необходимо. Например, можно написать обработчик, который имеет дело с ситуацией, когда клиентский код пе- передает неправильные параметры одному из методов библиотеки. Можно также написать другой обработчик, который будет иметь дело с ситуацией, когда только один из пере- переданных параметров содержит неправильное значение (например, при передаче темпе- температуры меньше абсолютного нуля в функцию перевода температуры в разные шкалы). Архитектура исключений гарантирует, что при возникновении ошибочной ситуации управление немедленно будет передано процедуре обработки исключения, которая более приспособлена для разрешения ошибочного состояния. Архитектура обработки исключений предоставляет удобные способы передачи объек- объекта, содержащего точную информацию об ошибочном состоянии, в процедуру обработки исключения. Эта информация включает в себя соответствующее сообщение для пользова- пользователя и сведения о том, где конкретно в коде была обнаружена ошибка. Большая часть архитектуры обработки исключений, в том числе контроль за ходом выполнения программы во время возникновения исключения, осуществляется высокоу- высокоуровневыми языками (С#, VB.NET, C++) и не поддерживается промежуточным языком. С#, например, обрабатывает исключительные ситуации с помощью блоков try{}, catch{} и finallyO (см. главу 6). Тем не менее .NET предоставляет возможности для поддержки компиляторами, предназначенными для .NET, функций обработки исключений. В частно- частности, она предлагает набор базовых классов .NET, которые могут представлять исключе- исключения, и совместимость языков, благодаря которой созданные объекты исключений могут интерпретироваться кодом обработки ошибок независимо от того, на каком языке напи- написан этот код. Языковая независимость отсутствует в реализациях обработки ошибок в C++ и Java, хотя в некотором объеме она представлена в механизме обработки ошибок СОМ, который предусматривает возврат кодов ошибок из методов и передачу объектов ошибок. Тот факт, что ошибки последовательно обрабатываются в различных языках, является важным аспектом, облегчающим многоязыковую разработку. Безопасность Подробно вопросы безопасности рассматриваются в главе 25. .NET способна проверять, что позволено делать конкретному приложению. Однако главное достоинство .NET в области безопасности заключается в том, что она предлага- предлагает безопасность на уровне кода, в то время как Windows может предложить только роле- ролевую безопасность. Разница состоит в том, что ролевая безопасность основывается на сущности учетной записи, под которой выполняется процесс. Другими словами, это тот, кто владеет про- процессом и исполняет его. Безопасность на уровне кода основывается на том, что конкрет- конкретно делает код и насколько ему можно доверять. Благодаря строгому контролю типов промежуточного языка, среда исполнения .NET способна проверять код перед его запус- запуском для того, чтобы определить политику безопасности. .NET предлагаег также меха- механизм, при помощи которого код может заранее указать, какая политика безопасности ему потребуется для запуска. Важность безопасности на уровне кода заключается в том, что она снижает риск, свя- связанный с запуском кода, полученного из неизвестного источника (например, из Интер- Интернета). Даже если код исполняется в профиле администратора, можно использовать безопасность на уровне кода для того, чтобы запретить некоторые типы действий, кото- которые профиль администратора обычно позволяет выполнять, например: чтение/запись переменных окружения, чтение/запись в журнал событий, чтение/запись в реестр и доступ к инструментам отражения .NET.
Архитектура .NET 21 Атрибуты Атрибуты являются особенностью, которая была введена в расширения Microsoft C++ в течение последних нескольких лет. Разработчики на Java и VB не имеют о ней представ- представления. Идея атрибутов заключается в том, что они изначально предлагают чуть больше информации о некоторых элементах программы, которая может использоваться ком- компилятором в процессе компиляции. В следующем примере применения атрибутов в С# указывается потоковая модель, которую должен использовать начальный поток: f'STAThread] void Main() { А этот атрибут может использоваться для отметки устаревшего метода: [Obsolete] ч public int 'SpmeObsoleteMethoct () { : Устаревший метод в случае его вызова другим кодом сгенерирует предупреждение компилятора, а в некоторых случаях и ошибку. Новым в отношении атрибутов в .NET является то, что существует механизм, с помо- помощью которого можно определять свои собственные атрибуты в исходном коде. Опреде- Определенные пользователем атрибуты размещаются вместе с данными метода для соответствующих типов данных или методов. Это может быть полезно в целях докумен- документирования, где они могут использоваться совместно с технологией отражения для вы- выполнения задач программирования, основанных на атрибутах. Кроме того, в контексте языковой независимости .NET, языки могут определяться в исходном коде на одном языке и читаться кодом, написанным на другом языке. Атрибуты рассматриваются в главах 6 и 7. Отражение Выше было сказано, что сборки содержат метаданные, включающие в себя сведения обо всех типах данных и членах типов, определенных в сборке. К этим данным можно осуще- осуществить доступ программно с использованием базовых классов .NET из пространства имен System.Reflection (см. главу 7). Отражение дает интересные возможности, так как оно означает, что управляемый код может исследовать другой управляемый код или даже сам себя для получения информации о коде. Чаще всего это применяется для полу- получения сведений об атрибутах. Отражение можно также использовать как непрямой путь создания экземпляров классов или вызова методов при применении строковых имен этих методов или классов. Таким образом можно выбирать классы, чтобы создавать эк- экземпляры методов для вызова во время исполнения, а не во время компиляции, на основе, например, информации, вводимой пользователем. Языки и технологии В этом разделе мы обсудим, как основные языки и некоторые из существующих технологий соответствуют платформе .NET. с# С# является новым объектно-ориентированным языком программирования, специально предназначенным для среды исполнения .NET. Компилятор С# способен генерировать только управляемый код, и он хорошо осведомлен о некоторых из базовых классов .NET. С# важен в двух отношениях. Во-первых, он совместим со средой исполнения .NET. Во-вторых, это современный объектно-ориентированный язык, при разработке кото- которого Microsoft использовала опыт похожих языков, которые существовали в течение примерно последних 20 лет, т.е. когда зародились и развивались принципы объект- объектно-ориентированного программирования. Важно понимать, что язык С# — это отдельный законченный продукт. Хотя он созда- создает код для платформы .NET, сам по себе он не является частью .NET. Существуют неко- некоторые особенности, которые поддерживаются .NET, но не поддерживаются С#, и вы, вероятно, будете удивлены, но есть некоторые особенности С#, которые не поддержива- поддерживаются .NET! Уже было отмечено, что в эту категорию попадает ряд моментов обработки
22 Глава Л исключений С#. На самом деле в случае использования этих особенностей в коде С# компилятор преобразует их в средства, поддерживаемые .NET. Другой пример — пере- перегрузка операторов. С# позволяет перегружать большое число операторов, таких как плюс, минус и т.п., в результате чего можно определить класс Vector (см. главы 5 — 7) и для него создать версии оператора сложения (+), что позволит написать: Vector VI, V2, V3; // инициализация V2 и V3 VI = V2 + V3; Этот код является абсолютно верным с точки зрения С#, но перегрузка операторов не определена в промежуточном языке. При использовании перегруженного оператора в ис- исходном коде С# компилятор внутренне сгенерирует некоторый метод в компилированном IL-коде, который будет делать то же самое, что и перегруженный оператор, и подставит этот метод везде, где в коде применяется соответствующий перегруженный оператор. C++ C++ содержит большое число специфических расширений Windows от Microsoft. Для поддержки платформы .NET было добавлено еще несколько расширений. Это означает, что существующий исходный код C++ будет компилироваться в исполняемый код без ка- каких-либо изменений. Однако это также означает, что он будет выполняться независимо от среды .NET. Для исполнения кода C++ на платформе .NET необходимо добавить следую- следующую строку в начало кода: fusing <mscorlib.ctll> Также требуется передать компилятору флаг /clr, указывающий компилятору на не- необходимость создания управляемого кода, в результате чего компилятор выдаст код на промежуточном, а не на машинном языке. Это означает, что компилятор сгенерирует ошибку в том случае, если вы попытаетесь применить особенности языка, не поддержи- поддерживаемые .NET, например шаблоны или множественное наследование классов. Кроме того, придется использовать определенные атрибуты и другие нестандартные особенности C++ для того, чтобы отмечать классы. Из-за того, что C++ допускает свободную низкоуровневую манипуляцию указателями, компилятор C++ не способен генерировать код, который выдержит тесты CLR на безопас- безопасность типа памяти. Важно отметить, что в случае, если исходный код должен распознава- распознаваться CLR как безопасный по типу памяти, его необходимо писать с использованием других языков (например, С# или VB.NET). J++ J++ поддерживается только в целях обратной совместимости. Он не был доработан для создания на нем программ под .NET. He рекомендуется писать новое программное обес- обеспечение с использованием J++. Поскольку существует некоторый объем уже созданного кода на J++, Microsoft поставляет несколько инструментов под общим названием JUMP (Java User Migration Path) и лозунгом JUMP to .NET1. Эти инструменты позволят Visual Studio.NET работать с существующим кодом J++. Также доступен инструмент миграции, который автоматически преобразует проекты на J++ в проекты на С#. Сходства в синтак- синтаксисе между J++ и С# настолько велики, что не потребуется даже производить больших из- изменений в структуре кода — в основном достаточно будет заменить ключевые слова J++ соответствующими ключевыми словами С#. VB Visual Basic претерпел полную перестройку для того, чтобы идти в ногу с .NET. Путь, ко- которым развивался Visual Basic в течение последних нескольких лет, означает, что его предшествующая версия, Visual Basic 6, не подходит для программирования в .NET. На- Например, он сильно интегрирован с СОМ и показывает в качестве исходного кода только обработчики событий, в то время как большая часть остального кода недоступна. Кроме того, он не поддерживает наследование реализаций, а стандартные типы данных Visual Basic не совместимы с .NET. Visual Basic переделан в Visual Basic.NET, и внесенные изменения значительны. Для всех практических нужд можно считать Visual Basic.NET новым языком. Существующий
Архитектура .NET 23 код на VB6 не будет компилироваться в код VB.NET. Преобразование программы VB6 в программу VB.NET требует значительных изменений кода, однако большинство из них может быть выполнено автоматически Visual Studio.NET. Если вы попытаетесь про- прочесть проект VB6 в Visual Studio.NET, то Visual Studio.NET автоматически конвертирует проект, т.е. код VB6 будет преобразован в код VB.NET. Несмотря на то, что большая часть работы выполняется автоматически, нужно просмотреть новый код VB.NET, чтобы убедиться, что проект функционирует правильно. Одним из побочных эффектов является то, что больше невозможно компилировать VB.NET в обычный исполняемый файл. VB.NET компилируется только в промежуточ- промежуточный язык, точно так же, как и С#. Можно, конечно, писать программы на VB6, однако полученный код будет полностью игнорировать платформу .NET, кроме того, придется сохранить Visual Studio 6 в том случае, если для разработки планируется использовать эту среду. ASP Страницы ASP считаются устаревшими в .NET. Среда .NET производит обновление IIS с целью поддержки страниц ASP.NET, для которых код может быть написан непосредствен- непосредственно в VB.NET, С# или JScript.NET и откомпилирован в классы, которые отвечают на web-за- web-запросы, генерируя соответствующий HTML на выходе. Повышение производительности, связанное с выполнением откомпилированного кода п ответ на запросы браузера, очевид- очевидно. Вдобавок Microsoft создала ряд классов, способных эмулировать сложные элементы управления в браузерах путем создания HTML на выходе. Синтаксис страниц ASP.NET также был переработан для того, чтобы сделать страницы более структурированными, с более четким разделением логики пользовательского интерфейса и бизнес-логики. ASP страницы будут работать вместе с .ХЕТ, однако в свете улучшений, предлагаемых ASP.NET, нет смысла использовать их, кроме как в качестве традиционных страниц. Языки сценариев Языки сценариев все еще существуют, хотя их важность, вероятно, уменьшится. С другой стороны, JScript был обновлен floJScript.NTET. Страницы ASP.NET могут быть написаны BjScript.NET. И TenepbJScript.NET может работать как компилируемый, а не интерпре- интерпретируемый язык, и к тому же можно создавать строго типизированный код JScript.NET. При наличии ASP.NET нет смысла использовать языки сценариев в web-страницах на стороне сервера. VBA, тем не менее, по-прежнему применяется в качестве макроязыка для документов Office и для макросов Visual Studio. ADO/OLE DB Управляемый код допускает использование ADO и OLE DB при помощи средств совмести- совместимости с СОМ, однако для связи с источниками данных проще применять ADO.NET — под- подмножество базовых классов .NET, которое предназначено для данной цели (см. главу 11). ADSI Базовый класс .NET System. DirectoryServices призван заменить ADSI. Он выполняет практически те же функции, хотя допускается применение старых ADSI-интерфейсов. СОМ и СОМ+ СОМ+ остается важным инструментом, так как его особенности не полностью воспроиз- воспроизводятся в .NET. Кроме того, СОМ-компоненты будут работать, a .NET реализует функции совместимости, которые позволяют управляемому коду вызывать СОМ-компоненты и на- наоборот (см. главу 19). В целом, вероятно, удобнее будет создавать новые компоненты как компоненты .NET, что даст возможность использовать преимущества базовых классов .NET и другие выгоды от работы в качестве управляемого кода. Возможности .NET: подведение итогов Наш тур по платформе .NET практически завершен. В заключение рассмотрим два при- примера того, как различные средства .NET — промежуточный язык, CTS, базовые классы .NET и язык С# — образуют код, имеющий высокую производительность, совмещенную с удобным синтаксисом:
24 Глава 1 Тип по ссылке А Базовый класс .NET: System.lnt32 4 i int Начнем с проверки того, что происходит, когда объявляется переменная типа int в коде С#: Infc X =.- 27; Ггл В предположении разработчика, С# определяет int как 32-разрядное целое со знаком, которое может содержать значения от -2 147 483 648 до 2 147 483 647 включительно. В предшествующих языках программирования, таких как C++ и Visual Basic, компилятор преобразовал бы этот тип в соответствующий тип для машинного кода. Однако компиля- компилятор С# преобразует тип int к одному из базовых классов .NET — System. Int32. Согласно спецификации языка С#, ключевое слово int — не более чем обычное предопределенное имя типа, которое представляет этот класс. Тот факт, что int на самом деле является классом, означает, что программист получает все синтаксические выгоды, проистека- проистекающие из определения класса. Например, разрешается вызывать методы класса. Можно написать такой код: String.* Text, = XiTqStringO •" int Y - int-"Parse (0") ; int;:MaxVfiiUe; = int-MaxValu©; // возвращает 2 147 483 647 To, что int имеет методы, намного упрощает программирование. Как показано выше, легко преобразовать целые числа в/из строк, а также получить их максимальные и минимальные значения. По мере усложнения решаемых задач свойства объектно-ори- объектно-ориентированного программирования были распространены на типы данных, включая те, что ранее считались сырыми (raw) типами данных, а не объектами. Удивительным, од- однако, является то, что это преобразование в объекты было достигнуто без каких-либо потерь производительности, которые обычно ассоциируются с объектами. В прошлом причина, по которой предопределенные типы данных не преобразовыва- преобразовывались в объекты, заключалась в том, что в структуре или классе существовала некая неод- неоднозначность, связанная с определением его членов. Это вызывало небольшие потери производительности, которые неприемлемы при работе с часто используемыми про- простыми типами данных, например с int и float. В случае .NET всех этих потерь просто нет. Дело в том, что Microsoft компилирует и записывает базовые классы так, что внут- внутренне System.Int32 соответствует предопределенному типу промежуточного языка, а не определенному пользователем типу по значению. При запуске кода в записи int X = 27; создается простое целое число. Просматривая иерархию на приведенном выше рисунке, можно определить оболочку, которая окружает основной машинный тип данных кода, следующим образом: О Промежуточный язык трактует тип как узнаваемый .NET предопределенный тип по значению. О Библиотека базовых классов .NET трактует предопределенный тип по значению как полноценную структуру. □ С# трактует эту структуру с помощью своего ключевого слова int. Не считая окончательной стадии JIT-компиляции, все эти оболочки раскрываются во время компиляции, а не выполнения, поэтому при исполнении кода не наблюдается связанных с ними потерь производительности. Результатом всего этого является син- синтаксическое удобство ключевого слова, для которого можно вызывать его методы и ко- которое будет во время исполнения трактоваться как предопределенный тип, а не как struct.
Архитектура .NET 25 Мы привели пример для int, но те же самые принципы применимы ко всем осталь- остальным предопределенным типам данных и к таким типам данных, как делегаты, интерфей- интерфейсы и перечисления. Перечисление — это набор значений с присоединенными к ним метками. Например, для указания типов пользователей в коде можно написать: Enum UserType {Administrator, Customer, Staff} Внутренне эти метки будут преобразованы в числа 0, 1 и 2. Эффект заключается в том, что приходится писать, например, UserType.Customer вместо числа 1, что делает код более самодокументированным. Именно таким образом перечисления используются во многих языках. Однако синтаксическое удобство С# на этом не заканчивается. Как и другие предо- предопределенные типы, перечисления в С# являются полноценными структурами. Это озна- означает, что для перечислений можно вызывать методы. Одним из наиболее полезных свойств является то, что можно легко переходить от перечислимых значений (любых перечислимых значений, а не только битовых полей) к строкам и обратно: UserType TheUser = UserType.Customer; string User = TheUser. ToStr ir.g (); // возвращает "Customer"; UserType AnotherUser ■ *= UserType. Parse ("Staff"); // возвращает UserType.Staf f ; Такие вещи практически невозможны в других языках программирования и чрезвы- чрезвычайно полезны, например, в том случае, если необходимо прочитать значения для пере- перечисления из текстового файла. В прошлом такой тип высокоуровневого синтаксиса мог быть реализован только ценой ухудшения производительности, в частности, за счет преобразования перечис- перечислимого значения в класс и доступа к нему как к члену класса. .NET достигает этого без потери производительности. То же самое справедливо для частного случая перечислений с битовыми полями. С# внутренне определяет структуру для представления перечислимого типа. Эта структура является производной от базового класса .NET System.Enum. Синтаксис клю- ключевого слова enum был добавлен языком С#. После того как С# преобразует определе- определение в структуру, процесс продолжается точно так же, как в примере с типом int. Структура, определенная компилятором, преобразуется в перечислимый тип промежу- промежуточного языка. В результате то, что создается в процессе исполнения, представляет со- собой простой целый тип. Результат — синтаксическое удобство struct, но без потерь производительности. Историческое примечание На протяжении этой главы мы ссылались на объектно-ориентированную методологию .NET и IL как на классическое объектно-ориентированное программирование. Однако в действительности существовало несколько версий того, что следует считать объект- объектно-ориентированным программированием. Причина, по которой модель .NET (и С#) была названа классическим ООП, в том, что so многих чертах она представляет собой возвращение к оригинальным концепциям ООП начала 1980-х. Объектно-ориентированное программирование первоначально основывалось на определении классов в коде и на наследовании от них других классов. Идея заключалась в упрощении повторного использования кода путем разбиения программы на более управляемые части. В этой методологии наследование реализаций было чрезвычайно важно как по способу использования нужного кода из уже существующих классов при на- наличии способности замены ненужного кода, так и по способу моделирования иерархии специализаций объектов, имеющей место в реальной жизни. На практике классическое ООП не работало так, как ожидалось, и даже в C++ наиболее типичным методом повтор- повторного использования кода был метод копирования и вставки исходного кода при помощи редактора! Проблема возникала частично из-за отсутствия независимости языков и час- частично из-за модели C++, требующей запутанного использования заголовочных файлов, что практически означало невозможность наследования, если только не имелся доступ по крайней мере к части исходного кода базовых классов. Для многих разработчиков, осваивавших принципы ООП, наиболее устрашающим было то, что основным языком для Windows, базирующимся на классической методоло- методологии ООП, являлся C++. C++ — язык низкого уровня. Он имеет в своем наборе много сложных языковых конструкций (включая указатели и шаблоны), которые на самом деле не требуются в объектно-ориентированном программировании, но которые своей сложностью оттолкнули многих разработчиков от изучения языка.
26 Глава 1 В начале 1990-х гг. Microsoft отреагировала на это введением СОМ и вместе с ней концепции (а) отделения объекта от интерфейса и (б) языковой независимости с помо- помощью двоичного стандарта. В терминах обеспечения повторного использования кода, СОМ была чрезвычайно успешной, возможно, потому, что она привела к появлению элементов управления ActiveX, которые можно было применять практически в любом языке. СОМ также означала, что во многих случаях усилия разработчиков сместились от разработки приложений к разработке компонентов. Однако СОМ сложна в изучении. Освоение тонкостей интерфейсов и GUID требовало длительного времени и навыков, которыми обладали в основном только программисты C++. VB обошел эту проблему, предоставив пользовательский интерфейс для программирования, скрывший многие тонкости СОМ, но за счет запрета компонентов, которые могли бы быть созданы в VB. На самом деле СОМ всегда страдала от того, что не допускала наследование реализаций, требуя вместо этого определения новых объектов. С появлением С# и .NET объектно-ориентированное программирование в некото- некотором роде возвращается к концепциям, лежащим в основе C++. Снова мы имеем классы, определяемые в исходном коде, без необходимости в применении интерфейсов, и снова стандартным способом повторного использования кода становится наследование реали- реализаций. Отличие заключается в том, что Microsoft, похоже, решила основные проблемы, которые мешали повсеместному восприятию объектно- ориентированного программи- программирования с использованием C++: С# гораздо проще в изучении по сравнению с C++, так как язык более выборочно сосредоточен на свойствах ООП. Присутствует также совмес- совместимость языков, причем на более высоком уровне, чем позволяла СОМ. Заголовочные файлы C++ исключены и заменены самоописывающимися сборками. Ясно, что .NET преуспеет в предоставлении способов повторного использования кода с помощью про- простых методологий объектно-ориентированного программирования до такой степени, которая превосходит все, что существовало прежде. Заключение В данной главе рассмотрено взаимодействие между С# и платформой .NET, затронуты, в частности, различные компоненты NET и то, как .NET и промежуточный язык обес- обеспечивают межъязыковую совместимость. Показано, как самоописывающиеся сборки .NET помогают убедиться в надежности кода и предотвращают проблемы с версиями. .NET избавляет разработчика от необхо- необходимости вручную освобождать память и предоставляет набор полезных служб, включая улучшенную безопасность на основе кода, области приложений, базовые классы .NET и отражение. Теперь можно перейти к написанию кода на С#.
г // \\ Л '/ w а В // \\ а Введете в С# В этой главе вы познакомитесь с С# — основным языком для разработки в .NET. Глава на- начинается с истории развития С#. Затем производится сравнение С# с другими языками программирования, используемыми в настоящее время. Рассматривается несколько типов проектов, которые можно создавать в С#. После обзора наиболее важных особенностей С# будет показано, как использовать компилятор С#. Разработка С# Истинная важность С# раскрывается в его историческом контексте. В этом разделе мы рассмотрим эволюцию различных языков программирования и покажем, как С# соотно- соотносится с каждым из них. Появление ассемблера Как известно, бит (двоичная величина) является наименьшей единицей информации, которая может быть представлена внутри компьютера. Память компьютера можно пред- представить в виде длинной непрерывной строки битов, имеющих значение 1 или 0. Вводя последовательности битов в процессор цифрового компьютера, можно заста- заставить процессор выполнять основные арифметические и логические операции: сложение, вычитание, умножение и сравнение. Соединяя вместе миллионы таких инструкций, можно заставить компьютер выполнять довольно сложные и полезные действия. Первоначально программирование битов цифровых компьютеров означало физиче- физическое соединение/разъединение проводов внутри процессора, так что он вел себя тем или иным образом. С появлением модели фон Неймана инструкции стали храниться в памяти для последующего выполнения. Каждая инструкция процессора хранилась как уникальный набор нулей и единиц. Например. 11110000 могла быть инструкцией для сложения различных значений, а 11110001 указывала процессору на необходимость выполнения операции вычитания. Сохранение последовательностей нулей и единиц в памяти для последующего испол- исполнения было длительным и утомительным процессом, часто приводящим к ошибкам. По- Появление ассемблера ускорило этот процесс. Он позволял программистам ссылаться на операции процессора с помощью простых для запоминания мнемоник. Например, в языке ассемблера 11110000 можно было заменить на ADD, a 11110001 обозначалась ин- инструкцией ассемблера SUB. Программист на языке ассемблера мог писать программу с применением простых мнемоник, а затем использовать программу ассемблирования, которая заменяла мнемо- мнемонические инструкции их эквивалентами в виде нулей и единиц (коды операций). Программы .NET состоят не из низкоуровневых машинных инструкций, готовых к не- немедленному исполнению процессором, а из последовательности команд промежуточного языка, которые должны быть интерпретированы программой во время исполнения. Эти инструкции являются более абстрактными, чем инструкции языка ассемблера, так как они не соотносятся напрямую с кодами операций, воспринимаемыми процессором.
28 Глава 2 Однако существует близкое соответствие между инструкциями промежуточного языка и командами машинного языка, в которые они транслируются. Этим частично объясняется скорость, с которой выполняются программы на промежуточном языке. Программа ILDASM.EXE, дизассемблер с промежуточного языка, входящая в состав .NET SDK, может транслировать последовательности команд промежуточного языка в последовательности мнемонических команд ассемблера. Программа на промежуточном языке в мнемонической форме выглядит как программа на ассемблере, в которую добав- добавлены команды для работы с объектами. .NET не единственная платформа, реализующая формат промежуточного языка. То же самое делает Java, компилированный код которого называется байт-кодом Java. В мире Microsoft формат промежуточного языка известен как IL (Intermediate Language), MSIL (Microsoft Intermediate Language) и, наконец, CIL (Common Intermediate Language). Первые компиляторы Писать на языке ассемблера было значительно проще, нежели использовать нули и еди- единицы, однако программисты продолжали мыслить в понятиях отдельных инструкций. Программисту все еще было трудно думать о коде на более высоких уровнях абстракции. Простые операции, которыми сегодня пользуются программисты, например цикл, требовали дюжины или более инструкций на языке ассемблера. Программисты тратили огромное количество времени на написание одних и тех же шаблонов мнемонических команд. Для того чтобы заставить компьютер сделать что-нибудь полезное на языке ас- ассемблера, приходилось проливать немало крови, пота и слез. Наконец появились высокоуровневые языки. Первым из них был FORTRAN. С его помощью программист мог писать код с использованием меньшего числа более абстрак- абстрактных инструкций, а затем передавать эти инструкции программе-компилятору, которая транслировала каждую высокоуровневую инструкцию в несколько десятков, сотен, а то и тысяч машинных инструкций. Теперь программисты могли сосредоточиться на том, что именно делает их про- программа, а не на деталях инструкций машинного кода, стоявших за всем этим. Вдобавок, благодаря тому, что языки высокого уровня абстрагировали программиста от аппарат- аппаратного обеспечения, программы, написанные на таких языках, могли быть перенесены на любую машину, для которой создан соответствующий компилятор. Транслируя программу, написанную на языке высокого уровня, в исполняемый файл в формате машинного языка, программа-компилятор делает несколько проходов по ис- исходному файлу. Результат, получаемый на выходе очередного прохода, пригоден для вы- выполнения следующего прохода. Результатом первого прохода может быть, например, дерево разбора в памяти, в кото- котором расписано иерархическое взаимоотношение между операторами и операндами. На следующем проходе программа идет по каждому узлу дерева, применяя метод синтаксиче- синтаксически управляемой трансляции для преобразования программы в промежуточный язык, близ- близкий к ассемблеру. На третьем проходе программа использует промежуточный язык для перевода его в язык ассемблера, из которого можно с легкостью получить машинный код. Конструирование компиляторов является сложной и долговременной процедурой, поэтому нередко компиляторы имели раздельные внешние и внутренние интерфейсы. Работа внешнего интерфейса заключалась в трансляции исходного кода в промежуточ- промежуточный язык, а затем использовался внутренний интерфейс для трансляции промежуточного языка в машинный код. Как видно, подход .NET к компиляции и выполнению программы лишь слегка изме- изменяет традиционный подход, выработанный много лет назад. В прошлом программы пол- полностью компилировались в инструкции машинного кода и передавались пользователю уже в этом формате. На платформе .NET пользователь получает программу на языке IL, а заключительный этап компиляции происходит одновременно с исполнением програм- программы на машине пользователя. В этом случае программисты могут использовать IL с боль- большей выгодой, так как программы, которые они распространяют, теоретически способны работать на любой аппаратной платформе, имеющей подходящую среду ис- исполнения .NET. Поскольку виртуальные машины для интерпретирования байт-кода Java уже есть на многих платформах, о программах на Java говорят как о переносимых. При- Придется немного подождать, чтобы увидеть, произойдет ли то же самое с .NET и станут ли .NET- программы переносимыми. Разработка компиляторов для языков высокого уровня, таких как FORTRAN, была значи- значительным прорывом, которая потребовала годы человеко-часов для своего достижения. Хотя FORTRAN был (и остается по сей день) полезным для научного применения, он не смог достичь такой популярности, какую приобрел другой язык высокого уровня — язык С.
Введение в С# 29 Язык программирования С С был придуман в то время, когда Кен Томпсон и Дэннис Ритчи из Bell Labs разрабатывали операционную систему UNIX. Сначала они создали часть компилятора С, затем использо- использовали ее для компиляции остальной части компилятора С и, наконец, применили получен- полученный в результате компилятор для компиляции UNIX. Операционная система UNIX первоначально распространялась в исходных кодах на С среди университетов и лаборато- лабораторий, а получатель мог откомпилировать исходный код на С в машинный код с помощью подходящего компилятора С. Распространение исходного кода сделало операционную систему UNIX уникальной; программист мог изменить операционную систему, а исходный код мог быть перенесен с одной аппаратной платформы на другую. Сегодня стандарт POSIX определяет стандар- стандартный набор системных вызовов UNIX, доступных в С, которые должны быть реализова- реализованы в версиях UNIX, являющихся POSIX-совместимыми. С был третьим языком, который разработали Томсон и Ритчи в процессе создания UNIX; первыми двумя были, разумеется, А и В. По сравнению с более ранним языком — BCPL, С был улучшен путем добавления ти- типов данных определенной длины. Например, тип данных int мог применяться для созда- создания переменной с определенным числом битов (обычно 16), в то время как тип данных long мог использоваться для создания целой переменной с большим числом битов (обычно 32). В отличие от других языков высокого уровня, С мог работать с адресами памяти напрямую с помощью указателей и ссылок. Поскольку С сохранил способность прямого доступа к аппаратному обеспечению, его часто относят к языкам среднего уров- уровня или в шутку называют "мобильным языком ассемблера". Что касается грамматики и синтаксиса, то С является структурным языком программи- программирования. В то время как многие современные программисты мыслят в категориях классов и объектов, программисты на С думают в категориях процедур и функций. В С можно определить собственные абстрактные типы данных, используя ключевое слово struct. Аналогично можно описывать собственные целые типы (перечисления) и давать другие названия существующим типам данных при помощи ключевого слова typedef. В этом смысле С является структурным языком с зародышами объектно-ориентированного программирования. Бьерн Страуструп высвободил объектно-ориентированный потенциал С путем пере- перенесения возможностей классов Simula 67 в С. Первоначально новый язык носил имя "С с классами" и только потом стал называться C++. Язык C++ достиг популярности, будучи разработанным в Bell Labs, позже он был перенесен в другие индустрии и корпорации. Сегодня это один из наиболее популярных языков программирования в мире. C++ наследует как хорошие, так и плохие стороны С. С в сравнении с C++ В руках опытного программиста С и C++ являются мощными инструментами для написа- написания эффективно работающих программ. При неграмотном использовании эти языки мо- могут приводить к появлению большого числа ошибок. Ниже приводятся некоторые из наиболее часто встречающихся ошибок. Легко ошибиться, записав оператор присваивания (=) вместо оператора сравнения (==). При этом выражения, возвращающие тип Boolean, будут выдавать true в неожи- неожиданных ситуациях. Способность C/C++ напрямую выделять память приводит к утечкам памяти. Они вы- вызываются программами, которые не освобождают ресурсы системы после завершения работы с ними, и поэтому медленно ставят на колени среду исполнения. Препроцессор C/C++ может запутать код. Во время компиляции кода препроцессор просматривает программу, связывая ее с файлами, указанными в директиве ttinclude, и расширяя макроопределения (#define) путем замены их эквивалентными значениями. Бывает сложно сделать так, чтобы определения функций в одном файле соответствова- соответствовали реализациям функций в другом файле. Еще один тип трудноуловимых ошибок связан с макросами препроцессора, которые расширяются неожиданным образом (например, при использовании в контексте составных аргументов на входе). Другой проблемой C/C++ являются null-указатели. Указатель — это адрес в памяти, который содержит еще один адрес в памяти, где хранятся данные. Посредством указате- указателей можно делать различные интересные вещи, например, ссылаться на одну и ту же структуру данных с помощью разных переменных или перемещаться между элементами иерархической структуры данных. В результате несложно получить "указатель в нику- никуда" — указатель, который не содержит адрес ожидаемого типа данных. Попытка записи
30 Глава 2 данных в структуру, на которую, по вашему мнению, указывает такой указатель, может вызвать ошибку времени исполнения или испортить память, содержащую другие данные. Несмотря на сложность, С и C++ предоставляют программисту большие возможно- возможности. Для платформ, где важна эффективность (например, сети коммутации телефонных линий и Palm OS), С и C++ остаются лидирующими языками. Однако во многих областях ИТ С и C++ не пригодны для разработки приложений. Помимо того, что эти языки сложны в изучении, они предполагают слишком большие сроки разработки и отладки, которые не годятся в сегодняшнем мире, движущемся вперед со скоростью Интернета. Программирование на Си C++ в Windows Первоначально С предназначался для мира командных файлов, процессов-демонов и перенаправляемых потоков ввода-вывода UNIX. Объединение С и C++ с Windows было сопряжено со значительными трудностями. В отличие от UNIX, Windows является системой, управляемой событиями. Операци- Операционная система постоянно опрашивает прерывания для получения информации от графического интерфейса пользователя, такой как нажатие мыши, движение мыши, на- нажатие клавиши на клавиатуре и т.д. Когда окно получает от пользователя такое сообще- сообщение, операционная система Windows должна передать это сообщение окну через очередь сообщений. Окна по своей природе являются иерархическими. Типичное приложение Windows может состоять, например, из главного окна, содержащего несколько окон документов, каждое из которых содержит несколько кнопок, каждая из которых тоже является ок- окном, и так до бесконечности. Написание подобных приложений на С требует вложения нескольких функций друг в друга, так что их конструкции switch. . . case должны пере- передавать сообщения операционной системы от окон нижнего уровня к окнам верхнего уровня, которые способны их обработать. Как можно догадаться, такой С-код будет бес- беспорядочным, накрученным и содержащим потенциально большое количество ошибок. При разработке своих продуктов Microsoft сделала все для того, чтобы противосто- противостоять туманности С. Во-первых, для внутреннего использования Microsoft разработала свои собственные компиляторы С и снабдила их современными отладчиками, способ- способными следовать за кодом построчно. Во-вторых, были стандартизированы приемы про- программирования, такие как assert, сообщения отладчика и "венгерский" стиль именова- именования объектов. В-третьих, был создан режим разработки, исключавший появление ошибок за счет того, что большое значение уделялось ежедневной компиляции программы и тесному взаимодействию между разработчиками и тестировщиками. Microsoft надеялась, что переход к C++ сделает программирование для Windows более легким. В конце концов, создание классов-оболочек для функций API казалось хорошим способом реорганизации API, число вызовов которого разрасталось до тысяч. К сожале- сожалению, Microsoft не удалось достичь популярности в массах. Microsoft Foundation Classes (MFC) не смогли приобрести той популярности, на которую надеялись разработчики, а большинство MFC-программ было переполнено непонятными константами и макросами. Появление VB Первоначально задумывавшийся как игрушка, Visual Basic от Microsoft невероятно быст- быстро завоевал программистский мир. Его популярность обусловлена двумя причинами: от- относительной простотой и продуктивностью. Программы на VB работают медленнее своих аналогов на C/C++, но все же они достаточно быстры для многих деловых целей и требуют гораздо меньше времени на разработку. Формы были той самой сберегающей усилия абстракцией, которую предложил VB программистам Windows. IDE VB позволила разрабатывать окна графически, перетаски- перетаскивая элементы управления, такие как кнопки и списки, с панели инструментов в форму. Получив удовлетворительный внешний вид формы, можно было переходить к кодовой части и писать обработчики событий для каждого элемента управления формы. Разра- Разработка приложения в VB, таким образом, состояла из создания нескольких форм, кото- которые общались друг с другом и, возможно, обращались к базе данных за необходимой информацией. В результате форма оказалась окном, которое предлагало использовать оконные методы гораздо более удобным способом. VB уменьшил число ошибок путем удаления некоторых скрытых элементов синтакси- синтаксиса C/C++. Кроме специальных случаев, выражения ограничивались одной строкой кода, а переменные должны были объявляться и инициализироваться в отдельных строках кода. Операторы присваивания и сравнения использовали один и тот же символ, однако грамматика VB требовала, чтобы эти операторы применялись таким образом, чтобы их намерения были четко обозначены.
Введение в С# 31 Возможно, самым важным было то, что отсутствовали указатели — требование Билла Гейтса, начиная с первых версий Microsoft BASIC. Хотя указатели полезны, так как раз- разрешают прямой доступ к памяти по любому адресу, их использование сопряжено с ошиб- ошибками в том случае, если они применяются неаккуратно. Требование грамматической простоты BASIC восходит к тому факту, что первоначально он был создан как язык для обучения: "Beginner's All-purpose Symbolic Instructional Code" (Многоцелевой символь- символьный командный код для начинающих). VB версии 6 — это уже мощный язык, который можно использовать для создания рас- распределенных приложений с применением компонентов СОМ и Microsoft Transaction Server. Microsoft предложила трехуровневый подход для архитектур "клиент-сервер", в котором "тонкие" пользовательские интерфейсы взаимодействовали с удаленными ком- компонентами VB для получения данных из базы данных или с другой машины. При помощи VBScript и VBA (VB для приложений) можно писать сценарии для web- браузеров и автоматизировать приложения Microsoft Office. Более того, VB6 можно ис- использовать для создания элементов управления Active-X, работающих вместе с Internet Explorer, хотя это делается крайне редко, поскольку требуется, чтобы на машине клиента, работающего в Интернете, была установлена библиотека времени исполнения DLL VB. Начиная с VB5, программы VB компилировались в машинный код, но они были осно- основаны на применении DLL, предоставляющей повсеместно используемые функции и реа- реализующей объектные возможности VB. Интересно то, что компилятор VB для трансля- трансляции использует многопроходный режим, а в конечном счете полагается на компилятор Microsoft C++ для получения выходного машинного кода после компиляции в промежу- промежуточный язык. В этом свойстве VB — использование библиотеки времени исполнения и внутреннего интерфейса C++ — видны зародыши .NET. Однако VB имеет ряд недостатков. Опытные программисты часто удивляются, что в VB далеко не просто реализовать, например, многогюточность. Целые книги были напи- написаны о том, как заставить VB делать такие сложные вещи, которые при обычных условиях стали бы писать только программисты С, скажем, просмотр (hooking) очереди сообщений, посылаемых операционной системой формам VB. Из- за того что программы на VB по- полагаются на среду времени исполнения, они, как правило, работают медленнее, чем программы на С, и требуют установки на машине компонентов среды исполнения VB. Java Требования информационных технологий середины 1990-х гг. отличались от требова- требований 1970-х гг., когда был создан С. В частности, в 90-х гг. наметилась серьезная тенден- тенденция развития распределенных приложений, в которых различные программные компоненты существовали на отдельных платформах и связывались друг с другом по- посредством локальных сетей и Интернета. Java был попыткой Sun Microsystems удовлетво- удовлетворить эти новые требования. Язык Java реализовал синтаксис в стиле C++ в эру Интернета. Наиболее очевидное свойство Java — платформенная независимость. В отличие от С, C++ и Visual Basic, исходный код на Java компилируется не в машинный код, а в байт-код Java, в котором каждый байт соответствует одному из типов инструкций машинного кода, поддерживаемого большинством микропроцессоров. Поскольку существует уровень абстракции между инструкцией байт-кода и эквивален- эквивалентом машинного кода, программа на байт-коде могла быть послана по Интернету для вы- выполнения на разных типах компьютеров. Компьютер, получающий байт-код, содержит программу, которая транслирует байт-код в машинные инструкции для их последующего выполнения. Благодаря тому, что соответствие между инструкциями байт-кода и инструк- инструкциями машинного кода составляет почти один к одному, процесс трансляции является быстрым, и программы на байт-коде выполняются с приемлемой скоростью. Первоначально Java предназначался для переносных устройств потребительской элек- электроники, например, для пультов дистанционного управления телевизоров, но впоследст- впоследствии он стал широко применяться для анимации web-страниц. Апплет Java является программой на байт-коде, доступной на web-сайте. Когда пользователь с включенной под- поддержкой Java в браузере открывает страницу с апплетом, браузер загружает код апплета и выполняет его. Однако с появлением технологий Flash и потоковых данных популярность апплетов Java снизилась. Web-сайты используют апплеты для сборки информации о заказах и для предостав- предоставления сведений. В целях предотвращения появления компьютерных вирусов возмож- возможности апплетов Java были сильно ограничены. Например, Java- апплет не может писать в файл на машине клиента. В процессе выполнения Java-апплета среда исполнения Зпк. 69
32 Глава 2 может динамически запрашивать необходимые компоненты Java из Интернета. По мере поступления байт-кода может осуществляться его проверка для исключения опасного поведения. По мере своего развития Java стал использоваться не только для апплетов, но и для приложений. Были созданы целые приложения иа Java, например браузер Hotjava. Одна- Однако реальное развитие приложения Java получили с появлением Java beans, которые были созданы в ответ на компоненты COM. Java beans могут существовать на отдельных маши- машинах в сети и удаленно общаться друг с другом. Кроме того, Java beans можно наделить воз- возможностями по проведению транзакций — очень важное свойство в приложениях клиент-сервер. Java привлекал разработчиков приложений, которые были нацелены на охват неско- нескольких платформ. Например, требуется создать программу обработки текста, способную работать на машинах Windows, Macintosh и Unix. Разве не было бы здорово, если бы можно было написать программу только один раз, откомпилировать ее и затем запус- запускать на любой из этих платформ без изменений? Красивым обещанием Java было "Напи- "Напишите единожды, запускайте везде". "Напишите единожды, отлаживайте везде",— таков был саркастический ответ разработчиков Java, которые обнаружили, что Java не всегда портируется на другие платформы. Так, пользовательский интерфейс программы на Java мог корректно отображаться на компьютере Macintosh, но некорректно на машине Windows. Другим привлекательным свойством Java была его доступность — Java SDK можно было бесплатно загрузить с сервера Sun. Однако для увеличения продуктивности разра- разработчикам на Java приходилось покупать коммерческие интегрированные среды разра- разработки (IDE), требовавшиеся для написания кода. Visual Cafe, Visual Age и Jbuilder стали теми IDE, которые снискали наибольшую популярность в сообществе разработчиков Java. Синтаксически Java очень похож на C++, однако существуют некоторые отличия: □ Java не поддерживает указателей, так как они являются источником слишком большого числа ошибок в C++. В Java объекты всегда доступны только по ссылке. О Java не поддерживает перегрузку операторов, поскольку неуместная перегрузка часто приводит к путанице, особенно в крупных проектах с участием многих разработчиков. О Java является полностью объектно-ориентированным языком. Это значит, что все функции должны определяться в области видимости класса. В связи с тем, что C++ допускает применение глобальных функций, программисты C++ могут использо- использовать процедурное программирование. О Java поставляется в комплекте с обширной иерархией классов, которые могут использоваться для построения оконных, сетевых приложений и для решения других задач. О Java следует практике объявления класса и реализации его методов в одном файле. О Среда исполнения Java предоставляет механизм сборки мусора, который предот- предотвращает утечки памяти. Microsoft последовала за массовым увлечением Java, включив J++ в среду разработки Visual Studio. J++ расширил Java несколькими новыми возможностями, в частности, воз- возможностью автоматической генерации интерфейсов.СОМ для класса Java в процессе компиляции. Из-за того что многие из этих особенностей были специфичны для платформы Win- Windows, Sun провозгласила, что J++ нарушает дух Java, генерируя не чистый код. В Sun счи- считали, что пользователи должны быть сверены в том, что любой код, носящий имя Java, должен гарантированно выполняться в любой среде исполнения Java, aj++ не мог обес- обеспечить этого из-за платформенно- специфических добавок, произведенных Microsoft. Это философское отличие было источником многочисленных упреков в сторону Рэд- монда и в результате привело к отказу от J++. Java — лаконичный и полезный язык, но для его применения разработчик должен об- обладать определенными навыками и опытом. В частности, разработка на Java более тре- требовательна, чем, скажем, на Delphi или VB, и, следовательно, нужны более опытные программисты. Труд таких разработчиков и переквалификация сотрудников для перехода в мир Java могут оказаться довольно дорогими. По части архитектуры Java был одним из первых языков, который в основу всего по- поставил среду исполнения. Среда исполнения Java не только предоставляет полезные функции, но и интерпретирует код, управляет памятью и обеспечивает безопасность.
Введение в С# 33 с# С# связан с каждым из упоминавшихся выше языков программирования. Как и С, он яв- является лаконичным и мощным. Как и C++, объектно-ориентированным. Как и VB, он предлагает мощные средства графической разработки, упрощающие создание пользова- пользовательских интерфейсов. Как и Java, C# компилируется в байт-код, который использует службы среды исполнения. Теперь более детально сравним С# с другими языками программирования. Мы огра- ограничимся рассмотрением универсальных языков, не затрагивая те, что предназначены для особых целей, например языки искусственного интеллекта и языки для обучения. С# в сравнении с другими языками Ниже производится сравнение С# с другими языками, используемыми сегодня для разра- разработки. Начнем с VB6. Сравнение С# с VB6 Наиболее очевидное отличие С# от VB6 в том, что С# компилирует в MSIL, a VB6 — в ма- машинный код. Программы, написанные на этих языках, требуют для запуска среду испол- исполнения. Преимуществом среды исполнения С# является то, что она позволяет коду СМ использовать функциональность, предлагаемую базовыми классами .NET. По части возможностей С# является более объектно-ориентированным, чем VB6. VB6, например, не имеет параметризованных конструкторов, перегрузки операторов и наследования реализаций. По части синтаксиса С# более лаконичный, чем VB6. Он до- допускает одновременное объявление и инициализацию переменных, а выражения могут занимать произвольное число строк. Означает ли появление С# то, что необходимо отказаться от кода, написанного на VB6? Нет. Огромное число программ на VB6, занявших определенную нишу на рынке, было раз- разработано людьми, которые понимают нужды рынка лучше, чем программирование. Если вы являетесь таким программистом-предпринимателем, новые замечательные возможно- возможности С# и .NET не избавят ваше приложение от оп;ибок разработки. Более того, особенно- особенности синтаксиса С# могут только усложнить процесс разработки и повысить вероятность появления ошибок. К тому же зависимость программ С# от среды исполнения требует, чтобы она поставлялась вместе со всеми приложениями, рассчитанными на .NET. Сравнение С# с VB.NET VB.NET способен делать практически то же самое, что и С#. Единственное, что не позво- позволяет VB.NET,— включать в программу блоки чистого кода на C++ с указателями и другие небезопасные идиомы. Однако это мало кому требуется. Уже потому, что VB.NET является довольно мощным, его не так просто использо- использовать. Добавление новых свойств к VB потребовало провести значительные изменения грамматики и синтаксиса языка. Если вы не являетесь программистом на VB с опытом разработки объектно-ориентированного кода, то изучение VB.NET не покажется вам легким делом. В примерах кода и в пресс-релизах Microsoft превозносит С# над VB.NET. Кроме гого, Microsoft планирует для последующих разработок использовать в основном С# и опубликовать стандарт С#. чтобы другие производители могли разрабатывать компиля- компиляторы С# для своих платформ. Эти факты подразумевают, что программисты на VB могут оказаться изолированными в будущем. С другой стороны, позиции VB сейчас настолько сильны, что в действительности VB.NET может вытеснить С#! В реальности не будет особой разницы в том, что использовать — С# или VB.NET. Од- Однако не следует выбирать VB.NET только из-за кажущейся простоты перехода на него, потому что этой простоты нет. Сравнение С# с Visual C++ Прежде всего С# является разновидностью C++. Это подтверждается хотя бы тем фак- фактом, что в код на С# можно включать целые куски небезопасного кода на чистом C++ (см. ниже). Так как предполагается, что С# более продуктивный, чем C++, С# запрещает некоторые идиомы C++, часто приводящие к ошибкам: О С# запрещает указатели и арифметику указателей (за исключением их применения в блоках небезопасного кода).
34 Глава 2 О С# исключает макросы препроцессора (но сохраняет условную компиляцию и константы ttdefine). Сложные макросы могут вызывать ошибки, когда они не- непредсказуемым образом применяются к составным аргументам. О С# требует, чтобы переменная была инициализирована начальным значением до того, как она будет использована. □ С# обходится без конструкций switch. . .case, допускающих последовательное исполнение инструкций (fall through). Для защиты программиста от ошибок те- теперь каждый case в конструкции switch должен закрываться командой break. О С# заставляет использовать объектно-ориентированное программирование, исклю- исключая возможность объявления глобальных функций. Каждая функция в любой про- программе должна быть членом класса, возможно, статическим. Даже открывающая функция Main () определена как член класса. С# считается более легким, чем C++, также потому, что: О Когда компилируются библиотеки классов С#, компилятор автоматически создает компоненты, которые могут использоваться другими приложениями. Не требуется разбираться с макросами ATL и с GUID. О С# поддерживает использование Windows Forms для быстрой разработки гра- графических интерфейсов пользоьателя. Не требуется писать кот для обработки событий OnPaint и т.п. О С# стандартизирует использование общих файлов, которые содержат как определе- определения классов, так и их реализации. Совмещение определения класса с его реализацией в одном файле облегчает работу с ним. Из-за того, что код С# интерпретируется, он не является столь эффективным, как код C++. Однако скорость С# достаточна для удовлетворения нужд большинства ИТ-ком- паний, которые приветствуют сокращение циклов разработки, обуславливаемое тем, что С# делает акцент на продуктивности и правильности. Благодаря тому, что С# компи- компилируется только в IL, а не в машинный код, он является прекрасным средством для со- создания web-страниц и деловых приложений, но меньше подходит для разработки драйверов и ядра операционной системы. Программисты, использующие чистый C++, будут долго ждать прихода в свой язык функций безопасности. Другим преимуществом С# является набор полезных типов данных, который он пре- предоставляет в пространстве имен System. Вам не нужно использовать отдельные заголо- заголовочные файлы, чтобы получить в свое распоряжение типы для финансовых величин, строковых величин и величин типа Boolean. Более того, поскольку эти типы являются классами, а не просто примитивами, каждый из них имеет полезные статические методы для преобразования и форматирования результата на выходе. В целях предотвращения ошибок типы С# являются строго типизированными. Это означает, что преобразования между определенными типами данных должны осуществля- осуществляться явно. В C++ целое значение можно было неявно трактовать как значение типа Boolean. В С# так делать нельзя. Целое должно быть явно приведено к значению Boolean для того, чтобы рассматриваться в этом виде. Сравнение С# с управляемым C++ Как уже говорилось в главе 1, компонент времени исполнения .NET применяется для управления клиентским кодом, обеспечивая сборку мусора, проверку безопасности и дру- другие службы. Вместе с VS.NET компания Microsoft поставляет набор заголовочных файлов, которые позволяют программистам на C++ использовать эти службы. Указывая заголовоч- заголовочные файлы и предваряя определения существующих классов C++ атрибутами, определен- определенными в них, программисты могут предоставить существующему коду C++ преимущества среды исполнения .NET. Если вы являетесь разработчиком на C++ и хотите перейти на платформу .NET, то в последующих разработках вам, видимо, придется использовать С#. Расширения, добав- добавленные для того, чтобы код C++ мог работать под управлением .NET, полезны для обнов- обновления существующего кода. Хотя управляемый код C++ разрешает объединять управляемые и неуправляемые классы в одном физическом компоненте, вам, вероятно, не потребуется делать это слишком часто. Сравнение С# с C++ Builder Среда разработки Borland, C++ Builder, сочетает подход к пользовательскому интерфей- интерфейсу, основанный на формах, с применением языка C++. С# тоже поддерживает формы.
Введение в С# 35 Преимущество С# по сравнению с C++ Builder заключается в том, что он обладает совме- совместимостью языков, обеспечиваемой средой исполнения .NET. Поскольку программы, от- откомпилированные в C++ Builder, являются машинным кодом, они должны быть основаны на службах СОМ для взаимодействия с кодом, написанным на других языках. Сравнение С# с Delphi Delphi, другой инструмент быстрой разработки от Borland, сочетает формы с использо- использованием собственного языка, основанного на языке Pascal. Delphi достиг наивысшей попу- популярности перед появлением VB6, будучи единственным простым языком для создания компонентов СОМ и элементов управления Active X, который могли бы использовать программисты, не знающие C++. Основанный на использовании пар BEGIN ... END для разделения блоков кода, син- синтаксис Delphi является более громоздким и прямолинейным, чем синтаксис С#. Так же как и C++ Builder, Delphi создает при компиляции машинный код и полагается на СОМ для обеспечения совместимости. Проект Delphi нельзя отлаживать по шагам в отладчике VS.NET, как это делается для проектов VB.NET. Сравнивая С# с языками, компилирующими в машинный код, например с Delphi и C++ Builder, необходимо иметь в виду такой фактор, как платформенная независимость. Для С# этого пока еще не достигнуто, однако многие считают, что платформенная неза- независимость скоро станет реальностью. Поскольку Delphi и C++ Builder не используют байт-код, платформенно-независимые версии для них появятся не ранее чем через одну версию и потребуют интенсивных усилий со стороны Borland. Сравнение С# с Java Очевидно, что Java оказал сильное влияние на С#. Кто-то однажды назвал С#как "Cava". Андерс Хелзберг (Anders Hejlsberg), лидер группы разработчиков С#, возглавлял также разработку J++, теперь уже не существующего компилятора Java от Microsoft. Синтаксисы Java и С# похожи. Даже структура библиотеки Java и базовых классов .NET практически одинакова. Разумеется, оба языка используют байт-код. В настоящее время Java имеет сильное преимущество перед С# — платформенную не- независимость. Поскольку существуют реализации среды исполнения Java для большинст- большинства вычислительных платформ, один и тот же байт-код Java может теоретически выполняться на любой из них. Это то, чего пока не могут достичь программы .NET. С# имеет три небольших преимущества перед Java: □ Синтаксис С# немного мощнее, чем Java, так как С# поддерживает перегрузку операторов и безопасные по типу перечисления. В случае необходимости можно использовать в коде С# указатели и другие "незаконные" идиомы, размещая код внутри "небезопасных" блоков. □ С# совместим с кодом, написанным на других языках .NET. Это означает, что ИТ-отдел не обязан использовать только лишь С# для своих разработок. По этой причине С# может рассматриваться как более дешевая альтернатива Java. □ Базовые классы .NET предоставляют С# унифицированный, стандартизирован- стандартизированный источник повсеместно требуемой функциональности, такой как XML, рабо- работа с сетью и графикой. Для доступа к аналогичным средствам программисты Java нередко вынуждены использовать множество различных источников. Сравнение С# с JavaScript Как известно, JavaScript не имеет ничего общего с Java. JavaScript — язык сценариев, применяемый на стороне клиента для оживления web-страниц. Присвоение ему назва- названия Jave&cript было бесстыдной маркетинговой политикой, направленной на получение прибыли от успеха Java. Во времена, когда Java в основном использовался для создания апплетов, JavaScript и Java иногда преследовали схожие цели. С# похож на JavaScript тем, что оба языка наследуют С-подобный синтаксис. С#, одна- однако, не строго интерпретируется, а компилируется в байт-код, который кэшируется и не может быть использован в качестве языка сценариев для web- браузера (по крайней мере, сейчас). В то время как JavaScript основное свое внимание уделяет откликам на события web-браузера, С# сосредотачивается на создании HTML и получении данных, которые посылает web-браузер. Как будет показано при обсуждении WebControls, JavaScript и С# могут использовать- использоваться совместно для достижения большего эффекта. В Интернет- приложении код на С# мо- может определять, какой тип браузера применяется на стороне клиента, и посылать ему
36 Глава 2 фрагмент кода на JavaScript, предназначенный для этого браузера. Разумеется, можно писать страницы ASP.NET с использованием JavaScript (JScript) точно так же, как это делается в ASP-страницах. Сравнение С# с VBScript До сих пор VBScript являлся наиболее широко используемым языком для написания ак- активных серверных страниц (ASP). При помощи ASP.NET, однако, программисты смогут использовать С#, VB.NET или другие языки .NET для создания страниц. Это означает, что на серверной стороне С# может заменить VBScript, хотя ожидается, что последую- последующие версии Internet Explorer будут поддерживать клиентский код на VBScript. Использование С# Обсудим специфику использования С# и посмотрим, где он может оказаться полезным. Начнем с приложений ASP.NET. Приложения ASP.NET ASP является технологией Microsoft для создания web-страниц с динамическим содержи- содержимым. В своей основе ASP-страница — это HTML-файл с внедренными в него фрагментами серверного VBScript или JavaScript. Когда клиентский браузер запрашивает страницу, web-сервер предоставляет куски HTML-текста страницы, обрабатывая на своей стороне встречающиеся фрагменты серверного сценария. Часто эти сценарии запрашивают данные из базы данных и представляют их в виде HTML. ASP предлагает простой способ построения приложений на основе браузеров. Однако ASP обладает рядом недостатков. Во-первых, страницы ASP медленно ото- отображаются, так как код на стороне сервера интерпретируется, а не компилируется. Во-вторых, ASP-файлы трудно поддерживать, поскольку они не структурированы; код серверной стороны и чистый HTML смешиваются в одну кучу. В-третьих, ASP иногда за- затрудняет процесс разработки, так как практически отсутствует поддержка проверки ошибок и типов. В частности, если требуется реализовать обработку ошибок на страни- страницах, необходимо использовать конструкцию On Error Resume Next и сопровождать каждый вызов компонента проверкой Err .Number для того, чтобы убедиться, что вызов прошел успешно. ASP.NET, первоначально известный как ASP+, является следующей версией ASP, ко- которая решает многие проблемы. Он не заменяет ASP, напротив, страницы ASP.NET мо- могут сосуществовать на одном сервере с уже написанными страницами ASP. Разумеется, писать на ASP.NET можно с использованием С#! Особенности ASP.NET Первое и, возможно, наиболее важное — страницы ASP.NET являются структурирован- структурированными. Это означает, что каждая страница представляет собой класс, унаследованный от класса .NET WebPage, и может переопределять набор методов, которые вызываются в течение периода существования класса WebPage. (Можно рассматривать эти события как аналог событий OnApp_Start и OnSession_Start, которые определялись в файлах global. asa в старом добром ASP.) Благодаря тому, что функциональность страниц можно разместить в обработчиках событий, имеющих ясный смысл, страницы ASP.NET легче в понимании. Другое достоинство страниц ASP.NET заключается в том, что их можно создавать в VS.NET — в той же самой среде, в которой создаются бизнес-логика и компоненты доступа к данным, используемые этой ASP-страницей. Проект VS.NET содержит все файлы, свя- связанные с приложением. Более того, ASP-страницы можно отлаживать в редакторе. Во вре- времена Visual Interdev приходилось раздражающе долго конфигурировать InterDev и web-сервер проекта для того, чтобы включить режим отладки. Эти дни миновали, о чем мы поговорим при обсуждении обработки ошибок ASP.NET и механизмов трассировки. Особенности ASP.NET дают возможность еще эффективнее использовать структури- структурированный подход. ASP.NET позволяет изолировать функциональность страницы на сто- стороне сервера в класс, откомпилировать класс в DLL и разместить эту DLL в каталоге следом за HTML-частью. Директива code_behind в верхней части страницы связывает файл с DLL. Когда браузер запрашивает эту страницу, web-сервер передает события в класс, расположенный в соответствующей DLL. И последний, не менее значимый момент: ASP.NET отличается своей производитель- производительностью. В то время как ASP-страницы компилируются при каждой загрузке страницы, страницы ASP.NET кэшируются сервером после компиляции. Это означает, что все по- последующие запросы к странице будут выполняться быстрее, чем первый.
Введение в С# 37 Традиционная мудрость заключается в том, что приложения на основе форм предла- предлагают более богатый пользовательский интерфейс, но их сложнее поддерживать, так как они работают на большом числе разных машин. По этой причине на приложения, осно- основанные на формах, полагаются тогда, когда богатые пользовательские интерфейсы яв- являются необходимостью, а пользователи могут быть обеспечены хорошей поддержкой. С появлением Internet Explorer 5 и блестящей производительности Navigator 6 преи- преимущества приложений, основанных на формах, несколько уменьшились. Хорошая и надежная поддержка IE5 DHTML позволяет программисту создавать приложения на осно- основе Web, которые так же хороши, как и их существующие аналоги на стороне клиента. Разу- Разумеется, подобные приложения потребуют стандартизации IE и отказа от Navigator. Сейчас такая стандартизация во многих случаях возможна. Web Forms Для упрощения создания web-страниц Visual Studio.NET предоставляет WebForms. Они по- позволяют строить web-страницы графически точно так же, как создаются окна в VB6 или C++ Builder, т.е. посредством перетаскивания элементов управления с панели инструмен- инструментов в форму, получения кода формы и написания обработчиков событий для этих элемен- элементов управления. При использовании С# для создания Windows Form формируется класс С#, который наследуется от базового класса WebPage, и страница ASP, которая применяет этот класс. Разумеется, для создания Windows Form не обязательно использовать С#, для этого подходит любой другой язык .NET. В прошлом сложности web-разработки оттолкнули многих от подобных попыток. Что- Чтобы преуспеть в разработке для Web, необходимо было знать большое число различных технологий, таких как VBScript, ASP, DHTML, JavaScript и т.п. Применяя к web-страницам концепции форм, WebForms обещают упростить web-разработку. Однако лишь время по- покажет, насколько успешны будут WebForms и WebControls в том, чтобы оградить разра- разработчика от сложностей web-дизайна. Web Controls Элементы управления, содержащиеся в web-форме, отличаются по смыслу от элементов Active X. Это тэги XML в пространстве имен ASP, которые web-браузер динамически трансформирует в HTML и сценарий на стороне клиента при запросе страницы. Удиви- Удивительно то, что web-сервер способен создавать один и тот же элемент управления разными способами, производя преобразование, подходящее для конкретного браузера. Можно использовать С# или VB.NET для расширения панели инструментов Windows Forms. Создание нового серверного элемента управления сводится к написанию класса web-сервера .NET (см. главу 7). Службы Web Сегодня страницы HTML составляют основной трафик Всемирной Паутины (WWW). Однако с внедрением XML компьютеры получают в свое распоряжение независимый от устройства формат для коммуникации друг с другом по сети. В будущем компьютеры, воз- возможно, будут использовать для передачи информации Web и XML, а не выделенные ли- линии и патентованные форматы типа EDI (Обмен электронными данными). Службы Web разработаны в расчете на сеть, ориентированную на предоставление услуг, где удален- удаленные компьютеры обеспечивают друг друга динамической информацией, которая может быть проанализирована и отформатирована перед окончательным представлением по- пользователю. Службы Web предлагают простой способ обмена информацией между компьютерами в среде Web в форме XML. В технических терминах, служба Web является страницей ASP.NET, возвращающей клиенту XML вместо HTML. Такие страницы основываются на DLL, содержащей класс, который подчиняется правилам интерфейса WebService. IDE VS.NET предоставляет механизм, который облегчает разработку web-служб. Та или иная организация может остановить свой выбор на web-службах по двум основным причинам. Первая причина: web-службы полагаются на HTTP и могут исполь- использовать существующие сети (WWW) как носитель информации. Вторая причина заключа- заключается в применении web-службами XML — непатентованного, самоописывающегося и платформенно-независимого формата данных. Последнее и, пожалуй, самое неприметное, это то, что С# может использоваться для создания консольных приложений — текстовых приложений, работающих в DOS-окне. Этот тип приложений лучше всего подходит для создания процессов-демонов, которые будут выполняться на заднем плане и периодически вызываться планировщиком задач, либо программ-драйверов для компонентов тестирования модулей (проекты библиотек классов).
38 Глава 2 Библиотеки классов Можно рассматривать компонент как файл компилированных классов, которые могут повторно использоваться клиентскими приложениями. В качестве подхода к созданию компонентной модели Microsoft выбрала СОМ — двоичный стандарт, в котором компо- компоненты могли быть реализованы в любой форме до тех пор, пока они поддерживали слож- сложный интерфейс (iunknown). Благодаря тому, что доступ к компонентам СОМ осуществлялся только с помощью этого интерфейса, один и тот же СОМ-компонент мог использоваться приложениями, написанными на разных языках. В новом подходе .NET совместимость языков достигается за счет того, что все ком- компоненты компилируются в общий формат — промежуточный язык. Это освобождает раз- разработчиков от сложностей программирования СОМ. Достаточно создать проект компонента, откомпилировать его и позволить компилятору молча сгенерировать то, что необходимо для предоставления класса клиентам. Другим преимуществом подхода .NET является регистрация. Компоненты .NET во- вообще не надо регистрировать; копия компонента помещается в каталог клиентского приложения, и оно немедленно может воспользоваться им. На самом деле серверный компонент содержит в себе свои собственные метаданные, в разделе компонента, извест- известном как манифест. Когда клиент создает объект .NET из серверного компонента, среда исполнения .NET получает информацию об интерфейсе из манифеста сервера с помощью процесса, называемого зондированием. Как говорится в главе 10, локализация метаданных компо- компонента подразумевает совместное существование версий — способность различных версий одного и того же компонента мирно сосуществовать на одном компьютере, таким обра- образом исключается "ад DLL". Для создания нового компонента .NET необходимо открыть новый проект Class Library в VS.NET IDE, реализовать некоторые классы и откомпилировать результат. Обычно реше- решения VS.NET будут состоять из проекта Windows Form (ASP.NET) и нескольких проектов библиотек классов .NET, которые используются ASP-страницами для работы с данными. Microsoft позволяет относительно легко использовать компоненты .NET вместе с су- существующими СОМ-компонентами. SDK .NET содержит две утилиты для создания обо- оболочек СОМ-компонентов для IL и оболочек компонентов .NET для СОМ. Эти утилиты создают компоненты-посредники, которые передают вызовы оригинальным компонен- компонентам СОМ. В качестве альтернативы можно делать существующие классы C++ предназна- предназначенными для .NET, предваряя их символами "х" — тэгами, которые предписывают компилятору C++ генерировать для них IL-интерфейсы. Методика х выполняет и другие задачи. Добавляя х к компонентам .NET, можно под- подготовить их к осуществлению служб СОМ+ (ранее MTS). Помимо поддержки транзакций и безопасности, основанной на методах, эти службы были расширены для поддержки со- событий и обработки сообщений. Приложения Windows Хотя С# и .NET частично предназначены для web-разработки, они по-прежнему предо- предоставляют отличную поддержку для создания так называемых программ "толстого клиен- клиента" — приложений, которые устанавливаются на машине пользователя, где происходит большая часть обработки. Эта поддержка осуществляется в виде Windows Forms. Формы Windows являются ответом .NET на VB6 Form. Для разработки графического оконного интерфейса необходимо перетащить элементы управления с панели инструмен- инструментов в форму Windows. Чтобы определить поведение окна, для элементов управления фор- формы пишутся обработчики событий. Проект Windows Forms компилируется в ЕХЕ-файл, который должен быть установлен вместе со средой исполнения .NET на клиентском компьютере. Как и другие типы проектов .NET, проекты Windows Forms поддерживаются как VB.NET, так и С#. Элементы управления Windows Хотя WebForms и Windows Forms создавались практически одинаково, для их использова- использования применяются разные типы элементов управления. WebForms используют WebControls, a Windows Forms — элементы управления Windows. Элемент управления Window Control похож на элемент Active X. По завершении реа- реализации Window Control компилируется в DLL, которая должна быть установлена на клиентской машине. На самом деле .NET SDK предоставляет утилиту, которая создает оболочку для элемента управления Active X, в результате чего он может быть помещен в форму Windows. Другая утилита позволяет использовать элементы управления .NET в
Введение в С# 39 качестве элементов Active X. Как и в случае с WebControls, создание Window Control включает в себя реализацию интерфейса, определенного конкретным классом — WindowControl. Консольные приложения С# может применяться для создания консольных приложений, т.е. текстовых приложе- приложений, работающих в DOS-окне. Как упоминалось ранее, консольные приложения могут использоваться при модульном тестировании библиотек классов и для создания процес- процессов-демонов. Место, занимаемое С# в корпоративных архитектурах Поскольку С# требует наличия среды исполнения .NET, он наилучшим образом подхо- подходит для сред исполнения, которые могут хорошо контролироваться. До тех пор пока .NET не достигнет такой популярности, когда производители программного обеспече- обеспечения будут уверены, что среда исполнения .NET присутствует на большинстве машин по- потребителей, они, скорее всего, оставят использование С# и .NET ИТ-отделам, ASP (провайдерам служб приложений) и другим организациям, которые гарантируют присут- присутствие среды исполнения .NET соответствующей версии и которые могут настроить ее для получения оптимальной производительности. Иначе говоря, С# предоставляет хоро- хорошую возможность для организаций, которые заинтересованы в построении надежных, многоуровневых приложений клиент-сервер. Используя ADO.NET, С# может быстро и обычным образом получать доступ к храни- хранилищам данных, таким как базы данных SQL Server и Oracle. Получаемые наборы данных могут легко обрабатываться с помощью объектной модели ADO.NET и автоматически преобразовываться в XML для передачи по офисной локальной сети. Как только для нового проекта будет установлена схема базы данных, С# предложит великолепную среду для реализации уровня объектов доступа к данным, каждый из кото- которых сможет обеспечить вставку, обновление и удаление записей для различных таблиц базы данных. Корпоративные разработчики, обращаемся к вам: создавайте программу, которая автоматически сгенерирует уровень объектов доступа к данным для любой базы данных SQL Server. Будучи первым компонентно-ориентированным С-языком, С# прекрасно подходит для ре- реализации ряда бизнес-объектов. Он скрывает в себе систему межкомпонентных связей, позволяя разработчику сфокусироваться на объединении своих объектов доступа к дан- данным в методы, которые точно реализуют деловые правила его организации. Более того, с помощью атрибутов бизнес-объекты С# могут быть снабжены проверками безопасно- безопасности на уровне методов, управлением пулом объектов и JIT-активизацией, предоставляе- предоставляемой службами СОМ+. Наконец, .NET поставляется с утилитами, которые позволяют новым бизнес-объектам .NET взаимодействовать с существующими компонентами СОМ. При разработке корпоративного приложения с применением С#, возможно, потре- потребуется создать проект Class Library для объектов доступа к данным и еще один проект для бизнес-объектов. В процессе разработки можно использовать Console-проекты для про- проверки методов классов. Фанаты экстремального программирования смогут создавать Console-проекты, которые будут автоматически выполняться из командных файлов для проверки того, что код не был испорчен. С# и .NET, вероятно, изменят способ, посредством которого повторно используемые классы упаковываются физически. В прошлом многие разработчики помещали большое число классов в один физический компонент, поскольку такое размещение упрощало установку приложения; как решать проблему с версиями, вы уже знаете. Благодаря тому, что распространение корпоративных компонентов .NET представляет собой простое копирование файлов в каталоги, разработчики теперь могут размещать классы в более логичные дискретные компоненты без проблем с версиями DLL. Наконец, страницы ASP.NET, созданные в С#, являются отличной средой для поль- пользовательских интерфейсов. Благодаря тому, что страницы ASP.NET компилируются, они быстро выполняются. Благодаря тому, что они могут быть отлажены в VS IDE. они надежны. Благодаря тому, что они поддерживают полный набор языковых особенно- особенностей, таких как раннее связывание, наследование и разбиение на модули, страницы ASP.NET, написанные на С#, понятны и легки в обслуживании. Опытные разработчики проявляют здоровый скептицизм по поводу активно рекла- рекламируемых новых технологий и языков и не склонны использовать новые платформы то- только потому, что их заставляют этим заниматься. Если вы являетесь корпоративным разработчиком в ИТ-отделе или предоставляете услуги, связанные с приложениями, че- через WWW, позвольте вас заверить, что С# и .NET предлагают по меньшей мере три
40 Глава 2 преимущества, даже в том случае, если некоторые из более экзотических особенностей типа Web Services и элементов управления на стороне сервера не устраивают вас: □ Конфликты между компонентами станут возникать гораздо реже, и установка бу- будет проще, так как различные версии одного компонента могут работать вместе на одной машине без конфликтов друг с другом. □ Код ASP больше не будет выглядеть как спагетти. □ Базовые классы .NET предлагают множество функциональных возможностей. Возможно, вы заметили, что приложения Windows Forms не упоминались в этом раз- разделе. Дело в том, что автор не стал бы использовать Windows Forms для любого из разра- разрабатываемых в настоящий момент пользовательских интерфейсов. Простота установки, предлагаемая приложениями на основе браузеров, и богатство возможностей IE5 дела- делают страницы ASP.NET более привлекательными. Если вы являетесь программистом VB6 и не имеете большого опыта web- разработки, то проект С# Windows Forms послужит хо- хорошим способом начала изучения С#, так как Windows Forms очень похожи на формы VB6. Если вы или ваши коллеги не имеют опыта работы с Javascript, ASP или связанными технологиями, то Windows Forms будут хорошим вариантом для быстрого и легкого созда- создания пользовательских интерфейсов. Только помните, что логика пользовательского ин- интерфейса должна быть отделена от бизнес-логики и кода доступа к данным. Это позволит вам при желании перенести свое приложение в браузер. Обзор свойств С# Базовые классы .NET Одним из самых больших недостатков C++ было отсутствие полезных типов данных. Бе- Безусловно, можно создать указатель на любой тип данных и любой класс, но требовалось потратить огромное количество энергии даже для того, чтобы создать типы данных string, boolean и типы для финансовых величин. Выражения, управляющие ходом выполнения программы С# предоставляет обычные выражения для управления ходом выполнения программы: if... else, циклы whi le, for и do. . . while. Дополнительно С# поддерживает конструк- конструкцию for each для итерации по элементам коллекций, массивов, списков и других кон- контейнеров. Для сокращения числа нажатий клавиш С# поддерживает тернарную форму оператора if.. .else: конструкцию (А?В:С). Во избежание ошибок С# требует, чтобы после каждого элемента case в блоке switch. . .case стояла команда break. Объектно-ориентированные свойства C++ привнес классы в С, и по этой причине объектно-ориентированное программирова- программирование в C++ всегда сопровождалось неудобствами. Можно было, например, сделать шаг на- назад и применить процедурный подход, полагаясь на глобальные функции там, где необходимо использовать объекты. Или вы могли быть ошеломлены, обнаружив огром- огромное число способов решения той или иной задачи и затрудняясь определить, какой из них будет лучшим. Как много разных способов существовало, например, для создания аб- абстрактного класса, который обеспечивал бы только интерфейс и не мог наследоваться? Ограничив число идиом несколькими ключевыми моментами, С# гордится своей объ- объектной ориентированностью, которая является зрелой и продуктивной. Например, в нем существует ключевое слово, позволяющее определять классы как абстрактные. Для под- поддержки построения хороших иерархий классов ряд ограничений наложен на наследова- наследование. В частности, класс может наследовать интерфейсы от любого числа классов, но может наследовать реализацию только от одного базового класса. Как будет показано в последующих главах, неоднозначности, касающиеся полиморф- полиморфных функций, были решены при помощи нового, более ясного синтаксиса для определе- определения виртуальных и чисто виртуальных функций. В частности, класс может заранее обеспечить реализацию метода и заставить дочерние классы сделать это путем добавле- добавления к названию метода ключевого слова abstract. Более того, ключевое слово sealed по- позволяет создавать классы, от которых нельзя формировать производные классы. Как и в C++, полный набор ключевых слов, задающих область видимости, позволяет управлять тем, кто может видеть данные и методы внутри класса. Одной из самых замечательных возможностей механизма классов С# является его под- подход к свойствам. Этот подход централизует аспекты read и write свойства в одном месте,
Введение в С# 41 что облегчает управление ими. Общий эффект заключается в получении объектов, кото- которые чувствуют себя как объекты потому, что их свойства синтаксически более похожи на характеристики и менее похожи на замаскированные вызовы функций. Специальная фор- форма свойства, называемая индексатором, показывает массивы внутри классов с помощью интуитивно понятного синтаксиса. Как упоминалось ранее, С#, в отличие от Java, поддерживает перегрузку операторов, т.е. способность указывать, как будут вести себя экземпляры классов при применении к ним операторов языка. Перегрузку операторов необходимо применять правильно; в слу- случае некорректного использования перегрузка операторов способна запутать код. Во из- избежание этого С# ограничивает набор операторов, которые могут быть перегружены. Нельзя, например, перегрузить оператор присваивания. Классы С# могут содержать многочисленные параметризованные конструкторы. Од- Однако они не реализуют деструкторы так, как это делают обычные классы C++. Вместо дест- деструкторов классы С# имеют методы finalize, которые предположительно должны вызываться не тогда, когда объекты выходят из зоны видимости, а когда во время выпол- выполнения программы начинает работать сборщик мусора. По этой причине метод finalize является, по крайней мере сейчас, ненадежным методом, он не дает полной гарантии того, что объект освободит ресурсы. Классы С# не поддерживают шаблоны для общего по отношению к типам кода. Microsoft отмечает, что общий по отношению к типам код будет возможен в следующих версиях С#, однако он будет основываться на других механизмах. В заключение подчеркнем, что первое и наиболее важное правило оценки програм- программного обеспечения состоит в выяснении того, насколько хорошо оно справляется со своей задачей. Не существует похвал программам с ошибками, использующим каждую из идиом, предлагаемых языком. Это говорится в основном для программистов VB6, которые впервые столкнутся с наследованием реализаций; возможность что-то сделать вовсе не означает, что вы хотите это сделать. Чрезмерное увлечение наследованием реализаций может привести к иерархиям классов, которые сильно связаны друг с другом и в которых отдельный класс будет невозможно изменить без изменения связанных с ним классов. Отражение и атрибуты Полиморфизм и позднее связывание являются двумя способами, с помощью которых объектно-ориентированное программирование реализует свои самые большие преиму- преимущества. Однако клиентам, использующим поздно связанные объекты, обычно приходит- приходится выяснять подтипы объектов, с которыми они имеют дело, чтобы обращаться с ними должным образом. Посредством отражения клиент может определить подтип серверно- серверного объекта и методы, поддерживаемые этим подтипом,— и все это без определения клас- класса сервера путем исследования кода. Код может рассмотреть, или отразить, самого себя. Атрибуты являются способом связывания метаданных с классами, так что метадан- метаданные могут быть определены отражением. В таких языках .NET, как С# и VB.NET, разре- разрешается создавать свои собственные атрибуты для предоставления информации через отражение или применять атрибуты, созданные другими пользователями. Атрибуты мо- могут, например, определять, как класс ведет себя в контексте служб СОМ+ (см. главу 20). Компилятор С# может интерпретировать эти атрибуты для обеспечения компилирован- компилированной версии компонента "зацепками", необходимыми для использования этих служб. Другие применения, в которых полезно отражение кода, включают в себя приложе- приложения анализа кода и реализации шаблонов разработки "Посетитель" (см. Design Patterns: Elements ofRe-Usable Object Oriented Design, Erich Gamma и др., ISBN: 0201633612). Небезопасный код Небезопасный код — это одна из концепций С#, часто понимаемая неправильно. Это не тот код, который выполняется вне контекста среды исполнения .NET, а код, использую- использующий идиомы C++, которые признаются небезопасным, так как они часто приводят к ошибкам (небезопасными идиомами, например, являются указатели и арифметика указателей). Блюдя дух свободы, С# разрешает использовать небезопасные идиомы, но только внутри специальных блоков кода, предваряемых ключевым словом unsafe. При примене- применении этого ключевого слова вы должны осознавать все опасности, которым подвергаете себя. Unsafe служит флагом, предписывающим компилятору С# снизить требования к небезопасному коду.
42 Глава 2 Использование компилятора С# Обзор Свободно распространяемый вместе с MS.NET SDK компилятор С#, csc.exe, может быть вызван из командной строки или из некоторых графических ШЕ. Эти IDE включа- включают в себя условно-бесплатные предложения, появившиеся после публикации техниче- технического обзора PDC С#, а также VS.NET IDE, которую, вероятно, будут использовать большинство читателей. Поведение компилятора управляется аргументами командной строки. При необходи- необходимости откомпилировать программу IDE, например VS.NET, сверяется со своими настрой- настройками для того, чтобы динамически сформировать строку аргументов и передать ее процессу esc. ехе. Использование IDE наподобие VS.NET экономит силы и время, тем не менее полезно знать опции командной строки для управления компиляторомС*, особен- особенно в том случае, если нужно автоматизировать процесс сборки приложения с помощью сценариев, командных файлов и т.п. Еще раз повторим, С# не способен генерировать машинный код. Программы на С# мо- могут компилироваться только в MSIL. Работа по преобразованию кода на IL в машинный код лежит полностью на плечах среды исполнения .NET. Входные и выходные файлы При вызове компилятора С# из командной строки исходный файл задается указанием его имени после имени компилятора (esc). Имя выходного файла указывается при помощи аргумента output: csc /out:TargetFile.exe SourceFile.ee Компиляция различных типов проектов Как говорилось выше, С# может применяться для создания различных типов проектов, включая консольные приложения, приложения Windows Forms, компоненты и т.д. Для того чтобы сообщить компилятору, какой проект необходимо собрать, используется команда 'target: □ Аргумент /target:ехе указывает компилятору С# на необходимость создания консольного приложения. О Аргумент /target :winexe указывает компилятору С# на необходимость создания приложения Windows Forms. О Аргумент /target: library указывает компилятору С# на необходимость создания отдельной сборки, содержащей манифест. О Аргумент /target:module указывает компилятору С# на необходимость созда- создания файла сборки, но без манифеста. Созданные этой командой сборки, не содер- содержащие манифестов, могут быть отнесены к другим сборочным компонентам, имеющим манифесты. Во всех этих аргументах 'target: может быть сокращено до /t:. Ответные файлы Для облегчения процесса автоматической сборки компилятор С# поддерживает ответ- ответные файлы. Ответные файлы содержат список опций командной строки и могут быть связаны по ссылке с файлом при вызове компилятора. Ответный файл в командной строке имеет префикс @. евс SourceFile. ев /out: TargetFile. ехе @<имя_ответного_файла> Заключение С# — это следующий шаг в эволюции языков программирования. С его способностью со- создавать различные типы проектов — от библиотек классов до консольных приложений и web-интерфейсов — он предоставляет корпоративному разработчику мощный инструмент для построения приложений клиент-сервер. Это полнофункциональный язык е поддерж- поддержкой дополнительных возможностей, таких как атрибуты и отражение. Компилятор С#, csc. ехе, может быть вызван из VS.NET IDE или из командной строки с аргументами.
г /f \Ч Л </ \ а в // \^ а Основы С# В этой главе даются начальные сведения об основах программирования на С#. Если вы имеете опыт разработки на C++, Java или VB6, то специально для вас в конце книге при- приводятся соответствующие приложения, призванные помочь быстро освоить С#, и при желании вы можете пропустить эту главу. ИГ..' Примечание для пользователей VB. В этой главе большое внимание уделяется сходству между С# и C/C++ по части ключевых слов и общего синтаксиса. Рекомендуем вам прочитать приложение С, где сравниваются С# и VB. Начало работы Начнем с простейшей программы на С# — консольного приложения, которое выводит сообщение на экран: iiS.ing System; class MyFirstCSharpClass [ « public static int MainO { ConBOle.WriteLine("This isn't at all like Java!"); return 0; ;. } I Эту программу можно набрать в текстовом редакторе (например, Notepad), сохра- сохранить в файле с расширением . cs (например, First, cs) и откомпилировать с помощью компилятора С# (csc.exe): esc First.cs В результате будет получен файл First. exe, который можно запустить из командной строки или Проводника Windows точно так же, как любой другой исполняемый файл: !'• С. .WINVT ,System32\cmdexe flic. ->..rt liir.^ot.s 2И! В [beriion S.И».21.51 CO С pyi-iyJt 1/8S-2 ■;!» ricr-.s-Hft C- rp. [C:\ProCSl»rp>crc First.cs Microsoft <й> Visual СЯ Compiler Uersion 7.00.9219 fCbB version vl.0.29011 Copyright <C> Hicrosoft Corp 2000-2001. (Ill rights vcserued. C:\ProCSharp>Firct This i^n't at all like Java? C:\Pi-oCShaJ4>>
44 Глава 3 Но, возможно, это сообщение не совсем верно! Здесь существует ряд очевидных сходств с Java, хотя есть моменты (например, слово Main с прописной буквы), свидетельст- свидетельствующие об отличии от Java и C++. Давайте быстро пройдемся по структуре этой простой программы на С#. Во-первых, необходимо сделать несколько общих комментариев. В С#, так же как и в других языках в стиле С, каждый оператор должен заканчиваться точкой с запятой, и операторы могут объединяться в блоки с помощью фигурных скобок ({ ... }). Операто- Операторы могут занимать несколько строк без использования символа переноса (такого, как знак подчеркивания в VB). Комментарии, занимающие одну строку, начинаются с двух символов прямого слэша (//), а многострочные комментарии начинаются с символов слэша и звездочки (/*) и заканчиваются теми же символами, идущими в обратном по- порядке (*/). Так же как и в Java, весь код должен содержаться в классе (в нашем случае это класс MyFirstCSharpClass) или в другом определении типа. Кроме того, классы (и другие типы) на платформе .NET организуются в пространства имен (очень похожи на пакеты Java). В данном случае пространство имен не указано, поэтому класс бу- будет помещен в безымянное общее пространство имен. Первая строка может быть непонятна разработчикам на Java и C++. Директива using указывает компилятору, где он должен искать классы, которые не определены в текущем пространстве имен. Это похоже на использование оператора import в Java и using na- namespace в C++. Разработчики на C++ должны знать, что директива using отличается от директивы #include в том плане, что никакой код реально не включается в компилиро- компилированный файл — директива using лишь сообщает компилятору, где искать те типы, кото- которые не определены в текущем пространстве имен. В нашем случае указано стандартное пространство имен System, где определена большая часть типов, применяемых .NET. Важно отчетливо понимать, что практически все, что делается в С#, зависит от классов .NET; в данном случае мы используем класс Console в пространстве имен System для того, чтобы осуществить вывод в консольное окно. В С# отсутствуют встроенные ключевые слова для ввода/вывода — он целиком полагается на классы .NET. Теперь посмотрим на определение класса. Так как класс простой, определение имеет вид: class имя_класса II тело класса Подробно классы рассматриваются в главах 4 и 5, а сейчас лишь скажем, что классы в С# похожи на классы Java и C++, и их можно сопоставить в первом приближении с моду- модулями классов в VB6. Код программы содержится внутри метода Main (). Каждый исполняемый файл С# (такой, как консольное приложение, приложение Windows или служба Windows) должен иметь точку входа — метод Main (прописная буква М). Этот метод вызывается при запуске программы аналогично функции main в C++ или Java и Sub Main в модуле VB6. Метод обязан либо ничего не возвращать (void), либо возвращать тип integer (int). Определе- Определения методов в С# имеют вид: [модификаторы] возвращаемый_типИмяМетода( [параметры]) // тело метода Здесь квадратные скобки обозначают необязательные элементы. Модификаторы ис- используются для указания тех или иных особенностей определяемого метода, например, откуда он может быть вызван. В нашем случае имеются два модификатора: public и static. Модификатор public говорит о том, что метод доступен отовсюду и может быть вызван извне класса. Модификатор static указывает на то, что метод не принадле- принадлежит определенному экземпляру класса и, следовательно, может быть вызван без созда- создания экземпляра класса. Это важно, так как формируется исполняемый файл, а не библиотека классов. Тип возвращаемого значения устанавливается в int, и в данном примере функция не принимает никаких параметров. I Примечание для пользователей VB. Применение static в С# не похоже на использование ключевого слова static в VB.
Основы С# 45 Наконец, перейдем к самим операторам. В данном случае вызывается метод WriteLine () класса System. Console для вывода строки текста на консоль. WriteLine () является ста- статическим (static) методом, поэтому не требуется создавать экземпляр объекта Console перед вызовом этого метода. Для выхода из метода применяется оператор return (так как это метод Main, осуществляется выход из программы). В заголовке метода указан тип int, поэтому необходимо вернуть целое значение (в данном случае нуль). Типы данных С# Теперь рассмотрим более подробно основные особенности языка. Начнем с типов данных, доступных в С#. Новые специализированные типы в CTS Одним из замечательных свойств VB, сделавшим его столь продуктивным, является на- наличие богатого набора типов данных, предоставляемого разработчикам. В частности, он имеет внутренний строковый тип, тип Boolean и тип для даты/времени. Языки же низ- низкого уровня вынуждают разработчика создавать собственные типы данных, собирая ба- базовые типы в классы или структуры, либо использовать беспорядочный код шаблонов, хранящийся в заголовочных файлах (STL), или сложные патентованные библиотеки, например MFC. Теперь программисты на С имеют свою собственную среду быстрой разработки при- приложений. Как и другие языки для .NET, C# поддерживает общую систему типов (CTS), представляющую собой набор типов данных, который включает в себя не только знако- знакомые примитивы, такие как int, char, float и др., но и более сложные типы, например тип string или тип decimal для денежных сумм. Более того, каждый из этих типов данных является не просто примитивом, а настоящим классом, обладающим методами, полезными для форматирования, сериализации, преобразования типов и т.д. Безопасность типов Несколько лет назад запущенная в космос ракета разбилась через несколько минут после запуска, что стоило NASA миллионов долларов. В результате расследования было обнару- обнаружено, что катастрофа произошла по вине программного обеспечения. В частности, ошибка была найдена в строке кода на С, в которой программист ошибочно использовал один знак равно (=) вместо требовавшихся двух (=), т.е. оператор присваивания, а не проверку на равенство. Из-за того что С не обеспечивает безопасности типов, программа для ракеты неявно преобразовала результат присваивания в логическое значение true. Если вы хоть немного программировали на C/C++, то наверняка совершали похо- похожие ошибки. Такие ошибки трудно обнаружить; можно провести часы, просматривая исходный код в поисках неявного приведения типа, вызывающего проблему. Трата вре- времени на поиски подобных ошибок неприемлема в корпоративных средах. С# является строго типизированным языком. Помимо всего прочего, это означает, что тип Boolean не будет автоматически преобразован в целый тип. Для выполнения та- такого преобразования необходимо использовать явное приведение типа (см. ниже). Более того, С# позволяет задавать, как определенные пользователем типы будут вести себя в контексте неявных и явных преобразований типов. Соответствующий синтаксис подробно рассматривается в главе 6. Типы по значению и типы по ссылке Данные в программе на С# хранятся в одном из двух мест в зависимости от их типа. Первое место называют стеком. Он используется для хранения данных фиксирован- фиксированной длины, например целых (int состоит из четырех байтов). Каждая программа во вре- время выполнения имеет свой собственный стек, которым не могут воспользоваться другие программы. При вызове функции все переменные, локальные по отношению к вызываю- вызывающей функции, заносятся в стек программы, чтобы быть извлеченными оттуда после того, как произойдет возврат из вызываемой функции. Если вы когда-либо писали рекурсивную программу и получали сообщение "ошибка: нет места в стеке", то вам уже знакома ситуа- ситуация, когда программа помещает в стек слишком большое число переменных, в результате чего исчерпывается его объем. Второе место, в котором программа может хранить данные, называется кучей. В бо- более ранних языках программирования, таких как С и C++, куча использовалась частично для хранения данных переменной длины, например строк, и частично для хранения
46 Глава 3 данных, время жизни которых должно было быть большим, чем время жизни метода, в котором они были впервые определены. Если функция создает экземпляр объекта в сис- системной куче, этот объект не будет выкинут и уничтожен, когда ссылка выйдет из зоны видимости, как это происходит с объектами в стеке. Объект может находиться в куче по- постоянно и передаваться по ссылке другой функции программы. В С# ситуация немного отличается: в нем имеется объект, называемый управляемой кучей. Управляемая куча похожа на обычную, но работает более эффективно. Кроме того, в отличие от более ста- старых языков, где разработчик должен был указывать объекты, для которых требовалось использовать кучу, в С# этот выбор осуществляется компилятором в зависимости от типа данных объекта. С# делит свои типы данных на две категории, основываясь на том, в каком из двух мест они хранятся. Переменные типов по значению хранят свои данные в стеке, а пере- переменные типов по ссылке хранят свои данные в куче (см. главу 5). Место хранения типа данных указывает на то, как он будет вести себя в контексте оператора присваивания. Присвоение одной переменной по значению другой перемен- переменной по значению приводит к созданию двух разных копий одних и тех же данных в сте- стеке. Присвоение одной переменной по ссылке другой переменной по ссылке приводит к появлению двух ссылок на одну и ту же позицию в памяти. В С# основные типы данных, такие как bool и long, являются типами по значению. Если мы объявим переменную типа Boolean и присвоим ей значение другой перемен- переменной типа Boolean, то у нас в памяти будут две переменные типа Boolean. Если позже из- изменить значение первой переменной типа Boolean, то значение второй переменной не изменится. Эти типы копируются по значению. Большинство сложных типов данных С#, включая классы, определяемые пользовате- пользователем, являются типами по ссылке. Они размещаются в куче, время их жизни может про- простираться на несколько вызовов функций, и доступ к ним может быть осуществлен с помощью одного или нескольких псевдонимов. CLR реализует тщательно продуманный алгоритм для определения того, какие переменные по ссылке все еще доступны, а какие отброшены. Периодически CLR "очищает дом", удаляя отброшенные объекты и возвра- возвращая занимаемую ими память системе. Необходимо понимать различие между типами данных по значению и типами данных по ссылке для того, чтобы знать, как переменные ведут себя в контексте оператора при- присваивания. Хотя переменные типа bool и long похожи на классы тем, что имеют методы, в то же время они похожи на примитивы тем, что создаются в стеке и, следовательно, копируются в операторах присваивания. Также необходимо помнить, что тип struct является в С# типом по значению. Посмотрим, как ведут себя два этих типа. Создадим класс (ссылочный тип) с именем RefTypeRectangle, который имеет два поля Width и Height. Сформируем экземпляр этого класса, инициализируем значения полей, создадим копию объекта и затем изме- изменим значения полей в оригинальном объекте. Распечатаем значения копии объекта до и после операции изменения полей оригинального объекта. Также определим структуру с именем ValTypeRectangle, которая идентична классу, и повторим туже последователь- последовательность действий (структура в С# похожа на класс с той лишь разницей, что она имеет тип по значению, а не по ссылке). Все эти действия выполняет код: using System; // Определение класса RefTypeRectangle (тип по ссылке) class RefTypeRectangle { public irit Width; public int Height; Э- ' // Определение структуры ValTypeRectangle (тип по значению) // Идентично определению класса за исключением того, что это тип по значению struct ValTypeRectangle { public int Width; public int Height; } // Тестовый клиент для обоих типов class RefvalTest { public static int Main()
Основы С# 47 + rect2.Width + RefTypeRectangle rectl = new RefTypeRectangle (); rectl.Width => 10;- rectl.Height = 15; RefTypeRectangle rect2 = rectl; Console.WriteLine<"Dimensions of rect2 are " + rect2 .Width + " x " + rect2.Height); Console.WriteLinel"Changing dimensions of rectl..."); rectl.Width = 20; rectl.Height - 25; - Console.WriteLinel"Dimensions of rect2 now are " x ч + rect2.Height); ValTypeRectangle rect3 = new ValTypeRectangle(); /У -Теперь установим высоту и ширину здесь rect3.Width = 10; rect3.Height - 15; ValTypeRectangle rect4 = rect3; Console.WriteLinel"Dimensions of rect4 are ' " x " + rect4.Height); Console.WriteLinel"Changing dimensions of rect3..."); rect3,Width = 20; rect3.Height = 25; iConsole.WriteLinel"Dimensions of rect4 now are " + rect4.Width " x " + rect4.Height); return 0 ; rect4.Width Результат выполнения этого кода таков: C:\Pi-oOSbarp>cu<: RefUalTypes.es' nicrosoft <K> Uisual С» Cunpilei- Version 7.00.9219 CCLR version vl.«.29013 Copyright <C> Hitrosoft Corp 2000-2001. fill rights reserved. tf: \Pro СИ }!Й r p >fie if U A IType s Dinfinuions of rect2.are 10 >« IS Changing dinensionc of rettl..i Dimensions of rect2 now ai*t? 20 x'25r ' •', Dimensions of rect4 are 10 x lSi*" " ' Changing dimensions of rect3..«''; r ■ ,. Dinensions nf rect4 now are 10 jf lw'-, , C:\ProCSJiai4»?» ' ' Заметьте, как изменение полей в первоначальном классе вызывает аналогичное из- изменение полей у копии. Это происходит потому, что классы являются типами по ссыл- ссылке — две переменные, rectl и rect2, ссылаются на одно и то же место в памяти, где хранится объект. Следовательно, изменение полей одной из переменных вызовет такое же изменение полей другой переменной. Поведение двух структур, однако, заметно от- отличается. Структуры являются типами по значению, поэтому когда мы объявляем вто- вторую структуру и инициализируем ее значениями из первой структуры, это действие физически создает структуру в памяти. Изменение полей первой структуры не влияет на вторую. Такое поведение показывает, зачем нужны две различные группы типов. При копи- копировании примитивов, таких как int, или передаче их внутрь методов мы, скорее всего, пожелаем скопировать значение, а не ссылку. Более важно то, что нам не нужны потери времени на создание в куче объекта каждый раз, когда требуется целое число. Однако при работе с большими объектами эффективнее копировать ссылку на объект, а не весь объект целиком. Например, если структура передается как парамегр метода, все данные структуры должны быть скопированы и размещены в куче. По этой причине необходи- необходимо осторожно обращаться со структурами и использовать их только как коллекции про- простых типов данных. Однако применение структур может повысить производительность, поскольку они уменьшают потери времени на создание экземпляров объектов.
48 Глава 3 Предопределенные типы С# Для облегчения работы с общими типами данных компилятор С# распознает ряд пре- предопределенных типов, которые отображаются на типы, описанные в CTS. Например, можно объявить целое число с использованием типа CTS: System. Infc32 х,- Однако это покажется неестественным любому, кто писал на других языках, и привык использовать такие типы, как int и float. К счастью, вместо применения каждый раз типа CTS System. Int32, для объявления целого числа можно использовать псевдоним (alias) C# int, т.е. можно записать: int х> Между двумя этими декларациями не существует никакой разницы, кроме улучшения читаемости кода. С# имеет пятнадцать предопределенных типов: тринадцать типов по значению и два (string и object) типа по ссылке. Типы по значению Встроенные типы по значению представляют собой такие примитивы, как целые числа и числа с плавающей точкой, символы и типы Boolean. С# использует тот же сжатый и гибкий синтаксис определения переменных, что и C/C++. Для определения типа по значению необходимо указать имя типа объекта, а сле- следом имя самого объекта. Ветераны VB отметят, что не требуется вспомогательных слов, таких как Dim или As: int a; // Объявление переменной а типа integer в стеке, а. = 10.0; // Присвоение переменной а значения 100. Для удобства можно одновременно объявить и инициализировать переменную: int a = 100; Более того, С# позволяет объединять несколько объявлений переменных в одной строке, однако такой стиль считается плохим, поскольку код труднее понимать и под- поддерживать: int а '= 100,-Ь, с = 200, d; Программисты на VB6 должны быть особенно осторожны при использовании подоб- подобного синтаксиса. Аналогичный синтаксис в VB6 создал бы первую величину типа integer, а все последующие переменные — variant. В языках С-стиля все последующие перемен- переменные будут иметь тот же тип данных, что и первая. Другими словами, в приведенном выше примере а определяется как int, и b, с и d также имеют тип int. Определение типов по значению демонстрирует еще одну особенность С#, касающую- касающуюся безопасности. Компилятор С# требует, чтобы каждая переменная по значению была явно инициализирована начальным значением до того, как ее можно будет использовать в операциях. Большинство современных компиляторов отмечает такого рода наруше- нарушения предупреждениями, но более бдительный компилятор С# будет считать их ошибка- ошибками. Это исключает получение из памяти случайных значений, оставленных там другими приложениями. В качестве примера рассмотрим следующий фрагмент: public static int Main() f ■ int й; Console".Writeljine(cl); // Невозможно! Перед использованием переменную // необходимо инициализировать, например, int d = 2; return 0; } Мы пытаемся использовать переменную d до присвоения ей какого-либо значения, поэтому при попытке компиляции этих строк мы получим сообщение: C:\MyDocuments\Visual Studio Projects\ConsoleApplication4\Classl .cs B1) : Use of unassigned local variable 'd'
Основы С# 49 Целые типы С# поддерживает восемь предопределенных целых типов: Имя Тип CTS Описание Диапазон sbyte System.SByte short System.Intl6 int System.Int32 long System. Int64 byte System.Byte 8-разрядное целое со знаком 16-разрядное целое со знаком 32-разрядное целое со знаком 64-разрядное целое со знаком 8-разрядное целое без знака ushort System.UIntl6 16-разрядное целое без знака uint System.UInt32 32-разрядное целое без знака ulong System.UInt64 64-разрядное целое без знака от -128 до 127 (от -2' до 27-1) от -32 768 до 32 767 (от -215 до 2'5-1) от -2 147 483 648 до 2 147 483 647 (от -2" до 231-1) от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 (от-263до263-1) от 0 до 255 (от 0 до 28-1) от 0 до 65 535 (от 0 до 216-1) от 0 до 4 294 967 295 (от 0 до 232 -1) от 0 до 18 446 744 073 709 551 615 (от 0 до 2н-1) Будущие версии Windows будут рассчитаны на 64-разрядные процессоры, которые бу- будут копировать биты в память и из памяти большими порциями для ускорения обработ- обработки. Соответственно С# предлагает обширную палитру целых типов со знаком и без знака, имеющих размерность от 8 до 64 бит. Тип byte является стандартным 8-разрядным типом для хранения величин в диапазо- диапазоне от 0 до 255 включительно. Имейте в виду, что С# в целях обеспечения безопасности считает типы byte и char совершенно разными, и преобразование этих типов необходи- необходимо осуществлять явно. Такрке нужно помнить о том, что тип byte, в отличие от других ти- типов семейства integer, по умолчанию используется без знака. Его версия со знаком носит имя sbyte. Переменная типа sbyte может иметь значения от -128 до 127 включительно. На платформе .NET тип short вовсе не является таким уж коротким. Это 16-разрядное число, которое может принимать значения в диапазоне от -32 768 до 32 767. Его версия без знака, ushort, покрывает значения в диапазоне от 0 до 65 535. Тип int является 32-разрядным и может содержать значения в диапазоне от -2 147 483 648 до 2 147 483 647. Для значений без знака можно использовать uint, способный хранить значения от 0 до 4 294 967 295. И, наконец, тип long представляет 64-разрядные числа из диапазона от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807. Версия без знака, ulong, может хранить значения от 0 до 18 446 744 073 709 551 615. Всем переменным целого типа могут быть присвоены значения в десятичной или шестнадцатеричной нотации. Последняя требует присутствия префикса Ох: long х =• 0xl-2ab; Если существует неоднозначность по поводу того, имеет ли целое число тип int, uint, long или ulong, то по умолчанию оно будет иметь тип int. Для указания другого целого типа можно использовать следующие символы, записываемые после числа: uint-, ui = J.234U; long 1 =• 1234L; ulong ul = 1234UL; Также можно использовать строчные и и 1, однако строчную букву 1 можно спутать с единицей. Числа с плавающей точкой С# поддерживает типы чисел с плавающей точкой. Программисты на С и C++ знакомы с
50 Глава 3 Имя float double Тип CTS System. Single System. Double Описание 32-разрядное число с плавающей точкой одинарной точности 64-разрядное число с плавающей точкой двойной точности Значащие позиции 7 15/16 Диапазон (примерный) от ±1.5 х 10 до ±3.4 х 10м от ±5.0 х 10'"* до ±1.7 х 10м8 Тип данных float предназначен для небольших чисел с плавающей точкой, которые не требуют большой точности представления. Каждая из переменных типа float имеет до 7 цифр. Следовательно, диапазон возможных значений для переменных типа float составляет от ±1.5 х КГ15 до ±3.4 х 1038 включительно. Тип данных double занимает больше места, чем float, и предлагает двойную точ- точность A5 цифр). Таким образом, диапазон возможных значений для переменных типа double будет от ±5.0 х 10324 до ±1.7 х 10308 включительно. Если существует неоднозначность по поводу того, имеет ли число с плавающей точкой тип float или double, то по умолчанию оно будет иметь тип double. Если же требуется тип float, необходимо добавить к записи числа символ Г(или f): float i s 12-3F; Десятичный тип В дополнение к предыдущим, существует десятичный тип, представляющий числа с пла- вающей точкой еще более высокой точности: Имя decimal Тип CTS System. Decimal Описание 128-разрядное число высокой точности в десятичной записи Число значимых позиций 28 Диапазон (примерный) от ±1.0 х 10" до ±7.9 х 10ю Одной из замечательных особенностей С# и CTS является наличие специального типа для финансовых вычислений. Это десятичный тип, позволяющий указывать денеж- денежные суммы в диапазоне от 1.0 х 1028до 7.9 х 1028. Как вы будете использовать 28 цифр, предоставляемых десятичным типом, зависит от вас. Другими словами, можно отслежи- отслеживать меньшие суммы в долларах или фунтах с большей точностью относительно центов или большие суммы с округлением дробной части (значений в центах). Для указания того, что тип является десятичным, а не double, float или целым, к значению можно добавить литеру м (или т), например: decimal d = 12.3ОМ; Десятичный тип разработан специально для обеспечения высокой точности финан- финансовых вычислений, тем не менее его можно использовать повсеместно, где требуется число высокой точности, имеющее дробную часть. Тип Boolean Для хранения значений типа Boolean используется тип С# bool, способный принимать значение true или false: Имя Тип CTS Значения bool System.Boolean true и false Тип bool нельзя преобразовать ни в какой из целых типов, равно как невозможно вы- выполнить обратную операцию. Если переменная (или возвращаемое функцией значение) объявлена как bool, она может принимать только значения true и false. При попытке использовать нуль в качестве false или ненулевое значение в качестве true возникнет ошибка.
Основы С# 51 Символьный тип Для хранения одного символа С# поддерживает тип данных char: Имя char Тип CTS System.Char Значения Представляет один символ Unicode A6 бит) Хотя этот тип данных имеет сходство с типом данных char в С и C++, все же сущест- существует значительная разница. Выше упоминалось, что неявные преобразования между типом char и 8-разрядным типом byte не разрешены. Это вызвано, в частности, тем, что типы имеют разные раз- размеры — длина типа char составляет 16 бит. Хотя для представления любого символа английского алфавита и цифр от 0 до 9 хва- хватит восьми битов, этого недостаточно для представления символов в более объемных символьных системах (например, китайский язык). В целях обеспечения универсально- универсальности компьютерная индустрия движется от 8-битовых наборов символов к 16-битовой схеме Unicode, в которой кодировка ASCII является подмножеством. Помните о том, что значения char являются 16-битовыми значениями Unicode, в частности тогда, когда вы будете обращаться к ним с помощью указателей в блоках небезопасного кода (см. главу 6). Константы типа char обозначаются одинарными кавычками, например ' А'. Если за- заключить символ в двойные кавычки, компилятор посчитает ее строкой и сгенерирует ошибку. Символы char можно представлять не только литералами, но и четырехзначными шестнадцатеричными значениями Unicode (например, '\u0041'), приведенными зна- значениями целого типа (например, (char) 65) или шестнадцатеричными значениями ('\x0041'). Они могут содержать также последовательности специальных символов: Последовательность специальных символов V \" \\ \0 \а \Ь \f \п \г \t \v Значение Одинарная кавычка Двойная кавычка Обратный слэш Null Внимание Возврат назад на один символ Подача страницы Новая строка Возврат каретки Символ табуляции Вертикальная табуляция Разработчики на C++ должны отметить для себя: поскольку С# имеет встроенный тип string, нет необходимости представлять строки массивами символов. Типы по ссылке С# допускает использование указателей только в определенных обстоятельствах. Как же программист на С# сможет управлять объектами в куче? Допустим, что имеется класс Something, и рассмотрим строку кода: Something otrjSomething; В C++ эта строка создала бы экземпляр класса something в стеке, a obj Something ука- указывал бы на него. В С# эта строка кода сформирует только ссылку на объект Something, который еще не создан. Любая попытка вызвать метод или свойство с помощью этой ну- нулевой ссылки вызовет ошибку.
52 Глава 3 В С# создание экземпляра объекта по ссылке требует использования ключевого слова new. Сначала формируется ссылка, а затем эта ссылка связывается с объектом, созданным в куче, с помощью ключевого слова new: Something objSomething; // Эта ссылка \пока ни на что не указывает objSomething = new Something(),• // Создание в куче экземпляра something Более подробно объектно-ориентированное программирование на С# рассматривается в главах 4 и 5. С# поддерживает два предопределенных типа по ссылке: Имя Тип CTS Описание object System.Object Корневой тип, от которого происходят все остальные типы CTS (включая типы по значению) string System.String Строка символов Unicode Тип Object Многие языки программирования и иерархии классов предлагают корневой тип, от ко- которого происходят все остальные объекты в иерархии. С# и .NET не являются исключе- исключением. В С# тип ob j ect является исходным типом-предком, от которого берут начало все внутренние и все определенные пользователем типы. Ссылку object можно использовать для привязки к любому объекту любого частного подтипа. Например, ниже будет показано, как с помощью типа object можно упаковать объект по значению в стеке для размещения его в куче. Объектные ссылки также полез- полезны в отражении, когда код должен работать с объектами, типы которых неизвестны. Дополнительно тип object реализует ряд основных универсальных методов, среди которых Equals (), GetHashCode (), GetType () и ToString (). Пользовательским клас- классам, возможно, придется заменить реализации некоторых из этих методов, используя объектно-ориентированный принцип, известный как перекрытие (overriding) (см. главу 4). К примеру, перекрывая метод ToStringO, можно снабдить класс методом, элегантно представляющим его содержимое в виде строки. Если для этих методов в классе не будут представлены собственные реализации, компилятор возьмет эти реализации у класса object, которые могут быть, а могут и не быть корректными или имеющими смысл в контексте вашего класса. Тип String Ветераны С и C++, возможно, имеют боевые ранения, полученные в борьбе со строками С-стиля. Строка С или C++ представляет собой массив символов, из чего следует, что программист должен проделывать большую работу только для того, чтобы скопировать одну строку в другую или объединить две строки. Для поколения программистов на C++ реализация строкового класса, который служил оболочкой для строковых операций, была задачей, требовавшей многих часов скрежета зубами и головной боли, проводимых в поиске утечек памяти и ошибочно перегруженных операторов. К счастью, С# имеет свой собственный тип string. Поэтому такие операции, как копирование и слияние строк, выполняются моментально! string strl - "Здравствуй, "; String str2 = "мир" ,- string str3 - Strl, + str2; Несмотря на то, что присвоение выполняется в стиле типов по значению, тип CTS System.String является типом по ссылке. Объект string размещается в куче, а не в стеке. Таким образом, когда одна строковая переменная присваивается другой строко- строковой переменной, в результате получаются две ссылки на одну и ту же строку в памяти. Однако если впоследствии будут сделаны изменения в одной из этих строк, это создаст совершенно новый объект string, в то время как другая строка останется неизменной. Приведем код: ysing System; class StrihgExample public static int MainO { string . si. = "Строка";
Основы С# 53 string s2*"=5 ;sl; "" i , • Console1 ;WrlteLines( "si содержит " ■+ sl^-f Consoie,.wriCeLi'ne("s2. содержи* " + s2'V; si = "Другая строка"; Cqnsole»;WriteLine< "si 'теперь содержит * + si) ; Console.WriteLine'Csa1 -тейерь содержит n + s2.); return 0 j (Для пользователей VB: строки объединяются в С# только с помощью оператора +, а не &.) Результат выполнения будет таков: si содержит строка s2 содержит строка s1 теперь содержит Другая строка s2 теперь содержит строка Изменение значения si не оказывает действия на s2, вопреки тому, что ожидалось от типа по ссылке! Происходит следующее. Когда si инициализируется значением "Строка", новый строковый объект размещается в куче. Во время инициализации s2 ссылка указывает на тот же объект, поэтому s2 содержит "Строка". Однако когда мы из- изменяем значение переменной si, вместо замещения первоначального значения в куче создается новый объект для нового значения переменной. Переменная s2 по-прежнему указывает на первоначальный объект, поэтому ее значение не меняется. Строковые литералы заключаются в двойные кавычки ("..."). При использовании для строки одинарных кавычек компилятор воспримет ее как символ и сгенерирует ошибку. Строки С# могут содержать те же символы Unicode и специальные символы, что и тип char. Однако из-за того, что символ обратной косой черты применяется для обозначения специальных символов, его нельзя указывать в строке просто так. Необхо- Необходимо поставить перед ним еще один символ обратной косой черты ("\\"): string Eilepath"» "C:\\ProfessionalCSharpV\First.cs"; Даже если вы уверены в том, что не забудете об этом, постоянный ввод двух слэшей может в конце концов надоесть. К счастью, С# предусматривает альтернативу. Строко- Строковый литерал можно предварить символом (@), тогда все символы в строке будут обраба- обрабатываться одинаково, при этом обратные слэши не будут интерпретироваться как специальные символы: string" fllepath = @"C:\ProfessionalCShari>\First.cs"; - : Это позволяет включать в строковые литералы даже переводы строк: г1пд;. JabberwbcSy;/= :>0".'Twas brillig and- the slithy toves е- and gimble* in the wabe-:"; Форматирование этих двух строк кажется неудачным в том смысле, что две строки текста не выровнены на странице. Если мы выровняем строки следующим образом: spring ааЬЬегдаеЦу:г:;<= "@,?'.Twas brillig, and-J;she -stithy toves :■„"'«-. " v ' Did gyre arid giiabXe in the wabe."-; то значением Jabberwocky будет: 'Twas brillig and the slithy toves Did gyre and gimble in the wabe. Здесь более 20 пробелов между двумя строками. Сложные типы С# и платформа .NET позволяют определять свои собственные сложные типы на основе имеющихся простых типов. Большинство сложных типов детально рассматривается в последующих главах, здесь же мы перечислим все их формы. Как и примитивы, сложные типы могут быть разделены на типы по ссылке и по значению.
54 Глава 3 Типы по значению Существуют два вида типов по значению, которые можно определить самостоятельно. Помимо структур, о которых говорилось выше, можно определять собственные перечис- перечисления (наборы целых чисел, которые представляют собой заранее заданные значения). Структуры Типы, размещаемые в стеке, имеют по крайней мере три преимущества перед типами, раз- размещаемыми в куче. Во-первых, стековые типы, или типы по значению, размещаются в памя- памяти гораздо быстрее. Во-вторых, значения в стеке немедленно и автоматически удаляются после того, как соответствующие переменные выходят из зоны видимости. В-третьих, и что самое важное, легко скопировать значение переменной одного типа по значению в другую переменную того же типа; для этого достаточно использовать знак равенства: int a = 100; int b; b = a; По сравнению с переменными типов по значению, копирование значений объектов в куче является сложным процессом. Обычно для выполнения этой операции приходится создавать специальный метод. В С# структура — это особый тип класса, который имеет тип по значению, а не по ссылке. Поскольку структура размещается в стеке, она может создаваться и копировать- копироваться более эффективно, чем класс. Рассмотрим следующую структуру, представляющую подписчиков: public struct Subscriber { public long SubscriberlD,- public string FirstName; public string MiddleName; public string LastName; public decimal Balance; } При необходимости скопировать значения из одной структуры в другую все, что нужно сделать,— это поставить знак равенства. Создадим один объект Subscriber, Subscriberl, и установим значения его полей. После этого для копирования всех полей из Subscriberl в Subscriber2 потребуется только одна строка кода: Subscriber Subscriberl; Subscriber Subscriber2; Subscriberl = new Subscriber)); Subscriberl.FirstName = "Иван"; Subscriberl„MiddleName = "Иванович"; Subscriberl.LastName = "Ковалев"; Subscriber!.Balance = 100; Subscribed = Subscriberl; Отметим, что для инициализации структуры используется оператор new, несмотря на то, что структуры являются типами по значению. Однако доступ к структуре возможен до вызова оператора new. Структура инициализируется сразу после своего объявления, а все поля устанавливаются в значение по умолчанию @). Отметим, что компилятор не позво- позволит копировать одну структуру в другую до ее инициализации с помощью ключевого слова new или прочитать значения полей до их установки. Помимо облегчения операции копирования, структуры делают более аккуратными вызовы функций. Программисты часто используют структуры для группировки связан- связанных данных с целью передачи их по значению в другую функцию; намного удобнее поме- поместить все связанные данные в одну переменную и передавать ее, чем 5 или 6 различных аргументов: 'I h Заполняем структуру данными DataAccess.LoadSubscriber(out SubscriberlD, Subscriber); // Изменяем данные ->. Subscriber.Balance = Subscriber.Balance- 100; // Заносим в базу данных новую информацию
Основы С# 55 DataAecess>UpclateSubseriber(SubscriberID, Subscriber) ; Если вы являетесь гуру С или C++, будьте осторожны со структурами — они измени- изменились! В С и C++ единственное различие между классом и структурой заключалось в том, что члены данных структуры по умолчанию были общедоступными, а не закрытыми. Структуры в С# совершенно новая игрушка не только потому, что их члены данных явля- являются по умолчанию закрытыми, но, как мы видели, экземпляры структур и экземпляры классов размещаются в различных местах в памяти. В С# структура может выполнять большинство функций класса (хотя они не поддер- поддерживают наследование реализаций), но их необходимо использовать очень аккуратно. Поскольку экземпляры структур занимают место в стеке, лучше всего они подходят для представления небольших объектов. Это одна из причин, по которой они применялись для реализации всех остальных типов данных по значению в платформе .NET. Перечисления Перечисление — это определяемый пользователем целый тип. При объявлении перечисле- перечисления указывается набор допустимых значений, которые могут иметь экземпляры перечисле- перечисления. Этим значениям можно дать осмысленные имена. Если где-нибудь в коде попытаться присвоить значение, не содержащееся в списке допустимых значений экземпляра пере- перечисления, компилятор сгенерирует ошибку. При программировании нередко встречаются ситуации, когда полезны перечисле- перечисления. Например, вы разрабатываете класс со свойством, которое может принимать одно из нескольких возможных заранее определенных значений, или же создаете функцию, которая может вести себя по-разному в зависимости от конкретного значения одного из ее аргументов (например, функция, подобная SetWindowLong). Существуют по меньшей мере три причины, по которым следует использовать пере- перечисления вместо значений целых типов: О Перечисления делают код более простым, гарантируя, что неременным присваи- присваиваются только законные, приемлемые значения. О Перечисления делают код более понятным, позволяя ссылаться на целые значения с помощью описательных имен, а не путем темной магии чисел. О Перечисления делают код более простым для набора. Когда вам понадобится присвоить значение экземпляру перечисления, IDE VS.NET, пользуясь своей ин- интеллектуальностью, покажет список приемлемых значений, что сократит число ударов по клавишам. Создание перечисления является одним из немногих полезных действий, выполняемых программистом самостоятельно, которое сберегает немало времени и избавляет от голов- головной боли в долгосрочном плане. Разумное и правильное использование перечислений является одним из признаков профессионализма. Перечисление можно определить следующим образом: public enum TimeOfDay ( .-* Morning - О, ,,'i Afternoon - 1» .-г* Evening = 2 I-'-1-- В данном случае целые числа используются для представления времени суток в пере- перечислении. Теперь к этим значениям можно обращаться как к членам перечисления. На- Например, TimeOfDay.Morning вернет значение 0. Обычно перечисления применяются для передачи определенных значений в метод; по возможным значениям можно пройти с помощью конструкции switch (конструкция switch рассматривается ниже, ее можно сравнить с конструкцией Select Case в VB): ^ int Main () .г**; " :"!чл v V . .WrlteGreeting(TimeOfDay.Morning); return 0; '■■' У static void WriteGreeting(TimeOfDay timeOfDay)
56 Глава 3 itcK \tt'imfeO,fDay) case 'TimeOfDay. Morning: Consqle. WriteLine (" Дс?брое утро!"); breaks- case TijneOf Day. Afternoon: Console:WriteLine{"Добрый день!"); break; case - TiineOf Day .Evening: Condole. WriteLine ("Добрый вечер!,1')? . break; /Cohsple.Wri>teLine*( "Привет!"); ' .break; - i- '' "' " Типы по ссылке Помимо классов С# поддерживает ряд других сложных типов по ссылке: интерфейсы, делегаты и массивы. Классы Классы являются основными определяемыми пользователем типами в С# и платформе .NET. Практически все исполняемые программы на С# имеют по крайней мере один класс (теоретически можно использовать вместо него структуру), который содержит ме- метод Main () — точку входа программы. Классы — это составные типы данных, включаю- включающие в себя члены данных (поля, константы и события) и функции (в основном методы и свойства, но также операторы и т.п.); другими словами, класс представляет собой инкап- инкапсуляцию данных и функциональных средств для доступа и работы с этими данными. Классы могут также содержать вложенные типы данных. Классы детально рассматриваются в главах 4 и 5. Интерфейсы Интерфейсы используются для определения функциональности реализующих их клас- классов. Интерфейсы могут содержать свойства, методы, события и индексаторы, но экземп- экземпляр интерфейса не может быть создан и интерфейс не содержит реализации этих функций. Например, если интерфейс содержит метод, то он будет определять типы воз- возвращаемого значения и аргументов метода, но не будет содержать код, который его реа- реализует. Любой класс, реализующий этот интерфейс, должен реализовывать и этот метод. Более подробно интерфейсы рассматриваются в главе 5. Делегаты Делегаты — это типы, которые ссылаются на методы. Они похожи на указатели функ- функций в C++, но разрешают создавать экземпляр класса и вызывать как статические мето- методы класса, так и методы конкретного экземпляра класса. Делегаты позволяют во время исполнения определять, какой метод из указанного набора необходимо вызвать. Более подробно делегаты рассматриваются в главе 6. Массивы В С и C++ имя массива является указателем на определенный адрес в памяти, а индекс массива задает смещение от этого адреса. На элемент массива в С и C++ можно ссылаться с помощью указателя, получаемого в результате сложения индекса этого элемента с адре- адресом первого элемента массива: // В С+* справедливы и равноправны оба присваивания а[100] = 1< *Са+1ОО) = 1; Хотя подход C/C++ к массивам очень гибкий — можно делать много забавных вещей с использованием арифметики указателей, перемещаясь от одного элемента массива к другому, — он является источником большого числа ошибок. Так как С и C++ не отслежи- отслеживают размеры массивов, ничто не остановит программу от ссылки на элемент за грани- границами массива, что может привести к повреждению других данных и к появлению ошибки:
Основы С# 57 // В C++.- следующие- строки молча- проглатываются компилятором char *pstring '= new char[50]; pStringlipOI = 'NO1'; /J. Д00 не находится в границах выделенной )I для массива памяти Для преодоления недостатков С и C++ язык С# поддерживает массив как определен- определенный, отдельный тип. Считая массив объектом, имеющим методы и свойства (включая диапазоны), CLR способна отслеживать эти типы ошибок. Более того, если ошибка "вы- "выход за границы массива" произойдет во время исполнения программы на С#, CLR сгене- сгенерирует специальный тип объекта ошибки, на появление которого функции обработки ошибок отреагируют соответствующим образом. Синтаксис массивов С# С# использует настолько отличный от других языков подход к массивам, что предлагает для них совершенно иной синтаксис. Как и в других языках С-стиля, массивы объявляют- объявляются с помощью пары прямоугольных скобок, располагаемых после указания типа перемен- переменной для отдельных элементов (отметим, что элементы массива не обязаны иметь один и тот же общий тип). Например, если int представляет собой одно целое число, то int [ ] будет описывать массив целых чисел: int[] Integers; Для инициализации массива можно использовать ключеьое слово new, указав размер массива в квадратных скобках после имени типа: '// Создаем новый' массив из 32 значений int inttJ iPtegejrg = new jnt.[32] ; Для получения доступа к отдельному элементу внутри массива применяется обычный синтаксис: индекс элемента помещается в квадратные скобки после имени массива. Ин- Индексация массивов в С# начинается с нуля, поэтому первый элемент массива имеет ин- индекс нуль: IntegerТО] = 35; Соответственно последний, 32-й элемент массива имеет индекс 31: Integert31] * 432,- Синтаксис массивов С# довольно гибкий. С# позволяет создавать массивы, не иници- инициализируя их, поэтому массив может динамически менять свой размер в процессе выпол- выполнения программы. По этой методике мы создаем нулевую ссылку, а позже связываем ее с динамически выделенным сегментом памяти с помощью ключевого слова new: intU meegers; Integers = new int[32]; Этот синтаксис гораздо проще, чем применение malloc и sizeof! Намного уменьша- уменьшается и вероятность появления ошибок. В С или C++ сегмент памяти, занимаемый масси- массивом, может оставаться в куче до тех пор, пока он не будет специально освобожден. Напротив, массивами С#, как и другими объектами С#, управляет CLR, которая помечает эти объекты для автоматического удаления сразу после завершения работы с ними. Одним из полезных свойств С и C++ является то, что они позволяют инициализировать массив значениями, перечисленными в фигурных скобках. С# сохранил эту возможность: string[] String = { "первый элемент", "второй элемент", "третий элемент"); Это является эквивалентом следующего: jstringH String = new string [J { "первый элемент", "второй элемент", " третий элемент"}; Однако инициализация массивов имеет несколько подводных камней. Например, для установки размера массива нельзя использовать переменную: int- lee f= 3; II Следующая конструкция не компилируется spring П.. String = new stringjlen] { "первый элемент", "второй элемент", "?* - '" "третий элемент"}; Однако можно объявить константу для длины массива: const ' int len = 3 ; /-/• Допустимо
58 Глава 3 str.ingU String = new -stringtlen] { "первый элемент", "второй элемент", "третий элемент",}; При применении этого синтаксиса нельзя определить, насколько большим будет мас- массив. Если требуется массив, длина которого задается динамически, или массив, который может быть увеличен во время выполнения программы, придется использовать экземп- экземпляр объекта ArrayList, который расположен в пространстве имен System.Collections (см. главу 7). Массиву нельзя присваивать больше значений, чем задано его размером: // Следующая конструкция не компилируется scringU String = new string[3) { "первый элемент1:, "второй элемент", "третий элемент", "четвертый элемент"}; Для пользователей VB: массив в С# использует квадратные, ■ а не круглые скобки. Работа с массивами Так как массивы представляют собой определенный тип в С#, они имеют свои-собственные методы, например, для получения длины массива. Это означает, что работать с массивами в С# очень просто! Например, для определения длины одномерного массива можно использовать свойство Length: int'ArrayLength' = Integers,Length; t В случае многомерного массива можно получить длину по каждому из измерений с помощью метода GetLength: // Получаем длину первого измерения dnfc ArrayLength - Integers.GetLength@); Если все элементы массива имеют определенный тип, можно отсортировать массив в возрастающем порядке с использованием метода Sort (): Array.Sort(String); Отметим, что в данном случае мы вызываем статический метод класса Array, а не ме- метод нашего экземпляра массива. Массив, который мы желаем отсортировать, передается в качестве параметра этого метода. Для сортировки можно также использовать метод System.Array. Sort и интерфейс icomparer. Эта возможность демонстрируется на примере делегатов в главе 6. Наконец, можно изменить текущий порядок элементов массива с помощью метода Reverse () (также вызываемого в качестве статического метода с передачей экземпляра массива в качестве параметра): Array.Reverse(String); Следующий короткий пример сохраняет список имен известных художников в стро- строковом массиве, сортирует массив в обратном алфавитном порядке, а затем проходит по всем элементам массива для отображения каждого имени в консольном окне: // Определяем массив имен художников string[] Artists = {"Leonardo", "Monet.", "Van .Gogh", "Klee"}; // Сортируем элементы массива в алфавитном порядке Array.Sort(Artists); // Меняем порядок следования элементов массива Array.Reverse(Artists); II Проходим по каждому- элементу и выводим имя for (int i = 0; ■■■ < Artists.Length; i++) { Console.WriteLine(Artists(i]);
Основы С# 59 Многомерные массивы в С# С# поддерживает многомерные массивы двух видов. Первый вид — прямоугольные мас- массивы. Двумерный прямоугольный массив — это массив, в каждом ряду которого имеется одинаковое число столбцов. Как показано в приведенном ниже примере, прямоугольные массивы несложно объявить и инициализировать. Определим двумерный прямоуголь- прямоугольный массив из четырех строк по два столбца в каждой: s.trihgt, J ''Beat'leNaine ~- { {''Ленной", "Джон"} , {"Наккартни","Пол"}, {"Харрисон"., "Джордж"}, 1 {"Старки","Ричард"} У; Отмстим, что для разделения размерностей в объявлении массива используется запя- запятая, несмотря на то, что на самом деле размер массива не указывается. Таким образом, для объявления трехмерного массива мы бы записали: string^, ,J Strings; В качестве альтернативного способа инициализации массива можно использовать вложенные циклы for: double [,] Matrix = new double[10, 10]; .for (int i = 0j i < 10,- i + ) < for (int.. j=0; j<10; j + + ) Matrixli, j] ~ 4; } Второй тип многомерных массивов, поддерживаемых С#, это ортогональные, или неровные, массивы. Каждый ряд неровного двумерного массива содержит разное число столбцов. Хотя они несомненно являются более гибкими, чем прямоугольные массивы, для неровных массивов гораздо труднее создавать и инициализировать экземпляр класса. При создании неровных массивов на самом деле формируется массив массивов: // Создаем неровный массив mCitHl a: := :new ihtt3]l],« e[6j! > new* int[4]; atl'3'- = new int[3]; a[2] - new intdl; Здесь для каждой размерности массива используется дополнительная пара квадратных скобок. Следовательно, для определения трехмерного неровного массива целых чисел необходимо написать: int[][][] Ints; Перемещение между элементами неровного массива требует больших усилий, чем в случае прямоугольного массива. По мере перехода к очередной строке приходится испо- использовать метод массива GetLe^gth для того, чтобы динамически определить число столбцов, которые необходимо обработать: // Определяем двумерный неровный массив имен авторов string[] [] Novelises = new strlr.g[3] [] ; Novelists[0] '- new stringt] { "Федор", "Михайлович", "Достоевский" }; Noveliststl] = new stringf] { "James", "Augustine", "Aloysius", "Joyce" }; Novelists[2] = new string[] { "Miguel", "de Cervantes", "Saavedra" } ; // Пройдем "в цикле по каждому писателю int i; for [i = 0; i < Novelists.GetLength(O); i++) { ' // Проходим по каждому элементу в имени, писателя int' j,- for (э-=0; j < Novelists [I] .GetLength(O); j++) i_-K } f .Отображаем текущую часть' имени 3'*? -J , ConspieCwrite(Novelists[i] fj] + * ■ *'f\ я '// -Начать с. новой строки для следующего писателя Console.Write<"\n,");
60 Глава 3 Преобразование типов Часто требуется преобразовывать данные из одного типа в другой. Рассмотрим код: bytes valuel » 10,- byte val\ie2: = 23; byte total; total = valuel + value2; // Когда складываются два значения byte, они // неявно преобразуются в int, поэтому попытка // записать результат обратно в byte // вызовет ошибку Console.WriteLineftotal); При компиляции этих строк будет выдано сообщение об ошибке Cannot implicitly convert type "inf to "byte" (Невозможно неявно преобразовать ' int' в ■ byte'). Проблема здесь со- состоит в том, что при сложении двух значений byte результат будет иметь тип int, а не byte. Это происходит потому, что byte может содержать восемь битов данных (до 255), и при сложении двух байтов можно получить величину, которая не поместится в один байт. Если необходимо все же сохранить результат в переменной byte, нужно явно преобразо- преобразовать результат в byte. Это может быть сделано двумя способами: неявно — компилятор выполнит преобразование за нас, не спрашивая подтверждения, или явно — компилятору необходимо специально указать на то, чтобы он преобразовал данные к другому типу. Неявные преобразования Преобразования типов могут быть выполнены автоматически, но только в том случае, если преобразуемое значение не изменяется. Следовательно, значение short или int может быть присвоено переменной long без каких-либо проблем. Переменную без знака можно присваивать переменной со знаком, пока значение переменной без знака попада- попадает в допустимый диапазон для переменной со знаком. Кроме того, могут быть выполне- выполнены определенные преобразования целых типов в типы с плавающей точкой. Предыдущий код нельзя было выполнить потому, что при преобразовании int в byte мы потенциально могли потерять 3 байта данных. Компилятор не позволит сде- сделать этого, если только мы специально не укажем ему, что это именно то, чего мы доби- добиваемся. Если мы сохраним результат в переменной long, а не byte, проблема исчезнет: byte valuel = 10; byte value2 = 23; i'ong total; total = valuel + value2,- Console.WriteLine(total); Тип long имеет больше битов данных, чем int, поэтому не существует риска потери данных. В этих обстоятельствах компилятор выполнит преобразование за нас, т.е. неявно. В таблице приводятся неявные преобразования типов, допустимые в С#: Из В sbyte short, int, long, float, double, decimal byte short, ushort, int, uint, long, ulong, float, double, decimal short int, long, float, double, decimal ushort int, uint, long, ulong, float, double, decimal int long, float, double, decimal uint long, ulong, float, double, decimal long, ulong float, double, decimal float double char ushort, int, uint, long, ulong, float, double, decimal Заметим, что выполнять неявные преобразования можно только по отношению к ти- типам, которые преобразуются из менее крупных целых типов в более крупные или из це- целых без знака в целые со знаком такого же размера. Это означает, что недопустимо преобразование из uint в int или наоборот, так как эти типы охватывают разные диа- диапазоны значений, и в процессе преобразования данные могут быть потеряны. Однако
Основы С# 61 можно осуществить преобразование из uint в long, так как диапазон uint по сути дела является верхней частью диапазона long (от нуля и выше). Также можно преобразовы- преобразовывать целые числа в числа с плавающей точкой. Здесь правила немного отличаются. Мож- Можно осуществлять преобразования между типами одного размера, например int/uint в float и long/ulong в double, и допустимо также преобразование long/ulong в float. В процессе этого можно потерять 4 байта данных, однако это всего лишь означает, что величина float будет иметь меньшую точность, чем в случае double; значение величи- величины вообще не затрагивается. Преобразование float в double следует тем же правилам, что для целых со знаком. Явные преобразования Существует большое число преобразований, которые не могут быть выполнены неяв- неявно. При попытке сделать это компилятор выдаст ошибку. Нельзя выполнить неявно преобразования: О int в short — возможна потеря данных a int в uint — возможна потеря данных О uint в int — возможна потеря данных a float в int — будет потеряно все, что идет после десятичной точки О любой числовой тип в char — будут потеряны данные О decimal в любой числовой тип — десятичный тип внутренне устроен по-другому, нежели форматы для целых чисел и чисел с плавающей точкой Однако эти преобразования могут быть выполнены явно с использованием приведения. Когда один тип приводится к другому, компилятор вынужден выполнять преобразование. Типичный синтаксис приведения: long val = 30000; int I = {int) val; // Допустимое преобразование. // Максимум int равен 2147483647. Тип, к которому необходимо привести значение, указывается в круглых скобках пе- перед этим значением. Для программистов, хорошо знакомых с С, такой синтаксис являет- является привычным. Что касается специальных ключевых слов для приведения в C++, таких как static_cast, то в С# их нет, и вам придется использовать более ранний синтаксис С. Эта операция может быть опасной. Вы должны точно знать, что делаете. Даже простое преобразование из long в int способно вызвать проблему, если значение переменной long больше, чем максимально возможное значение int. Например: long val = 3000000000; int 1 = (int)val; // Неправильное преобразование. II Максимальное значение int равно 2147483647. В данном случае не выдается сообщение об ошибке, но и ожидаемый результат не по- получается. Если мы выполним этот код и выведем на экран величину, которая содержится в I, то получим: -1294967296 Никогда не следует полагаться на то, что преобразование типа даст ожидаемый резуль- результат. В С# есть оператор checked, который можно использовать для проверки того, что one рация не вызывает переполнение стека. С помощью этого оператора можно выяснить, безопасно ли приведение типа, и заставить среду исполнения сгенерировать исключение переполнения, если это не так: long val = 3000000000; int i = checked){int)val); Помня о том, что любое приведение типов потенциально небезопасно, в программе необходимо предусмотреть код, который будет обрабатывать возможные ошибки приве- приведения типов. Ниже мы рассмотрим обработку исключений с помощью конструкции try catch, которая является полезным и необходимым инструментом программиста. С помощью приведения типов можно выполнять самые разные преобразования, например: double price = 25.30; int approximatePri r-r- = (int) (price + 0.5);
62 Глава 3 В результате будет получена цена, округленная до ближайшего доллара. Однако при таком преобразовании теряются данные — все, что идет после десятичной точки. Это недопустимо, если планируется использовать величину стоимости в последующих расче- расчетах. Однако полезно в том случае, если необходимо вывести на экран приблизительный результат выполненного или частично выполненного расчета и вы не хотите пугать пользователя большим числом знаков после запятой. Посмотрим, что происходит при преобразовании целого без знака в char: ushort с - 43; char symbol = (char) с; Console.WriteLine(symbol); На экран будет выведен символ, имеющий ASCII-код 43, т.е. знак +. Можно произво- производить практически любые преобразования между числовыми типами (включая char), каки- какими бы нелепыми они ни являлись, и они будут работать. Например, можно преобразовать decimal в char и наоборот: decimal d = 65m; char symbol = ichar)d; /' В результате получается буква А char с = ' А' ; decimal val = (decin.a7 ) с; ' В ре !учьт ате получается число 65 Если значение, полученное в результате приведения, не может поместиться в новый тип данных, преобразование все равно будет выполнено, однако результат будет не таким, какой ожидается. Например: int I = -1; char symbol = (char) I,- Это преобразование не должно работать, так как тип char не может принимать отрицательные значения. Однако ошибки не происходит, вместо этого переменной symbol присваивается значение в виде вопросительного знака (?). Преобразование между типами по значению не ограничивается отдельными пере- переменными. Например, можно преобразовать элемент массива типа double в переменную структуры типа irt: struct ItemDetails { public string Description; public int ApproxPrice,- doublet] Prices = { 25.30, 26.20, 27.40, 30.ОС }; ItemDetaiis id; id.Description = "Что-нибудь"; id.ApproxP^ ce = (int)(Prices[0] ^ .5); Используя явное приведение типа, можно преобразовать любой экземпляр простого типа по значению в любой другой тип. Однако существуют ограничения на то, что мож- можно делать с помощью явного преобразования типов — типы по значению допускают пре- преобразования только в/из типов cha-, числовых типов и типов епшп. Нельзя напрямую приводить тип Boolean к любому другому типу и наоборот. Если требуется выполнить преобразование, например, между числовым типом и стро- строкой, то библиотека классов .NET предлагает соответствующие методы. Класс object име- имеет метод ToStr : г q (), который должны реализовывать все классы. Этот метод возвращает строковое представление объекта, так ч го строковое представление int можно получить с помощью кода: int 4 = 10; string s = i.ToStringf) ; Аналогично, если требуется обработать строку и получить из нее числовое значение или значение типа Boolean, можно использовать метод Parse, поддерживаемый всеми предопределенными типами по значению: string s = 00"; int I = int.Parse (s) j .;
Основы С# 63 Console.WrbteliineA+50) ; // Прибавляем 50, чтобы показать, •\ • - •// что это действительно int В главе 6 будет показано, как определять преобразования для собственных классов и структур. Упаковка и распаковка Все типы, как простые предопределенные, например int и char, так и сложные, напри- например классы и структуры, являются производными от типа object. Это означает, что даже с числовыми значениями можно обращаться так, как будто это объекты: string s =*10.ToSt:ring:()'; Типы данных С# делятся на типы по значению, которые содержат набор данных и размещаются в стеке, и на ссылочные типы, которые не имеют заранее определенного размера и располагаются в куче. Как это сочетается со способностью вызывать методы для int, если int является всего лишь четырехбайтовой величиной в стеке? Способ, с помощью которого С# достигает этого, заключается в использовании неболь- небольшой хитрости, называемой упаковкой (boxing). Упаковка и распаковка позволяют преобра- преобразовывать типы по значению в типы по ссылке и наоборот. Мы говорим об этом в разделе, посвященном приведению типов, так как при упаковке мы приводим нашу величину к типу object. Упаковка — это термин, используемый для описания преобразования типа по значению в тип по ссылке. По существу среда исполнения создает временную "коробку" ссылочного типа для объекта в куче. Это преобразование может осуществляться неявно, как в примере выше, но его можно выполнить и вручную: int I = 20; object о = I? Распаковка — это термин, используемый для описания обратного процесса, когда зна- значение типа по ссылке преобразуется в тип по значению. В данном случае преобразование должно быть сделано явно. Синтаксис похож на синтаксис явного преобразования типов: int I = 20; object 0 = 1; // Упакуем int int j = (int)о; // Распакуем обратно fi int Распаковать можно только переменную, которая была перед этим запакована. Единственное предупреждение: переменная, в которую производится распаковка, должна иметь достаточный объем для размещения в ней всех байтов распаковываемой переменной. К примеру, в С# переменные типа int являются только 32-разрядными, поэ- поэтому рягггякоика значения long F4 бита) в int приведет к ошибке. Рассмотрим код: long а - 333333423; // а является локальной переменной типа long, 4 / / ,хранящейся в стеке. object b - (bbject)a; // b является ссылкой на копию а, ;■ II упакованную в куче. int с = (lnt)b; // Ошибка: переменная типа long распаковывается / / в переменную типа int. В данном случае 64-разрядная величина long упаковывается в кучу, а затем распако- распаковывается в 32-разрядный int. Переменная с не имеет достаточно места для хранения ве- величины такого размера. Попытка распаковать величину из кучи в неподходящую для этого переменную по значению приведет к возникновению ошибки времени исполне- исполнения. Когда вы упаковываете переменную по значению, вашей заботой как программиста является запоминание первоначального типа переменной; попытки распаковать ее в другие типы, скорее всего, приведут к ошибке. Переменные Как уже было показано, переменные объявляются в С# с использованием следующего синтаксиса: [ модифика торы] тип_данных щдентифика тор ; Например: public int I; I/ .Поле класса. Локальные переменные не могут иметь . . - .- t JI модификаторов доступа.. iilk 6V
64 Глава 3 Это выражение объявляет переменную типа int с именем I и модификатором досту- доступа public (т.е. переменная доступна отовсюду). Компилятор не позволит использовать эту переменную до тех пор, пока она не будет инициализирована каким-либо значением, однако объявление переменной выделяет четыре байта в стеке для хранения ее значения. После объявления переменной можно присвоить ей значение с помощью оператора присваивания: Программистам на VB6 следует помнить, что С# не делает различий между объекта- объектами и простыми типами, так что нет нужды использовать такие ключевые слова, как Set, даже если требуется, чтобы переменная указывала на объект. Если в одном выражении объявляется и инициализируется несколько переменных, все они будут иметь одинаковый тип данных и будут объявлены с использованием одина- одинаковых модификаторов: public. ,stat'i<S iAt: x = 10, у 20; // х и ,у являются общедоступными II статическими переменными типа int Внутри множественного определения переменных нельзя указывать разные модифи- модификаторы или типы данных, поэтому недопустима запись: public int x = 10, private byte у = 20; / / He будет компилироваться Здесь мы не сэкономим на ударах по клавишам, и более того, нет ясности, какие типы и модификаторы относятся к какой переменной! Наконец, необходимо отметить, что переменные с одинаковыми именами не могут быть объявлены дважды в пределах одной зоны видимости, поэтому нельзя написать: int ,.х - >20; // другой „код »- irif 'ж -~ 30; Области видимости переменных будут рассмотрены чуть позже. Идентификаторы Идентификаторы — это имена, которые мы даем переменным, определенным пользова- пользователем типам (классам и структурам) и членам этих типов. Идентификаторы чувствитель- чувствительны к регистру символов, поэтому identifier и Identifier являются разными переменными. В С# идентификаторы обязаны подчиняться двум правилам. Во-первых, идентификатор должен начинаться с буквы или знака подчеркивания, хотя он может содержать и цифры. Во-вторых, в качестве идентификаторов нельзя использовать клю- ключевые слова С#. С# имеет 76 зарезервированных ключевых слов: abstract as base bool break byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long namespace new null object operator out override params private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe ushort using virtual void while
Основы С# 65 Если одно из этих слов требуется использовать в качестве идентификатора (напри- (например, если осуществляется доступ к классу, написанному с применением другого языка), то перед ним ставится знак @, который указывает компилятору, что это слово необходимо трактовать как идентификатор, а не как ключевое слово С# (таким образом, abstract не является допустимым идентификатором, a @abstract является). Наконец, идентификаторы могут записываться с помощью символов Unicode, указанных с использованием синтаксиса \uXXXX, где ХХХХ — четырехзначный шестнад- цатеричный код Unicode символа. Приведем примеры допустимых идентификаторов: Name, uberflu?, _Identifier, \u005fIdentifier Последние два идентификатора являются идентичными и взаимозаменяемыми @05f — Unicode-код для знака подчеркивания), поэтому они не могут быть объявлены оба в пределах одной области видимости. Отметим, что хотя и можно использовать в ка- качестве идентификаторов знаки подчеркивания, все же не рекомендуется делать это, по- поскольку противоречит рекомендациям Microsoft по именованию идентификаторов, которые были созданы для стандартизации соглашений по именам и для облегчения чтения кода других программистов. Соглашения по именованию обсуждаются в главе 8. Область видимости переменных Выше отмечалось, что одноименные переменные ие могут быть объявлены в пределах одной области видимости. Областью видимости переменной яьляется участок кода, из которого к переменной может быть осуществлен доступ. Рассмотрим код: using. System; : * *'? public class ScopeTe#t { * public static int Ha'inO { fpr (int i = 0; i < 1С; i++) Д '-. Console.writeLine(i) ,- '*■ --л> .- }. II здесь- t выходит из области рйдиарсти: •'•;..* '^;<*&.C*jN " -ж // 'Теперь мы можем снова объявить переменную с именем i, так как ~ '' /-/' '-в области видимости нет другой переменной с таким именем for (int i - 9; i >= OW i-) • • '••*•'■'■«( ConsoXe,WriteLine(i.)- ; ;,) ,} // здесь -, i выходит из области видимости ... „ , * " return 0; ■ } } Этот код выводит на экран числа от нуля до девяти, а затем в обратном порядке от де- девяти до нуля, используя цикл for для итерации по значениям. Синтаксис цикла for будет рассмотрен ниже. Здесь переменная i объявлена в коде дважды в одном методе. Причи- Причина, по которой это возможно, заключается в том, что в обоих случаях переменная i объ- объявляется внутри цикла, поэтому переменные являются локальными по отношению к циклу. Как только соответствующий цикл заканчивается, переменная выходит из зоны видимости и становится недоступной. Приведем другой пример: using System; public class Scopefest t » public static int Main О . { int j = 20; ; ■ for (int i = 0; i < 10; i++) :3 { int j = 30,- // нельзя — j по-прежнему находится в пределах '.// области видимости Console. WriteLine,{ j + i); ■ -4. return .С;
66 Глава 3 При попытке компиляции этого кода возникнет ошибка: ScopeTest.csA0,14): error CS0136: A local variable named 'j' cannot be declared in this scope because it would give a different meaning to 'j', which is already used in a 'parent or current' scope to denote something else Дело в том, что переменная j, объявленная до начала цикла for, находится в области видимости цикла for и не выйдет за ее пределы до тех пор, пока не завершится выпол- выполнение метода Main. Область видимости, в которой находится вторая переменная j (не- (неправильно объявленная), является вложенной в метод Main. Компилятор не может различить эти две переменные, поэтому не позволит объявить вторую переменную. Поля и локальные переменные В определенных обстоятельствах можно отличить два идентификатора с одинаковыми име- именами, но с различными областями видимости, и компилятор позволит объявить вторую пе- переменную. Причина этого заключается в том, что С# делает фундаментальное различие между переменными, объявленными на уровне типа и известными как поля, и перемен- переменными, объявленными в теле метода и известными как локальные переменные. Рассмотрим код: using System; -,.*' - .'*,' ■-'.', public class ''Scofe'elest ; * static int j = 20; public static int MainO { , , int j = 3U; s Console.WriteLine(j); return 0; и ■-> Этот код откомпилируется даже с учетом того, что в области видимости метода Main есть две переменные с именем j. Первая переменная j объявляется на уровне класса и не выходит за пределы области видимости, пока не будет уничтожен класс (в данном слу- случае, до тех пор пока не завершится метод Main и не закончится программа). Вторая пе- переменная под именем j, объявленная в методе Main, скрывает переменную класса с тем же именем, поэтому в результате выполнения кода на экране появится число 30. Но как быть, если необходимо обратиться к переменной класса? Ссылаться на поля класса или структуры извне объекта можно с помощью синтаксиса object. имя_поля. Например, если объявить класс с одним полем: class SorneClass { "» public int i - 20; ^ To к переменной i можно будет обращаться так: SomeClass objSomeClass = new, SomeClass ()_; int x = objSomeclass.i; Обычно это не требуется делать внутри класса, так как поле доступно в текущей облас- области видимости. Если же поле было скрыто объявлением локальной переменной с таким же именем внутри текущей области видимости, к этому полю можно обращаться с использо- использованием представленного синтаксиса. В приведенном примере осуществляется доступ к статическому полю из статического метода, поэтому не требуется наличие экземпляра класса; достаточно указать имя класса: Console.WriteLdne(ScqpeTest.j); Если бы мы осуществляли доступ к полю экземпляра (поле, которое принадлежит конкретному экземпляру класса), пришлось бы указать ключевое слово this. Ключевое слово this используется внутри класса или структуры для получения ссылки на текущий экземпляр (см. главу 4).
Основы С# 67 Модификаторы переменных К определению переменной можно добавить модификаторы, которые служат для указа- указания дополнительных параметров переменной — в частности, будет ли она видна коду вне текущего класса и может ли меняться ее значение. Используются следующие модификаторы: О internal О new О private О protected О public О readonly О static Эти модификаторы могут применяться только к полям, но не к локальным переменным. Модификатор new используется только в классах, которые являются производными от другого класса, и скрывает существующее поле в этом классе. Наследование классов рассматривается в главе 4. Модификаторы доступа Модификаторы internal, private, protected и public служат для установки уровня доступа переменной. В объявлении переменной может указываться только один из этих модификаторов, за исключением совместного использования protected и internal. Таким образом, существует пять возможных уровней доступа: Уровень доступа Описание public internal protected protected internal private Переменная доступна отовсюду как поле типа, к которому она принадлежит. Переменная доступна только из текущей программы. Переменная доступна только из типа, к которому она принадлежит, или из типов, производных от данного. Переменная доступна из текущей программы или из типов, производных от данного типа (т.е. отовсюду, как если бы она была объявлена protected или internal). Переменная доступна только из типа, к которому она принадлежит. Теперь посмотрим, как это работает на практике. Допустим, что у нас есть програм- программа с двумя классами. Класс AccessTest имеет одну переменную (переменную х типа int). В другом классе создадим экземпляр класса AccessTest и попытаемся получить доступ к переменной х: //- 'AccessTest.qs ; - ' using "System; . ' ' Ч ' '""", public class AccessTest {,' public int x = 12; public class AccessClient {, ' ■"' public static int Main{) AccessTest Test - hew- AccessTest(): <,f int x =• Test.x,- , Con&pl;e.Wj'xteLine(x); } }
68 Глава 3 Сохраните этот файл под именем AccessTest.cs и откомпилируйте с помощью команды esc AccessTest.cs. В данном случае программа компилируется и исполняет- исполняется, поскольку х объявлена как public и доступна отовсюду. Не будет проблем и в том случае, если изменить модификатор на internal или protected internal, так как два класса составляют одну программу и компилируются вместе: protected' internal int x - 12; Однако если изменить модификатор на private или protected, переменная пере- перестанет быть видимой в классе AccessClient, а при попытке компиляции будет выдана ошибка: AccessTest.csA4.15): error CS0122: 'AccessTest.x1 is inaccessible due to its protection level Теперь попытайтесь откомпилировать два класса раздельно. Сохраните класс AccessTest в отдельном файле AccessTest.cs: // AccessTest.cs public class AccessTest { public int x = 12; } Откомпилируйте его как библиотеку классов с помощью команды: esc /t:library AccessTest.es Затем сохраните класс AccessClient в файле AccessClient.cs (в верхнюю1 часть файла необходимо добавить строку using System;): // AccessClient.es usfejevSyatefti,-:' . public class AccessClient { public static int MainO { AccessTest Test = new AccessTest(); int x = Test.x; Console.WriteLine(x); return 0; Теперь откомпилируйте его как консольное приложение, добавив ссылку на компи- компилированную AccessTest.dll (опции компилятора рассматриваются ниже): esc /r:AccessTest.dll AccessClient .cs Такой код должен работать и компилироваться без проблем, пока модификатором до- доступа х является public. Если изменить его на protected или protected internal и по- попытаться заново откомпилировать эти два файла, то при компиляции AccessClient.cs возникнет ошибка. Дело в том, что два файла больше не являются одной программой с точки зрения компилятора С#, так как они компилируются отдельно. Статические переменные и переменные экземпляра В примере ПС# не похож на Java" в начале главы было показано, что можно объявить ме- метод как static, а затем вызвать этот метод без создания экземпляра класса, к которому он принадлежит. То же самое можно делать и по отношению к полям, используя модифи- модификатор static. По умолчанию поля являются полями экземпляра — для каждого экземп- экземпляра класса создается отдельная копия переменной. Однако если добавить к объявлению переменной ключевое слово stat ic, то поле будет статическим. Это означает, что будет существовать только одна копия этого поля независимо от количества создаваемых экземпляров класса. В приведенном выше примере для получения доступа к переменной х нам пришлось сначала создать экземпляр класса AccessTest. Если же к объявлению переменной доба- добавить модификатор static, то доступ к переменной должен осуществляться напрямую через имя класса, а не через экземпляр класса: using System; public class AccessTest
Основы С# 69 ? public static int x = 12; ) public class AccessClient { public static inc MainO . { int x = AccessTest.x; Console.WriteLine(x); return 0; Переменные только для чтения Модификатор readonly делает переменную доступной только для чтения — после инициа- инициализации переменной изменить ее значение уже нельзя. Значения полей только для чтения могут быть установлены при первом объявлении переменной или в конструкторе для типа, к которому она принадлежит. Если значение не задано на этом этапе, поле будет иметь значение по умолчанию (нуль для числового типа, null для типа по ссылке и т.д.). Отметим, что статические поля только для чтения не могут быть установлены в конструк- конструкторах экземпляра, а поля только для чтения экземпляра класса не могут быть установлены в статических конструкторах. Единственное исключение из правила, запрещающего повторное присвоение значе- значения полю только для чтения, заключается в том, что если значение переменной присво- присвоено при объявлении ее в качестве поля класса или структуры, то это значение может быть перекрыто в конструкторе. Переменные только для чтения не могут быть переданы по ссылке или в качестве выходных параметров метода, за исключением возврата из конструктора. Выходные параметры и параметры по ссылке рассматриваются ниже. Если вы не знаете, что такое конструктор, то мы поговорим о нем чуть позже. Константы Константы похожи на статические поля только для чтения. Добавление перед переменной ключевого слова const при ее объявлении и инициализации помечает ее как константу. Та- Таким образом, константа это переменная, чье значение не может быть изменено в течение всего времени ее жизни: const int a = 100;~ // Эта величина не может, изменяться Имеются четыре важных отличия констант от полей только для чтения: О Локальные переменные и поля не могут быть объявлены константами. О Константы обязаны быть инициализированы при объявлении — их нельзя объявить на уровне типа, а затем присвоить им значение в конструкторе. После установки значение константы не может быть изменено. О Значение константы должно быть вычислимым во время компиляции. Следовате- Следовательно, константу нельзя инициализировать значением переменной. Если все же требуется сделать это, необходимо использовать поле только для чтения. О Константы всегда являются статическими. Однако отметим, что не требуется (и на самом деле запрещается) включать в определение константы ключевое слово static. Существуют как минимум три преимущества использования констант (или переменных только для чтения) в программах: О Константы упрощают понимание программы, заменяя "волшебные числа" и "вол- "волшебные строки" удобочитаемыми именами, чей смысл легко понять. О При использовании констант легче изменять программу. (Например, в одной из программ на С# у вас объявлена константа SalesTax, и она имеет значение 6%. Если процент налогов с продаж позже изменится, то поведение всех расчетов на- налогов можно изменить, присвоив новое значение константе; вам не потребуется просматривать весь свой код в поисках значения .06 и замены его новым, надеясь, что все они были найдены.)
70 Глава 3 D Константы позволяют избегать некоторых ошибок в программе. Если попытаться присвоить константе другое значение где-нибудь в программе, компилятор выдаст ошибку. Профессиональные разработчики предпочитают применять константы наряду с пе- перечислениями, таким образом дисциплинируя себя для повышения аккуратности своих программ. Операции Большинство операций С# знакомо разработчикам на С и C++. Здесь мы приведем опи- описание наиболее важных из них для новичков и тех, кто переходит на С# с VB, а также расскажем о некоторых изменениях, появившихся в С#. С# поддерживает следующие операции. Отметим, что четыре из них (sizeof, *, - >, &) доступны только в небезопасном коде (код, для которого С# не выполняет проверку безопасности по типу), они рассматриваются в главе 6: Категория Операция Арифметические Логические Конкатенация строк Инкремент и декремент Побитовый сдвиг Сравнение Присваивание Доступ к членам (для объектов) Индексирование (для массивов и индексаторов) Преобразование типа Условная (тернарная операция) Создание объекта Информация о типе Управление исключениями переполнения Разыменование и адресация + - * / % == != < > <= >= = += -= *= /= %= &= |= л= «= »= О ■>. sizeof (только небезопасный код) is typeof checked unchecked * -> & (только небезопасный код) [] Причина одной из самых серьезных ошибок при использовании операций С# кроет- кроется в том, что, как и другие языки С-стиля, он использует различные операции для при- присваивания (=) и сравнения (==). Оператор * = 3'; // Один знак "равно" означает присваивание означает "присвоить переменной х значение три". При присвоении значения перемен- переменной используется один знак равенства (=). Если же необходимо сравнить х с тем или иным значением, указываются два знака равенства (==): if (х==3) IJ Два знака ""равно" означают сравнение Жесткий контроль за безопасностью типов в С# предотвращает появление распро- распространенной ошибки С, когда использование оператора присваивания вместо оператора сравнения приводит к непредсказуемому поведению. В С или C++ оператор: if (x = 3) не вызовет ошибки. Переменной х будет присвоено значение 3, и если эта операция удаст- удастся, результатом станет значение true. Следовательно, этот оператор всегда будет возвра- возвращать true, каково бы ни было значение х, и программа не будет работать должным образом. К счастью, С# не допускает подобных неявных преобразований в bool, поэтому данный оператор в С# сгенерирует ошибку и не будет компилироваться.
Основы С# 71 Программистам на VB, использующим знак амперсанда (&) для конкатенации строк, придется привыкать к новому синтаксису. В С# для конкатенации строк применяется знак +, в то время как & означает операцию битового AND для двух различных целых значений. Операция I выполняет побитовый OR для двух целых значений. Операция % (остаток) возвращает остаток после деления, например, х % 5 вернет два, если х равен семи. В С# редко приходится пользоваться указателями, и, следовательно, не так уж часто нужны операции разыменования (->). Единственное место в программе, где разрешено их применять,— это блоки небезопасного кода, поскольку только там допускается применение указателей. Сокращенная запись операций Одной из отличительных черт С является его лаконичный синтаксис, применяемый для записи сложных операций. Хорошим примером являются операции инкремента и декре- декремента (++ и -). При их использовании значительно упрощается запись соответствующих действий по сравнению с другими языками программирования. Эти операторы могут стоять как до, так и после переменной. Выражения х++ и ++х оба эквивалентны выражению х = х + 1. Однако существует разница в их поведении. Операторы инкремента и декремента могут действовать как выражения или в составе выражений. Другими словами, х++ и ++х могут являться отдельными выражениями, и в этом случае они идентичны и соответствуют выражению х = х + 1. Однако они могут также использоваться в составе других выражений, и в этом случае они ведут себя по-разному. Префиксный оператор (++х) увеличит значение х до того, как будет вычислено выражение, т.е. сначала инкрементируется х, а затем полученное значение используется в выражении. Напротив, постфиксный оператор увеличивает значение х после вычисления выражения — выражение вычисляется с применением первоначального значения. Например: xai х =» 5;;' - It (++x ч=4" £\ с -•"" , -•. .'- ConsQlg.WriteLine(r3TO будет, выполнено"); ^ ........... if (х+*=7) I -•' Console.WriteLineC'A это нет"); } /*- Первое условие i f выполняется, так как х изменяется с 5 на 6 до того, как вычисляет- вычисляется все выражение. А вот второе условие не выполняется, так как х принимает значение 7 после того, как вычисляется все выражение. Префиксный и постфиксный операторы --х и х-- ведут себя точно так же, но умень- уменьшают, а не увеличивают операнд. Другие сокращенные операторы, такие как += и -=, требуют двух операндов, и ис- используются для изменения значения первого операнда путем выполнения над ним арифметической, логической или битовой операции. Например: является точным эквивалентом: X г 5с"*;: В таблице приводится полный список сокращенных операторов, доступных в С#: Х++, х" X X X X X += -= * = /= %= + +Х --Х У У У У У X = X + 1 выражения; х = х - 1 выражения; х = х + у X = X - у X = X * у X = X / у X = X % У (первая форма инкрементирует вторая форма — до вычисления (первая форма декрементирует вторая форма — до вычисления х после вычисления выражения) х после вычисления выражения)
72 Глава 3 X X X X X »= у «= у &= у 1= У л= у X = X = X = X = X = X X X X X » У « У & у 1 У л У Тернарный оператор Тернарный оператор ?: является сокращенной формой конструкции if... else. Он полу- получил такое имя потому, что включает в себя три операнда. Оператор вычисляет условие и возвращает одно значение в случае, если условие верно, и другое значение, если условие неверно. Синтаксис оператора: условие ?значение_истина : значение_ложь Здесь условие — это выражение типа Boolean, значение_истина представляет собой значение, которое возвращается, если условие равно true, и значение_ложъ возвращается в противном случае. При уместном использовании тернарный оператор делает программу лаконичной. Особенно он удобен в случае, когда необходимо передать вызываемой функции один из двух аргументов. Например, можно использовать оператор для быстрого перевода значения Boolean в строковый эквивалент: // X является переменной типа int Console.WriteLine( Х>= 0 ? "Положительное" : "Отрицательное"); Также удобно с его помощью показывать единственную и множественную формы: int. х = 1; string s = x.TqString<) + " " ; S += (x==l ? "man" : "men"); Console,WriteLine(s); Этот код выведет на экран 1 man, если х равен 1, и покажет правильную множествен- множественную форму для всех остальных чисел. Однако отметим, что если результат вывода дол- должен быть локализован в различные языки, возможно, потребуется написать гораздо более сложные функции для учета грамматических правил других языков. Операторы checked и unchecked Операторы checked и unchecked позволяют указать, как CLR будет обрабатывать пере- переполнение стека при выполнении тех операций над целочисленными типами, которые приводят к выходу значения результата за границы допустимых значений для конкретного типа данных. Рассмотрим код: byte Ь "= 255; Console.WriteLine(b.ToString()); Тип данных byte может хранить значения только от 0 до 255, поэтому инкремент b вызывает переполнение. Как CLR будет обрабатывать подобную ситуацию, зависит от множества моментов, включая опции компилятора. Если существует риск переполне- переполнения, нужен способ, позволяющий убедиться в том, что результат является таким, какой ожидается. Для этих целей С# предоставляет два оператора: checked и unchecked. Если отме- отметить фрагмент кода как checked, то CLR будет выполнять проверку переполнения и генерировать исключение при возникновении переполнения. Изменим код, добавив в него оператор checked: byte b = 255; checked !' ■&'*' г„ , - Console.WriteLine(b.ToString()); При попытке запуска этого кода произойдет ошибка:
Основы С# 73 Unhandled Exception: System.OverflowException: Exception of type System.Overflow Exception was thrown. at ConsoleApptication3.Class1.Main(String[] args) Проверку на переполнение можно включить сразу для всего кода, выполнив компиляцию программы с опцией /checked. Если нужно запретить проверку на переполнение, следует пометить код как unchecked: byte b = 255; unchecked • - * »-в ■ '',?■ [' ■» >;:д? Ь*',+ ;' 5 Console.WriteLine(b.ToString()); Исключение не возникнет, однако данные будут потеряны. Тип byte не может хранить значение, равное 256, поэтому лишние биты будут отброшены, а переменная b будет содержать значение, равное нулю. Оператор is Оператор is позволяет проверить, является ли объект совместимым с определенным типом. Например, можно проверить, совместима ли переменная с типом object: infc !■ =?> 10? ,,:♦" *?". ч ■ it It- is ' object)" O* "'/■'■ i if ё ci все имеет тип object! "■' CorjsoleiWriteLitief"! имеет тип object"); .» Тип int, как и все остальные типы данных С#, происходит от object, поэтому резуль- результат выражения I is object будет равен true, и на экран будет выведено сообщение. Оператор sizeof Размер (в байтах), требуемый для хранения в стеке переменной по значению, можно определить с помощью оператора sizeof: string s « 'Строка"-; : ■ ■* ' ' ^■•■' unsafe " , 5-...;»" pojjsole,WriteEine{sizeof (int)); "' В результате будет выведено число 4. Отметим, что оператор sizeof можно использовать только в блоках небезопасного кода. По умолчанию компилятор С# не воспринимает код, содержащий блоки небезо- небезопасного кода, поэтому его поддержку необходимо включить либо с использованием оп- опции командной строки компилятора /unsafe, либо путем установки в значение true пункта Allow unsafe code blocks в Visual Studio.NET. Этот пункт можно найти на страницах свойств проекта, указав в меню Configuration Properties | Build. Небезопасный код подробно рассматривается в главе 6. Оператор typeof Оператор typeof возвращает объект Туре, представляющий указанный тип. Например, typeof (string) вернет объект Туре, представляющий тип System.String. Это полезно при использовании отражения для динамического получения информации об объекте (см. главу 7). Приоритет операций Нижеследующая таблица показывает порядок действия операций С#. Операции в верхней части таблицы имеют наивысший приоритет (т.е. в выражениях, содержащих несколько операторов, они вычисляются первыми):
74 Глава 3 Группа Унарные Умножение/деление Сложение/вычитание Операции побитового сдвига Отношения Сравнения Побитовый AND Побитовый XOR Побитовый OR Логический AND Логический OR Тернарный оператор Присваивание Операции О . [] Х++ х— new typeof sizeof checked unchecked +■ - ! ~ ++X —x и операции приведения типов * / % + - « » <><=>= is == ! = & 1 && 1 1 ? - = += - = *= /= %= И Т.Д. Управление ходом выполнения программы В этом разделе рассматриваются основные элементы языка: операторы, которые позволя- позволяют управлять ходом выполнения программы, а не просто исполнять ее последовательно строка за строкой. На самом деле остальные функциональные возможности С#, не считая этих ключевых слов, заимствуются непосредственно у платформы .NET. Условные операторы Условные операторы позволяют разветвлять код в зависимости от выполнения опреде- определенных условий или от значения выражения. В С# имеются две конструкции для ветвле- ветвления кода: оператор if, который проверяет выполнение условия, и оператор switch, который сравнивает выражение с некоторым числом различных значений. Оператор if Для условного ветвления С# наследует конструкцию if... else языков С и C++. Синтак- Синтаксис оператора должен быть понятен каждому, кто хотя бы немного программировал на процедурном языке: if {условие) оператор (ы) else оператор (ы) Если необходимо использовать несколько операторов, то они должны быть заключены в фигурные скобки ({...}) (это применимо и в других конструкциях С#, где операторы могут быть объединены в блок, например в циклах for и while): bpol "Zero; if (I==0) f ' ' . Zero - true; Console.writeLine{"Нуйь*'); } else :{ Zero J=;. false; _ « 'Console-WriteLineCite- нуль") ;' ' . Ь, " "'■ ' " " ~ * ' \ Допустимо использование оператора if без оператора else:
Основы С# 75 If (Condition) I • ■ IJ Этот- блок выполняется, если Condition = true } . В блок else можно включать операторы if для проверки нескольких условий: if (conditionl) :i "//' Этот блок выполняется, если conditionl = true else" if (condition2) I // Этот, блок выполняется, если conditionl = false /У1 и condition2 = true else-" // Этот!, блок выполняется, если conditionl = false - f( /7 и qpndifc£on2 s false 1Г," В случае, если в условной конструкции используется только один оператор, записывать его в скобках не надо: If (j*=?0) Console»WriteLine.( "Нуль"); // Эта строка будет выполнена только ; //в том случае, если i = О Consol6.WriteLine(TO угодно"); // Эта строка будет выполнена в любом д«- • ...- // случае Однако, как и большинство других программистских сокращений, такая форма записи может доставить неприятности. Если в будущем потребуется ассоциировать с i f больше операторов, но при этом не будут поставлены фигурные скобки, то вы будете сильно оза- озадачены тем, что часть операторов выполняется независимо от значения условия в конст- конструкции if. Именно поэтому многие программисты предпочитают всегда использовать фигурные скобки в операторах if. Одно из значительных отличий между операторами i f (это касается и других операто- операторов управления ходом выполнения программы) в С# и C/C++ состоит в том, каким обра- образом С# производит вычисление выражения в данной конструкции. Как уже упоминалось, целочисленные типы в С# не преобразуются неявно в тип bool. Это означает, что нельзя вызвать метод, который возвращает целочисленное значение, и использовать это значе- значение непосредственно в конструкции i f. В C++ можно сделать так: // Это код на C++, а не на С#! // objSomething->DoSomething() возвращает значение типа int if (objSomething->DoSomething()) { // Функция вернула ненулевое значение ) else { // Функция вернула нуль } В С# придется преобразовать полученное целочисленное значение в значение true или false, например, сравнив его с нулем или null: if <<*3Sometbin^V>DoSdmething{) != 0) '{ , //'^Функция вернула, кенулевое значение else .. (: " •■ ,..- ■'"■:#/• Функция--,вернула куяь Нельзя рассчитывать на то, что нуль будет автоматически преобразован в false; этого не произойдет. Хотя такая мера требует большей дисциплинированности программиста, она направлена на улучшение вашего кода.
76 Глава 3 Оператор switch Оператор switch. . . case служит для выбора одного из вариантов выполнения програм- программы из нескольких взаимоисключающих. За ключевым словом switch и аргументом идет последовательность меток case. Если выражение в аргументе switch становится рав- равным значению в одной из меток case, то исполняется код, следующий сразу за этой мет- меткой. Это один из случаев, когда не нужно применять фигурные скобки для объединения отдельных выражений в блоки кода; вместо этого окончание блока кода для каждой case указывается с помощью ключевого слова break. В операторе switch можно также исполь- использовать метку default, код после которой будет выполнен в том случае, если выражение не примет ни одного из перечисленных значений: switch (IntegerA) <"' ■ - case 1: ! Console.WriteLineC IntegerA = 1"); break,- case 2: , Console ■.Writetinet" IntegerA = 2°); {.■„■ break; case, 3: Console.WriteLinef"IntegerA = 3"); break; default: Console.WriteLinel "IntegerA не равно 1, 2 или 3"); '• break; } Значениями меток case могут быть только константы, использование переменных не разрешается. В С# оператор switch. . .case чуть безопаснее, нежели в С и C++. В частности, он, как правило, запрещает "проваливающиеся" конструкции. Это означает, что если выпол- выполняется код более ранней метки case, то после него не может выполняться код более поздней метки case, если только для явного указания этого не используется оператор goto. Компилятор реализует это ограничение, выдавая ошибку для каждой метки case, за которой не следует оператор break: C:\My DocuraentsWisual Studio Projects\ConsoleApplication9\Classl .cs B4) : Control cannot fall through from one case label ("case 2:') to another Использование "проваливающихся" конструкций возможно в некоторых ситуациях, но в большинстве случаев оно является непреднамеренным и приводит к возникнове- возникновению логической ошибки, которую трудно выявить. Логичнее писать код, выполняющий нормальные действия, чем тот, что является исключением. С помощью операторов goto (которые поддерживаются в С#) можно написать "прова- "проваливающуюся" конструкцию в выражениях switch. .. case. Однако если вам действитель- действительно требуется реализовать такую функциональность, возможно, следует пересмотреть подход к ее организации. Единственным исключением, при котором можно "проваливаться" сквозь несколько значений case, является случай, когда после метки сразу же идет следующая метка case. Это позволяет одинаково реагировать на две или более меток case (без использования операторов goto): switch (Country) { case "аи": case "uk": case ■'us": Language = "Английский"; break; case "at": case"de": • tangiiage *= "Немецкий"; break; ) . Одной из интригующих особенностей оператора switch в С# является то, что поря- порядок меток case не имеет значения — можно даже в начале поместить метку default! Из этого следует, что две метки case не могут иметь одинаковых значений, в частности, двух разных констант с одинаковым значением, поэтому, например, нельзя сделать так:
Основы С# 77 const string ENGLAND = "uk"; const string Britain ~ "uk"; switch (Country) { ■. case ENGLAND: case BRITAIN: X/ Вч этом месте компилятор выдаст ошибку, так как ! ■ // метка для "uk" уже существует ■ Language = "English"; ». ■ , ' break; } Циклы С# предлагает четыре различных механизма, позволяющих циклически исполнять фраг- фрагмент кода до тех пор, пока не будет выполнено какое-либо условие. Рассмотрим сначала цикл for. Цикл for Циклы for предоставляют механизм для прохода по циклу, при котором инициализиру- инициализируется некоторая локальная переменная и выполняется оператор в цикле до тех пор, пока заданное условие истинно, причем перед переходом к следующей итерации производится какой-нибудь простой шаг. Синтаксис цикла: for (инициализатор; условие; итератор) оператор (ы) где инициализатор — выражение, вычисляемое до начала выполнения цикла (обычно здесь инициализируется локальная переменная, используемая в качестве счетчика цик- цикла), условие — выражение, которое вычисляется перед выполнением каждой итерации цикла (например, проверка того, что счетчик цикла меньше определенного значения), а итератор — это выражение, которое будет выполняться после каждой итерации цикла (например, увеличение счетчика цикла). Цикл for известен также как цикл с предусловием, поскольку условие цикла вычис- вычисляется до выполнения операторов цикла, а в том случае, если условие сразу оказывается равным false, операторы цикла вообще не будут исполнены. Перед каждой итерацией цикла условие вычисляется вновь. Цикл завершается, как только результат вычисления условия становится равным false. Цикл for превосходно подходит для повтора оператора или нескольких операторов заранее определенное число раз. Следующий пример по- показывает типичное использование цикла for: for tint I = б; I < 100;-I++) I ■'■' I/ Цикл будет выполнен 10.0 раз } ' Циклы for можно вкладывать друг в друга, так что внутренний цикл полностью вы- выполняется один раз для каждой итерации внешнего цикла. Эта схема обычно использу- используется для прохода по каждому элементу в многомерном прямоугольном массиве. Внешний цикл осуществляет проход по всем строкам, а внутренний — по каждому столбцу соответствующей строки. // Этот цикл проходит по строкам for (int i = 0; 1 < 100; i++) { // Этот цикл проходит по столбцам - for (int 3=0; j < .25; j++) '{, a[i,j] = Ot, Программисты на С должны обратить внимание на одну особенность приведенного при- примера. Переменная цикла во внутреннем цикле объявляется заново для каждой итерации внешнего цикла. Такой синтаксис допустим не только в С#, но и в C++. Хотя в условии цикла for технически допустимо вычисление, помимо переменной цикла, чего-нибудь еще, обычно такая возможность не используется. Кроме того, в цик- цикле for можно опустить одно (или даже все) выражения. Однако в такой ситуации лучше применять другой тип цикла.
78 Глава 3 Цикл while Как и цикл for, цикл while является циклом с предусловием. Это означает, что если условие цикла сразу же окажется равным false, цикл while не выполнится ни разу. Синтаксис цикла whi le аналогичен синтаксису цикла for с тем исключением, что в нем используется только одно выражение: while [условие) выражение (я) ; В отличие от цикла for, цикл while чаще всего применяется для повтора операто- оператора или блока операторов такое число раз, которое неизвестно перед началом цикла. Обычно на определенной итерации оператор внутри цикла устанавливает флаг Boolean в false, заставляя цикл завершиться: boql Condition = ^ while (JCondition) I // "Цикл 'выполняется до тех пор, пока условие не станет равным true Do.SomeVjork(); .' Condition = CheckConditionO; } Все циклы в С#, включая while, разрешают опускать фигурные скобки в том случае, если в цикле выполняется только один оператор, а не блок операторов. Однако во избе- избежание в будущем ошибок, связанных с добавлением новых операторов в тело цикла, ре- рекомендуется использовать фигурные скобки, даже если циклически повторяется всего один оператор. Цикл do...while Цикл do.. .while является версией цикла while с постусловием. Это означает, что усло- условие цикла проверяется после выполнения тела цикла. Следовательно, циклы do.. -while полезны в том случае, когда блок кода должен быть выполнен как минимум один раз: bool Condition do ' { // Цикл выполнится как минимум один раз даже в том случае, У/ "если Condition равно false DpSomeWork(); Condition = CheckConditi'onO ; j While (Condition); Цикл foreach И еще одним механизмом циклов, предлагаемых С#, является цикл foreach. Если дру- другие циклы применялись в более ранних версиях С и C++, то оператор foreach новый (заимствован из VB). Цикл foreach позволяет производить итерацию по каждому объекту в контейнерном классе, поддерживающем интерфейс IEnumerable. К контейнерным классам относятся массивы С#, классы коллекций в пространствах имен System. Collection и определен- определенные пользователем классы коллекций: int[] Ints = {1. 2, 3},- foreach (int temp in Ints) { ," Console.WriteLine(temp); } Здесь важно помнить, что значение объекта в коллекции изменять нельзя, поэтому код, наподобие приведенного ниже, не будет выполняться: irit[] thts = ~{1, 2, 3); foreach (int temp in Ints) { temp++; //> Эта строка не будет выполнена, так как объекты // в цикле foreach доступны только для чтения! Console.WriteLine(temp); } Если необходимо производить итерации по элементам коллекции и менять их значения, нужно использовать цикл for.
Основы С# 79 О реализации собственных классов коллекций рассказывается в главе 7. Интересно то, что такие классы можно обрабатывать и с помощью оператора foreach языка VB.NET. Операторы перехода С# предоставляет ряд операторов, которые позволяют немедленно переходить к другой строке программы. Первый из них — это хорошо знакомый всем оператор goto. Оператор goto Оператор goto позволяет передать управление строке программы, отмеченной меткой (метка — это идентификатор с двоеточием): gotp' , Label! ; Consoie.WriteLlrie(Ta строка не будет выполнена"); Labell: Console.WriteLinei"Выполнение будет продолжено отсюда"); С goto связаны определенные ограничения. Нельзя передать управление в блок кода, например в цикл for; нельзя передать управление за пределы класса и нельзя выйти из блока finally, идущего после блоков try.. .catch (обработка исключений с помощью try. . .catch. . .finally рассматривается ниже). Как правило, не рекомендуется использовать оператор goto. Вообще говоря, его применение противоречит хорошему стилю объектно-ориентированного программиро- программирования. Однако в одном случае этот оператор очень удобен — в случае переходов между метками в операторе switch, в частности потому, что оператор switch в С# не позволяет "проваливаться" сквозь метки case. Как говорилось выше, для облегчения перехода на другие метки в операторе switch не требуется определять отдельные метки — можно использовать метки оператора switch, включая метку default. Оператор break Мы уже знаем, что оператор break применяется для выхода из кода метки в операторе switch. Также break может использоваться для выхода из цикла for, foreach, while или do. . .while. Управление будет передано на первый оператор, следующий сразу за соответствующим оператором: for- (int I «■ 0; I < 1Q; I++) i "'-, ' Console.Write("Введите слово:"); string s, = Console. ReadLine (); ''•*" . if (c <==- "&id") j { *f- // Выходим, из цикла, если пользователь ввел "End" :■■" break; й } - Cpnsqle.WriteLine("Bbi ввели: " + s) ; I N Сюда будет передано управление после того, .лсак пользователь введет "End" Если break используется во вложенном цикле, управление будет передано в конец внутреннего цикла. Если break указывается вне оператора switch или цикла, произойдет ошибка на этапе компиляции. Оператор continue Оператор continue похож на break и также должен использоваться внутри цикла for, foreach, while или do.. .while. Однако continue прерывает только текущую итерацию, т.е. выполнение будет продолжено со следующей итерации цикла, а не выйдет из цикла. Отметим, что (как и другие операторы перехода) continue не может применяться для выхода из блока finally. Если он осуществляет выход из блока try, то связанный с ним блок finally будет выполнен до передачи управления следующей итерации цикла. Оператор return Оператор return используется для выхода из метода класса с передачей управления вы- вызвавшему этот метод. Если метод возвращает значение, то return обязан вернуть значе- значение соответствующего типа; в противном случае return должен применяться без выражения.
80 Глава 3 Оператор using Применение оператора using (в отличие от директивы using) гарантирует, что объек- объекты, интенсивно использующие ресурсы, будут освобождены сразу по завершении работы с ними. Синтаксис этого оператора: using (объекг) { // Код, использующий объект > Здесь объект — это экземпляр класса, который реализует интерфейс iDisposable. Все классы, реализующие IDisposable, обязаны реализовать метод Dispose, который слу- служит для освобождения ресурсов, используемых объектом. Метод Dispose вызывается сра- сразу по завершении блока using. Оператор using и интерфейс IDisposable подробно рассматриваются в главе 5. Обработка исключений Структурированная обработка исключений является относительно недавним нововведе- нововведением в C++, и С# тоже реализует ее. С точки зрения синтаксиса, структурированная обра- обработка исключений состоит из трех различных блоков кода. Для того чтобы реализовать в методе структурированную обработку исключений, необходимо добавить в метод каждый из этих трех блоков. □ Блок try инкапсулирует код, который программа пытается выполнить. Если в про- процессе исполнения этого кода возникает ошибка, или исключительное состояние, то говорят, что происходит исключение. □ Блок catch следует за блоком try. Он инкапсулирует код, который предпринимает действия по обработке ошибки, сгенерированной в блоке try. D Блок finally располагается в самом конце процедуры обработки ошибок. Этот код выполняется всегда, независимо от того, успешно ли закончилось выполне- выполнение функции или было сгенерировано исключение. Из блока finally нельзя выйти (например, используя goto). Если оператор перехода приводит к передаче управления за пределы блока try, соответствующий блок finally все равно будет выполнен: public voi.d SomeRoutine () { try { // Здесь начинается выполнение. // Этот код может сгенерировать или не сгенерировать исключение. ... } catch : ,{ f * У/ Если в блоке try сгенерировано исключение, •// вызывается код, расположенный в этом блоке. " } . finally { // Код в данном блоке исполняется по завершении выполнения процедуры ,// независимо от того, было ли сгенерировано исключение. '*•-■ ) с }» Ошибки генерируются автоматически, если во время исполнения происходят неверные действия, такие как деление на нуль, выход за границы диапазона или переполнение целого типа. Более того, код может явно вызвать исключение, если он определит, что возникло исключительное состояние. (Например, он может вызвать исключение, если не удается открыть важный файл или введенное значение аргумента является недопустимым.) После обнаружения исключения блок catch, который обрабатывает эту ошибку, по- получает объект, содержащий информацию о ней. В частности, блок catch получает ссыл- ссылку на объект, который является производным от класса System.Exception. Для большинства ошибок времени исполнения существует подкласс класса Exception. Вы можете реализовать свои подклассы для данного класса. Для повышения надежности можно реализовать в методе несколько блоков catch для обработки каждого типа возможных ошибок. Эта мера позволяет коду более
Основы С# 81 интеллектуально реагировать на конкретные типы ошибок. Например, для некоторых ошибок процедура может выполнить какие-то восстановительные работы и продолжить исполнение. В случае других, более серьезных ошибок процедура может произвести запись в журнал и прекратить дальнейшее исполнение кода. Каждый метод в программе на С# необходимо снабжать обработчиком ошибок. Более подробно структурированная обработка ошибок рассматривается в главе 6. Структура программы К настоящему моменту мы познакомились с основными "строительными блоками", вхо- входящими в состав языка С#: с типами данных и с операторами управления ходом выполне- выполнения программы. Но как соединить эти кирпичики вместе для того, чтобы получить завершенную программу? Ключевой момент здесь заключается в работе с классами. Классы Классы играют важнейшую роль в программе на С#, и следующие две главы полностью посвящены объектно-ориентированному программированию в С#. Для начала же необ- необходимо получить представление об основах работы с классами в С#, так как совершенно невозможно написать программу на С# без их использования. Классы по существу явля- являются шаблонами, из которых можно создавать объекты. Каждый объект содержит дан- данные и имеет методы для работы с этими данными и для доступа к ним. Класс определяет, какие данные может содержать каждый объект этого класса, но не содержит самих дан- данных. Например, класс, представляющий потребителя, может определять такие поля, как CustoraerlD, FirstName, LastName и Address, которые будут использоваться для хране- хранения информации о конкретном клиенте. Позже можно создать экземпляр объекта этого класса для представления конкретного клиента и заполнить поля этого объекта. Члены класса Данные и функции внутри класса называют членами класса. Официальная терминология Microsoft делает различие между данными класса и функциями класса. Помимо этих членов, классы могут содержать вложенные типы (например, другие классы). Данные класса Данные класса — это те члены, которые содержат данные для класса: поля, константы и события. □ Поля представляют собой любые переменные, связанные с классом. Если опреде- определить переменную на уровне класса, то на самом деле это будет поле класса. Если поля объявлены как public, они доступны за пределами класса. Например, можно определить класс PhoneCustomer с полями CustomerlD, FirstName и "LastName: class PhoneCustomer { ■ * -■ '*■ public intl CustoraerlD; •> % '-public surifig FirstName; public string tastName; } ■ После того как будет создан экземпляр объекта PhoneCustomer, к этим полям можно осуществлять доступ с помощью синтаксиса объект.имя_поля: PhoneCustomer ..Customer! - new PhoneCustomer (),- Cu'stomerl, CustomerlD = 1000; Customerl.FirstName = "Иван"; Customerl.Las.CName-'i^ "Петров".; Console-WriteLirie (Customerl.t'irstName + " " + Cuscomerl-. LastName ■); □ Константы могут быть ассоциированы с классами точно так же, как и переменные. Константы, объявленные как public, будут доступны вне класса. □ События являются членами класса, которые позволяют объекту уведомлять вы- вызывающего о том, что произошли некоторые программные изменения, напри- например, изменилось поле или свойство класса либо имела место некоторая форма взаимодействия с пользователем. Клиент может содержать код, известный как обработчик ошибок, который реагирует на событие (см. главу 6).
82 Глава 3 Функции класса Функции класса являются теми членами, которые обеспечивают функциональность для работы с данными класса. Они включают в себя методы, свойства, конструкторы и дест- деструкторы, операторы и индексаторы: □ Методы являются функциями, связанными с конкретным классом. Это могут быть либо методы экземпляра, которые работают для конкретного экземпляра класса, либо статические методы, которые предоставляют общую функциональность, не требующую создания экземпляра класса (подобно методу Console. WriteLine ()). □ Свойства являются функциями, доступ к которым со стороны клиента может быть осуществлен точно так же, как к открытым полям класса. С# предоставляет специа- специальный синтаксис для реализации в классах свойств read и write, поэтому не тре- требуется писать методы, в чьих именах присутствуют слова Get и Set. Поскольку свойства имеют специальный синтаксис, отличный от синтаксиса обычных функ- функций, для клиентского кода усиливается иллюзия того, что объекты являются реаль- реальными вещами. Более того, свойства централизуют возможности чтения/записи свойства, упрощая поддержку кода свойства разработчиком класса. □ Конструкторы являются функциями, которые вызываются при создании экземпля- экземпляра объекта. Они обязаны иметь имя, совпадающее с именем класса, и не должны возвращать никаких значений. Конструкторы служат для установки значений полей при создании экземпляра объекта. □ Деструкторы аналогичны конструкторам, но вызываются при уничтожении объ- объекта. Они имеют то же имя, что и класс, но с предшествующим значком тильды (-). Так как сборкой мусора занимается CLR, нельзя сказать точно, когда будет вызван деструктор. В С# деструкторы используются менее часто, чем в C++. □ Классы могут также содержать определения операций, поэтому можно описать свои собственные операции или указать, как существующие операции будут работать с классом (см. главу 5). □ Индексаторы позволяют индексировать объекты точно так же, как массивы и коллекции (см. главу 5). Методы В VB, С и даже C++ можно определить глобальные функции, не связанные с конкретным классом. В С# этого сделать нельзя. В С# каждая функция должна быть связана с классом или структурой. Другими словами, каждая функция является методом. Отметим, что официальная терминология С# делает различие между функциями и методами. Так, термин "функция" обозначает не только методы, но и другие не- данные класса или структуры: индексаторы, операции, конструкторы, деструкторы, а также - возможно, неожиданно для вас - свойства. Им противоположны данные класса - поля, константы и события. Объявление методов В С# синтаксис для объявления методов является именно таким, какой можно ожидать от языка С-стиля: сначала указываются модификаторы метода (такие, как доступность метода), затем тип возвращаемого значения, следом идет имя метода, потом в скобках список аргументов, далее тело метода, заключенное в фигурные скобки. Каждый пара- параметр состоит из имени типа параметра и имени, под которым он будет доступен внутри метода: public // модификатор доступа bool //' тип возвращаемого значения isSguai;e // имя функции (Rectangle' obj'J // аргументы { // тело метода return <bbj.Height == obj-Width); T Если метод ничего не возвращает, то в качестве типа возвращаемого значения указы- указывается void, так как его нельзя опустить. Если же у функции отсутствуют аргументы, все равно необходимо указать пустую пару скобок () после имени метода.
Основы С# 83 Метод может принимать один или несколько из следующих модификаторов: Модификатор Описание new Метод скрывает унаследованный метод с такой же сигнатурой, public Метод доступен отовсюду, в том числе извне класса. protected Метод доступен изнутри класса, к которому он принадлежит, или из типа, производного от данного класса. internal Метод доступен только из той же программы. private Метод доступен только из класса, к которому он принадлежит. static Метод не предназначен для работы с определенным экземпляром класса. virtual Метод может быть переопределен в производном классе. abstract Виртуальный метод, который определяет сигнатуру метода, но не содержит реализацию. override Метод переопределяет унаследованный виртуальный или абстрактный метод. sealed Метод перекрывает унаследованный виртуальный метод, но не может быть перекрыт классами, которые являются производными от данного. Должен использоваться совместно с override. extern Метод реализован вне программы на другом языке. Вызов методов Для вызова (активизации) метода необходимо указать имя объекта, для которого вызыва- вызывается метод, затем имя метода и список аргументов в скобках. Если функция возвращает ре- результат, его можно сохранить в переменной, использовать в качестве аргумента другой функции или отбросить. В следующем примере определяется класс под именем MathTest с одним методом Square, который возвращает квадрат переданного ему в качестве параметра числа. Для проверки этого метода мы реализуем клиентский класс: using System; // Определим класс MathTest, для которого будет вызываться метод class MathTest ('.-;..• ■, // Определим метод Square public int Square'dnt x) { jreturn x*x; /•/ Клиентский", класс для проверки класса MathTest class MathClienc public "static int Main О /•/ Создаем экземпляр объекта MathTest MathTest Math = new MathTest() ; // Вызываем метод Square int x = Math.Square! 10); // Выводим результат 'Console.WriteLine(x): return 0 ; *} При вызове статического метода необходимо использовать ИМЯ типа класса этого метода, а не имя экземпляра класса:
84 Глава 3 stringH Names = { "John", "Joe", "Jeff" }; ArrayJSort(Names); // Array.Sort — статический метод, * "II поэтому нельзя вызвать Names.Sort {); Имя объекта не требуется указывать, если вы вызываете его из того же класса. Но если класс является исполняемым, помните, что экземпляр этого класса не существует, и поэтому для него можно вызывать только статические методы. Аргументы методов Аргументы могут быть переданы в методы по ссылке или по значению. Переменная, пе- передаваемая в метод по ссылке, подвергается любым изменениям, которые производит с ней вызываемый метод, в то время как переменная, передаваемая по значению, не меня- меняет свое значение в результате изменений, сделанных внутри метода. Это происходит по- потому, что в первом случае метод получает ссылку на саму переменную, а во втором случае он получает ее копию. В С# все параметры передаются по значению, если только специально не указывает- указывается иное. Однако тип данных параметра определяет фактическое поведение передавае- передаваемых в метод параметров. Так как типы по ссылке хранят только ссылку на объект, они передадут в метод эту ссылку. Напротив, типы данных по значению содержат действите- действительные данные, поэтому в метод будет передана копия самих данных. Например, int пере- передается в метод по значению, и любые изменения, которые метод производит со значением этого int, не изменяют значение оригинального объекта int. Если же в метод передается массив или любой другой тип по ссылке, например класс, и метод изменяет значение в массиве, то новое значение записывается в оригинальный массив: using System; class ParameterTest { . г " // В качестве аргументов функция принимает массив типа int // (тип яо ссылке) и переменную типа ■ int (тип по .значению) . static void SomeFuriction(int [] Ints, iint. I). ', { IntstQ] s 100,- %>*■=■■ ioe,- ) -» ■;: i public static int Main() " { " int I % 0; ■ int[] Ints = { 0, 1, 2, ;4, G }; // Отобразим первоначальные значения Console.WriteLine!"I = ' + I); Console, Wr£teLine( "Ints [0]=, ■• + ints[0]); Console.WriteLine("Вызываем SomeFunction..."); //' -После выполнения этого, метода Ints изменится, // а I останется прежним. ,i.. SomeFunction(Ints, I); Console.WriteLine("I = " + I); Console.WriteLine("Ints[0]= " + Ints[0]); return »0; В результате выполнения этой программы будет получено: 1 = 0 lnts[O] = 0 Вызываем SomeFunction... I = 0 lnts[O] = 100 Отметим, что значение I осталось неизменным, а значение, модифицированное в Ints, изменилось и в первоначальном массиве. Следует помнить, что строки являются неизменными (при изменении строки созда- создается совершенно новая строка), поэтому они не ведут себя как тип по ссылке. Любые изменения строки, сделанные внутри метода, не затронут ее оригинал.
Основы С# 85 Мы рассмотрели поведение по умолчанию. Однако можно сделать так, чтобы перемен- переменные по значению передавались по ссылке. Для этого используется ключевое слово ref. Если в метод передается параметр, а аргумент метода помечен как ref, то любое измене- изменение переменной, сделанное методом, вызовет соответствующее изменение оригинальной переменной: // Функция принимает в качестве параметров массив (тип по ссылке) //и значение типа int (тип по значению). Но поскольку // второй аргумент помечен как ref, аргумент // будет передан по ссылке. static void SomeFunctioiHint'f] lilt's ,■> ref int 1) ■'-•:-"-'-r if { Ints[0] = 100; I = 100; } Ключевое слово ref должно указываться и при вызове метода: SoroeFunctiontlftts-,1 ref I); Ключевое слово out В языках С-стиля общей для функций является способность возвращать более одного значения. Это достигается с помощью выходных параметров: выходные значения присваиваются переменным, переданным в метод по ссылке. Обычно начальные значения передаваемых по ссылке переменных не важны. Эти значения будут затерты функцией, которая, возможно, даже ни разу не использует их. Было бы удобно применить то же самое в С#, но, если вы помните, С# требует присво- присвоения переменной начального значения перед ее использованием. На самом деле можно инициализировать переменные бессмысленными значениями перед передачей их в фун- функцию, которая присвоит им осмысленные значения, однако эта практика в лучшем слу- случае выглядит ненужной, а в худшем — сбивает с толку. Нельзя ли обойти требование компилятора С#, касающееся инициализации передаваемых переменных? Можно, и делается это с помощью ключевого слова out. Если аргумент метода пред- предварен ключевым словом out, то в этот метод может быть передана переменная, которая не была инициализирована начальным значением. Переменная передается по ссылке, так что все изменения этой переменной, произведенные методом, останутся после пере- передачи управления из вызываемого метода. Ключевое слово out должно указываться и при определении метода, и при его вызове: // Данная функция .может принимать неинициализированные I/ аргументы" ъ\ виде чисел int, передаваемых по -ссылке, static void , Sc4neF,unction (out 'int i'J . ., public static- int. Main 0 k { ss« , / ' , : ь" '// *i" объявляется, но ;не инициализируется ^■; хЩ ^ 1; к ■.:','' "Som6Eunct.ion(<3Ut i) ; " Console.Writetine.(i) ; return, 0; 1 } ' *; Если параметру, отмеченному out, в теле функции не будет присвоено значение, код не откомпилируется. Пространства имен Пространства имен предоставляют способ объединения связанных классов и других ти- типов. В отличие от файла или компонента, пространство имен является логической, а не физической группировкой. При определении класса в файле С# его можно включить в определение пространства имен. Позже, при определении другого класса, который вы- выполняет связанную с первым работу, в другом файле, можно включить его в то же самое пространство имен, создав логическую группировку, которая показывает разработчикам, применяющим эти классы, как они связаны и используются: V/VСтруктура Customer, определена в '- >-- ■
86 Глава 3 /,/ пространстве имев CustomerPhoneBookApp namespace CustomerPhoneBookApp I using System; public struct Subscriber { public long CustomerlD; public string FirstName; public string MiddleName; public string LastName; public decimal Balance; Размещение типа в пространстве имен автоматически дает этому типу длинное имя, со- состоящее из названия пространства имен этого типа в виде последовательности названий, разделенных точками (.) (в приведенном примере полное имя структуры Subscriber представляет собой CustomerPhoneBookApp.Subscriber), и имени конкретного класса. Это позволяет применять в одной программе разные классы с одним и тем же коротким именем без возникновения неопределенностей. Пространства имен можно вкладывать друг в друга, создавая иерархическую структуру типов: namespace Wrox { namespace ProfessionalCSharp { ! namespace ChapterO3 { class NamespaceExample { // Код класса } } 1 } } Имя каждого пространства имен состоит из названий пространств имен, внутри кото- которых оно располагается. Имена разделяются точками и указываются, начиная от самого внешнего пространства имен и заканчивая данным. Таким образом, полное имя простран- пространства имен ProfessionalCSharp будет Wrox.ProfessionalCSharp, а полное имя класса NamespaceExample — Wrox. ProfessionalCSharp.Chapter03 .NamespaceExample. Такой синтаксис можно использовать для объединения пространств имен в их опре- определениях, так что приведенный выше код может быть записан следующим образом: namespace Wrox.ProfessionalCSharp.ChapterO3 { class NamespaceExample -■ { // Код класса Отметим, что не разрешается определять пространство имен, состоящее из нескольких частей, внутри другого пространства имен. Также не допускается объявление двух типов с одинаковыми именами внутри одного пространства имен, поэтому код, приведенный ниже, не будет компилироваться, хотя классы определены в разных пространствах имен: namespace Wrox. ProfessionalCSharp .,'.* { namespace ChapterO3 { class NamespaceExample { // Код класса } } 1 namespace Wrox.ProfessionalCSharp.ChapterO3 , . < - -. - ' - , --.'" - ■
Основы С# 87 Jclass NamespaceExample // Этот класс имеет такое же полное имя, что и *- II класс выше, поэтому код не откомпилируется Л '' . II, Код класса "■'} ' } Оператор using Очевидно, что пространства имен могут иметь очень длинные названия, которые утомите- утомительно набирать постоянно. К тому же не всегда требуется помечать конкретный класс та- таким образом. К счастью, С# позволяет сокращать полное имя класса. Для этого необходимо перечислить пространства имен класса в верхней части файла, предварив их ключевым словом using. В остальной части файла можно ссылаться на путь с использова- использованием более короткого относительного, а не абсолютного имени: // Файл 1: // Без ключевого слова using необходимо указывать полное пространство имен System.Int32 а; // Файл 2: '•■ /I С using можно использовать относительное имя using System; Jnt32 а; Если два пространства имен, указанные в директиве using, содержат типы с одина- одинаковым именем, придется использовать полное (или по крайней мере более длинное) имя типа, чтобы компилятор знал, к какому типу осуществляется доступ. Например, в приведенном ниже коде из-за того, что классы по имени NamespaceExample существу- существуют и в пространстве имен Wrox. ProfessionalCSharp.Chapter03, и в Wrox.Professio- nalCSharp.ChapterO4, необходимо указать, о котором из этих двух пространств имен идет речь: using Wrox.professionalCSharp.Chapter03; vising Wrox:ProfessionalCSharp.Chapter04; namespace Wrox.ProfessionalCSharp t class Test , { public static int Main() f { Tl Эта строка не будет кок. «лироваться ■ 5 . как ссылка неоднозначна: **' /У "NamespaceExample NSEx = new NamespactExample () ; •// Вместо этого необходимо указать, какое пространство имен // имеется в виду: ChapterO3.NamespaceExample NSEx = new ChapterO3.NamespaceExampleО; return 0; namespace ChapterO3 , class NamespaceExample ;; ~ II Код класса ' namespace GhapterO4 class. NamespaceExample // Код класса
88 Глава 3 Если вы уже просмотрели программы-примеры на С#, то наверняка отметили присут- присутствие в начале многих из них оператора using System;. В этом пространстве имен со- содержатся типы CTS, а также большинство функциональных средств ядра .NET, в частности консольный ввод/вывод. Многие программисты включают оператор using System; в каждый файл, так как практически каждая программа на С# пользуется про- пространством имен System. Из-за того что операторы using располагаются в верхней части файла С#, в том же месте, где в С и C++ перечисляются включаемые файлы (операторы flinclude), про- пространства имен часто неправильно принимаются за заголовочные файлы. Не делайте этой ошибки! Оператор using не осуществляет физической связи файлов. Возможно, ваша организация решит уделить внимание разработке схемы пространства имен, чтобы разработчики могли быстро находить необходимые функциональные средст- средства, а имена собственных классов организации не конфликтовали с именами из внешних библиотек. Указания по созданию схемы пространств имен приведены в документации Microsoft .NET SDK. Псевдонимы пространств имен Другое использование ключевого слова using заключается в присвоении псевдонимов классам и пространствам имен. Если имеется очень длинное название пространства имен, которое необходимо несколько раз упомянуть в коде, но не требуется включать в директиву using (например, во избежание конфликтов имен типов), такому пространству имен можно присвоить псевдоним. Синтаксис этой операции: using alias = название_пространства_имен; Следующий пример присваивает псевдоним ChapterO3 пространству имен Wrox.ProfessionalCSharp.Chapter03 и использует его для создания экземпляра объ- объекта NamespaceExample, определенного в этом пространстве имен. Объект Namespace- Example имеет один метод, GetNamespace, который использует метод GetType, сущест- существующий в каждом классе, для доступа к объекту Туре, представляющему тип класса. Этот объект применяется для получения имени пространства имен класса: using System; using ",Chap6exO3 =' Wrox.FrofessionalCSharR,ChapterQ3; class Test { public static int Main О Л GhapterOS-NamespaceExample 'NSEx = '.new Chapter~03.NamespaceExampleU ; Console.WriteLine(NSEx.GetNamespace()); return 0; namespace Wrox.ProfessionalCSharp.Chapter03 { class NamespaceExample { public string GetNamespaceО { return this.GetTypeO .Namespace; Метод Main Как упоминалось выше, программы на С# начинают выполняться с метода Main (). Это должен быть статический метод класса (или структуры), который обязан иметь тип воз- возвращаемого значения int или void: public fetaStic; int MainU,; или public staticivoid Main (-);., ,. .- ... . .- , . .■ .?■,■..., -■■'
Основы С# 89 Обычно явно указывается модификатор public, но так как по определению метод должен быть вызван извне программы, не важно, какой уровень доступа мы присвоим методу,— он будет работать, даже если пометить метод как private. Множественные методы Main Когда компилируется консольное или Windows-приложение, по умолчанию компилятор ищет метод Main, соответствующий указанной выше сигнатуре, и делает этот метод класса точкой входа программы. Если существует более одного метода Main, компилятор выдаст ошибку. Например: // MainExample.cs using Systems namespace Wrox.ProfessionalCSharp.ChapterO3 '{ class Client { • public static, infi Main(); MathExample.Main(); return Oi . • \ ■-:: ■■)•■.. ' ■class MathExample*1 " , r v :^ T-* .. , ...,. . .;;:, static- infc1 Addjint x, in$r *y) { ■ -,- f, - ' V. к»м£.. '»fa->-l :. TetUTft Jt + У;-- i *f. re- republic, static" int Mainj;:) '■-("_■«■- •.■:■-.-•' . :<- iht i »< XddE,- '.}.?); Cpnso.le.wri teLifte;(i)-; ifeturn 0; * - " У Пример содержит два класса, и оба имеют метод Main. При компиляции примера обычным способом (с использованием esc MainExample.cs) будут выданы ошибки: MainExample.cs(8,25): error CS0017: Program ■MainExample.exe' has more than one entry point defined: 'Wrox.ProfessionalCSharp.Chapter03.Client.Main(J MainExample.csB2,25): error CS0017: Program 'MainExample.exe' has more than one entry point defined: "Wrox.ProfessionalCSharp.Chapter03.MathExample.Main(J Однако можно явно указать компилятору, какой из этих двух методов использовать в качестве точки входа. Для этого нужно добавить ключ /main вместе с полным именем (включая пространство имен) класса, которому принадлежит метод Main: esc MainExample.cs /main:Wrox.ProfessionalCSharp.Chapter03 .MathExample Передача аргументов в Main До сих пор мы применяли метод Main без параметров. Однако при вызове программы можно заставить CLR передать в программу аргументы командной строки, используя со- соответствующий параметр. Этот параметр представляет собой массив строк, традицион- традиционно именуемый args (хотя С# воспримет любое имя). Этот массив можно прочитать, чтобы определить, какие аргументы были переданы в командной строке при запуске программы. Следующий пример проходит в цикле по массиву строк, передаваемому в метод Main, и выводит значение каждого параметра в окно консоли: uswig" S namespace Wrox.ProfessionalCSharp. Chapter03' i[ ■''■■-■ : ' ") ' - class, ArgsExample
90 Глава 3 public static int Main(string[] args) for (int I = 0; I < args.Length; I++) Console.writeLine(args[I]); return 0; } Этот пример можно компилировать как обычно, с использованием esc ArgsExample.cs. При запуске откомпилированного исполняемого файла ему можно передать параметры, указав их за именем программы, например: ArgsKxample /a /b /с В результате выполнения команды на экран будет выведено: If « pii Co 0- /, /1 /1. Cc i.c«nf i t, /■ SPpof r Vt iyc tiV» Ft- lit <4, SI., ■1 din i'jl.C "iB e d<H,. 1?» Uii Hi* ■ >*«*'■.... - ■ '■■ ■■'"'■^-ij* S -ЯСЮЙ flcracef» Cfti>i ftru НЛ1 rosi CM Conpilci' Ufti'ffion >»00.92i9 )U Cofp i.'HUa-'JWVi., fill right? ' ■: • $ ' ■ * ■.■».. . ■: -'. ■ ^ - ' - '" v '; * * '. и и).й.29ИИ Компиляция файлов С# Мы знаем, что с помощью csc.exe можно откомпилировать консольные приложения. А как быть с другими типами приложений? Что, если мы пожелаем сослаться на библиоте- библиотеку классов? В приложении D приводится полный список параметров esc. exe. Здесь же мы рассмотрим наиболее важные из них. Что касается первого вопроса, то можно указать тип файла, который необходимо по- получить, используя параметр /target (часто сокращаемый до /t). Он может принимать одно из следующих значений: Параметр /t:exe /t:library 't:module /t:winexe Результат на выходе Консольное приложение (по умолчанию) Библиотека классов (с манифестом) Компонент без манифеста Приложение Windows (без консольного окна) Если требуется получить не-ехе-файл (например, DLL), который будет загружаться средой времени исполнения .NET, необходимо откомпилировать его как библиотеку. Если файл С# компилируется как модуль, сборка не будет создана. Хотя модули не могут быть загружены средой времени исполнения, их можно компилировать в другой мани- манифест с помощью ключа ,' г h Imo-iu 1 е. Это полезно для включения в сборку метаданных, содержащихся в файлах Apseub] ylnlo.c з (см. главу 19). Ключ /out позволяет указать имя выходного файла, создаваемого компилятором. Если /ovL не указан, компилятор сформирует имя выходного файла на основе имени входного файла С#, добавив расширение по получаемому типу (например, . ехе для кон- консольных или Windows-приложений, .dll для библиотеки классов). Отметим, что ключи /out и /t (или /taraet) должны предшествовать имени компилируемого файла.
Основы С# 91 Если необходимо сослаться на типы в сборках, которые по умолчанию не указывают- указываются, можно использовать ключ /reference или /г совместно с путем и именем файла сборки. В следующем примере показано, как откомпилировать библиотеку классов, а за- затем сослаться на эту библиотеку в другой сборке. Применяются два файла — библиотека классов и консольное приложение, которое вызывает класс из библиотеки. Первый файл содержит исходный код для DLL. Для простоты используется лишь один класс Math с одним методом, который складывает два целочисленных значения: Ц Math.cs namespace Wrox.ProfessionalCSharp.Chapter03 { ■public .class Math // Класс помечен как public, так как // мы будем обращаться к нему из другой сборки { public int Add(int x, int у) { return x + у; -} , } } ■ Компилируем этот С#-файл в .NET DLL с помощью команды: esc /t: library Math, cs Консольное приложение создаст экземпляр этого объекта и вызовет метод Add, ото- отобразив результат в консольном окне: // -CLient.es using System; namespace .Wrox ."PfofessionalCSharp. ChapterO3 i ".. class' client '' { -' public static int Main() Vi f * Math Math = new Math () ; h: Console. WriteLine (Math. Add G, 8)); return 0; Для компиляции этого файла можно использовать ключ /г, указав откомпилирован- откомпилированную DLL: esc Client.cs /r:Math.dll После этого можно запустить полученное приложение как обычно — набрав в командной строке Client. На экране должно появиться число 15 — результат сложения. Консольный ввод/вывод Вы уже имеете базовое представление о типах данных С# и о том, как выполняется про- программа. Для того чтобы вы могли приступить к написанию программ, мы покажем некото- некоторые простые методы ввода/вывода в С#. Соответствующие главы, где рассматриваются приложения Windows, приложения ASP.NET и службы Web, познакомят вас с более бога- богатыми возможностями ввода/вывода. Класс Console Методы для чтения с консоли и записи в нее предоставляются платформой .NET, а не языком С#. Класс System. Console предлагает два метода для вывода на консоль и два ме- метода чтения с консоли. Консольный ввод Для чтения с консоли символа текста служит метод Console. Read (). Он прочтет входной поток с консоли (например, при вводе пользователем некоторого текста) и вернет следу- следующий символ в потоке в виде int. Представленный ниже код разрешает пользователю ввести строку текста и отображает первый символ этой строки:
92 Глава 3 int x ="• Cons.pl е, Read (); Console.WriteLine( <char)x); Метод Console. ReadLine () аналогичен, но возвращает всю строку текста как string: jstring *s к Console.ReadLine() ; Console.WriteLine(s); Консольный вывод Существуют два метода для вывода на консоль: метод Console. Write О , который выводит указанное значение в окно консоли, и Console.WriteLine О, который делает то же са- самое, но добавляет символ новой строки в конец выходной строки. Предлагаются различ- различные формы (перегруженные версии) этих функций для всех предопределенных типов (включая object), поэтому в большинстве случаев для отображения значений не придется предварительно преобразовывать их в строки. Метод Console .WriteLine () может отображать форматированный текст примерно так, как это делает функция printf в С. Для этого необходимо передать WriteLine () ряд параметров. Первый из них является строкой, содержащей маркеры в фигурных скобках, вместо которых будут подставлены соответствующие параметры. Каждый мар- маркер — это начинающийся с нуля индекс параметра в последующем списке. Например, {0} представляет собой первый параметр в списке, поэтому код: int i-- = 10; int j = 20; Console.Writeuine(" {0} плюс {1} равно {2}", i, j, i" + j;); отобразит на экране: 10 плюс 20 равно 3 0 Также можно указать ширину для вывода значения и выровнять текст в пределах этой ширины, используя положительные значения для выравнивания по правому краю и отрицательные — для выравнивания по левому краю. Для этого применяется формат {л, w], где л — индекс параметра, w — значение ширины: int i = .9,40; int , j =;; r?3; Coris'oie.WriteLinet" {0,4}\n+{l,4}^n —\n B,4)"', f, j, i + I')'; В результате на экран будет выведено: 940 + 73 1013 Наконец, можно добавить строку форматирования, состоящую из одного символа, с необязательным значением точности. Для форматирования применяются следующие символы: Строка Описание С Формат национальной валюты. D Десятичный формат. Преобразует целое п число по основанию 10 и добавляет недостающие нули в начале, если указано значение точности. Е Научный (экспоненциальный) формат. Спецификатор точности устанавливает число десятичных позиций (по умолчанию 6). Регистр строки форматирования ("е" или "Е") определяет регистр экспоненциального символа. F Формат с фиксированной точкой. Спецификатор точности управляет числом десятичных позиций. Можно указывать нуль. G Общий формат. Использует либо форматирование Е, либо F в зависимости от того, какое из них является более компактным. К Числовой формат. Форматирует число с использованием запятых в качестве разделителей тысяч, например 32.767.44. Р Процентный формат. X Шестнадцатеричный формат. Спецификатор точности может указываться для добавления нулей перед числом.
Основы С# 93 Отметим, что строки форматирования не чувствительны к регистру. Например, для форматирования значения decimal в качестве валюты данной мест- местности с точностью в две десятичные позиции можно использовать С2: decimal i = 940.23m; decimal j = 73.7ffi; Console.WriteLinef" {0.9:C2}\n+{l,9:C2}\n *- \n {2.9:C2}«, i, j, i+j); Результат для Соединенных Штатов будет таким: $940.23 + $73.70 $1 013.93 Наконец, для обозначения форматирования вместо строк форматирования можно использовать символы-заполнители. Например: dduble d =5 0.234; Cons6ie,WriteI,ine {," {6: #.00} ", й); В результате будет выведено .23, так как символ решетки (#) игнорируется, если в этом месте нет значения, а нуль либо заменяется символом в данной позиции, либо пе- печатается как нуль. Комментарии Последней темой этой главы является добавление в код комментариев. С# использует традиционные комментарии в стиле С для однострочных (//...) и многострочных (/* ... */) комментариев: II Это однострочный комментарий ')* „Это" многострочный ■! 'комментарий '*/- Все, что располагается в однострочном комментарии, от / / и до конца строки, игно- игнорируется компилятором. Также игнорируется все, что содержится внутри комбинации /* ... */. Можно (хотя не рекомендуется) размещать многострочные комментарии в строке кода: Console.WriteLine(/* Пожалуйста, не делайте так! */ "Это откомпилируется"),- Комментарии такого рода могут быть полезны при отладке, если, например, необходимо временно выполнить код с другим значением: DoSomething(Width, /"Height*/ 100); Однако не забудьте убрать комментарии по завершении процесса отладки! Символы комментариев, расположенные в строковых литералах, трактуются как обычные символы: string s = "/* Это обычная строка */"; Единственное, чего не стоит делать,— это включать комбинацию */ в многостроч- многострочные комментарии, так как она будет трактоваться как конец комментария. Например, строка: /* string s = "/* ... */"; */ не откомпилируется, поскольку компилятор будет считать первую последовательность */ окончанием комментария и попытается откомпилировать оставшиеся символы ("; */) как код С#. Документация XML В дополнение к комментариям С-типа, С# имеет приятную особенность — возможность генерировать документацию в формате XML автоматически, используя специальные комментарии. Это однострочные комментарии с тремя косыми черточками (///). Внутри этих комментариев можно размещать теги XML, содержащие сведения о типах и членах типов в коде. Компилятор распознает следующие теги. Синтаксис некоторых из них контролируется компилятором:
94 Глава 3 Тег <с> <code> <example> <exception> <include> <list> <param> <paramref> <permission> <remarks> <returns> <see> <seealso> <summary> <value> Описание Отмечает текст в строке как код, например <oint I = 10,-</c> Отмечает многострочный код Отмечает пример кода Документирует класс исключения (синтаксис контролируется компилятором) Включает комментарии из другого файла документации (синтаксис контролируется компилятором) Вставляет список в документацию Отмечает параметр метода (синтаксис контролируется компилятором) Отмечает, что слово является параметром метода (синтаксис контролируется компилятором) Документирует доступ к члену (синтаксис контролируется компилятором) Добавляет описание для члена Документирует тип значения, возвращаемого методом Предоставляет ссылку на другой параметр (синтаксис контролируется компилятором) Обеспечивает возможность размещения в описании секции "Смотри также..." (синтаксис контролируется компилятором) Приводит короткую сводку информации о типе или члене Описывает свойство Чтобы посмотреть, как это работает, включим в файл Math.сs (см. выше) несколько комментариев. Добавим элемент <summary> для класса и его метода Add, а также эле- элемент <returns> и два элемента <param> для метода Add: // Math.сs « namespace Wrox. ProfessionalCSharp.ChapterO3 { > /A/<summary> /// Класс Wrox.ProfessionalCSharp.Chapter03.Math. /// Реализует метод для сложения двух целых чисел. ///</summary> v public class Math ," f ///<summary> /// Метод Add позволяет складывать два целочисленных значения. ///</summary> ///<returns>Pe3ynbTaT сложения имеет тип (int)</returns^ ///<param пате="х">Первое число для сложения</рагат> ///<рагат пате="у">Второе число для сложения</рагат> public int Add(int x, int у) { return x + у; Компилятор С# может извлечь элементы XML из специальных комментариев и испо- использовать их для генерации файла XML. Чтобы компилятор сгенерировал XML- докумен- документацию для сборки, необходимо указать при компиляции ключ /doc и имя создаваемого файла: esc /t:library /doc:Math.xml math.cs
Основы С# 95 В том случае, если XML-комментарии не образуют правильный XML-документ, компилятор выдаст ошибку. В результате будет сгенерирован XML-файл с именем Math.xml, который будет иметь вид: <?xml <doc> version= ".I ..0 ",?->- ) "> <name>Math</name> </assembly> • <members> <meraber narae="T:Wrox.ProfessionalCSharp.Chapter03.Math"> <sutnmary> Класс Wrox.ProfessionalCSharp.Chapter03.Math Реализует метод для сложения двух целых чисел. </summary> : </member> <member narae= "M:Wrox.ProfessionalCSharp.Chdpter03.Math.Add(System.Int32,System.Int32 <summary> Метод Add позволяет складывать два- целочисленных значения. </summary > <returns>pe3ynbTaT сложения имеет тип (int)</returns> <param naifte="x">nepBoe число для сложения</рагат> <рагат пате="у">Второе число для сложения</рагат> </member> </members> </dbc> Компилятор выполнил за нас часть работы — он создал элемент <assembly>, а также добавил элементы <member> для каждого типа или члена типа в файле. Каждый элемент <member> имеет атрибут паше, значением которого является полное имя члена с добав- добавлением буквы, показывающей, что это тип ("Т:"), поле ("F:") или член ("М:"). XML-документация в Visual Studio.NET В этой книге мы не уделяем большого внимания функциональности VS.NET, она довольно проста. Однако в данном контексте следует сказать, что использование VS.NET экономит силы и время при компиляции XML-документации. Откройте файл Math. cs в Visual Studio.NET и добавьте пустую строку перед определе- определением метода Add. Теперь наберите три косые черты (///) в начале строки. Visual Studio тут же добавит несколько XML-тегов: ' / <зшгаагу> ' / I / / /гшяпагу; /,'. врагам natrf ":<"" /// -; param natne="y">-../par&ir> /// <c_ urns></returns> public int Add(int x, int y) { return x + y; Все, что требуется сделать,— набрать описания! Обратите внимание, что Visual Studio поставила корректные значения для имен параметров в тегах <param>. Для того чтобы сгенерировать XML-документацию при компиляции проекта, отметьте галочкой пункт XML Documentation File в меню Build на вкладке Configuration Properties на страницах свойств проекта. Visual Studio способна генерировать из XML-комментариев документацию в формате HTML. Выберите lools | Build Comment Web Pages... в меню. Будет задан вопрос, следует ли генерировать документацию для всего решения или только для выбранных проектов. Нужно будет также указать место для хранения сгенерированных HTML-страниц. Когда мы нажмем OK, Visual Studio сгенерирует целый набор HTML-страниц, документирую- документирующих проект. Их можно просматривать с помощью Visual Studio или любого другого web-браузера: : Зак. 69
96 Глава 3 Code Comment Web Report solution .( Project MAWi Wrox ТЪ. .ProCSharp ChatperO3.Math.Add %<J4 method Allow* u» to ddd two integer» Public int Add (Int. Int) int me int Hi f\r%\ number to «dd Steontf numti*r to «И fteujlt of tb« eddAql) (int) Function .-.1 Заключение В этой главе вы познакомились с основными особенностями синтаксиса С#. Были рассмотрены темы: О Система типов С#, в частности, различие между типами по ссылке и типами по значению О Преобразование данных между различными типами О Объявление и инициализация переменных в С# О Область видимости переменной и уровни доступа О Операции С# О Операторы управления ходом выполнения программы О Структура программы и компиляция а Комментарии и автодокументация XML В следующих двух главах мы более подробно изучим особенности объектно-ориенти- объектно-ориентированного программирования на С#.
Классы и наследование До сих пор мы изучали основы синтаксиса С#" как объявлять переменные, как управлять ходом выполнения программы и т.д. Однако С# является полностью объектно-ориенти- объектно-ориентированным языком. Следовательно, для получения кода, который будет не просто синтак- синтаксически корректным, но и хорошо написанным (т.е. хорошо разработанным) необходимо изучить объектно-ориентированные свойства языка, т.е. освоить объект- объектно-ориентированное программирование (ООП). ООП — общий термин для мощной методологии программирования, которая позволяет создавать легко поддерживаемые и повторно используемые фрагменты кода для выполнения сложных задач. В этой главе мы рассмотрим принципы объектно-ориентированного программирования и узнаем, как использовать С# для написания хорошо структурированного объектно-ориентиро- объектно-ориентированного кода. Таким образом, в этой главе основное внимание уделяется пониманию самой концепции ООП. ООП — чрезвычайно мощная методология. Начав пользоваться ею, вы будете все время удивляться, как же раньше удавалось обходиться без нее. Вы обнаружите, что этот способ программирования придает вашему коду интуитивную, естественную структуру, которая невозможна в процедурных языках и даже в VB6, реализующем некоторые объ- объектно-ориентированные свойства. К сожалению, это такая тема, где необходимо позна- познакомиться с целым рядом новых концепций, прежде чем вы увидите, насколько они полезны. При чтении этой главы у вас может возникнуть вопрос: а зачем все это нужно? Не беспокойтесь, вы изучаете именно то, что является важным не только для С#, но и для написания хорошего кода во многих других языках. Перед тем как перейти к наследованию, мы рассмотрим природу объекта. Наследо- Наследование позволяет повторно использовать код классов удобным способом, и оно составля- составляет сердцевину объектно-ориентированного программирования. Мы обсудим концепцию наследования и его реализацию в С#. Несколько слов для начала Если вы имеете за плечами опыт программирования на C++ или Java, разумеется, вам уже известны концепции объектно-ориентированного программирования и то, как исполь- использовать ООП и наследование для создания хороших модульных программ. Классы исполь- используются в С# точно так же, как в C++ и Java. Вы можете быстро пролистать эту главу и сконцентрироваться на приложениях В и С, где описаны отличия С# от C++ и Java. Если вы являетесь опытным разработчиком на Visual Basic, но не имеете опыта в C++ или Java, то многие из концепций, представленных в этой главе, будут для вас совершен- совершенно новыми. Visual Basic позволяет создавать то, что часто называют объектом,— модуль класса VB. Некоторые источники даже утверждают, что VB использует объектно-ориен- объектно-ориентированное программирование, хотя на самом деле здесь наблюдается очень мало сход- сходства с оригинальными концепциями ООП. Правильнее было бы говорить, что VB реализует небольшую часть основных свойств ООП. Модуль класса VB по существу явля- является компонентом СОМ, но обернутым так, что он скрывает большую часть своих дейст- действий. В частности, он не поддерживает наследование своих методов таким способом, каким наследование используется в С# и в обычном ООП.
98 Глава 4 Начав чтение этой главы, вы можете предположить, что здесь описывается то, что вы использовали в модулях классов, но на самом деле это не так. Благодаря поддержке насле- наследования, классы С# гораздо мощнее модулей классов VB и часто используются совсем по-другому. Если вы хотите создавать хорошие приложения и сборки C#/.NET, вам при- придется внимательно изучить эту главу. Объекты и наследование — это не просто новая осо- особенность языка. В хорошо проработанной объектно-ориентированной программе вся ее архитектура обычно выстраивается вокруг наследования, поэтому, вникнув в эту концеп- концепцию, вы начнете структурировать программы совершенно иначе, чем в VB, и в результате ваши программы будут лучше восприниматься другими людьми. Знакомство с объектами и классами Начнем с повтора того, что представляет собой класс в самых общих чертах. Затем рас- рассмотрим, как можно написать класс в С#. Для того чтобы показать вам, насколько уни- универсальны объекты в нашей жизни, мы приведем примеры, ие имеющие ничего общего с компьютерами, после чего применим эти концепции в программировании. Объекты в повседневной жизни В повседневной жизни объектом является все, что представляет гобой матери? льмый предмет. Это может быть машина, дом, книга, документ, платежный чек. Для талгих це- целей мы немного расширим это понятие и будем рассматривать объект как нечто, что можно представить в виде одного элемента в программе. Мы будем говорить о живых объектах (человек, работник, клиент) и о более абстрактных объектах (компания, база данных, страна). Причина такого рассмотрения объектов состоит в том, что мы хотим писать код, при- приближенный к реальной жизни в том смысле, что он содержит объекты, а также в том, что нам нужен способ разбиения большой программы на более мелкие, более управляемые модули. Идея исходит из концепции черного ящика, которую вы, возможно, изучали в школьном курсе физики. Идея черного ящика заключается в том, что в жизни существует множество объек- объектов, которыми вы можете пользоваться, не зная при этом, как они работают. Посмотри- Посмотрите, например, на свою автомагнитолу. Вам известно, что находится внутри нее? Если вы не являетесь специалистом в этой области, скорее всего — нет. Однако вы знаете, что на- нажатие определенных кнопок заставит ее проигрывать компакт-диск или кассет)' или принимать сигналы радиостанций. Кроме того, вы можете выкинуть свою магнитолу, а на ее место поставить совершенно другую, и она будет выполнять те же самые функции, хотя принципы ее внутреннего построения могут заметно отличаться. Концепция чер- черных ящиков формализуют идею о том, что существует разница между тем, "что объект делает" (вам обычно это известно), и тем, "как он это делает" (обычно не требуется знать), и что два объекта могут делать одно то же, но работать совершенно по-разному. Другим примером "черного ящика" может быть электрическая лампочка. Мои родст- родственники недавно заменили пару обычных лампочек новыми лампами дневного света, по- потребляющими меньше энергии. Традиционные лампы работают, нагревая небольшой кусочек провода с большим сопротивлением (нить накала) до такой температуры, что он начинает светиться с достаточной силой, чтобы осветить комнату. Люминесцентные лампы работают, возбуждая молекулы газа, которые испускают свет потому, что элект- электроны переходят в атомах с одного энергетического уровня на другой, и при этом лампа не нагревается выше комнатной температуры. Столь разное внутреннее строение не имеет для нас значения, так как результат один и тот же (за исключением разве что более низких счетов за электроэнергию и самодовольных комментариев со стороны домашних, что, дескать, "мы дружим с природой"). Приведу еще один пример объектов, не имеющих отношение к электронике. Недав- Недавно я купил два новых джемпера. Один из них изготовлен из мягкого шерстяного матери- материала, а другой имеет зернистую текстуру. Очевидно, что они изготовлены на разных фабриках и даже связаны по-разному, но меня это не беспокоит. Все, что мне известно,— это то, что они служат одной и той же цели — не дать мне замерзнуть! Это также означа- означает, что я могу менять их, не опасаясь, что произойти что-то плохое. В этих примерах замена одного объекта другим имеет едва заметные эффекты. Счета за электроэнергию могут уменьшиться, один материал может ощущаться иначе, чем дру- другой, но то, что делают объекты, не изменяется. Например, объект освещает комнату или не дает человеку замерзнуть. Другим важным моментом является то, что остается неиз- неизменным интерфейс с пользователем: я вставляю люминесцентную лампу в патрон точно
Классы и наследование ?? так же, как обычную лампу с нитью накаливания. Джемперы могут быть изготовлены на совершенно разных фабриках, но надеваю я их одинаково (просовываю голову, затем ищу рукава, просовываю руки в рукава и т.д.). Если вам ясны эти примеры, то вам понятно и объектно-ориентированное програм- программирование, поскольку в ООП применяются те же самые концепции. Если в жизни мы используем для работы вещи, которые имеют хорошо продуманный интерфейс, и мы знаем, что они делают и как их применять, но нас не беспокоит то, как они работают, то почему бы ни прибегнуть к такому же подходу в программировании? Другими словами, нужно разбить программу на большое число модулей, каждый из которых предназначен для выполнения в программе одной четко очерченной задачи. Это, собственно, то, что представляет собой объект (или класс). Такой подход к программированию дает несколько преимуществ. Прежде всего упро- упрощается разработка программ. Архитектура программ становится более понятной, так как фактически программа является абстракцией реального мира. Большому количеству разработчиков легче трудиться вместе. Каждый из них работает над отдельным объек- объектом кода, и единственное, что ему требуется знать,— это то, что делают другие объекты кода, не беспокоясь о том, как работает сам код. Объекты в программировании Мы выяснили, что такое объект, и привели примеры из повседневной жизни. Теперь по- посмотрим, как приложить эти концепции к программированию. Если вам приходилось программировать в Windows, то наверняка вы уже применяли объекты в своих программах. Вспомните о различных элементах управления, которые мож- можно разместить в окне: текстовые поля, поля списков, просмотр списков и т.п. Microsoft со- создала эти элементы управления для вас, и вам не требуется знать, как, скажем, текстовое поле работает внутри. Известно, что оно способно выполнять определенные действия: для него можно установить свойство Text, и новый текст немедленно появится на экране, или можно изменить свойство Width, и при этом изменится ширина текстового поля на экране. В предыдущей главе объекты использовались при программировании на С#. Например, метод Console.WriteLine () служит для вывода на экран: Console.WriteLinel "Здравствуй, мир!\п") ,- Console является объектом, a WriteLine — методом этого объекта. Все примеры предыдущей главы включают в себя определение класса, обычно называемого Classl. Он содержит точку входа программы: pubSL'c class Classl { public static int Main(string[] args) { При программировании необходимо различать класс и объект. Класс является об- общим определением того, чем является объект, — можно сказать, его шаблоном. Напри- Например, в повседневной жизни классом мог бы быть "дом" — абстрактная идея дома. С другой стороны, мой дом — это объект, ваш дом — другой объект, но оба они принадле- принадлежат к одному классу "дом". Однако в предыдущей главе класс применялся только для размещения в нем главной точки входа программы с целью удовлетворения требования компилятора в том, что все методы должны являться частью класса. Этот класс не использовался для истинного объ- объектно-ориентированного программирования. В настоящей главе мы определим другие классы в коде и покажем, как создавать программы таким образом, чтобы задействовать объектно-ориентированные особенности. Члены класса В примерах из повседневной жизни постоянно подчеркивалось, что у объекта есть две сто- стороны: "что он делает" (обычно широко известно) и "как он это делает" (обычно скрыто). Не требуется знать принцип работы объекта, чтобы его использовать. В программирова- программировании сохраняются эти две концепции. То, "что он делает", обычно представляется метода- методами объекта, которые можно вызвать. Метод — это название функции в С#. То, "как он работает", представляется методами и данными, которые хранятся в объекте, т.е. любыми переменными. В Java и C++ эти данные называются переменными-членами, а в VB они представляются любыми переменными на уровне модуля в модуле классов. В терминоло- терминологии С# это поля. В целом, класс полностью определяется своими полями и методами.
100 Глава 4 Существуют и другие составляющие класса в С#: индексаторы, свойства, конструкторы, операции, делегаты и события. По большей части они схожи с методами, но для них используется другой синтаксис, упрощающий применение класса. Можно делать доступными извне поля как часть того, "что делает класс", но по причинам, которые мы рассмотрим позже, это считается плохой практикой программирования. Мы используем термин "член" для обозначения всего, что является частью класса, будь то поле, метод или любой из вышеперечисленных объектов, который может быть объявлен внутри класса. Пример: класс Authenticate* Проще всего понять, как создается класс, на примере. Создадим простой класс по имени Authenticator. Предположим, что мы разрабатываем большое приложение, которое требует, чтобы в определенное время пользователь зарегистрировался (вошел) в про- программе, указав пароль. Класс Authenticator будет заниматься этим аспектом програм- программы. Разумеется, нам не нужно беспокоиться об остальной части приложения — мы сконцентрируемся на построении этого класса. Напишем также небольшой тестовый код, который проверит, что класс Authenticator работает должным образом. Authenticator позволяет выполнить две вещи: установить новый пароль и прове- проверить, что пароль правильный. Код С#, необходимый для определения класса, выглядит следующим образом: public class! Authenticatof-,i . X .private string Password; pu$ft.*c bool IsPasswordCqrrfect(string password} t *'■■:*■'•-■. . ■ ■ ' 'r' ■■-..• reeiirK '^password == Password) 7 true ; false; * } I. » ■ О :■ ,i i Л gjublic bool ..ChangePassword (string oldPassword, string NewPasswprd) if (oldPassword --< Password) » { Password = newPassword; return true,- }' else - return false; По используемому здесь синтаксису вы можете понять, что происходит. Ключевое сло- слово class в С# показывает, что мы собираемся определить новый класс (тип объекта). Сло- Слово, следующее за class, является именем, которое мы хотим дать классу. Затем в фигурных скобках идет собственно определение объекта. Обычно оно состоит из некоторого числа переменных (полей) и методов, в нашем случае — из одного поля и двух методов. Повсеместность классов иллюстрируется тем фактом, что в данном примере мы используем другой класс: string. Класс string представляет собой строку Unicode-символов и определяется как один из базовых классов, созданных для нас Microsoft. Следовательно, его можно свободно применять в других частях кода. Единственное поле в Authenticator, Password, хранит текущий пароль. Оно отме- отмечено ключевым словом private. Это означает, что поле невидимо за пределами клас- класса — компилятор и платформа .NET не позволят никакому коду получить доступ к этому полю, кроме кода, который сам является частью класса Authenticator. Определение поля или метода как private гарантирует, что это поле или метод будет частью внутрен- внутренней структуры класса, в отличие от внешнего интерфейса. Преимущество здесь в том, что если вы решите измеиить внутреннюю логику (возможно, вы захотите использовать для хранения Password не строку, а некоторый другой, более специализированный тип данных), то сможете сделать это, будучи уверены в том, что изменение не испортит и не затронет никакой другой код вне определения класса Authenticator. Это происходит потому, что никакой иной код не может получить доступ к этому полю данных.
Классы и наследование 101 Отметим, что в данном примере мы воспользовались чувствительностью С# к регистру символа, применив Password в качестве имени поля-члена и password в качестве имени параметра метода isPasswordCorrect <). Любой код, использующий класс Authenticator, может получить доступ только к методам, отмеченным ключевым словом public, т.е. в нашем случае к методам IsPas- IsPasswordCorrect () и ChangePassword (). Оба метода реализованы таким образом, что ни- ничего не происходит (кроме возврата true или false), если код не передает корректный текущий пароль — это и обязано делать программное обеспечение, реализующее безо- безопасность. Обе функции имеют доступ к полю Password, что допустимо, поскольку этот код является частью самого класса Authenticator. Отметим, что эти открытые функ- функции одновременно предоставляют интерфейс для внешнего мира (другими словами, для остального кода, использующего класс Authenticator) и определяют, какие действия класса Authenticator видны внешнему миру. Private и public не единственные модификаторы, которые доступны для определения того, что может знать код о существовании члена. Ниже в этой главе мы используем protected, который делает члена доступным в данном классе и в некоторых связанных классах, а в главе 5 познакомимся с internal и protected internal, запрещающими доступ со стороны другого кода внутри одной сборки. Итак, мы объявили класс Authenticator. Как можно использовать его в коде? Что- Чтобы понять это, предположим, что класс является новым типом переменной. Вы уже при- применяли предопределенные типы переменных. В С# это int, float, double и т.п. Определяя класс Authenticator, мы говорим компилятору, что существует новый тип переменной, называемый Authenticator. Под этим подразумевается, что мы создаем новый, более сложный тип переменной, которая состоит из соединенных вместе других переменных (полей класса), и говорим компилятору, что может делать этот новый тип переменной (методы). Определение класса содержит все. что требуется знать компилятору для обработки этого типа переменной. Например, компилятор знает, что double содержит число с пла- плавающей точкой, хранящееся в определенном формате, и с этим числом можно выполнять такие действия, как сложение, и точно так же ему теперь известно, что переменная типа Authenticator содержит string и позволяет вызывать методы IsPasswordCorrect () и ChangePassword(). Мы определили класс как новый тип переменной, однако более общим термином является тип данных или просто тип. Если Authenticator является еще одним типом переменной, то его можно исполь- использовать в коде следующим образом: Authenticator Simon .■= new Authenticator();; . botA Done; Done; = Sim6n.ChangePassword{"", "MyNewPassword"); '.-.'" if. {Done =■= true) * Console.WriteLineCPassword for Simon changed"); else Console.WriteL'ine("Failed to change password for Simon"); Done == Simon-CharigePasswordl"", "AnotherPassword"); : if (Done ==' true) Console,WriteLine("Password for Simon changed"); else . ^ ■'• fonspJje:,ittitel.ine'("Failed to change password "for Simon") ; if (Sinion.isPassw6rdCbrrect("WhatPasswbrd")) • Console.WriteLine.t "Verified SimonVs password"); else, _ i •>-.. Console.WriteLine("Failed to verify SimonV's password"); Другими словами, мы определяем переменную типа Authenticator и используем ее подобно другим переменным, указывая точку (.), сообщающую о том, что вызывается ме- метод класса. Этот код демонстрирует различие между классом и объектом. Authenticator являет- является классом, который мы определили ранее. Объявляя переменную Simon, мы создаем объект — экземпляр класса Authenticator.
102 Глава 4 В большинстве случаев необходимо создавать экземпляр класса посредством создания объекта типа этого класса прежде, чем его можно будет использовать. Часть строки "= new Authenticates () ", в которой мы определяем переменную, предназначена для выделения памяти под Authenticates (см. ниже). В С# переменная, типом которой является определенный вами класс, на самом деле содержит только ссылку на то место, где в памяти хранятся данные для этой переменной. Память сама по себе должна распределяться отдельно, что и делается с помощью ключевого слова new. Программисты на C++ могут сравнить это с хранением указателя на объект. Для программистов на Java это то же самое, что и ссылка Java. Программисты на VB могут сопоставить это с хранением ссылки на объекты ActiveX. В данном контексте ключевое слово new в С# делает практически то же самое, что и в C++, а это в свою очередь похоже на то, что делает new в VB6. Как упоминалось выше, вызов методов класса осуществляется с помощью символа точки (.), добавляемого к имени переменной. Эта нотация знакома программистам на C++, Java и VB. Отметим, однако, что нельзя делать так: string SimonsPassword = Simon.Password; Этот код вызовет ошибку на этапе компиляции по причине того, что поле Password явно отмечено как private, поэтому другой код не может получить доступ к нему. Нель- Нельзя делать и так: Authenticator Simon = new AuthenticatorO; Authenticator Karli' = new AuthenticatorO; Authenticator Result = Simon * Karli; //■ Неверно! Первые две строки кода будут восприняты нормально, но третья строка вызовет ошиб- ошибку компиляции, поскольку компилятор не знает, как складывать объекты Authenticator. Компилятору не известно, что делать с таким выражением. Это не слишком важно для Authenticator, так как для этого класса сложение не имеет какого-либо видимого смыс- смысла. Однако в коде вы можете определить классы, для которых операции сложения и умно- умножения будут уместны (например, можно написать класс, представляющий денежную сумму или математическую матрицу). В следующей главе мы рассмотрим перегрузку опе- операций и покажем, как можно сообщить компилятору, что означают математические дей- действия для класса, который был определен, так что можно будет написать код, который складывает, вычитает, умножает, делит или выполняет другие операции с классами. Для завершения примера приведем код программы, использующей класс Authenti- Authenticator, и посмотрим, как все это действует. Код можно загрузить с web-сайта Wrox Press (www.wrox.com): using System,- namespace Wrox.ProfessionalCSharp.Chapter4.Authenticator { class ProgramEntryPoint { static void Main(string[] args) { Authenticator Simon = new AuthenticatorO; bool Done ; Done = Simon.ChangePassword("", "MyNewPassword"); if (Done == true) Console.WriteLineС Password for Simon changed"); else Console.WriteLine("Failed to change password for Simon"); Done = Simon.ChangePassword("", "AnotherPassword"); if (Done == true) Console.WriteLine("Password for Simon changed"); else Console.WriteLine("Failed to change password for Simon"); if (Simon.IsPasswordCorrect("WhatPassword")) Console.WriteLine("Verified Simon\■s password "); else Console.WriteLine("Failed to verify Simon\'s password ");
Классы и наследование 103 public class Authenticates { private string Password; public bool IsPasswordCorrect(string password) ( return (password -- Password) ? true : false; } public bool ChangePassword(string oldPassword, string NewPassword) { if (oldPassword == Password) { Password = newPassword; return true; } else return false; Выполнение этой программы дает результат, который, если проследить по логике программы, соотносится с ожидаемым при условии, что Simon имеет в качестве перво- первоначального пароля пустую строку символов: C:\WINNT\System32\cmd.eHe jC: \>Й ut lien t icator jFailed to change password For Simon -Failed to change password fo»4 Simon Failed to verify Sinon's password C:\>_ ь Отметим несколько моментов. Во-первых, в этом примере нет ничего нового, все это уже рассматривалось нами ранее. Все ключевые слова использовались в примерах предыду- предыдущей главы, а программисты на VB увидят практически то же самое, что происходит в моду- модулях классов VB. Мы привели этот код, чтобы убедиться, что вам понятна концепция классов. Во-вторых, в примере класс Authenticator используется внутри одного исходного файла. Часто требуется писать классы, применяемые в других проектах, над которыми работаете вы или ваши коллеги. В этом случае необходимо писать класс точно так же, как в приведенном примере, но компилировать его в библиотеку (см. главу 10). В^ретьих, в коде существует второй класс, ProgramEntryPoint, который содержит основную точку входа программы. Это такой же класс, как и Authenticator, он может иметь свои собственные члены (т.е. свои собственные поля, методы и т.д.). Однако мы используем этот класс исключительно в качестве контейнера для хранения точки входа программы — функции Main (), а не для того, чтобы помещать в него те или иные члены. Это означает, что класс Authenticator является законченным и может использоваться в других программах, если потребуется (путем копирования кода либо компиляции его в отдельную библиотеку), т.е. мы продемонстрировали объектно-ориентированное про- программирование. В данном случае ProgramEntryPoint применяется только по причине синтаксического требования С#, заключающегося в том, что даже точка входа программы должна быть определена в классе, а не являться независимой функцией. Реализация классов Рассмотрим некоторые аспекты работы и внутренней реализации классов, что позволит использовать их эффективнее. В процессе этого расширим класс Authenticator. Экземпляр и статические поля В данном разделе мы выясним, как поля связываются с классами и какими способами это может быть сделано. Прежде всего обсудим терминологию. Рассмотрим код:
104 Глава 4 aoubie;'Height'= 2oo.fl., Depth = 4og,o,- Здесь объявлены две переменные типа double, т.е. double — это тип данных, a Height и Depth — экземпляры double. Или если, например, записать: ■ new. AuthenticatorO; new AuthenticatorO ; Authentipator l Authenticator^ Karli то Julian и Karli будут являться двумя различными экземплярами класса Authentica- tor. Идея ясна? В обоих случаях мы объявляем переменные и инициализируем их, хотя синтаксис double отличается от синтаксиса наших классов, где используется ключевое слово new. Важно понимать, что каждый экземпляр класса (каждый объект) имеет свой собст- собственный набор полей, которые определены в классе. Например: Karli„ChangePasswprd("Q^dKarliPassword", "NewKariiPassword"); Julian,ChangePasswordt"OldJulianPassword", "NewJulianPassword"); Каждая из переменных, Karli и Julian, содержит свою собственную строку Pas- Password. Изменение пароля для Karli не окажет воздействия на пароль Julian, и наобо- наоборот (если только две ссылки не указывают на один и тот же адрес в памяти, с чем мы познакомимся позже). Ситуация выглядит примерно так: Это решающий момент в понимании того, что такое класс, нередко сбивающий с толку программистов, пытающихся освоить объектно-ориентированное программирование: каждый раз, когда вы объявляете переменную, чьим типом является некоторый класс, эта переменная по умолчанию получает свою собственную копию всех полей класса. В ряде случаев требуется совсем другое поведение. Предположим, что в классе Authenticator мы хотим определить минимальную длину, которую должны иметь все пароли, т.е. все экземпляры класса Authenticator. Нам не требуется, чтобы каждый па- пароль имел свою собственную минимальную длину. Поэтому минимальная длина должна храниться в памяти только в одном месте независимо от того, как много экземпляров Authenticator будет создано. Для того чтобы показать, что поле должно сохраняться один раз независимо от того, как много экземпляров класса будет создано, перед объявлением поля в коде размещается ключевое слово static: public class Authenticator й. private 'static tiint mlnPasswordljehgth = 6; private string Password; Хранение копии minPasswordLength для каждого экземпляра Authenticator не толь- только привело бы к потере памяти, но и вызвало бы проблемы в том случае, если бы потребо- потребовалось изменить его значение! Объявляя поле как static, мы можем быть уверены в том, что будет существовать всего одна копия этого поля. Отметим, что в данном фрагменте кода также устанавливается начальное значение. Поля, объявленные с ключевым сло- словом static, называют статическими полями или статическими данными, в то время как поля без static именуют полями экземпляра или данными экземпляра. Поле экзем- экземпляра можно считать принадлежащим объекту, а статическое поле — принадлежащим классу.
Классы и наследование : 405 Разработчикам на VB не следует путать эти поля со статическими переменными в VB. Статические переменные VB - это нечто совсем иное. Они сохраняются в промежутках между вызовами метода, но не видны извне метода. Соответствующей концепции в С# не существует - в этом нет необходимости, поскольку можно определить поля класса. В С# static означает не то же самое, что в VB Г Поле, объявленное как static, существует, пока программа выполняется, начиная с того момента, как был загружен соответствующий модуль или сборка, содержащая определе- определение класса. Это происходит, когда код пытается использовать что-либо из сборки, поэ- поэтому можно гарантировать, что статическая переменная всегда будет существовать, если к ней необходимо обратиться. Это не зависит от того, были ли объявлены какие-нибудь переменные данного класса. Напротив, поля экземпляра существуют только тогда, когда в зоне видимости находятся переменные этого класса — один набор полей экземпляра для каждой переменной. Как только что было показано, можно рассматривать поля эк- экземпляра как часть каждой отдельной переменной (объекта), тогда как статические данные являются частью определения класса как целого. В некотором смысле статические поля выполняют те же функции, что и глобальные переменные в старых процедурных языках, таких как С и FORTRAN. Приобретя достаточный опыт в объектно-ориентированном программировании, вы обнаружите, что используете статические поля в тех же случаях, что и глобальные переменные в других языках. VB не имеет эквивалента статических полей С#. Ключевое слово static не зависит от модификатора, доступа члена, для которого оно указано. Член класса может быть public static или private static. Аналогично полям, метод, не объявленный как static, например ChangePas sword (), по умолчанию вызывается для конкретного объекта (экземпляра класса), т.е. перед точ- точкой (.) указывается имя переменной. Этот метод имеет явный доступ ко всем полям (а также методам, индексаторам и т.п.) данного объекта. Методы экземпляра и статические методы Аналогично полям, можно объявить методы как статические при условии, что они не будут пытаться получить доступ к данным экземпляра или его методам. Например, реализуем метод, с помощью которого пользователь может узнать минимальную длину пароля: public class Authenticator { private static uint minPasswordLength = 6; public static uint GetMinPasswordLengthO "' { return minPa'sswordLerigth; ■ "} "■ Этот код доступен на web-сайте Wrox Press как пример под именем Authenticated. Отметим, что хотя этот пример является корректным с точки зрения С#, при рассмотрении свойств мы приведем более правильный способ определения этого метода. Если метод не объявлен статическим явно, то это метод экземпляра, и он связан с конк- конкретной переменной, т.е. с конкретным экземпляром класса, точно так же, как и поля экземпляра. Доступ к статическим методам и методам экземпляра Тот факт, что статические методы и поля связаны с классом, а не с объектом, отражается и в способе доступа. Перед точкой указывается имя класса, а не переменной, например: Consofe.WriteLinetAuthenticator.GetMinPasswordLengthO ); // Верно Console,WrlteLine (Julian.GetMi'nPasswordLength ()) ; // Неверно Напротив, bool bCorrect = Authenticator.IsPasswordCorrect("Hi"); // Неверно fcool, bCofrect *- Julian.IsPasswordCorrectCHi"); // Верно
106 Глава 4 тс-: Правило: всегда вызывайте статические методы, используя имя класса, а методы экземпляра, указывая имя экземпляра класса. Отметим, что здесь синтаксис С# отличается от синтаксиса C++ и Java: эти языки по- позволят также вызвать статические члены путем указания имени объекта. С# не разреша- разрешает использовать такой синтаксис. Разумеется, если доступ к методам или полям класса осуществляется изнутри класса, можно указывать имя члена напрямую. '/-/ Этот код находится ~в -методе strung Passwd '= Password; int MinLeri = rninPasswordLength; вйутри класса. Authenticator /7 поле экземпляра данного объекта // статическое поле, данного класса Реализация методов экземпляров и статических методов Ранее было сказано, что каждый объект сохраняет свою собственную копию полей эк- экземпляра данного класса. Однако это неверно в случае методов. Если бы каждый объект имел свою собственную копию кода метода, это было бы огромной тратой памяти, поско- поскольку код метода остается одним и тем же для каждого экземпляра объекта. Методы экземп- экземпляра, так же как и статические методы, хранятся в памяти только в одном месте и связаны с классом в целом. Позже мы изучим другие типы членов классов (конструкторы, свойства и т.п.), которые содержат код, а не данные, и поэтому к ним относится то же самое. Полная картина выглядит следующим образом: Класс Authenticates Статические поля rninPasswordLength Все методы ChangePassword() IsPasswordCorrectf) Экземпляр Karli Поля экземпляра Password Экземпляр Julian Поля экземпляра Password Если методы экземпляра хранятся только в одном месте, то как метод получает доступ к правильной копии каждого поля? Другими словами, когда мы записываем: Karli. ChangePassword ("OldkarliPassword"., "NewKariiPassword"); Julian.ChangePassword("OldJulianPassword", "NewJulianPassword"); как компилятору удается сгенерировать код, который в первом случае обращается к па- паролю Karli, а во втором — к паролю Julian? Ответ заключается в том, чтр методы экземп- экземпляра на самом деле принимают дополнительный неявный параметр, который является ссылкой на место в памяти, где хранится соответствующий экземпляр класса. Этот пара- параметр никогда не требуется указывать явно, однако он всегда присутствует. Можно даже считать, что приведенный выше код — это лишь версия, удобная для пользователя, и она записывается именно так в силу того, что таков синтаксис С#. На самом же деле в отком- откомпилированном коде имеет место следующее: // На самом деле в откомпилированном коде ChangePassword(Karli, "OldKarliPassword", "NewKarliPassword"); ChangePassword (Julian, "OldJulianPassword", "NewJulianPassword"); Объявление метода как static делает его вызов более эффективным, поскольку ему не будет передаваться этот дополнительный параметр. С другой стороны, если ме- метод объявлен статическим, то при попытке получения доступа к любым данным экзем- экземпляра компилятор выдаст ошибку по той простой причине, что доступ к этим данным невозможен из-за отсутствия адреса экземпляра класса! Нельзя получить доступ к полям экземпляра из метода, объявленного как static. Это означает, что в примере Authenticator нельзя объявить ChangePassword О или IsPasswordCorrect () как static, так как оба метода осуществляют доступ к полю Password, которое не является статическим.
Классы и наследование 107 Хотя скрытый параметр, сопровождающий методы экземпляра, никогда не объяв- объявляется явно, на самом деле в коде можно получить к нему доступ. Это делается с помо- помощью ключевого слова this. В качестве примера можно было бы переписать код метода ChangePassword следующим образом: public bool ChangePassword(string oldPassword, string newPassword) ({ /if. {oldPasswprd '== this.Password) { . this.Password = newPassword; return true; } else return false; } Вообще, не следует писать такой код — все, чего мы добились, это сделали метод более длинным и трудным для чтения. Компилятор автоматически интерпретирует все ссылки на поля класса, не отмеченные именем переменной, как this.<none>. Ссылка this нужна для того, чтобы можно было явно указывать ее в тех случаях, когда это действительно необходимо (см. ниже). Ссылка this похожа на ключевое слово this в C++ и Java и на ключевое слово Me в VB. Единственное отличие состоит в том, что в C++ this представляет собой указатель, а не ссылку. Другим важным моментом, который необходимо отметить, является то, что приведен- приведенные выше примеры относятся к случаю, когда метод осуществляет доступ к полям и дру- другим членам класса. Если метод — неважно какой, статический или метод экземпляра,— определяет свои собственные локальные переменные или принимает параметры, то они ассоциируются с методом и потому сохраняются отдельно каждый раз, когда вызывается метод. Типы по ссылке и по значению В С# важно понимать различие между типами данных по ссылке и типами данных по значению. Когда объявляется переменная любого из основных предопределенных типов, та- таких как int, uint, double, float, bool и т.д., создается экземпляр типа по значению, т.е. каждая переменная содержит свою собственную копию данных. Например: int I = 1:0; int; J., К; J = I; К = -10; После запуска этой программы будут существовать три переменные: I, J и К, каждая из которых содержит число 10. И хотя все переменные имеют одинаковое значение, оно хранится в памяти три раза. Если добавить: «W; то J будет увеличена до 11. Ни I, ни К не изменятся — их значения по- прежнему будут равны 10. Для вас это, вероятно, настолько очевидно, что вы удивляетесь, зачем вообще этот во- вопрос рассматривается в книге профессионального уровня. Дело в том, что когда вы созда- создаете переменную типа, определяемого классом, который вы сами создали (или типа string, а также любого другого типа базовых классов), то происходит не то же самое, что с простыми типами. Классы являются типами по ссылке, и это означает, что каждая пе- переменная содержит лишь ссылку на то место в памяти, где хранится экземпляр класса, а не сами данные, представляющие собой экземпляр класса. Как уже упоминалось, это одна из причин, по которой необходимо использовать оператор new при создании экземпляра класса. Atithenticator Userl; // На самом деле не создаёт Authenticator, ■;■< // а только объявляет; что переменная Userl может .. У /ссылаться на Authenticator Authenticator User2 ■= new Authenticator() : // Создает экземпляр Authenticator одновременно с i : j II объявлением переменной, которая на него ссылается
108 Глава 4 Тот факт, что классы являются типами по ссылке, может иметь некоторые неожи- неожиданные результаты при сравнении экземпляров классов на равенство и при присвоении одного экземпляра класса другому. Например: Authenticator Userl; Authenticator User2 = new Authenticator(); Authenticator' USer3 = new Authenticator(); Userl-= Us%r,2; DEer2.SetPassword<"", "Tardis"); // Устанавливается пароль и для Userl! User3,SetPasswordr'% "Tardis") ; j.f (User2 ==" User3) 4. // Содержимое, .этого блока if не выполнится даже в том случае, // «ели объекты, на которые ссылаются User2 и User3, будут идентичны, // так как переменные 'ссылаются на разные объекты } it {Oseri ~~ Userl) {.. % It Код, расположенный здесь, будет выполнен, так как Userl и User2 /'/ ссылаются на' один и тот же участок памяти В этом коде мы объявили три переменные типа Authenticator: Userl, User2 и User3. Однако мы создали экземпляры только двух объектов класса Authenticator, так как только два раза использовали оператор new. Затем мы присвоили переменной userl переменную User2. В отличие от типов по значению, содержимое User2 не будет скопи- скопировано. Напротив, Userl теперь ссылается на тот же участок памяти, что и User2. Это означает, что любые изменения, произведенные для User2, затронут и Userl, поскольку это не отдельные объекты — обе переменные ссылаются на одни и те же данные. (Мож- (Можно также сказать, что они указывают на одни и те же данные. Такие данные иногда на- называют объектом ссылки.) Таким образом, установив пароль User2 в Tardis, мы неявно устанавливаем пароль Userl в Tardis. Это поведение заметно отличается от того, как вели бы себя переменные по значению. Ситуация становится еще менее понятной, когда мы пытаемся сравнить User2 и User3: if (User2 == User3) Можно предположить, что это условие вернет true, так как User2 и User3 имеют одинаковый пароль, поэтому оба экземпляра содержат одинаковые данные. Однако опе- операция сравнения для типов по ссылке не производит сравнение их содержимого — он проверяет, указывают ли ссылки на одно и то же место в памяти. Так как они не указыва- указывают на один адрес, проверка вернет false, следовательно, все, что расположено в блоке if, выполнено не будет. Напротив, сравнение Userl и User2 вернет true, поскольку эти переменные указывают на один и тот же адрес в памяти. Позже при рассмотрении перегрузки операций мы увидим, что можно изменить поведение операции сравнения (==) для данного класса таким образом, чтобы он сравнивал содержимое, а не ссылки. Необходимо отметить, что для такого важного класса, как String, Microsoft уже сделала это. Сравнение двух строк с помощью == всегда будет сравнивать их содержимое (любое другое поведение строк было бы весьма странным!)- Помните, что так ведут себя только типы по ссылке, T.t. классы, но не предопреде- предопределенные типы по значению. Если вам нужно будет копировать или сравнивать содержи- содержимое экземпляров классов, то придется определить методы или операции в классе, чтобы позволить клиентскому коду выполнить эти действия (см. ниже). Однако имейте в виду, что многие классы содержат большое число полей-членов, поэтому копирование или сравнение содержимого экземпляра класса часто означает обработку большого числа данных. Копирование и сравнение ссылок обычно является более предпочтительным: это значительно улучшает производительность, так как ссылка всегда представляет со- собой одно 32-разрядное число (или 64-разрядное на 64-разрядных машинах), содержащее адрес.
Классы и наследование 109 Свойства Мы показали вам, как пользоваться методами и полями. Существует несколько других ти- типов членов класса: конструкторы, индексаторы, свойства, делегаты и события. Как пра- правило, эти объекты применяются в более сложных ситуациях и не важны для понимания принципов объектно-ориентированного проектирования. Они рассматриваются в гла- главах 5 и 6. Здесь же мы поговорим о свойствах. Они нашли широкое применение и способ- способны значительно упростить внешний интерфейс пользователя, предлагаемый классом. Свойства С# в точности соответствуют свойствам модулей классов VB и используются аналогичным образом. Поэтому программисты на VB могут не читать этот раздел. Свойства предусмотрены на тот случай, когда необходимо сделать вызов метода по- похожим на поле. Вернемся к нашему классу Authenticator. Расширим класс, чтобы поль- пользователи могли читать и модифицировать минимальную длину пароля. В качестве первой попытки попробуем поступить следующим образом — мы определим два метода, SetMinPasswordLength() и GetMinPasswordLength(): ft*■ Толькр^для .дримера.< Это не лучший cfrocofe раббтй со свойствами. ■ ,% ' " ' " " v -„ " .j , 'puBlic/ static u'lnt GetMinPaeswordLengthU .return fninpasswordLength; '" „.. public; static void SetMinPasswordLength (uint value) :? {',■,",,. 3' •« ffiint'asswQrdLength = value; }..--■■■-"-- При реализации этого класса, вероятно, следовало бы установить привилегии безопасности для метода SetMinPasswordLength О, чтобы он мог быть вызван только определенными пользователями с соответствующими правами. Однако здесь мы не будем заниматься этим, вопросы безопасности рассматриваются в главе 25. При использовании данного подхода код будет выглядеть примерно так: Л Authenticator, .-SetMinPasswordLength G); :. Console.WriteLine("Минимальная длина пароля = " + -,*■■. . f- - ' Authenticator.GetMinPasswordLengthО); Здесь все в порядке, но этот код нельзя назвать дружественным по отношению к пользо- пользователю. Поскольку SetMinPasswordLength () лишь меняет значение, a GetMinPassword- GetMinPasswordLength () осуществляет доступ к числу, было бы гораздо удобнее сделать так: •'■-, Autlienti'catpr'.MinPasswordLeogth = 7;. ' ■ " ; v , Consble".'Writ%Line("Минимальная" длина, пароля ;= " * ■"•*'- .^ь">: ■ Authenticatpr^MiriPassworJiLengtn)': Как раз для этих целей и нужны свойства. Свойство — это метод, или пара методов, которые для окружающего мира выглядят так. будто это поля. Чтобы создать свойство для минимальной длины пароля, изменим код в классе Authenticator: ,Г publdlc static uint MinPasswordLength ' gefc . return \ t i Ш % -seti <*•> t : ■ B f '-. rainPasswordLength ■= value; \ 1 ' ' Свойство определяется практически так же, как поле, за исключением того, что по- после имени свойства идет блок кода, заключенный в фигурные скобки. В блоке кода могут быть два метода доступа, называемых get и set. Они известны еще как аксессор get и аксессор set. Отметим, что хотя в определении аксессора set параметр не указан явно, он все же передается ему неявно и доступен под именем value. Кроме того, аксессор get всегда возвращает тот же тип данных, какой имеет объявленное свойство (в данном случае uint).
110 Глава 4 Теперь посмотрим, что происходит. Если к свойству обращаются так, что значение свойства должно быть возвращено (обычно в правой части выражения), то неявно вызывается аксессор get, который вернет соответствующее значение. Приведем пример: pint I; I = Autheriticator.MinPasswordLength; На самом деле здесь вызывается аксессор get для MinPasswordLength. В данном слу- случае метод реализован таким образом, что он возвращает значение поля minPassword- Length. Поэтому переменной будет присвоено это значение. С другой стороны, если свойству должно быть присвоено значение (наиболее оче- очевидный пример — когда оно расположено с левой стороны оператора присваивания), неявно вызывается аксессор set: Authenticator. MinPasswordLgngtii = 7; В этом случае исполняется аксессор set, который реализован таким образом, что полю minPasswordLength будет присвоено требуемое значение. Аксессор set имеет не- неявный параметр value. Величина, передаваемая в качестве этого параметра, т.е. число с правой стороны оператора присваивания, в данном случае равна 7. При выполнении кода minPasswordLength примет значение 7. Самый простой синтаксис используется при модификации свойства в одном выраже- выражении. Предположим, что к минимальной длине пароля необходимо прибавить 2. Если minPasswordLength реализована как свойство, можно сделать это при помощи выражения: Authenticator.MinPasswordLength += 2; Если бы minPasswordLength была реализована парой методов, то соответствующее выражение было бы таким: Authenticator.SetMinPasswordLength(Authenticator.GetMinPasswordLengthО + 2); Вам решать, что легче читать! Отметим, что в данном примере свойство является статическим. На самом деле это необязательное требование. Как и методы, свойства объявляются с татическими, только если они ссылаются на статические данные. На практике большинство свойств не являются статическими и объявляются без использования ключевого слова static. Свойства экземпляров объектов рассматриваются ниже. Преимущества свойств Может возникнуть вопрос: а не проще ли сделать открытым поле minPasswordLength так, чтобы можно было напрямую осуществлять доступ к нему, и не думать ни о каких свойствах? Да, это было бы проще, но поступив таким образом, мы создадим себе массу проблем в будущем. Дело в том, что при объявлении поля открытым размывается разница между тем, что делает объект, и тем, как он работает. Поля на самом деле показывают, как работает объ- объект: они представляют данные, которые хранятся внутри объекта. Важно, что они не вид- видны внешним пользователям, так как в противном случае изменение внутренней логики класса повлияло бы на работу другого кода, использующего этот класс. Принцип со- сокрытия полей от клиентского кода известен как инкапсуляция данных. Если все это по-прежнему выглядит немного абстрактно, предположим, что при со- создании более поздней версии класса Authenticator было решено, что вся информация о паролях, включая минимальную длину пароля, должна храниться в реляционной базе данных (это может быть сделано, к примеру, по причинам масштабируемости, так как несколько серверов могут одновременно запрашивать информацию о паролях). Если бы поле minPasswordLength было открытым, осуществить это изменение было бы сложно. Гораздо проще сделать это, если использовать свойства и оставить поле minPas- minPasswordLength как private. Можно удалить поле minPasswordLength и переписать реа- реализации свойств get и set так, чтобы они обращались к реляционной базе данных. Так как внешний интерфейс класса Authenticator не изменился (т.е. сигнатуры открытых членов остались прежними), можно использовать новую версию класса Authenticator с уверенностью в том, что она не нарушит клиентский код. Другой аспект, связанный со свойствами, заключается в том, что их можно использо- использовать для ограничения величин, присваиваемых соответствующим полям, чтобы автома- автоматически исключать неверные данные. Например, мы решаем, что вряд ли понадобится
Классы и наследование ЛЛЛ пароль, минимальная длина которого будет более 10. Это правило можно реализовать, например, так: public static uint MinPasswordLength { get { return minPasswordLength; } set f if (value <= 10) minPasswordLength = value; else // Сделаем что-нибудь для того, чтобы сообщить о возникновении // ошибки. Как правило, генерируется исключение // (см. главу 6) . Console.WriteLine<"Ошибка!"); Свойства можно использовать для разрешения только чтения данных. Допустим, что код клиента не может менять минимальную длину пароля в классе Authenticator (или это может быть сделано путем вызова определенного метода с предоставлением специальной аутентификационной информации). Для этого достаточно убрать из свойства аксессор set. В результате свойство объявляется так: public static int MinPasswordLength { get { return minPasswordLength; Теперь клиентский код, который осуществляет доступ к свойству, будет работать прекрасно, но код, который попытается установить его значение, не будет компилиро- компилироваться (так как компилятор не сможет найти и вызвать аксессор set): uint I ; I = Authenticator .MinPasswordLength,- // верно Authenticator. MinPasswordLength = 8,- //не будет компилироваться ++ Authenticator.MinPasswordLength; // не будет компилироваться В принципе, точно так же можно создавать свойства только для записи, оставляя ак- аксессор set и опуская аксессор get. Один из примеров такого свойства — возможность для администраторов заново назначать пароль пользователя без необходимости предоставле- предоставления старого пароля. Однако Microsoft не рекомендует делать этого. Лучше использовать обычные методы, так как свойства только для записи могут сбить с толку разработчиков, пишущих клиентский код. Наконец, еще одним большим преимуществом использования свойств является возмож- возможность размещения в аксессорах get и set довольно сложного кода. Хорошими примерами этого являются оконные формы и элементы управления. Например, диалоговое окно инкапсулируется одним из базовых классов, созданных Microsoft,— классом Form. Каж- Каждое диалоговое окно имеет связанную с ним ширину width — число пикселей, которое оно занимает на экране. В клиентском коде это значение можно просматривать и изменять, например, так: •JI MyFortn является экземпляром класса, который представляет форму (окно) int 1 = MyForm.width; MyForm.Width = 600; Получение данного значения, вероятно, не включает в себя ничего больше, чем воз- возврат соответствующего числа. Строка, которая устанавливает ширину, равную 600, на самом деле выполняет большое количество операций, так как диалоговое окно автома- автоматически меняет свой размер на экране, чтобы соответствовать новому значению. Други- Другими словами, функция установки данного свойства внутренне выполняет большой объем работы. Это было бы невозможно, если бы Width была обычным полем.
112 Глава 4 Определение ее как свойства делает доступ к ней таким же удобным, как к полю, плюс возможность автоматического изменения размера или любой другой обработки, которая может потребоваться. То же самое относится к высоте (Height) и к другим свойствам элементов управления. Отметим, что использовать свойства следует только тогда, когда целью метода явля- является сделать что-то для клиентского кода, например, установить или получить значение. Другими словами, аксессор set должен принимать только один параметр и возвращать void, в то время как аксессор get не может принимать параметров. Например, невоз- невозможно переписать метод IsPasswordValid () в классе Authenticates как свойство, так как его типы параметров и возвращаемое значение не удовлетворяют указанным условиям. Пример Authenticator3 иллюстрирует Authenticates с установкой минимальной длины пароля. Отметим, что в этом примере минимальная длина пароля используется для проверки корректности новых паролей: public bool ChangePasswordlstring oldPassword, string newPassword) { if (oldPassword == Password && newPassword,Length >- minPasswordLength) { Password = newPassword; return true; } else return false; Наследование Теперь мы знаем, как работает типичный объект, и можем приступить к изучению клю- ключевого фактора объектно-ориентированного программирования: наследования. Для реализации наследования берется класс и создаются его более специализирован- специализированные версии. Наследование полезно в том случае, когда имеется множество объектов, ко- которые отличаются, но имеют много общих функций. Это большая тема с целым рядом подводных камней. Чтобы полностью понять ее, потребуются некоторые усилия и боль- большая практика в написании программ на С#. Однако в результате вы сможете писать объ- объектно-ориентированные программы с удобной архитектурой и выполнять практически любую программистскую задачу, стоящую перед вами. Мы будем изучать наследование точно так же, как объекты: сначала рассмотрим концепцию в контексте повседневной жизни, а затем применим ее к компьютерам и программированию. Наследование в повседневной жизни Для начала обратимся вновь к примеру с джемперами, только на этот раз я вспомню о своей старой хлопчатобумажной рубашке Общества танца Университета Ланкастера, ко- которую купил пару лет назад. (Это еще один из терминов, используемых нами, британца- британцами: хлопчатобумажная рубашка напоминает спортивную куртку, но тоньше, чем джемпер.) Я не ношу ее сейчас потому, что этот тип вещей нынче не в моде. Она похожа на джемпер, но имеет две дополнительные особенности: большой карман спереди, куда можно опустить руки, когда холодно, и капюшон. Рубашка была разработана на основе джемпера, но с добавлением нескольких особенностей. Объекты в повседневной жизни можно характеризовать в общих чертах следующим образом: не существует огромного множества абсолютно разных типов объектов, на са- самом деле они образуют семейства связанных вещей, которые различаются по своему ди- дизайну. Моя софа похожа на кресло за тем исключением, что на ней может сидеть более одного человека. Аудиодиск делает то же самое, что и аудиокассета, но с дополнитель- дополнительными возможностями прямого доступа. В следующем примере рассмотрим машины. На данный момент моя машина — 13- лет- летний Форд Эскорт с розовым пятном. В 1980-х гг. Форд продавал три вида машин, которые имели схожий дизайн: Эскорт, Орион и Фиеста. У этих машин много общего помимо того, что они являются машинами Форд. Они имеют разную форму кузова, а Фиеста ме- меньше по размеру, но внутри их двигатели и другие компоненты были построены очень похоже и часто были одинаковы. В примере с Фордом имеет место несколько иной тип наследования. Если вернуться к джемперу/рубашке, то основным моментом, унаследованным рубашкой от джемпера.
Классы и наследование 113 является интерфейс пользователя: рубашка одевается и снимается точно так же, как джем- джемпер. У них общий дизайн, но рубашка сделана из другого материала и, вероятно, сшита по-другому. Это пример наследования интерфейса, а эквивалентом в программировании были бы два класса. Sweatshirt и Jumper, обладающие методами с похожими именами, целями и сигнатурами, но с различными реализациями. Напротив, в примере с Фордом машины не только имеют схожий пользовательский интерфейс, но и работают они одинаково — внутри используются одни и те же компоненты. Это пример наследования реализаций, а эквивалентом в объектно-ориентированном программировании были бы классы (возможно, EscortCar, OrionCar и FiestaCar), которые не просто имели бы ме- методы с одинаковыми именами, а эти методы на самом деле были бы одинаковыми в том смысле, что при вызове метода исполнялся бы один и тот же код. Опытные разработчики на C++, Java или СОМ поймут, что наследование реализаций является типом наследования, который поддерживается Java/C++ и другими традиционными объектно-ориентированными языками, в то время как более ограниченное наследование интерфейсов было единственной формой наследования, поддерживаемой СОМ и объектами СОМ. С другой стороны, VB поддерживает только наследование интерфейсов при помощи ключевого слова Implements. Необходимо отметить, что эти примеры не являются точными. Можно поспорить насчет того, насколько полно внутренний дизайн джемпера присутствует в рубашке. Тем не менее, если говорить о программировании на С#, то мы рассматриваем вопрос, как определить новый класс, применяя особенности уже существующего класса. Выгоды здесь в следующем: во-первых, наследование предлагает удобный способ повторного ис- использования существующего, полностью проверенного кода в различных контекстах, экономя таким образом немало времени на программирование, во-вторых, оно может сделать программы более структурированными, обеспечивая более высокий уровень модульности классов. В следующем примере кода, предназначенном для компании сотовой связи, будет по- показано, как в Сопрограмме работает наследование реализаций. В С# наследование клас- классов всегда является наследованием реализаций. Наследование интерфейсов тоже имеет место в С#, мы рассмотрим его в главе 5. Наследование в С#: пример Mortimer Phones Пример, используемый нами для демонстрации наследования, будет связан с несуществу- несуществующей компанией сотовой связи, которую мы назовем Mortimer Phones. Мы собираемся разработать класс, который представляет собой счет пользователя за телефон и несет ответственность за вычисление суммы выплат. Это более длинный и сложный пример, чем класс Authenticates, и по мере его разработки мы увидим, что одним простым клас- классом здесь не обойтись, нам потребуется несколько связанных классов, и наследование волшебным образом решит проблему. Если вам интересно, Mortimer - это имя ворона в одной из моих любимых серий книг "Сказки ворона Арабеллы" ("The tales of Arabel's Raven"), автором которой является Джоан Айкен (Joan Aiken). Ворон принадлежит маленькой девочке Арабелле Джонс, может произносить "Никогда!" и "Карр!" и обладает неприятной особенностью поедать все, включая золотые слитки и целые лестницы. Он не ест мобильные телефоны потому, что книги были написаны до их изобретения, но я уверен, что он способен и на это. Мы хотим написать класс, который будет генерировать месячный счет для каждого клиента Mortimer Phones. Класс называется Customer, а каждый экземпляр этого класса представляет собой один пользовательский счет. Что касается открытого интерфейса, то класс будет содержать два свойства: О Name — имя клиента (чтение-запись) О Balance — количество денег на счету (только чтение) Также понадобятся два метода: О RecordPayment (), который будет указывать на то, что пользователь заплатил определенную сумму по счету. □ RecordCall (), который будет вызываться после того, как пользователь сделает телефонный звонок. Он вычисляет стоимость звонка и добавляет ее к балансу клиента.
114 Глава 4 Метод RecordCall () потенциально представляет собой довольно сложную функ- функцию, так как в реальности сначала потребовалось бы выяснить, что за тип звонка был осуществлен с данного номера, потом выбрать соответствующий тариф и, кроме того, пришлось бы поддерживать историю звонков. Не усложняя задачу, допустим, что име- имеются два типа звонков: звонки на обычные телефоны и звонки на другие мобильные те- телефоны. Оплата каждого из них производится из расчета 2 цента в минуту для обычных телефонов и 30 центов в минуту для мобильных телефонов. Метод RecordCall будет по- получать тип звонка в качестве параметра. Историю звонков поддерживать не будем. В ре- реальности нам пришлось бы дополнительно иметь дело со звонками WAP, различать звонки в разные мобильные сети, а также принимать во внимание продолжительность разговора и наличие различных тарифов для разных клиентов. Напишем код с учетом этих упрощений. Проект создан как консольное приложение, и первое, что в нем присутствует,— это список типов звонков: namespace Wrox.ProfessionalCSharp.Chapter4.MortimerPhones { using System; public enum TypeOfCall { CallToMobile, CallToLandline } Определим класс Customer: public class Customer { private string name; private decimal balance; public string Name { get { return name; } set { name = value; } } public decimal Balance { get { return balance; } } public void RecordPayment(decimal amountPaid) { balance -= amountPaid; } public void RecordCall(TypeOfCall callType, uint nMinutes) { switch (callType) \ { case TypeOfCall.CallToLandline: balance += @.02M * nMinutes); break ,- case TypeOfCall.CallToMobile: balance += @.30M * nMinutes); break; default: break; Этот код не требует пояснений. Отметим, что значения стоимости звонков 2 цента в минуту и 30 центов в минуту жестко указаны в программе. В реальности, скорее всего, они бы считывались из реляционной базы данных или из файла, который допускает изменение этих значений.
Классы и наследование 115 Теперь добавим код в метод Main () программы, который будет отображать значения счетов: риЬДаС MainEntryPoint * public, static int Main'{string[] args) {. • -; • 'Customer.: Arabel '= 'new Customer}); ■Arabel-Name = "ДгаЬе1 Jones"; Ciis£pmer Mrjohes = new Customer f) ; Mr Jones,Name = "Ben Jones"; Arabel .RecordCall(TypeOfCall.CallToLandline, 20); /A.rabel.RecordCall(TypeGfCall.CallToMobile, 5) ; „ Mrtfone's.ReqordCflKTypeOfCall.CaliToLandline, 10); Conso.le.WriteXine(" {0,-20} owes ${1:F2}", Arabel.Name, Arabel.Balance); .tqnsole.Wri'teLinet.MO,-?©} owes ${1:F2}", Mr Jones. Name, Mr Jones. Balance ) return 0; Выполнение этого кода дает следующие результаты: C:\WINNT\System32\<:ma.e С:\>Mort inerPhones fli-abel Jones Ben Jones oues 51.90 owes $0.20 Добавление наследования Пример с Mortimer Phones очень упрощен. В частности, для каждого пользователя существу- существует только один тариф. Проблема в том, что это не соответствует действительности. Не знаю, как ваша, а моя компания сотовой связи имеет миллион различных схем оплаты. Пер- Первыми идут клиенты по типу плати-по-мере-разговора, которые ничего не платят авансом, но оплачивают каждый звонок по очень высоким тарифам. Затем идут клиенты по подписке. Я зарегистрирован по тарифу, согласно которому я плачу фиксированную сумму каж- каждый месяц, за что получаю 60 бесплатных минут разговора с обычными абонентами и абонентами в той же сотовой сети, а остальные звонки оплачиваю по уменьшенным тари- тарифам. Можно использовать другую схему, в которой необходимо платить большую сумму в месяц, но при этом будет больше бесплатных минут. Существует также деловая схема, которая предлагает более высокие тарифы в выходные, но более низкие в рабочие дни. Разумеется, нельзя заранее предугадать, когда компания придумает новый тариф. Если пытаться учесть все это, используя обычный способ, то метод RecordCall () бу- будет содержать вложенные конструкции switch и выглядеть примерно следующим образом: // Допустим, что tariff — поле-член типа Tariff, который представляет собой // тариф для экземпляра Customer. Это тип-перечисление, значениями которого // являются Tariffl, Tariff2 и т.д. « public void RecordCall(TypeOfCall callType, uint nMinutes) { switch (tariff) case Tariff.Tariff1: { Switch (callType) { case TypeOfCall.CallToLandline: / / вычисляем сумму case TypeOfCall.CallToMobile: / / вычисляем сумму
116 Глава 4 // другие метки case // и т.д. } case Tariff.Tariff2: { switch (callType) { // и т.д. } Это неудовлетворительное решение. Маленькие операторы switch хороши, но огром- огромные операторы switch, содержащие большое число параметров, да еще и вложенные конструкции switch, сильно затрудняют код. Кроме того, при появлении нового тарифа придется изменить код всего метода. При этом могут быть внесены ошибки в те части кода, которые отвечают за обработку существующих тарифов. Здесь важно то, каким образом код для различных тарифов распределяется по конст- конструкциям switch. Если бы можно было четко разделить код для различных тарифов, проблема была бы решена. Это один из вопросов, которые решает наследование. Давайте разберем, что же мы делаем. Мы хотим отделить код для различных типов клиентов. Начнем с определения ново- нового класса, представляющего клиентов с новым тарифом, который мы назовем тарифом Nevermore60. Nevermore60 разработан для клиентов, которые много говорят по мобиль- мобильному телефону. По этому тарифу клиенты платят 50 центов в минуту за первые 60 минут разговора с другими абонентами сотовой сети, а после этого — 20 центов в минуту, поэто- поэтому если клиент делает большое число звонков, он будет экономить деньги по сравнению с предыдущим тарифом. Реализацию новых вычислений по оплате мы оставим на потом, а сейчас определим Nevermore60Customer следующим образом: public class Nevermore6QCustomer : Customer Класс не имеет ни методов, ни свойств. Но он определяется несколько иначе, чем предыдущие классы. После именем класса указываются двоеточие и имя более раннего класса, Customer. Это говорит компилятору, что NevermorebOCustomer является про- производным от Customer, т.е. каждый метод, поле, свойство и т.д. Customer присутству- присутствует и в Nevermore60Customer. Если использовать правильную терминологию, то каждый член Customer унаследован в Nevermore60Customer. Также говорят, что класс Nevermore60Customer является производным, а класс Customer — базовым. Иногда производные классы называют подклассами, а базовые классы — суперклассами. Так как мы еще ничего не поместили в класс Nevermore60Customer, он является точной копией определения класса Customer. Мы можем создавать экземпляры класса Nevermore60Customer и вызывать его методы точно так же, как для класса Customer. Чтобы убедиться в этом, сделаем клиента Arabel клиентом Nevermore60: public static int Main[string! ] args) { Nevermore60Customer Arabel = new Nevermore60CustomerО; Arabel.Nairn ■ "Arabel Jones"; Customer MrJones = new Customer)) ,- MrJones.Name = "Ben Jones"; Arabel.RecordCall(TypeOfCall.CallToLandline, 20) ; Arabei.RecordCal1ITypeOfCall.CallToMobile, 5) ; MrJones.RecordCall(TypeOfCall.CallToLandline, 10) ; Console.Wr-teLinet"{0,-20 I owes ${1:F2}", Arabel.Name, Arabel.Balance); Console.WriteLinel"{C,-20} owes ${1:F2}", MrJones.Name, MrJones.Balance); Console.ReadLine(); return 0; } Мы изменили всего одну строку, объявление Arabel, для того, чтобы сделать этого клиента экземпляром Nevermore60Customer. Все вызовы методов остались прежними, и код приведет к тем же результатам (см. выше). Если вы хотите убедиться в этом, восполь- воспользуйтесь примером MortimerPhones2.
Классы и наследование 117 Сама по себе копия определения класса Customer может показаться не слишком полез- полезной. Преимущество здесь состоит в том, что мы можем внести некоторые изменения или до- добавления в Nevermore60Customer, т.е. сказать компилятору: "клиент Nevermore60Customer почти такой же, как клиент Customer, но вот с такими отличиями". В частности, мы со- собираемся изменить способ, которым Nevermore60Customer вычисляет счет за каждый звонок по сотовому телефону согласно новому тарифу. Мы можем указать следующие отличия: О Можно добавить новых членов (любого типа: поля, методы, свойства и т.д.), которые не определены в базовом классе, к производному классу. О Можно заменить реализацию существующих методов или свойств и т.п., которые уже представлены в базовом классе. (Но не поля, так как они не имеют реализа- реализаций!) Это можно сделать двумя разными способами, которые дают разные резуль- результаты в плане вызова метода. Мы применим способ, при котором реализация перекрывает предыдущую реализацию метода. Итак, попытаемся решить проблему многочисленных тарифов. Мы перекроем метод RecordCall () в классе Customer новым методом RecordCall () в классе Nevermore6OCus- tomer. Как только нам потребуется добавить новый тариф, мы сможем создать новый класс, производный от Customer, с новым перекрывающим методом RecordCall (). Та- Таким образом можно добавлять код для работы со многими различными тарифами, в то же время держа новый код отдельно от уже имеющегося кода, ответственного за расчеты с использованием существующих тарифов. Изменим код для класса Nevermore60Customer, чтобы он реализовывал новый тариф. Для этого придется не только перекрыть метод RecordCall (), но и добавить новое поле, которое показывает число использованных "дорогих" минут: public class Nevermore60Cus^omer : Customer { private uint highCostMinutesUsed; public override void RecordCall(TypeOfCall callType, uint nMinutes) { switch (callType) ■{ case TypeOfCall.CallToLandline; balance- += @.02M * nMinutes); • break ,- case TypeOfCall.CallToMobile: uint HighCostMinutes, LowCostMinutes; uint HighCostMinutesToGo = (highCostMinutesUsed < 60) ? 60 - highCostMinutesUsed : 0; df (nMinutes > HighCostMinutesToGo) ( HighCostMinutes = HighCostMinutesToGo; LowCostMinutes = nMinutes - HighCostMinutes; l У else { HighCostMinutes = nMinutes; LowCostMinutes. = 0; . } j highCostMinutesUsed += HighCostMinutes; balance += @.50M * HighCostMinutes + 0..20M * LowCostMinutes); break; default: break; Алгоритм вычисления стоимости звонка в данном случае намного сложнее, хотя если проследить за логикой, то можно увидеть, что он соответствует тарифу Nevermore60. От- Отметим, что в определение метода RecordCall О добавлено ключевое слово override. Это сообщает компилятору, что данный метод перекрывает метод, представленный в ба- базовом классе. Перед компиляцией этого кода необходимо внести изменения в базовый класс: public class Customer
Глава 4 private string name; * protected decimal balance; // и т.д. public virtual void ReeordCalKTypeOfCall cailTypfe', uint ttftenutes) { switch (callType) Первое изменение касается поля balance. Прежде оно было объявлено как private, это означает, что код, находящийся за пределами класса Customer, не может получить прямой доступ к этому полю. К сожалению, и класс Nevermore60Customer, хотя и порож- порожден от Customer, не может обращаться к нему напрямую (хотя поле balance присутству- присутствует в каждом объекте Nevermore60Customer). Это не позволяет Nevermore60Customer изменять баланс при вызове его метода RecordCall (), и, таким образом, невозможно откомпилировать сам метод Nevermore60Customer .RecordCall (). Модификатор доступа protected решает проблему. Он указывает, что любой класс, являющийся производным от Customer, а также сам Customer имеет доступ к этому чле- члену. Член, однако, по-прежнему не виден коду в других классах, не являющихся производ- производными от Customer. Помечая член как protected, мы, по сути, говорим, что этот член является частью внутреннего устройства класса, но не частью его открытого интерфей- интерфейса, однако любые производные классы в некотором смысле привилегированны и имеют доступ к этому члену. Мы допускаем, что благодаря тесной связи класса и производного от него класса последнему позволено знать немного о внутреннем устройстве базового класса, по крайней мере то, что касается защищенных (protected) членов. Это, однако, спорный момент, если говорить о хорошем стиле программирования. Многие разработчики посчитали бы, что лучше объявить все поля как private, а для того чтобы производный класс мог изменять значение баланса, написать для него защищенный (protected) аксессор. В данном случае то, что поле balance является protected, а не private, не усложняет пример. Второе изменение внесено в объявление метода RecordCall () в базовом классе. До- Добавлено ключевое слово virtual. Его действие будет рассмотрено позже. Сейчас лишь скажем, что оно меняет способ, которым вызывается метод во время выполнения про- программы, что облегчает его перекрытие. С# не позволит производному классу перекрыть метод, если только этот метод не был объявлен как virtual в базовом классе. И последнее замечание. Важно понимать, что добавленное поле highCostMinutesUsed хранится только в экземплярах Nevermore60Customer. Его нет в экземплярах базового класса Customer. Базовый класс никогда не претерпевает явных изменений при введе- введении производного класса. Знание всегда передается в одну сторону: производный класс знает о своем базовом классе (за исключением членов, объявленных private), но базовый класс ничего не знает о производных классах. Это должно выполняться всегда, так как в процессе создания базового класса неизвестно, какие другие производные классы могут быть добавлены в будущем. Кроме того, было бы крайне неприятно, если бы код базового класса был нарушен при добавлении производного класса! С другой стороны, необходимо решить, какие методы могут быть перекрыты, чтобы явно отметить их как виртуальные. Это напоминает работу вслепую, так как неизвестно, ка- какие производные классы будут добавлены, но с приходом опыта объектно-ориентированно- объектно-ориентированного программирования вы обнаружите, что, как правило, нетрудно предугадать, какие методы должны быть виртуальными. Важно также отметить, что не существует предела числу производных классов, кото- которые могут быть созданы на основе базового класса. Предположим, что Mortimer Phones собирается ввести новый тариф Gold, который предлагает уменьшенную стоимость теле- телефонных звонков для сотрудников компании Mortimer Phones. Вероятно, вы реализовали бы этот класс так: public class GoldCustomer : Customer { public override void RecordCall(TypeOfCall calType, uint nMinutes) { // Реализация метода для клиента из числа персонала Похожим образом можно определить столько классов, сколько потребуется. Gold- Customer ничего не знает о Nevermore60Customer или любом другом его классе-"собрате", ему известно лишь о существовании базового класса.
Классы и наследование 119 С другой стороны, каждый производный класс может наследоваться только от одно- одного базового класса. Этот процесс обозначается термином одиночное наследование. Не- Некоторые языки позволяют создавать классы, которые имеют более одного базового класса (множественное наследование), но это не допускается в С#. Класс Object Итак, вы поняли, что такое наследование. В этом контексте необходимо сказать, что класс Customer, который мы использовали в нашем примере, сам является производным от другого класса. То же самое справедливо и для класса Authenticator. Платформа .NETопределяет класс, известный как System.Object (т.е. класс Object в пространстве имен System), и требует, чтобы все классы являлись производными от Object. В С#, если не указать, что класс унаследован от другого класса, компилятор автоматически сде- сделает класс, унаследованным от Object. Практическая значимость этого: помимо мето- методов, свойств и всего прочего, что вы определяете в своем классе, вы также имеете доступ в нем к некоторому числу public и protected методов-членов, которые определены для класса Obj ect. Доступны методы: Метод string ToStringO int GetHashTableO bool Equals(object obj) bool Equals(object objA, object objB) bool ReferenceEquals (object objA, object objB) Type GetType() Object MemberwiseClone() void Finalize() Доступ public virtual public virtual public virtual public static public sLatic public protected protected virtual Цель Возвращает строковое представление объекта. Возвращает хэш объекта для обеспечения эффективного поиска экземпляров объекта в различных таблицах. Сравнивает экземпляры объекта на равенство. Сравнивает экземпляры объекта на равенство. Проверяет, указывают ли две ссылки на один и тот же объект. Возвращает сведения о типе объекта. Создает копию объекта. Может использоваться в некоторых ситуациях для освобождения ресурсов. Эта таблица приведена здесь только для информации. Перечисленные методы и их применение подробно рассматриваются в главе 7. Здесь же мы изменим пример Morti- merPhones, чтобы использовать метод ToStringO и продемонстрировать некоторые принципы перекрытия методов. Но перед этим скажем несколько слов о методах Object: 1. Некоторые из методов объявлены как virtual, поскольку предполагается, что вы може- можете перекрыть их в своих классах. Для других же методов такая возможность даже не рассматривается, и поэтому они не объявлены как virtual. Кроме того, некоторые из методов являются защищенными (protected). Они предназначены для внутреннего использования классом и не имеют отношения к внешнему интерфейсу. 2. Существуют две различные версии метода Equals (). Они имеют одинаковое на- назначение, но позволяют вызывать метод по-разному. Особенностью С# является то, что два метода могут иметь одинаковое имя при условии, что их можно раз- различить по набору параметров. Эта особенность называется перегрузкой метода (см. главу 5). 3. Определены два типа возвращаемых значений. GetType () возвращает экземпляр класса System.Type — это еще один из многочисленных базовых классов, созданных Microsoft (см. главу 7).
120 Глава 4 4. MemberwiseClone() выглядит так, будто возвращает нечто совершенно иное — тип возвращаемого значения для него определен как object. На самом деле это класс System.Object. C# облегчает обращение к этому классу, определяя ключе- ключевое слово object, которое служит обозначением данного класса. Следовательно, вместо System.Object можно записывать просто object. Обратите внимание на разницу в регистре — это важно, поскольку С# чувствителен к регистру символов. Ключевое слово object используется в С# и тогда, когда не требуется явно указывать конкретный класс,— например, при объявлении переменных или типов параметров. Метод ToStringO Модифицируем пример MortimerPhones для перекрытия метода ToStringO. В Sys- System. Object метод ToStringO отображает имя класса, однако не это является его на- назначением: на самом деле он предоставляет клиентскому коду простой способ получения строкового представления содержимого объекта. Однако для того, чтобы добиться этого, необходимо перекрыть этот метод в собственных классах. В примере используется таюке метод GetType (). Оба метода определены и реализо- реализованы в System.Object, поэтому нам не требуется создавать собственную реализацию. Начнем с изменения кода, где применяются классы Customer и Nevermore60Customer, добавив строки, которые покажут результаты вызова методов ToString() и GetType () для экземпляров Arabel и MrJones. Код находится в примере Mort imerPhones4: public static int Main(string[] args) Nevermore60Customer Arabel = new Nevermore60Customer(); Arabel.Name = "Arabel Jones"; Customer MrJones = new Customer(); MrJones.Name = "Ben Jones"; Arabel.RecordCalj.Ciyne^fCdll.CallToLandline, 20) ; Arabel.RecordCall(TypeOfCu. 1.CallToMobile, 5) ; MrJones.RecordCall(TypeOfг : 1 .CaljToI _ dline, 10); Console.WriteLineC {0,-20' nWes $fi:F2]' Console.WriteLine("{0,-20} owes ${1:F2}' Arab :1 .Name, Arabel.Balance); MrJones.Name, MrJones.Balance); Console.WriteLinef "Arabel.ToStringO is : " + Arabel. ToStr ing ()) ; Console. WriteLine ("Arabel. GetType () is : " + Arabel. GetTypeO ); Console. WriteLine (>",\nMr Jones is : " + MrJones); •Console. WriteLine ("MrJones. ToStringO is : " + MrJones. ToString ()); Console. WriteLine ("MrJones »GetType() is : " + Mr Jones. GetTypeO ); return 0,- Заметим, что никакие изменения в классах сейчас не производились, поэтому вызы- вызываются реализации ToStringO и GetType О по умолчанию, обеспечиваемые Object. Выполнение этого кода дает следующий результат: C:\WIfWT\Sy5tem32\crr»dexu С:\>Mort inerPhones4 Arabel Jones oues $2.90 Ben Jones ones $0.20 Arabel.ToStrinsrO is : Urox.ProfessionalCSharp.Cliapter4.Mort inerPhones'J .Neuernore60Custonei* Arabel.GetTypeO is : Urox.ProfessionalCSharp.Chapter4.Morti f»erFhones3.Nevernore6eCustoner' ■ IHrJones is : Wrox.ProfessionalCSharp.Chapter4.MortinerPbones 3.Custoner ,MrJones.ToStringO is : Urox.ProfessionalCSharp.Chapter4.Мог It inerPliones3 .Custoner WrJones .GetTypeO is : Urox.Prof essionalCSharp.Chapter4.Mort inerPhones3.Customer C:\>_
Классы и наследование 121 Что же происходит? Сначала рассмотрим вызовы GetType (). GetType () делает здесь именно то, что он должен делать: предоставляет сведения о природе класса, в данном случае выдает полное имя класса, включая пространство имен, в котором он определен. Вызов ToString () привел к таким же результатам, что и вызов GetType (). На самом деле это не является назначением ToString (); этот метод должен возвращать строковое представление содержимого экземпляра класса. Однако мы еще не реализовали этот ме- метод в классах Customer и Nevermore60Customer, поэтому среда исполнения .NET берет реализацию для базового класса System.Object. Если в производном классе вызывается метод, определенный в базовом классе, но не перекрытый в производном, то выполняет- выполняется версия метода для базового класса. Microsoft реализовала System. Obj ect. ToString () так, что он возвращает имя класса — очевидно, это лучшее, что можно было придумать, так как программисты Microsoft, написавшие класс Onject, вряд ли могли что-нибудь знать о тех классах, которые мы собираемся реализовать. Возможно, вы заметили, что реализации ToString') и GetType() в System.Object каким-то образом выяснили имена производных классов, хотя ранее было сказано, что базовый класс ничего не знает о производных классах. На самом деле здесь применяется довольно хитрая технология, включающая в себя использование отражения, которое позволяет классам получать информацию о других классах (см. главу 7). Теперь перекроем метод ToStrinjOu классе Cuctc . Это несложно сделать: public class Customer { private string name; protected decimal balance; public "override «tiing ToString() I • String Result = "Customer: " + name; it, Result += ", owing: " + balance; return Result; Отметим, что используется ключевое слово overr^i e, которое указывает компиля- компилятору на то, что мы перекрываем функцию в базовом классе. Перекрывающий метод ToString () возвращает строку так же, как и версия метода 1 oStrina () для Object. Это основной принцип перекрытия методов: необходимо сохранять тип возвращаемого значения и параметры метода б?нового класса. В противном случае возникнет ошибка компиляции. Перекрывающий метод ToSt- -.r.g () дает сводку о состоянии экземпляра класса: имя клиента и сумму на счету (хотя здесь мы не побеспокоились о том, чтобы перевести денежную сумму в значение в валюте). Если теперь мы запустим приложение (пример ¥ >rt imerPhoi чС), то получим следую- следующий результат: ^:\>MortimerPhones5 .Irabel Jones ' owes $2.90 Ben Jones owes $0.20 Arabel.ToStringO is i Customer: ftrabel Jones, owing: 2.9 [ftrabel.GetTirpe О is : Wrox.Prof essionalCSharp.Chaptei*4.Morti merPhonesS.Neverroore60Custoner MrJones is : Customer:-Ben Jones, owing: 0.2 ItirJones.ToStringO is : Customer: Ben Jones, owing: 0.2 |ИгJones.GetTypeО is : Wrox-ProfessionalCSbarp.Cbapter4.Mort |itnerPhonesB .Customer GetType () по-прежнему возвращает имя класса, однако ToString () выдает более по- полезные сведения о содержимом переменных Arabel и MrJones. Отметим, что нам не при- пришлось реализовывать метод ToString () для класса Nevermore60Customer. Это вызвано
122 Глава 4 .-тем, что Nevermore60Customer является производным от класса Customer, поэтому он автоматически наследует его реализацию ToString (). Если бы в Nevermore60Customer потребовалось выполнять что-то другое, то для него пришлось бы реализовывать метод ToString () отдельно. Отметим, что System.Object.GetTypeO не был объявлен виртуальным, поэтому компилятор не позволит переопределить его так же, как метод ToString (). Реализация GetType () в System.Object уже выполняет все, что должен делать этот метод, так что у других классов нет причин для его перекрытия. Диаграммы иерархии классов При разработке классов с наследованием удобно использовать диаграмму, известную как диаграмма иерархии классов. Она иллюстрирует взаимоотношения между базовыми и производными классами в программе. Традиционно диаграммы иерархии классов стро- строят с базовым классом вверху, а от производных классов проводят стрелки, указывающие на базовые классы. Например, иерархия для Mortimer Phones из MortimerPhones3 выглядит так: Nevermore60Customer Systern.Root 4 k Customer \ | StaffCustomer Классы, с другими тарифами Последовательно располагая классы на диаграмме, говорят, что основной базовый класс, System.Object, находится на вершине иерархии классов. Перемещение вверх по иерархии означает переход от производного класса к базовому, а перемещение вниз по иерархии — переход от базового класса к производному. Отметим, что такая терминология противоречит тому, что ожидается от понятия "базовый класс". Виртуальные и невиртуальные методы В данном разделе мы рассмотрим действие ключевого слова virtual на определяемый метод. Разница между обычным и виртуальным методами заключается в вызове метода во время исполнения и является малозаметной. Однако она демонстрирует мощный способ использования наследования. Ключом к пониманию виртуальных методов является следующее: если объявить пере- переменную, которая ссылается на определенный тип, например А, то компилятор позволит этой переменной хранить ссылку на любой класс, производный от А. Для пояснения этого вернемся к примеру MortimerPhones. Прежде мы создавали экземпляры клиентов с помощью такого кода: Nevermore60Customer Arabe^ = new (); Arabel.Name = "Arabel Jones"; Customer MrJones = new Customer(); MrJones.Name = "Ben Jones"; На самом деле переменные можно было бы объявить проще: Customer Arabel = new Nevermore60Customer(); Arabel.Name = "Arabel Jones"; Customer MrJones - new Customer(); Mr Jones. Name - "Fen Junes"; Поскольку Nevermore60Customer является производным от класса Customer, ссылка на Customer может ссылаться как на Customer, так и на Nevermore60Customer, а также на экземпляр любого класса, который явно или неявно унаследован от класса Customer. Отметим, что мы всего лишь изменили объявление переменной по ссылке. Реальный объект, экземпляр которого создается с помощью ключевого слова new, по-прежнему яв- является объектом N 'V* 1 ] ebOCujton.er. Если, например, вызвать для него GetType (), то он сообщит, что объект является Nevermore60Customer.
Классы и наследование 123 Способность указывать на производные классы с помощью ссылки на базовый класс может показаться простым синтаксическим удобством, но на самом деле она очень важ- важна с точки зрения облегчения использования производных классов. Почему — можно по- понять, если подумать о том, как компании сотовой связи будут хранить различные классы, порожденные от Customer. В нашем примере имеются только два клиента, поэ- поэтому несложно объявить для каждого из них отдельные переменные, но на практике число клиентов может составлять сотни тысяч, и, скорее всего, придется обрабатывать их при помощи массива: Customer [] Customers = new Customer [NCustomers] ,- // Инициализируем массив foreach (Customer NextCustomer in Customers) { Console. WriteLineC {0,-20} owes ${1:F2}", NextCustomer .Name, NextCustomer.Balance); } Имеется массив ссылок на Customer, каждый элемент которого может указывать на любой тип клиента, не важно, какой из производных от Customer классов используется для представления этого клиента. Если бы переменные не могли хранить ссылки на про- производные классы, потребовалось бы огромное число массивов — массив для Customer, массив для Nevermore60Customer, а также отдельные массивы для всех остальных ти- типов классов. Это сделало бы подход к применению производных классов совершенно неработоспособным в реальной ситуации. Разные типы классов можно помещать в один массив, однако это создаст новую проб- проблему для компилятора. Предположим, что имеется такой фрагмент кода: Customer ACustomer; // Инициализируем ACustomer некоторым типом клиента, возможно, // на основе данных, прочитанных из файла ACustomer.RecordCa I (TypeCfCall.Ca lToLaruj.ine, 20); Компилятор видит ссылку на Customer, мы вызываем для него метод RecordCall (). Проблема в том, что ACustomer может содержать ссылку на экземпляр Customer, а также на экземпляр Nevermore60Customer или на экземпляр любого другого класса, производ- производного от Customer. Каждый из этих классов может иметь свою собственную реализацию RecordCall (). Как компилятор определит, какой метод вызвать? Возможны два варианта в зависимости от того, является ли метод виртуальным: О Если метод не является виртуальным, компилятор использует тот тип, который ссылка имела при объявлении. В данном случае, поскольку ACustomer имеет тип Customer, будет вызван метод Customer .RecordCall О независимо от того, на что в действительности ссылается ACustomer. О Если метод является виртуальным, компилятор сгенерирует код, который будет во время исполнения проверять, куда на самом деле указывает ссылка ACustomer. Затем код определяет, экземпляром какого класса является данный экземпляр, и вызывает соответствующий перекрывающий метод RecordCall О. Например, если выяснится, что ACustomer ссылается на экземпляр Nevermore60Customer, то будет вызван метод Nevermore60Customer. RecordCall (). Такая проверка во время исполнения требуется каждый раз, когда вызывается соответствующий метод. Так, если виртуальный метод вызывается в цикле foreach, который вы- выполняется 100 раз, программа проверит тип экземпляра 100 раз. Это необходи- необходимо, поскольку каждый раз в цикле ссылка может указывать на другой экземпляр, а следовательно, на другой класс объекта. В большинстве случаев предпочтительным является второй вариант поведения. На- Например, если имеется ссылка на Nevermore60Customer, то маловероятно, что мы пожела- пожелаем вызвать какой-либо из перекрывающихся методов, кроме того, который принадлежит экземплярам Nevermore60Customer. В связи с этим возникает вопрос, зачем же тогда ну- нужен первый, невиртуальный подход, ведь он означает, что во многих случаях будет вы- вызван "неверный" метод. Почему бы ни сделать все методы виртуальными и говорить, что каждый метод автоматически является виртуальным? Такой подход используется в Java, где все методы объявляются как virtual. Однако существуют три причины, по которым не стоит делать этого:
124 Глава 4 1. Производительность. При вызове виртуальной функции необходимо осуществ- осуществлять проверки во время выполнения, чтобы определить, какой из перекрываю- перекрывающихся методов нужно вызвать. Для невиртуальной функции эта информация доступна во время компиляции (компилятор может определить соответствующий перекрывающий метод по типу объявленной ссылки!). Очевидно, что дополните- дополнительная проверка во время выполнения снижает производительность. Потеря небо- небольшая, так как способ, которым виртуальные функции реализуются внутренне, означает всего лишь применение дополнительного ссылочного уровня — поиска адреса в таблице методов, которая известна как vtable. Однако если требуется, чтобы приложение работало хорошо, нежелательно, чтобы это происходило при вызове каждого метода — возможны миллионы вызовов в секунду для приложе- приложения, активно использующего процессор! По этой причине методы становятся виртуальными только в том случае, если они явно объявлены таковыми. 2. Структура. Класс может содержать методы, которые вообще нельзя перекрывать. На самом деле такое происходит довольно часто, особенно с методами, которые разработаны в основном для использования внутри класса другими методами или реализации которых отражают внутреннюю структуру класса. При разработке класса вы решаете, какие особенности его реализации будут открытыми, какие вы не собираетесь делать открытыми, но готовы сделать доступными для производ- производных классов (защищенные методы), а какие особенности будут доступны строго внутри данного класса. Маловероятно, что вы пожелаете сделать перекрываемыми методы, связанные с внутренней работой класса, поэтому обычно такие методы не объявляются как виртуальные. 3. Конфликт версий. Виртуальные методы могут, в частности, создать потенциальную проблему, связанную с выпуском новых версий базовых классов (см. ниже). Способность вызывать метод по ссылке и заставлять его делать то, что свойственно дан- данному классу, известна как полиморфизм. Однако следует отметить, что вызываемый метод должен существовать в классе, на который определена ссылка. Например, мы добавляем другой метод, например свойство HighCostMinutesLeft, к Nevermore60Customer для того, чтобы позволить коду извне воспользоваться этой информацией. Тогда такой код будет корректным: Nevermore60Customer MrLeggit = new Nevermore60Customer(); // обработка int. MinutesCeft = ^MrLeggit.HighCostMinutesLeft; Однако такой код некорректен: Customer MrLeggit = new Nevermore60Customer (},- 1 // обработка in£ Minut«iai.eft = MrLeggit. HighCostMinutesLef t ; Дело в следующем: так как MrLeggit определен как ссылка на Customer, возможен ва- вариант, что эта переменная будет содержать ссылку на чистый Customer, который не реа- реализует HighCostMinutesLeft. Следовательно, нельзя точно знать, реализует ли на самом деле этот метод объект, на который указывает ссылка, и поэтому компилятор выдаст ошибку при попытке вызвать этот метод по ссылке MrLeggit. Сокрытие методов Даже если в базовом классе метод не был объявлен виртуальным, в производном классе все равно можно объявить другой метод с такой же сигнатурой (сигнатура метода — это вся информация, которая требуется для описания вызова метода: имя, возвращаемое значение, число параметров, типы параметров). Новый метод, однако, не перекроет метод базового класса. Напротив, говорят, что он сокроет метод базового класса При этом компилятор, решая, какой метод вызвать, всегда будет рассматривать тип данных, на который указывает переменная, как тип данных, заданный при ее объявлении. Если метод скрывает метод базового класса, то к его определению необходимо добавить клю- ключевое слово new. Если не сделать этого, ошибки не будет, однако компилятор выдаст предупреждение. Сокрытие методов применяется не часто, тем не менее мы покажем, как оно работает, добавив новый метод под названием GetFunnyString () в класс Customer и сокрыв его
Классы и наследование 125 в Nevermore60Customer. GetFunnyString () выводит некоторую информацию о классе и определяется следующим образом: public class Customer public string GetFunnyString() return "Plain ordinary customer. Kaark!"; Чд } // И Т.Д. public class Nevermore60Customer : Customer public new string GetFunnyString() '. returji " Nevermore60. Nevermore!"; // и т.д. Версия этой функции для Nevermore60customer будет вызываться только в том случае, если используется переменная, которая была объявлена как ссылка на Nevermore60customer (или на другой класс, являющийся производным от Nevermore60Customer). Это можно продемонстрировать с помощью кода: public static int Main(string[] args) { '.Customer Custl ; Nevermore60Customer Cust2 ; : ' 'Custl = new Customer () ; . Console.WriteLinel"Customer referencing Customer: Ц + Custl.GetFunnyString()); .Custl « new Nevermore6GCustomer ()■;. h Console.WriteLinef"Customer referencing Nevermore60Customer: " ; + Custl.GetFunnyStringО); '• Cust2 = new Nevermore60Customer() ; Console.WriteLine("Nevermore60Customer referencing" + Cust2.GetFunnyString()); return 0; } Этот код находится в примере MortimerPhones5Funny (данный пример не является частью основного процесса разработки Mortimer Phones). В результате выполнения кода будет получено: K,C:\WINNT\Systeanri32\cm4exe ' " '•"■'"" ^ ; '■'! С: 4>Moi*t inerPhonesSFunny Customer referencing Customer: Plain ordinary customer. Kaar k? iCustoner referencing Newernore60Custoner: Plain ordinary cus toner. Kaark? Newermore60Custoner referencingNewerrnore60. Newernoref C:\>_ Конфликт версий Сокрытие методов может показаться необычной особенностью С#. В большинстве случа- случаев лучше перекрыть, а не скрыть методы, так как сокрытие методов представляет собой серьезную опасность того, что для данного экземпляра класса будет вызван "неправиль- "неправильный" метод. Однако сокрытие приходится применять в особых случаях с версиями. Это редкая и необычная ситуация, поэтому если вы читаете главу первый раз и не желаете пока погружаться в подробности, то можете пропустить данную тему. Это не повлияет на понимание последующего материала. Представим, что кто-то написал класс HisBaseClass: class -HisBaseClass
126Глава 4 /•/ Различные члены } На каком-то этапе работы вы создаете производный класс, который добавляет функцио- функциональность в HisBaseClass. В частности, вы пишете метод MyGroovyMethod (), который не представлен в базовом классе: class MyDerivedClass { int MyGroovyMethod() { // Какая-то реализация Годом позже автор базового класса решает расширить его функциональность. По слу- случайному стечению обстоятельств он также добавляет метод по имени MyGroovyMethod (), который имеет такую же сигнатуру, что и ваш метод, но делает не то же самое. Когда вы в следующий раз компилируете код с использованием новой версии базового класса, возни- возникает потенциальный конфликт по поводу того, какой метод необходимо вызвать. Это до- допустимо с точки зрения С#, но так как ваш метод MyGroovyMethod () никак не связан с методом базового класса MyGroovyMethod (), результат выполнения этого кода, вероятно, будет не таким, как вы ожидаете. Подобные вещи происходят нечасто, но тем не менее они случаются. К счастью, С# хорошо справляется с этой ситуацией. Прежде всего вы будете предупреждены о проблеме: ваша версия MyGroovyMethod () не была объявлена как new, поэтому компилятор заметит то, что она скрывает метод в ба- базовом классе, но не была явно объявлена для этого, и сгенерирует предупреждение (вне зависимости от того, был ли объявлен MyGroovyMethod () как virtual или нет). Если вы пожелаете, то можете переименовать свою версию метода, и это будет наилучшим реше- решением, поскольку в будущем оно исключит возможную путаницу. Однако замечательным свойством является то, что если вы решите не переименовывать метод (например, пото- потому, что для этого пришлось бы переписать слишком большой объем клиентского кода, или потому, что вы распространяете свое программное обеспечение в качестве библиоте- библиотеки для других компаний и не можете менять имена методов) и оставите все как есть, то весь существующий клиентский код по-прежнему будет работать правильно, вызывая вашу версию MyGroovyMethod (). Это происходит потому, что любой имеющийся код, который осуществляет доступ к методу, должен делать это с помощью ссылки на MyDerivedClass (или класс, производный от него). Существующий код не может делать это с помощью ссылки на HisBaseClass, так как это сгенерировало бы ошибку в процессе компиляции с использованием более ранней версии HisBaseClass. Проблема может возникнуть только в том коде, который будет на- написан в будущем. С# выстраивает события таким образом, что вы получаете предупрежде- предупреждение о возможности возникновения проблемы в будущем. Необходимо уделить внимание этому предупреждению и не пытаться вызывать свою версию MyGroovyMethod () с помо- помощью ссылки на HisBaseClass в последующих программах. Но весь существующий код бу- будет по-прежнему работать как надо. Это, возможно, незначительный момент, но тем не менее довольно впечатляющий пример того, как С# способен управляться с различными версиями классов. Абстрактные функции и базовые классы До сих пор каждый раз при определении класса мы создавали экземпляр этого класса, од- однако это не всегда должно происходит), именно гак. Во многих ситуациях определяется один общий класс, от которого производятся другие, более специализированные классы, но базовый класс при этом вообще не используется. Для этой цели в С# имеется ключевое слово abstract. Если класс объявлен как abstract, создать его экземпляр невозможно. Предположим, что класс MyBase объявлен следующим образом: abstract class MyBase { // и т.д. Тогда оператор: . MyBase MyBaseRef = new MyBase();
Классы и наследование 127 не будет компилироваться, поскольку класс MyBase объявлен как абстрактный. Однако по-прежнему законны ссылки MyBase, указывающие на производные классы. Например, можно создать новый класс от MyBase: class MyDeriyed; -: l^yBase " ' **" j. - ■ И такой код будет корректным: MyBasekef ; 5, MyBase, = new MyDerivedO; ? MyDer-ived MyDerivedRef; Также можно объявить абстрактный метод. Это означает, что метод будет рассмат- рассматриваться как виртуальный и что вы не реализуете этот метод в классе в расчете на то, что он будет перекрыт в производных классах. Для абстрактного метода не требуется приводить тело: abstract class MyClass { public abstract int MyAbstractMethod() ; // тело метода отсутствует // *дру'ше методы я т.д. Если хотя бы один метод в классе является абстрактным, то и весь класс обязан быть абстрактным — если не объявить его как abstract, компилятор выдаст ошибку. Кроме того, любой класс, порожденный от абстрактного класса, должен перекрыть все его аб- абстрактные методы. Эти правила могут потребовать некоторых усилий, однако они обе- оберегают вас от создания экземпляров классов, которые не содержат реализации всех своих методов. Абстрактные методы и классы чрезвычайно полезны по двум причинам. Во-первых, они облегчают разработку структуры иерархии классов, более точно отражающей ситуа- ситуацию, которую вы пытаетесь смоделировать. Во-вторых, использование абстрактных классов может перенести некоторые потенциальные ошибки из разряда трудноопреде- трудноопределяемых ошибок времени исполнения в разряд легкообнаруживаемых ошибок этапа ком- компиляции. Чтобы понять все это, рассмотрим пример. Мы собираемся улучшить архитектуру программы для MortimerPhones путем перестройки иерархии классов. Перестройка Mortimer Phones: добавление абстрактного класса На данный момент в иерархии классов примера MortimerPhones имеются некоторые недоработки. Класс Customer представляет клиентов "плати-по-мере-использования" и является базовым классом для всех остальных типов клиентов. Мы рассматриваем этот тариф так, будто это особый тариф, от которого происходят все остальные. На самом деле это не совсем точное представление ситуации. В действительности тариф "пла- "плати-по-мере-использования" является лишь одним из возможных тарифов, в котором нет ничего особенного, и более тщательно продуманная иерархия классов отразит этот факт. Мы переработаем пример MortimerPhones для того, чтобы получить иерархию классов, показанную на диаграмме: Nevermore60Customer System.Root Generic Customer (абстрактный класс) PayAsYouGoCustomer \ StaffCustomer Классы, представляющие клиентов с другими тарифами Наш старый класс Customer исчез. На его месте располагается новый базовый класс GenericCustomer. GenericCustomer реализует то, что является общим для всех типов клиентов, например методы и свойства, которые реализуются одинаково для всех поль- пользователей и поэтому не являются виртуальными. Это включает в себя такие моменты, как получение счета на имя клиента и внесение платежа. 6 Зак. 69
128 Глава 4 Однако GenericCustomer не будет реализовывать метод RecordCall О, который рассчитывает стоимость звонка и прибавляет ее к счету клиента. Реализация этого мето- метода различна для каждого тарифа, поэтому мы потребуем, чтобы каждый производный класс реализовывал свою собственную версию метода. Метод RecordCall () и сам класс GenericCustomer будут объявлены как абстрактные. После того как мы сделаем это, необходимо будет добавить класс, представляющий клиентов "плати-по-мере-использования". Эту работу выполнит класс PayAsYouGo, он реа- реализует перекрывающий метод RecordCall (), который в предыдущей иерархии классов объявлялся в базовом классе Customer. Вы можете усомниться в том, что усилия по переделке иерархии классов в примере оправданы. В конце концов, старая иерархия прекрасно работала, не так ли? Однако на практике новая иерархия убирает возможный источник ошибок. В реальном приложении RecordCall () был бы, скорее всего, не единственным виртуальным методом, который необходимо реализовывать отдельно для каждого тарифа. Приложения, как правило, имеют большое число подобных методов. Что произойдет, если позже кто-то напишет новый производный класс, представля- представляющий новый тариф, но забудет добавить некоторые из перекрывающихся методов? В старой иерархии классов компилятор автоматически подставил бы соответствующий метод базового класса. В этой иерархии базовый класс представляет собой клиентов "плати-по-мере-использования", поэтому в конечном итоге все закончилось бы ошибка- ошибками во время исполнения, связанными с вызовом не тех версий методов. В новой же иерархии будет получена ошибка во время компиляции, где компилятор сообщит о том, что соответствующие абстрактные методы не были переопределены в новом классе. Аб- Абстрактные классы не просто представляют синтаксическое удобство — они способствуют выполнению во время компиляции дополнительных проверок того, что в программе присутствуют все соответствующие перекрывающиеся методы. Новый код содержится в примере MortimerPhones6. С использованием новой иерархии код для GenericCustomer выглядит следующим образом. Большая часть кода та же, что и в старом классе Customer, отличающиеся строки выделены. Заметьте, что метод RecordCall () объявлен абстрактным: ^ public -abstract class QenericCustomer "~ { private string name; protected decimal balance; public override string ToStringO { string Result = "Клиент: " + name; Result += ", на счету: " + balance; return Result; } public string Name { get { return name; } set { name = value; } } public decimal Balance { get { return balance; } ) public void RecordPayment(decimal amountPaid) { balance -= amountPaid; } ^^ 'void"'RecoruCalitTypeO£Call* callType, uint
Классы и наследование 129 Теперь реализуем клиентов "плати-по-мере-использования". Опять же большая часть кода взята из старого класса Customer. Единственное реальное отличие в том, что RecordCall () теперь override, а не виртуальный метод: public class PayAsYouC ' customer : GenericCustomer . { public override void RecordCall(TypeOfCall 'callType, uint nMinutes) { switch (rallTyne) { case TypeOfCa:l.CallToLandline: balance ■•-- @.02М * nMinutes); break; case TypeOCCall . CaHTcMobile: balance •*■= @.30M * nMinuties) ; break; defau]t: break; Мы не будем приводить здесь полный код для Nevermore60Customer, так как Record- Tall () для этого класса не изменился по сравнению с более ранней версией примера. Единственное изменение, которое необходимо сделать в этом классе,— унаследовать его от класса GenericCustomer, а не Customer, который больше не существует: public, class NevermorefiQCustomer Generic.Customer«s. private uxnt highCostMinutesUsed; public override void RecordCal1(TypeOfCall callType, { // к т.д. uint nMinutes) Для завершения примера добавим новый клиентский код для демонстрации работы новой иерархии классов. На этот раз для хранения различных клиентов применяется массив. Код показывает, как может использоваться массив ссылок на абстрактный ба- базовый класс для указания экземпляров различных производных классов, для которых вызываются соответствующие перекрывающие методы: public static void Mam(string[] argj) GenericCustomer' Arabel = new Nevermorr'50Custorner() ; Arabel.Name = "Arabel Jones"; Г GenericCustomer MrJones = печ PayAsYouGoCustomer<); MrJones.Name = "Ben Jones"; GenericCustomer [ J Customers .- new GenericCustomer [2 ] ; Customers («0] = Arabel; GustomersfO] .RecordCall(TypeO£Call,CallToLandline, 20) ,- Customers [OJ .RecordCall.{TypeOfCall--£aUTQMobile, 5) ; Customers[1] = MrJones; Cus.tpmersfl] .'RecordCall (TypeOfCall.CallToLandline, 10) ; ■ -f©reach {GenericCustomer.' NextCustomer in * Customers) Console. WritebineC {0,-20} owes ${i:F2}" NextCustomer. Name, ' NextCustomer.Balance); При выполнении этого кода выдаются корректные результаты: C:4>Moi>tiinerPhones6 Arabel Jones Ben «Jones owes $2.90 owes $0.20
130 Глава 4 Запечатанные классы и методы Запечатанные (sealed) классы и методы можно рассматривать как противоположность абстрактным классам и методам. В то время как объявление чего-либо абстрактным озна- означает, что должно быть произведено наследование или перекрытие, объявление класса или метода запечатанным означает, что это сделать невозможно. Класс можно объявить запечатанным, поместив перед его определением ключевое слово sealed. В случае если какой-либо класс попытается наследовать от данного класса, произойдет ошибка компиляции: sealed class iFinaltlass .V "" 'V ' ■4"'i- . i class Derivedclass : finaiciass- У/. Неверно. Произойдет ■'ошибка компиляции. // И'Т.Д. Пометка метода класса как запечатанного предотвращает его перекрытие в дальнейшем. В этом смысле sealed делает то же самое, что и ключевое слово final в Java: class MyClass , " "'у.' ' *гг^ ''{■'"'' ' *• ' ' -л -"'■■«■■:-1 publiCi sealed override finalMethodii) ,. ,vjt - в '•^•'i'1' '•' '."-' ) i "" '■ ;■. -V" lC ■ "'""" ?; aH■- - * // И Ф.Д.. .'• ■ ч ;■ i ' - 4- class :periveaciass { * * u -. ^ si"***: '•': public,'_ bverride FihalMe'thod'O // -Неверно* Произойдет: о'шибка гкомииляции* >■'{■'"'- '■"■- . " : Отметим, что не имеет смысла использовать ключевое слово sealed для метода, если только этот метод сам по себе не перекрывает другой метод в некотором базовом классе. Если вы определяете новый метод и не желаете, чтобы кто- нибудь мог его пере- перекрыть, с самого начала не надо делать его виртуальным. Однако если вы перекрыли ме- метод базового класса, то ключевое слово sealed гарантирует то, что перекрытие метода является "последним" в том смысле, что никто уже больше не сможет его перекрыть. Класс или метод стоит запечатать, если он выполняет множество внутренних дейст- действий по отношению к библиотеке, классу или другим классам, которые вы создаете, и вы абсолютно уверены, что любая попытка перекрыть его функциональность вызовет проб- проблемы. Также можно сделать класс или метод запечатанным по коммерческим соображени- соображениям для того, чтобы предотвратить расширение библиотеки третьими лицами способами, противоречащими лицензионному соглашению. В целом необходимо быть осторожным при объявлении класса или члена как sealed, поскольку это накладывает жесткие ограни- ограничения на способ его использования. Даже если вы думаете, что нет смысла наследовать класс или перекрывать его конкретный метод, все же остается вероятность того, что в определенный момент в будущем кто-нибудь столкнется с ситуацией, которая не была вами предусмотрена и в которой было бы полезно поступить именно таким образом. Вызов базовых версий методов До сих пор мы использовали перекрытие и сокрытие методов только в контексте замены функциональности метода чем-то другим. В ряде случаев это не совсем то, что нужно. На- Например, вы хотите перекрыть метод для того, чтобы расширить его функциональность, т.е. делать то, что делает версия метода базового класса и плюс что-нибудь еще. В данном случае необходим способ, с помощью которого можно явно указать компилятору, чтобы он вызвал метод базового класса. Для этого предназначено ключевое слово base. C++ не имеет эквивалента base в СМ. В Java есть ключевое слово super, которое выполняет ту же роль. Для иллюстрации сказанного оставим на время Mortimer Phones и рассмотрим похожий пример. Допустим, что мы управляем компанией по продаже машин и у нас имеется базо- базовый класс CustomerAccount, который представляет собой счет пользователя. Он содержит метод CalculatePrice (), который возвращает десятичное число, показывающее,
_ К : ~" ■ -■ Классы и наследование 131 сколько мы запросим за конкретную продажу (нельзя использовать MortimerPho- nes.RecordCall () для этих примеров, так как они основаны на том, что метод что-то возвращает; RecordCall () возвращает void): <j!ass GustomerAccount ;, X \ public virtual ."decimal CalculatePrice(CustomerAccount account) •{ / / Обработка данных. Допустим, ** /J что результат вычислений равен $2000 return 2000.ОМ; ■.т . . -Э- .' • ' Тогда в любом напрямую унаследованном классе мы можем получить доступ к этому конкретному методу, ссылаясь на него как на base. DoSomething ();. Что делать с этим методом — ограничивается только вашим воображением. Вот несколько примеров того, что можно произвести в перекрывающем методе CalculatePriceO: 1. Возвратить результат умножения на величину, получаемую из базового метода: : eustdmerAbcuimt w ■ ч' .- £ /. : <_ : publyc,j6vmx:'i:xuet decimal CalculatePriqe() ц t' ' -'-''.(" .'■'".'■'■■.■ 4. . i ^ • : return- basje.,CalculateP.rice() *0.9; V &'■ 14! 1 ;" ■ . f * - ■ - - .-.. Одним из применений этого может быть реализация в примере Mortimer Phones метода RecordCall О для GoldCustomer, который бы делал скидку для стоимости, рассчитанной другим классом. 2. Сделать что-то еще в дополнение к обработке в базовом классе: .'-* 3" class* GoldAccbunt : CustomerAccount .-, - f - - public- override decimal CalculatePriceO X , " , - ' IniqrmPentagonQfSale(); .return ba.se.Ca'lculatePrice(-}; ■ ' У -...- - ^ 3. Сделать то же, что делает базовый класс, но только при выполнении определенного условия: class QualifyingAccount : CustomerAccount , {, _ 11 /"public" ,'override;-.decimal CalculatePrice() { ■ - .InfdrmCustomerHesWQnAEreeCarJ)'"' " return. :0;0M>' >,.-..-■ , «■■-,'.■"-• "■- ■ .else -. ( return base.CalculatePriceO ; Можно даже явно вызвать версию метода для базового класса из другого метода про- производного класса (хотя трудно найти ситуации, в которых это потребуется): class MyDerived : MyBase -public double DoSomethingEl'se () base.DoSomething(); // выполним другие вычисления return 12.0; }
i32 Глава 4 В этом примере мы объявили DoSomething () виртуальным, а версию в MyDerived как override, но те же самые принципы были бы применимы, если бы версия в базовом классе не являлась виртуальной, а версия в MyDerived была бы объявлена как new, т.е. скрывала бы базовую версию. Даже если базовая версия метода скрыта, мы можем вызвать ее, используя ключевое слово base. Наследование: дополнительные аспекты Мы завершили рассмотрение основных концепций, связанных с наследованием. Однако в последующих главах мы будем возвращаться к этой теме, чтобы показать некоторые дру- другие ее аспекты. Возможное число задач, которые можно решать с помощью унаследован- унаследованных классов, перекрытия или сокрытия членов, огромно, и в одном примере невозможно рассмотреть все случаи. Однако несколько моментов стоит отметить: □ Не только методы могут быть перекрыты или скрыты. То же самое можно сде- сделать с любым членом класса, который имеет реализацию. Это означает, что мож- можно объявлять виртуальными свойства и перекрывать их по желанию. То же самое справедливо по отношению к индексаторам и перегрузке операций (см. главу 5). □ Поля не могут быть объявлены виртуальными или перекрытыми. Однако можно скрыть базовую версию поля, объявив другое поле с тем же именем в производ- производном классе. Если в этом случае требуется получить доступ к полю базового класса из производного, необходимо использовать синтаксис base. <имя_поля>. В лю- любом случае вам не придется так делать, поскольку все ваши поля будут объявлены как private. □ Статические методы и т.п. не могут быть объявлены виртуальными, но могут быть скрыты таким же образом, как и методы экземпляров. Не имеет смысла объ- объявлять виртуальным статический член. Виртуальность означает, что компилятор будет искать при вызове этого метода экземпляр класса, но статические методы не связаны ни с одним экземпляром! О Хотя ссылочная переменная, имеющая тип Т, может ссылаться на любой экземп- экземпляр класса, производного от Т, нельзя использовать эту ссылку для вызова члена, если этот член не определен в Т. Другими словами, если метод MyMethod впервые объявляется в производном классе D, то MyMethod можно вызвать только с помощью ссылки, указывающей на D или класс, производный от D. О Когда мы говорим о том, что класс Derived является производным от класса Base, то наследование может быть прямым или непрямым. Например, Derived может происходить от промежуточного класса Inter2, который в свою очередь может быть производным от класса Interl, который может быть производным от Base. Допустим, что некоторый метод MyMethod определен в Base, перекрыт в Derived, но в Interl и Inter2 версия этого метода не определена. Компилятор всегда будет искать для использования версию метода в более ранних базовых классах. Например, если MyMethod вызывается для экземпляра Inter2, то компи- компилятор использует версию, определенную в Base, так как ни Inter2, ни Interl не содержат реализацию этого метода, а класс Base находится выше в иерархии: Base Определен MyUethodO t Interl t Inter2 t Derived MyMethodf) перекрыт
Классы и наследование 133 Архитектура ООП-программы В этой главе было введено множество понятий. Ее целью было не только то, чтобы дать представление о языке С#, но и то, чтобы познакомить вас с идеей объектно-ориентиро- объектно-ориентированного программирования и показать, как строятся хорошие объектно-ориентирован- объектно-ориентированные программы. В процедурном языке и даже в языках типа Visual Basic главными являются функции: программа разбивается на части, а затем для каждой части пишется функция, выполняющая соответствующие действия. Объектно-ориентированное про- программирование смещает акцент с глагола на существительное: вы думаете не о том, что делает программа, а о том, из каких объектов она состоит, а затем разрабатываете клас- классы, которые представляют эти объекты. Создав классы, вы начинаете думать о том, что они делают, и писать для них методы. Наследование также чрезвычайно важная особенность объектно-ориентированного программирования, а ключевым моментом в разработке программы является выбор иерархии классов. Обычно создается несколько специализированных объектов, каждый из которых представляет собой частный вид более общих объектов. Например, если вы пишете программу управления или моделирования дорожного движения, то в вашей си- системе будет много машин. Одни из них — автобусы, другие — легковые машины, третьи — грузовики и т.д. Возможно, вы пожелаете еще более тонко разделить машины по типам, например, выберите иерархию классов наподобие следующей: System.Object Транспортное средство / Машина Дорожное транспортное средство 'Ч Автобус \ Грузовик Железнодорожное транспортное средство Пассажирский поезд Товарный поезд В данном случае Транспортное средство будет абстрактным классом, но он будет реализовывать некоторые функции, являющиеся общими для всех типов машин, такие как трогание с места и остановка. Автобус, вероятно, добавит к ним методы для посадки пассажиров и свойства для учета автобусных маршрутов. Идея ясна? В качестве другого, более реального примера можно привести одну из иерархий базо- базовых классов .NET. В главе, посвященной программированию в Windows, рассказывается о том, как использовать базовые классы, которые инкапсулируют окна (или, выражаясь более современной терминологией .NET, формы). Здесь мы не будем обсуждать сами классы, а покажем фрагмент иерархии, чтобы вы могли понять ее структуру. Готов поспорить, что вы и не представляли себе, насколько богатая иерархия может стоять за некоторыми элементами управления, размещаемыми в следующем окне (см. рисунок на стр.134). Класс Form представляет собой общее окно, a ScrollBar, StatusBar, Button, Text- Box и RichTextBox — соответствующие элементы управления. Богатая иерархия классов позволяет аккуратно настраивать, какие реализации каких методов могут быть общими для определенного числа различных классов. Многие из этих классов будут также реали- реализовывать определенные интерфейсы, при помощи которых для клиентского кода они будут отображаться как окна. Большое приложение будет иметь не одну иерархию классов и реализовывать боль- большое количество классов, возможно, исчисляемое сотнями. Это звучит пугающе, но до по- появления объектно-ориентированного программирования альтернативой были буквально тысячи функций, составляющих программу, с отсутствием возможности их объединения
134 Глава 4 t MarshallByRefObject A MarshallByRefComponent ScroIIBar StalusBar A L Control i RichControl ScrollableControl к ContainerControl i t Form ButtonBase FormatControl , Button TextBoxBase I TextBox RichTextBox в управляемые модули. Классы предлагают эффективный способ разбиения программы на более мелкие части. Это не только облегчает поддержку программы, но и упрощает ее понимание, так как классы представляют реальные объекты интуитивным образом. Важно тщательно продумать выделение в классе открытого интерфейса, который предлагается клиентскому коду, и закрытой внутренней реализации. Вообще говоря, чем большую часть класса вы сделаете закрытой, тем более модульной будет ваша программа в том смысле, что вы сможете вносить изменения во внутреннюю реализацию одного клас- класса с уверенностью в том, что это не разрушит и даже не окажет никакого воздействия на любую другую часть программы. Именно по этой причине было подчеркнуто, что по- поля-члены должны быть закрытыми, если только они не являются константами или не со- составляют часть структуры, главная цель которой заключается в соединении вместе нескольких полей. Мы не всегда жестко следовали этому правилу в данной главе, посколь- поскольку старались упростить примеры. На практике всегда существует равновесие между коли- количеством времени, выделяемого на проверку того, что объекты разработаны качественно и в соответствии с принципами объектно-ориентированного программирования, и скоро- скоростью написания кода, которая удовлетворит вашего начальника или клиента, и затратами на поддержку и обеспечение качественного дизайна в дальнейшем. Хорошая архитектура объектов и программы окупит себя очень быстро, особенно когда вы начнете выполнять действия по поддержке или обновлению существующих приложений. С# здорово помога- помогает в этом, так как он был разработан с учетом анализа 20 лет объектно-ориентированного программирования. Заключение В этой главе было введено множество понятий. Ее целью было не только то, чтобы дать представление о языке С#, но и то, чтобы познакомить вас с идеей объектно-ориентиро- объектно-ориентированного программирования и показать, как строятся объектно-ориентированные про- программы. Мы рассмотрели пример хорошо структурированного кода с приемлемой иерархией классов в окончательной версии MortimerPhones. Наследование и объектно-ориентированное программирование являются темой, освоение которой потребует времени, но эти усилия стоят того. В результате вы научи- научитесь писать хорошо структурированный и качественный код. В этой книге будут исполь- использоваться классы, унаследованные от других классов. По мере изучения базовых классов .NET мы увидим, что в основе их разработки лежат ООП и наследование. Microsoft вы- выбрала этот путь потому, что при нынешнем понимании компьютерных технологий это наиболее подходящий способ для создания большой библиотеки или приложения.
П л! 4 a В Объектно -ориентированный C# В предыдущей главе вы познакомились с идеями объектно-ориентированного програм- программирования. Было показано, как можно определять классы, представляющие в программе различные элементы, и как использовать наследование для придания объектам специа- специализации. На примерах Мог timer Phone вы увидели, как принципы ООП могут привнести новую и полезную архитектуру в ваш код. В этой главе продолжается рассмотрение объектно-ориентированного программиро- программирования. Здесь обсуждаются некоторые специфические возможности, предлагаемые С# для упрощения объектно-ориентированного программирования, в частности: а Перегрузка методов. С# позволяет определить для класса различные версии од- одного метода, а компилятор автоматически выберет одну из них, основываясь на наборе представленных параметров. □ Конструкторы и деструкторы. Можно указать, как должны инициализироваться объекты, а также какие действия необходимо предпринять при их уничтожении. О Память. Хотя С# освобождает вас от ответственности за управление памятью, понимание того, как С# автоматически управляет памятью, поможет в разработке эффективного кода □ Структуры. В некоторых случаях требуется то, что имеет свойства класса, но не вызывает потерь производительности, связанных с созданием экземпляра класса. Такую возможность предоставляют структуры. О Перегрузка операций. Мы рассмотрим, как определять операции, которые позволят, например, складывать, вычитать, умножать или делить экземпляры ваших классов. □ Индексаторы. Позволяют рассматривать класс синтаксически так, как если бы он являлся массивом, и упрощают использование классов, которые содержат наборы объектов. □ Интерфейсы. С# поддерживает не только наследование реализаций, но и насле- наследование интерфейсов. Перегрузка методов Для знакомства с перегрузкой методов обратимся к примерам базовых классов .NET. Если необходимо отобразить целочисленное значение, то можно записать: Для отображения строки мы можем написать:
136 Глава 5 ЙгАйд,: Щ$$ще '-в "Привет!"; ~* Console .Writ'eLine (Message),; Как такое возможно? Какие типы параметров принимает метод Console. WriteLine () ? Если бы он принимал строку, тогда первый пример вызвал бы ошибку на этапе ком- компиляции, так как не существует неявного преобразования из int в string. Если бы Console.WriteLine () принимал целочисленное значение, то второй пример вызвал бы ошибку компиляции, поскольку нельзя явно преобразовать string в числовой тип. Однако на самом деле оба примера будут откомпилированы без проблем и при запуске выдадут ожидаемые результаты. Дело в том, что существуют два разных метода Console. WriteLine (): один из них в качестве параметра принимает int, а другой — string. В действительности их гораздо больше. Например, есть версия для двух параметров, один из которых string, а другой object, поэтому можно записать: s:txiij,g Message, ~* ^Лривет.!%• Consols-WriteLine {.'"'еЬс-Щениё; {О}", Mesaatje) ; Это работает, поскольку любой тип всегда может быть неявно преобразован в ob j ect, поэтому не возникает проблем с передачей строки в качестве второго параметра. Microsoft создала все эти методы Console .WriteLine (), так как понимала, что суще- существует большое количество различных типов данных, значения которых вы будете выво- выводить на экран. Следовательно, в классе Console существует некоторое число методов, имеющих одно и то же имя. Такое допускается в С#, а различные методы (или, если хо- хотите, различные версии метода WriteLine ()) известны как перегруженные методы. Пе- Перегрузка методов означает, что в классе можно определить несколько методов с одним и тем же именем при условии, что эти методы принимают разные типы или разное число параметров. Не путайте перегрузку методов с перекрытием методов. Это совершенно разные, не связанные друг с другом концепции. Перегрузка методов не имеет ничего общего с наследованием или виртуальными методами. Перегрузка методов знакома разработчикам на Java и C++. В С# она реализована точ- точно так же, как в этих двух языках. Однако эта концепция нова для разработчиков на VB. С другой стороны, С# не разрешает использовать значения по умолчанию для парамет- параметров .(необязательные параметры) в отличие от VB и C++. Как мы увидим, одна из много- многочисленных выгод от перегрузки методов состоит в том, что она предоставляет более мощный (хотя синтаксически более громоздкий) способ достижения тех же целей. Используйте перегрузку методов в том случае, когда необходимо, чтобы класс выпол- выполнял какие-то действия, но при этом существует несколько способов передачи информа- информации методу, выполняющему задание. Метод Console. WriteLine О является хорошим примером. Определение перегруженных методов В качестве примера, иллюстрирующего переопределение методов, рассмотрим метод RecordCall О из классов иерархии Customer примера MortimerPhones (см. главу 4). Сигнатура этого метода: public virtual void RecordCall (TypeOfCall callType, uint nMinutes) Это означает, что ему передается такая информация, как тип звонка и число минут, составляющих длительность разговора. Допустим, вы знаете, что в некоторых случаях клиенту доступна информация о типе звонка и о времени начала и окончания разговора. Тогда вы, возможно, пожелаете иметь еще один метод со следующим определением: public virtual void RecordCall (TypeOfCall callType, DateTime startTime, DateTime endTime) Другими словами, есть еще один метод, который имеет то же имя и концептуально делает то же самое, но использует несколько иную информацию. Наш новый перегруженный метод RecordCall О использует один из основных базовых классов .NET, называемый System.DateTime. Зависимость С# от базовых классов настолько сильна, что в примерах нам часто придется обращаться к услугам этих классов. Использование DateTime не требует пояснений. Мы рассмотрим лишь те методы DateTime, которые нам понадобятся. Класс, так же как и остальные базовые классы, описан в документации MSDN.
Объектно-ориентированный С# 137 Число перегруженных методов не ограничено. Они объявляются в классе, как если бы это были разные методы, имеющие одно имя. В примере RecordCall () вам, возмож- возможно, захочется иметь метод, который позволит вычислять стоимость звонка по набранно- набранному номеру, а также по времени начала и окончания разговора. С добавлением перегруженного метода основная часть определения класса Customer может выглядеть следующим образом: public virtual void RecordCall(TypeOfCall callType, uint nMinutes) { // реализация } public virtual void RecordCall(TypeOfCall callType, DateTime startTime, DateTirae endTime) { / / реализация yoifl jRecordCall{string jiumberCalled*;. jJateTijne sjtartTiine; ■-,.'.'.■''■•.• ■ -: DafeeTime,'endTiroe) щ' .•:• // "реализация" ■■-,• '■ . Реализация перегруженных методов целиком зависит от вас, но совершенно очевидно, что ваш класс должен быть удобен в применении, следовательно, различные перегружен- перегруженные функции необходимо реализовать так, чтобы они выполняли одни и те же действия: как только разработчик поймет, как пользоваться одним из перегруженных методов, ему станет ясно, как использовать все остальные. Продемонстрируем один из способов, с по- помощью которого можно реализовать вторую перегруженную функцию RecordCall (), и до- добавим ее в пример MortimerPhone7 (его можно загрузить с web-сайта Wrox Press). Новый перегруженный метод RecordCall () помещается в класс GenericCustoraer. ;* public! Щхй' RecordCall (TypeOfCall callType, DateTime' startTirte, -' ''-■, . DateTime endTime) _.. . .. .. , ::* * . r t // Будем считать,- что конечное- время .боладе Начального. // В* реальном приложении это необходимо проверите явно. TimeSpari Calitength "- endTime - startTime; // В реальном приложении это необходимо проверите яо. >ь- ■• TimeSpari Cal.itength "- endTime - startTime; ( a_..._ 4 f " Jyibtf-aiMinute's j=" 4uint)CaJlbength.TotalMi;nutes; -.4 ">•'** J -л; RecordCallrcallTvpe, nMinutresii; ,.- •;*• • . г' ;il. B этой реализации первая перегруженная функция RecordCall О собственно вы- выполняет работу, а вторая перегруженная функция преобразует перед вызовом первой функции параметры в эквивалентные данные, выраженные в виде типа звонка и числа минут. Реализация перегруженных методов именно таким образом является широко распространенной практикой — всегда следует избегать повтора кода. В этой реализации использован дополнительный базовый класс, TimeSpan, который представляет интервал времени. Вы можете предположить, что для получения временного интервала необходимо вычислить разницу между двумя абсолютными значениями времени, и именно это делается в приведенном выше коде - из времени окончания звонка вычитается время начала звонка для вычисления его продолжительности. Отметим также, что изменилось определение перегруженной функции — она более не является виртуальной. Почему? Посмотрите на то, что она делает. Есть ли в этом ме- методе то, что захотелось бы переопределить в производном классе? Мы выполняем лишь преобразование данных (и то, как мы это делаем, не зависит от типа клиента!) перед тем, как передать управление первоначальному методу RecordCall (). Поэтому оригиналь- оригинальный перегруженный метод остается виртуальным, по-прежнему для данного экземпляра класса автоматически выбирается его корректная реализация, но нет необходимости в том, чтобы новый перегруженный метод также был виртуальным. В качестве дополнительной награды приведенный выше код демонстрирует также следующее положение: рассматривая виртуальные методы, мы говорили, что некоторые методы по конструктивным соображениям не потребуется перекрывать в производных классах, и, следовательно, не нужно объявлять их виртуальными (см. главу 4).
138 Вызов перегруженных методов Вызов перегруженных методов ничем не отличается от вызова обычных методов, за исключением того, что компилятор выполнит чуть больше действий, решая, какой из перегруженных методов вы собираетесь вызвать. Например, рассмотрим код: // Start и End имеют тип UateTime к инициализированы £ uint; Mins, ~< 30; «- Arabel ^ecordCall/(TypeOfCa:l;l.CallToLandline, Mins) ,- Arabel.RecbrdCalltTYpeOfCail.CallToIiandlxne, Start, End); Здесь метод RecordCall () вызывается дважды. Первый раз мы передаем ему Туре- Of Call и uint. Когда компилятор встречает этот код, он ищет наиболее похожий пере- перегруженный метод RecordCall (). В данном случае существует точно соответствующий метод. Вспомним, какие перегруженные методы у нас имеются: О public virtual void RecordCall(TypeOfCall callType, uint nMinutes) О public void RecordCall(TypeOfCall callType, DateTirae startTirae, DateTime endTirae) D public void RecordCall(string numberCalled, DateTirae startTirae, DateTime endTirae) Мы предположили также, что третий перегруженный метод выполняет некоторые действия, а затем вызывает оригинальный метод, поэтому он не обязан быть виртуальным. Первый перегруженный метод требует указания параметров TypeOfCall и uint — это именно то, что мы передаем. Компилятор сгенерирует код, который будет приме- применять этот перегруженный метод. Поскольку он объявлен виртуальным, сгенерирован- сгенерированный код будет использовать vtable, чтобы обеспечить вызов правильного метода для соответствующего объекта. Второй вызов RecordCall () пытается передать CallType и два значения DateTime. И вновь имеется точное соответствие, так как мы предусмотрели перегруженный метод, который требует CallType и за ним два значения DateTirae. Именно этот метод будет вызван. Он не является виртуальным, следовательно, vtable не будет применяться. Этот метод вызывается как метод Customer, и сгенерированный код всегда будет исполь- использовать этот метод так, как он определен в Customer. Теперь рассмотрим код: '- ulbng' jjOngMins = 16; Arabei.kecordCalltTypebfCair.CaliTQLandlitie, Mins) ; , Arabei.Recor<fcaii(TypeOfCall5CailToLandline^ LongMins).;. //'Неверно Теперь задача компилятора усложняется, поскольку не существует точного соответ- соответствия ни для одного из вызовов. Рассмотрим первый вызов. Мы не объявляли перегру- перегруженную версию RecordCall (), которая принимает CallType и ushort — эти параметры мы передаем ей,— поэтому компилятор попытается найти наиболее подходящий пере- перегруженный метод. Так как мы передаем только два параметра, компилятор сразу сузит выбор до тех методов, которые принимают два параметра. Единственный метод в на- нашем списке, принимающий два параметра,— метод номер 1. Компилятор попытается со- сопоставить передаваемые параметры с параметрами этого метода — перечислимое значение TypeOfCall и uint. К счастью, неявное преобразование из ushort в uint до- допустимо, поэтому будет вызван именно этот метод. Второй вызов RecordCall () также передает два параметра: TypeOfCall и ulong. Снова метод 1 из нашего списка выглядит наиболее подходящим, однако С# не позволит неявно преобразовать ulong в uint из-за риска переполнения. Эта строка кода вызовет ошибку компиляции, так как компилятор не сможет найти наиболее подходящий пере- перегруженный метод для вызова. Если бы мы написали: ArabeI,.RecordCall (TypeOfCall.CallToIjandline^., (uiht)IiOngMins?) С Л Верно то все было бы в порядке, поскольку компилятор позволит преобразовать ulong в int, если мы явно попросим его сделать это. С точки зрения компилятора этот метод переда- передает TypeOfCall и uint, т.е. имеется строгое соответствие. Вообще лучше явно приводить типы аргументов к типам аргументов одного из перегруженных методов, так как это не оставит места сомнениям по поводу того, какой метод должен вызываться. При отсутствии строгого соответствия компилятор всегда будет искать наиболее близ- близкое соответствие для передаваемых параметров, решая, какой из перегруженных методов вызвать. Существуют некоторые правила по поводу того, как это делается и какие
Объектно-ориентированный С# 139 преобразования данных будут выполняться первыми. Например, компилятор считает, что преобразование из int в long лучше, чем преобразование из int в double, поэтому если имеются методы: • "•**.■■■/ " *• ?£ "int GetNumbYlqng &)■ {^return i?) public ■> int - Gethurab'Tdouble xj dJetufh' *2-;> то при попытке вызвать GetNumb() с использованием: будет вызван первый перегруженный метод, а у будет содержать значение 1. Здесь мы не будем подробно рассматривать правила выбора перегруженных методов. Они описаны в документации Microsoft по языку С#. Если вы придерживаетесь хорошего стиля програм- программирования, вам следует убедиться в том, что параметры, принимаемые вашими перегру- перегруженными функциями, не вызовут непонимания, связанного с тем, какой из методов необходимо использовать компилятору в конкретных обстоятельствах, а разработчи- разработчикам, использующим ваш код, не придется обращаться к правилам преобразования типов. Перегруженные методы customer .RecordCall() из предыдущего примера не имеют проблем в этом отношении и могут быть отнесены к хорошему стилю программирования. А вот пример с GetNumb () демонстрирует плохой стиль программирования потому, что в клиентском коде можно легко ошибиться, вызвав не тот метод. Требования к перегрузке методов Компилятор накладывает ряд ограничений на определение перегруженных методов. Одно из правил состоит в том, что перегруженные методы должны иметь различные сиг- сигнатуры, т.е. разные имена, число и типы параметров. Перегруженные методы, отличаю- отличающиеся только типом возвращаемого значения, не допустимы. Например, следующий код вызовет ошибку компиляции: public void RecordCall(TypeOfCall callType, uint nMinutes) { // Реализация } public int RecordCall(TypeOfCall callType, uint nMinutes) { // Реализация return 1; } Дело в том, что в данном случае можно непреднамеренно написать неоднозначный код, для которого неочевидно, какой из перегруженных методов будет вызван. Количество перегруженных методов может быть любым при условии, что все они отли- отличаются либо числом параметров, либо типом по крайней мере одного параметра; только при выполнении этого условия можно использовать различные возвращаемые типы. Применение перегрузки Использовать перегрузку следует в основном тогда, когда по некоторым причинам требу- требуются методы, которые принимают разные параметры, но концептуально делают одно и то же. Хорошим примером является Console.WriteLineO. Другой пример — метод Sign () в базовом классе System. Math, который возвращает знак числа и перегружается для возврата знака для double, decimal и других типов со знаком. Не следует применять перегрузку тогда, когда методы в действительности делают разные вещи. Вы просто запутаете разработчиков, которые будут использовать ваш класс. Перегрузка обычно используется в следующих случаях. Параметры по умолчанию Одно из общих применений перегрузки методов: нужно разрешить определенным пара- параметрам метода иметь значения по умолчанию, даже если код клиента не указывает эти значения явно. Например:
140 ГАО8О5 .public void DoSomethin^dnt X.)f I DoSomething(X, 10); } Эти перегруженные методы позволяют клиентскому коду вызывать DoSomething, пе- передавая ему в качестве параметров либо два целочисленных значения, либо только одно, причем в последнем случае второй параметр принимается равным 10 — как если бы Y было присвоено значение по умолчанию 10. Некоторые языки, такие как VB и C++, разрешают указывать параметры по умолчанию явно в определениях функций с использованием синтаксиса типа public void DoSomethihg(int X, int Y=10). C# не позволяет этого, ■ поэтому в нем необходимо эмулировать параметры по умолчанию, реализуя многочисленные перегруженные методы, как показано выше. Различные входные типы ■ Мы уже рассмотрели примеры для этой типичной причины определения перегруженных функций. Могут быть различные типы данных, для которых необходимо выполнять одни и те же операции, например Console. Wri teLine (), и могут быть различные комбинации информации, которые позволяют выполнять одну и ту же обработку, как показано в при- примере MortiraerPhones. Различные выходные типы Эта ситуация является менее распространенной, но иногда может потребоваться метод, который рассчитывает или получает некоторую величину и в зависимости от обстоя- обстоятельств должен возвращать ее различными способами. Например, в авиакомпании могут понадобиться класс, представляющий расписание полетов, и метод, сообщающий, где са- самолет должен находиться в определенное время. В зависимости от ситуации нужно возвра- возвращать либо строковое описание положения ("над Атлантическим океаном по маршруту в Лондон"), либо широту и долготу. Как уже упоминалось, нельзя отличить перегруженные методы, используя только тип возвращаемого значения. Однако это можно сделать с помощью параметров out. Допустимо такое определение функции: void. betAircraftLocationiDateTiTne Time, put string Location) {- // и т.д.- } void'GetftircraftLocatio"n(Date*Time Time, out float Latityde, ■:~ й" ' " " out float Longitude) { // и- т.д. } Отметим, однако, что в большинстве случаев применение перегруженных функ- функций, возвращающих различные параметры, не приводит к архитектурно четкому реше- решению. В данном случае, например, лучше было бы определить структуру Location, которая содержала бы строку о местоположении, широту и долготу, и возвращать эту структуру из вызываемого метода, избегая таким образом необходимости перегрузки. Конструирование и освобождение В этой части рассматриваются возможности С# по указанию конкретных действий, кото- которые должны всегда выполняться при инициализации и уничтожении экземпляра класса. Начнем с инициализации. Конструкторы Что касается простых типов данных, но не классов, то С# очень строг по части того, что переменные должны быть инициализированы до их использования. Вообще в любом языке, если переменная по какой-то причине не инициализирована путем присвоения ей определенного значения до ее применения, то она будет содержать случайные данные, находящиеся в соответствующем месте памяти. Эти данные будут использоваться в каче- качестве значения переменной, что является причиной возникновения трудноуловимых ошибок времени исполнения. Для простых типов данных (по значению) С# поддерживает два способа инициализации в случаях, когда начальное значение переменной не было указано: О Если переменная является локальной для метода или out-параметром метода, то компилятор будет настаивать, чтобы ваш код явно устанавливал значение переменной до того, как она будет использована.
Объектно-ориентированный С# 141 D Во всех остальных ситуациях (включая статические поля-члены классов и поля экземпляров) компилятор инициализирует переменные после их создания значе- значениями по умолчанию. Для целочисленных типов и типов с плавающей точкой это значение 0, а для типа bool — false. Для строк значение по умолчанию представ- представляет собой пустую строку, а все ссылки инициализируются как null. Во всех слу- случаях значение по умолчанию будет таким, какое получается при простом обнулении памяти, используемой для хранения переменной. Очевидно, что если указать начальное значение переменной при ее объявлении (на- (например, int X = 10), это значение будет всегда использоваться для ее инициализации. С# способен инициализировать простые типы данных значениями по умолчанию, так как эти значения указаны в определении языка С#. Однако для классов значений по умолчанию не существует по той очевидной причине, что когда вы создаете свои собст- собственные классы, только вам известно, какими должны быть значения по умолчанию. Вот для чего предназначены конструкторы. Разработчики на VB отметят некоторые сходства между конструкторами и методами Initialize и Class_Load модулей классов VB. Конструкторы, однако, являются более гибкими и мощными. Определение конструктора выглядит практически так же, как и определение метода, разница заключается в том, что обычно конструкторы не вызываются явно. Они подоб- подобны методу, который по вашему поручению вызывается всегда, когда создается экземп- экземпляр класса. Поскольку этот метод никогда не вызывается явно, не существует способа получения доступа к возвращаемому значению, т.е. конструкторы никогда ничего не воз- возвращают. Конструктор можно указать в определении класса, так как он всегда имеет то же имя, что и сам класс. Например, для класса MyClass каркас конструктора может быть определен как: public class MyClass { public MyClass() Этот конструктор ничего не делает — мы еще не добавили для него код. Отметим, что не указан тип возвращаемого значения, даже void. Компилятор распознает конструктор исходя из того, что его имя совпадает с именем класса. (Следовательно, невозможно на- написать метод, имеющий такое имя, поскольку компилятор будет воспринимать его как конструктор.) Конструктор объявляется как метод, имеющий то же самое имя, что и содержащий его класс. Он не должен иметь возвращаемого значения. I Необязательно указывать конструктор для класса. До сих пор ни в одном из приме- примеров этого не делалось. Если не указать явно ни один конструктор, то компилятор неза- незаметно для вас создаст конструктор по умолчанию. Это будет очень простой конструктор, который инициализирует все поля своими значениями по умолчанию (т.е. пустыми строками для string, нулями для числовых типов, false для bool). Часто этого доста- достаточно; в противном случае придется писать свой собственный конструктор. Добавление конструкторов в класс Authenticator Для иллюстрации использования конструкторов вернемся к классу Authenticator и улучшим его. Напомним, что всякий раз, когда мы объявляли переменную типа Authen- Authenticator, начальный пароль устанавливался в значение "пустая строка" (значение по умолчанию для строк). Для нашего класса это не подходит. Я не являюсь экспертом по ча- части безопасности, но подозреваю, что пустые пароли не приемлемы1 Поэтому предполо- предположим, что всякий раз, когда создается объект Authenticator, начальное значение пароля устанавливается равным строке ERRE34dj. Использование одинаковых начальных паро- паролей для всех экземпляров, вероятно, не намного лучше, чем пустые пароли, но все же. Позже мы приведем более приемлемое решение. Для того чтобы задать пароль по умолчанию равным ERRE34dj, произведем следующие изменения в классе Authenticator: // Authenticator 4
142 Главо5 public class Authenticator . publ ici'-Aiithent icator (.) !' 'Password ,v private string Password; private static uint minPasswordLength = 6; Теперь допустим, что мы хотим создать экземпляр Authenticates и присвоить па- пароль MyPassword45. Код для этого случая будет выглядеть так: ■ Authenticator NewUser- -. new AuthenticatorO ;. - j NewUgervChenaePasswordlчЕККЕЗйаз", "MyPassworcl45"}'; Конструкторы, принимающие параметры Рассмотренный выше код будет работать, однако такой, способ установки пароля неудо- неудобен. Хорошо бы иметь возможность указывать начальный пароль в момент создания эк- экземпляра Authent icator. Для этого и служат конструкторы, которые принимают параметры. В этом отношении конструктор ведет себя как метод, в котором можно опре- определить любые требуемые параметры, и здесь конструкторы превосходят Class_Load в VB. Более того, конструктор можно перегрузить для класса точно так же, как метод, создав различные конструкторы, принимающие разные типы или число параметров. Для класса Authenticates следует добавить конструктор, принимающий в качестве параметра начальный пароль: // Authenticator 4 public class Authenticates { public Authenticates () { Password = "ERRE34dj"; } % public Authenticator(string initialPassword) { -as , Password' ■= ini'tialPassword; ;-'V, it •'•■_" . private string Password; private static uint minPasswordLength = 6; Преимущество от использования такого конструктора состоит в том, что объект Authenticator гарантированно инициализируется сразу же после создания. Следователь- Следовательно, другой код не может получить доступ к объекту до его инициализации, что было бы возможно, если бы инициализация производилась путем вызова метода после создания экземпляра объекта. Как гарантировать, что будет вызван данный конструктор при создании экземпляра объекта? Параметры помещаются в скобки вслед за оператором new в той точке, где со- создается экземпляр класса. Например, если требуется создать два экземпляра объекта Authenticator, один с паролем по умолчанию, а другой с паролем MyPassword45, то можно написать так: '" 'AuthebtAcitjor itewUser = new AuthenticatorO; Authenticates NewUser2 = new Authenticator("MyPassword45")г Если конструкторы были перегружены, компилятор трактует их точно так же, как пе- перегруженные методы. Иными словами, при создании экземпляра объекта он смотрит на типы параметров в скобках после оператора new и пытается найти наиболее подходя- подходящий конструктор. Если подходящий конструктор найти не удается, возникает ошибка компиляции. Пример Authenticator4 иллюстрирует использование двух этих конструкторов. Также можно применять конструкторы для того, чтобы заставить клиентский код указать параметры при создании экземпляра класса. Например, в классе Authenticator нельзя использовать пароли по умолчанию из соображений безопасности, и лучше было бы, если бы клиентский код всегда указывал начальный пароль при создании экземпля- экземпляра Authenticator. Этого легко добиться, убрав конструктор без параметров, и оставив только конструктор, принимающий в качестве параметра строку:
Объектно-ориентированный С# 443 public class Authenticates ( public Authenticates)string initialPassword) { Password = initialPassword; } private string Password; private static uint minPasswordLength = 6; Теперь еще раз обратимся к клиентскому коду: Authenticator NewUser = new Authenticator(); // Неверно Authenticator NewUser2 = new Authenticator("MyPassword45"); Первая строка кода не будет компилироваться, так как в ней не передаются парамет- параметры, а единственный доступный конструктор требует наличия одного параметра. Следо- Следовательно, нельзя создать экземпляр объекта Authenticator, не указав начальный пароль. Код находится в примере Authenticated. Выше было упомянуто, что если в классе не определить ни одного конструктора, компилятор неявно создаст конструктор по умолчанию. Однако это происходит только в том случае, если не было явно объявлено вообще ни одного конструктора. В приведенном примере мы явно определили конструктор с одним параметром, так что компилятор предположит, что это единственный требуемый конструктор, и не будет неявно создавать никакие другие. Следует упомянуть, что того же эффекта можно добиться, объявив конструктор без параметров как private или protected, так что он не будет виден коду в других классах: public class Authenticator { f private'' Authenticator () { Password = "ERRE34dj"; } public Authenticator(string initialPassword) { Password = initialPassword; private string Password; private static uint minPasswordLength = 6; Такие конструкторы можно использовать внутри класса или (если они объявлены как protected) в производных классах. Это может быть полезно, например, для метода Clone () или Сору () либо для аналогичного метода в классе, которому требуется созда- создавать другие экземпляры данного класса. Другие применения конструкторов До сих пор единственное, что мы делали в конструкторах,— инициализировали значения полей. Во многих случаях больше ничего и не требуется. Однако конструктор работает как обычный метод, поэтому внутри него можно использовать любые инструкции. На- Например, для вычисления начальных значений полей можно выполнить какие-нибудь рас- расчеты. Если класс инкапсулирует доступ к файлу или базе данных, то конструктор может попытаться открыть файл. Единственное, что необходимо помнить, это то, что конст- конструктор не может возвращать какое-либо значение (например, показывающее результат выполнения операции) вызывающему коду. Другой пример использования конструктора — подсчет числа экземпляров класса, со- созданных во время выполнения программы. Если бы требовалось реализовать эту возмож- возможность для класса Authenticator, то мы создали бы статическое поле nlnstancesCreated и изменили бы код конструктора следующим образом: public class Authenticator { 'private static iunt nlnstancesCreated - 0;. . public Authenticatorlstring initialPassword)
144 Глава 5 ++nlnstancesCreated; .. Password = initialPassword,- private string Password; private static uint minPasswordLength = 6; Этот пример приведен здесь больше для демонстрации гибкости, получаемой при со- создании собственных конструкторов, нежели из-за его практической ценности. Подсчет числа экземпляров вряд ли является тем, что вы пожелаете делать в окончательных вер- версиях кода, но это можно использовать при отладке. В следующей главе мы покажем, как отмечать участки кода, которые будут компилироваться только для отладочных версий программы. Статические конструкторы Для класса можно также написать статический конструктор без параметров. Такой конструктор будет исполнен всего один раз, в отличие от конструкторов экземпляров (см. выше). Выполняется он при создании объекта данного класса. Статический конст- конструктор может понадобиться для инициализации статических переменных. Например, до сих пор мы использовали жестко установленное начальное значение, равное 6, для (статической) минимальной длины пароля в классе Authenticator. Если потребовалось бы считать это значение из базы данных или каким-то образом вычислить его, то нужно было бы инициализировать длину в статическом конструкторе: class Authenticator { static Authenticator() { // Рассчитаем минимальную длину пароля minPasswordLength = 6; // или присвоим ей какое-то значение } public Authent:cator() { Password = "ERRE34dj"; } public Authenticator(string initialPassword) { Password = initialPassword; private string Password; private static uint minPasswordLength; Отметим, что статический конструктор не имеет модификатора доступа. Он никогда не вызывается кодом С#, а только самой .NET при загрузке класса, поэтому модификато- модификаторы доступа, такие как public или private, были бы совершенно бессмысленными. По той же причине статический конструктор не принимает никаких параметров, и может су- существовать только один статический конструктор на весь класс. Также должно быть оче- очевидно, что статический конструктор может осуществлять доступ только к статическим полям класса. Отметим, что в приведенном выше коде присутствует конструктор без параметров, который прежде был удален. Это сделано для иллюстрации того, что конструктор без па- параметров легко уживается в классе вместе со статическим конструктором. Это единст- единственный случай, когда имеются два перегруженных "метода" с одинаковым списком аргументов. Здесь не наблюдается конфликта, так как статический конструктор выпол- выполняется при загрузке класса, а конструктор экземпляра — при создании соответствующего экземпляра, поэтому не возникнет никакой путаницы, когда и какой конструктор должен выполняться. Необходимо отметить, что при наличии нескольких классов, имеющих статические конструкторы, не известно, какой из них будет выполнен первым. Это означает, что в статический конструктор не следует помещать код, который зависит от выполнения дру- другого статического конструктора. Также не гарантируется, когда именно будет вызван статический конструктор, ясно только, что это произойдет до того, как будет создан хотя бы один экземпляр класса или осуществлен доступ к любому из статических полей класса. С другой стороны, если статическим полям присваиваются значения по умолчанию, это выполняется до вызова статического конструктора.
Объектно-ориентированный С# 145 Константные поля и поля только для чтения Говоря о конструкторах, следует упомянуть о возможностях, предлагаемых языком С# для определения полей, которые нельзя изменить. Для указания того, что поле должно рассматриваться как константа и его значение не может изменяться, служат два ключе- ключевых слова: const и readonly. В последующих примерах мы часто будем использовать открытые поля. Это нормально, так как поля const и readonly являются постоянными величинами, а не переменными, поэтому требование хорошего стиля программирования, чтобы поля были закрытыми, не так строго К полям const и readonly. Константные поля Поле, объявленное как const, не является переменной — оно трактуется как фиксированное жестко установленное значение в программе. Для примера добавим жестко установленную максимальную длину пароля в класс Authenticator: -public, const, uint MaxP'asswordLength =, 20; MaxPasswordLength является константой, поэтому ее значение нельзя изменить. Лю- Любая попытка присвоить ей какое-либо значение обернется ошибкой на этапе компиляции. Это также означает, что ее значение должно быть установлено при ее определении. Код • Bubl'ic const uint MaxPasswordLength; // Неверно вызовет ошибку компиляции, так как значение MaxPasswordLength не определено и не может быть задано в дальнейшем. Другая особенность константных значений в том, что они неявно статические. Величи- Величина является постоянной по определению, поэтому нет необходимости хранить ее копии для каждого экземпляра класса. Это означает, что на нее всегда можно ссылаться, используя синтаксис статического члена, т.е. указывая имя класса, а не конкретного экземпляра: .» Authenticator jylian = new Authenticator U ; / uint. Maxl = AuthenticatQr.inaxPassworciLength; // Верно 'uint; Max2 ~- Juliari-maxPasswordLength; // Неверно, возникнет /,/ ршибка компиляции. Как это ни странно, но, хотя величины const неявно статические, синтаксически не- некорректно явно объявлять их следующим образом: ;*. public const" uint .niaxPasswordLenjgth --= 20; // Верно "ptjb'lic static 'const uint maxPasswordLength = 20; 11 Неверно, возникнет // ошибка компиляции Поля только для чтения Ключевое слово readonly допускает, что поле будет константным, но для определения его начального значения необходимо выполнить некоторые вычисления. Правило заключает- заключается в том, что вы можете присваивать значение полю readonly только внутри конструкто- конструктора, но нигде больше. Поле readonly может быть полем экземпляра, а не статическим полем, и иметь различные значения для каждого экземпляра класса. Это означает, что, в отличие от поля const, для того чтобы поле readonly было статическим, его необходимо явно объявить таковым. Если требуется, чтобы класс Authenticator читал (фиксированную) величину макси- максимальной длины пароля из базы данных, то это поле необходимо объявить как readonly, а не const. Поскольку эта величина одинакова для всех учетных записей, она также бу- будет статической и определяться в статическом конструкторе. В самом деле, компилятор выдаст ошибку, если попытаться установить значение статического поля readonly где-нибудь, кроме как в статическом конструкторе. Мы сделаем так: // Authenticator 6 public class Authenticator { .public static readonly uint MaxPasswordLength; static Authenticator() f. // Читаем значение из база данных. Допустим, что результат равен 20. -ч MaxPasswordLength- « 20; ■ ■. ■ , ■ . - . ■
146 Гловоб minPasswordLength = 6; } Приведем пример поля readonly, не являющегося статическим. Предположим, что каждая учетная запись имеет связанную с ней дату создания, которая также читается из базы данных. Дата создания показывает, когда учетная запись была первоначально созда- создана, т.е. то, что вряд ли придется изменять, поэтому поле отмечено как readonly. Вот как выглядит значимая часть определения класса Authenticator с добавлением даты созда- создания. Кстати, отметим, что дата создания представляется с помощью экземпляра базового класса System.DateTime. В данном коде используется конструктор System.DateTime, который принимает три параметра (год, месяц и день месяца): // Authenticator 7 public class Authenticator { private string Password; private static uint minPasswprdLength; public readonly bate'Bime * CreationDate; ' -' // Отметим, что используется запись в стиле Pascal, так как поле // объявлено как public public static readonly uint MaxPasswordLength; static Authenticator() { // Читаем значение из базы данных. Допустим, что результат равен 20. MaxPasswordLength = 20; minPasswordLength = 6; } public Authenticator() { // Читаем дату создания из файла. Допустим, что получек результат // 1 января 2000, ■// но это значение может., быть различным, для разных" экземпляров класса, CreationDate *= new DateTi)ne{2000, 1, 1); ' "■" II В действительности пароль также будет прочитан из базы данных. // Допустим, что паролем является "ERRE34dj" Password = "ERRE34dj"; } public Authenticator(string initialPassword) \ 6" ' II Читаем- дату создания" из файла. Допустим^, что -;пояуяев результат ; . //, I января 2000.» " * - ., ' .CreationDate ^Vriew DateiismeB0Q0., 1, X) s . - Password = initialPassword; } Заметьте, что мы отдельно инициализировали CreationDate для конструкторов экзем- экземпляров. Это важно, так как для конструирования конкретного экземпляра Authenticator может быть использован любой конструктор, а мы не хотим, чтобы для некоторых эк- экземпляров CreationDate осталось неинициализированным! В следующем разделе будет показано, как сделать это более элегантно путем вызова одного конструктора из другого. Помимо этого, CreationDate трактуется как любое другое поле экземпляра, за исключе- исключением того, что, поскольку оно readonly, ему нельзя присвоить значение за пределами конструктора экземпляра. Приведенный выше пример для полей readonly содержится в Authenticator7. Также следует заметить, что не требуется присваивать значение свойству readonly в конструкторе. Если не сделать этого, свойство будет иметь значение по умолчанию для данного типа или то значение, которое было присвоено ему при объявлении. Это каса- касается как статических полей readonly, так и полей экземпляра.
Объектно-ориентированный С# 147 Кстати, при использовании концепции чтения данных из базы данных наш класс Authenticator становится совершенно непонятным, и выглядит как запутанная смесь полей. При дальнейшей его разработке необходимо было бы решить, для чего предназначен этот класс. Например, в конструктор пришлось бы передавать имя пользователя, чтобы он знал, для какого пользователя читать аутентификационпую информацию из базы данных! Конструктор с одним параметром, принимающим пароль, тоже выглядит странно - он нам больше не нужен, поскольку пароль должен быть получен из базы данных. Вызов конструкторов из других конструкторов В данной части мы внесем еще одно изменение в класс Authenticator. Состояние на- наших конструкторов экземпляров пока неудовлетворительно. Есть два конструктора, ко- которые независимо друг от друга содержат один и тот же код (помечен комментарием "здесь содержится код") для чтения даты создания из файла. Такой тип дублирования кода является опасной программистской практикой, поскольку может возникнуть ситуа- ситуация, когда один из конструкторов будет обновлен, а другой — нет. Разумеется, можно обойти эту проблему, инкапсулируя общий код в методе, который будет вызываться из обоих конструкторов, но не проще ли было бы вызывать один конструктор из другого? На самом деле мы имеем ситуацию, когда конструктор принимает одно значение — па- пароль, который может иметь значение по умолчанию. Встретившись с аналогичной проб- проблемой в перегруженных методах, мы решили ее за счет использования перегруженного метода с меньшим числом параметров, который устанавливал значение по умолчанию и затем вызывал другой перегруженный метод. То же самое допустимо и в случае конструкторов, однако необходимо использовать специальный синтаксис, так как не предполагается, что вы будете вызывать конструкторы напрямую. Вот как это делается (код доступен в примере Authenticator8): >'-, public- Authent'icafcor.O, . -г: : ■ th'M.(?ERKE3&j'-J . =;>•■ ' ■■ >i--- -■■ • '■• * ■ : ••'.: ,Sj,,. .;•*... ...■.., ■. л t - • " '■ \ . - // Друго2£ код р;гсу1гстйучат:., '.однако его можно, дописать ^ \ '-!;"/'/ щк. ^необходимости - " ', " ! с' . ;}'-Г ■': ' ""' . •-'■ ''".--.' " - . . s' -. public Authenticator(string initialPassword) { // Читаем дату создания из файла. Допустим, что получен результат // 1 января 2000. CreationDate = new DateTimeB000, 1, 1); Password = initialPassword; } Выше упоминалось, что this используется в редких случаях, когда требуется сослать- сослаться на содержащий объект. Это один из таких случаев. Сразу за объявлением конструкто- конструктора, но до открывающей фигурной скобки, обозначающей начало его реализации, помещаются двоеточие и строка, сообщающая о том, что сначала должен быть вызван другой конструктор. Конструктор указан с помощью ключевого слова this, за которым следуют параметры в скобках. Заметьте, что точка с запятой отсутствует. Необходимо сказать, что этот синтаксис не дает такой гибкости, как перегруженные методы, для которых можно вызвать другой перегруженный метод в любой части кода. Для конструкторов может быть указан только один из них, причем он всегда будет выпол- выполняться первым. Не существует способа, позволяющего сначала произвести какие-то дейст- действия, затем вызвать конструктор и после этого снова выполнить какие^го действия. Из-за этих ограничений придется внимательно продумать организацию межконструкторных вызовов для конкретного класса. Отметим, что приведенный пример иллюстрирует стан- стандартный способ, с помощью которого можно реализовать конструкторы с параметрами по умолчанию. Конструкторы производных классов Мы изучили, как конструкторы работают для простых классов и как классы могут наследо- наследоваться от других классов. Возникает интересный вопрос: что происходит, когда вы начи- начинаете определять свои собственные конструкторы для классов, которые расположены в иерархии и унаследованы от других классов, которые также могут иметь свои конструкто- конструкторы. Нас это не беспокоило при разработке npHMepaMortimerPh@ReS для иллюстрации
148 Глава 5 наследования — мы не определяли в этом примере вообще никаких конструкторов, а по- поручили компилятору создать конструкторы по умолчанию. Когда компилятор создает все конструкторы по /молчанию, иерархия классов работает отлично, и каждое поле в каж- каждом классе инициализируется своим значением по умолчанию. Однако как только мы добавляем свой собственный конструктор, то начинаем управлять конструированием по всей иерархии. Поэтому мы должны быть уверены в том, что нечаянно не сделаем чего-нибудь, что нарушит процесс конструирования иерархии. Почему же возникает особая проблема с порожденными классами? Дело в том, что при создании экземпляра производного класса на самом деле работает не один конструк- конструктор. Конструктор создаваемого экземпляра класса сам по себе недостаточен для его ини- инициализации — должны быть вызваны и конструкторы базовых классов. Вот почему мы говорим о конструировании по всей иерархии. Для пояснения того, зачем нужно вызывать конструкторы базовых классов, вернемся к примеру MortimerPhones. (Иерархия классов для этого примера приведена в главе 4.) Посмотрим, что происходит, когда используется оператор new для создания экземпляра Nevermore60Customer: GenericCustomer Arabel = new Nevermore60Customer () ; Очевидно, что все поля Nevermore60Customer должны быть инициализированы в конструкторе Nevermore60Customer. Если мы вернемся к более раннему примеру в этой главе, то обнаружим, что в Nevermore60Customer определено одно такое поле: class Nevermore60Custcmer : GenericCustomer . { . private uint highCostMinutesUsed; Здесь нет проблем, конструктор по умолчанию Nevermore60Customer, созданный компилятором, инициализирует его значением по умолчанию для uint, т.е. нулем. Однако это не единственное поле, которое необходимо инициализировать. Есть еще два поля, определенных в базовом классе GenericCustomer: abstract class GenericCustomer { private, string name; protected decimal balance; Как они инициализируются? Из кода видно, что Nevermore60Customer не может инициализировать эти значения. Поле name объявлено как private, следовательно, производные классы не имеют к нему доступа. Поэтому конструктор по умолчанию Nevermore60Customer даже не будет знать о том, что это поле существует. Единствен- Единственные, кому известно об этом,— члены GenericCustomer, значит name должно инициали- инициализироваться конструктором по умолчанию GenericCustomer. Неважно, насколько велика ваша иерархия классов, те же самые рассуждения применимы вплоть до самого верхнего базового класса, System.Object. Теперь посмотрим, что происходит при использовании конструкторов по умолчанию. Сначала компилятор берет конструктор того класса, для которого он пытается создать эк- экземпляр, в данном случае это Nevermore60Customer. Конструктор по умолчанию для Ne- vermore60Customer пытается выполнить конструктор для своего базового класса, т.е. для GenericCustomer (конструкторы по умолчанию всегда ведут себя так). В свою очередь конструктор GenericCustomer пытается выполнить конструктор своего базового класса, т.е. System. Root. System. Root не имеет базовых классов, поэтому выполняется его конструктор, а затем управление передается конструктору GenericCustomer. Теперь выполняется этот конструктор, присваивая name пустую строку, a balance — нуль, а за- затем управление передается конструктору Nevermore60Customer. Он выполняется, ини- инициализируя highCostMinutesUsed нулем, и завершает работу. В результате экземпляр Nevermore60Customer успешно сконструирован и инициализирован. Таким образом, конструкторы вызываются в порядке от System. Obj ect и далее вниз по иерархии до тех пор, пока не будет достигнут класс создаваемого экземпляра. Отме- Отметим также то, что в этом процессе каждый конструктор занимается инициализацией по- полей своего собственного класса. Все должно работать именно так, и когда вы начинаете добавлять свои собственные конструкторы, вы должны по возможности придерживаться этого принципа. Кстати, важен порядок обхода иерархии. Это означает, что при необходимости кон- конструктор производного класса может в своей реализации вызвать те методы, свойства и другие члены базового класса, которые ему позволено использовать, будучи уверенным в том, что базовый класс уже был сконструирован и его поля инициализированы. Это
Объектно-ориентированный С# 149 также означает, что если производному классу не понравится способ инициализации ба- базового класса, то он сможет изменить начальные значения данных при условии, что он имеет к ним доступ. Однако хороший стиль программирования рекомендует избегать воз- возникновения такой ситуации и разрешать конструктору базового класса самому работать со своими полями. Теперь попробуем добавить свои собственные конструкторы. Добавление конструктора без параметров в иерархию Начнем с простейшего случая и посмотрим, что произойдет, если заменить конструктор по умолчанию где-нибудь в иерархии другим конструктором, который не принимает па- параметров. Предположим, что начальным значением имени должно быть <no name>, а не пустая строка. Изменим код в GenericCustomer следующим образом: public abstract class GenericCustomer { private string name; protected decimal balance; public' GenericCustomer() ' : : base О // Эту строку можно опустить без каких-либо // последствий для откомпилированного кода { name = "<no name>"; } Nevermore60Customer по-прежнему имеет конструктор по умолчанию, а только что описанная последовательность событий будет работать так же, как и раньше, за исклю- исключением того, что компилятор будет использовать наш конструктор GenericCustomer, a не создавать конструктор по умолчанию, поэтому поле name будет инициализироваться значением <no name>. Отметим, что в нашем конструкторе мы добавили явный вызов конструктора базового класса до выполнения конструктора GenericCustomer, воспользовавшись тем же синтак- синтаксисом, который мы применили для того, чтобы заставить перегруженные конструкторы вызывать друг друга. Единственное отличие состоит в том, что сейчас мы использовали ключевое слово base, а не this, чтобы показать, что вызывается конструктор базового, а не этого класса. В скобках после ключевого слова base параметры отсутствуют. Это означает, что мы вызываем конструктор без параметров System.Object — точно так же делается по умолчанию. На самом деле эту строку можно было бы опустить и написать обычным образом: ..public. GenericCustomer () '" { j name = "<no, names-"; Г } Если компилятор не встречает ссылки на другой конструктор до открывающей фи- фигурной скобки, то он предполагает, что мы намерены вызвать конструктор базового класса — это не противоречит работе конструкторов по умолчанию. Ключевые слова base и this являются единственными допустимыми Щ ключевыми словами, которые можно указывать в строке вызова другого У конструктора. Все остальное сгенерирует ошибку компиляции. Заметим „ также, что может быть указан только один конструктор. Г™ До сих пор код работал нормально. Однако один из простейших способов нарушить перемещение по иерархии конструкторов — объявить конструктор как private: ;" 'private: GenericCustomer() { name = "<no name>"; } При попытке сделать это вы получите ошибку на этапе компиляции, которая может озадачить вас, если вы не понимаете, как работает конструирование вниз по иерархии. Сообщение об ошибке для этого примера: 'Wrox.ProfessionalCSharp.Chapter5.MortimerPhones7.GenericCustomer.GenericCus tomer()" is inaccessible due to its protection level
150 Глава 5 Что интересно, ошибка возникает не в классе GenericCustomer, а в производных классах Nevermore60Customer и PayAsYouGoCustomer (напомним, что в примере эти классы наследуются непосредственно от GenericCustomer). Компилятор попытался сге- сгенерировать конструкторы по умолчанию для этих классов, но у него ничего не получи- получилось, так как конструктор по умолчанию вызывает конструктор без параметров GenericCustomer. Объявив его как private, мы сделали его недоступным для произ- производных классов. Аналогичная ошибка возникнет, если мы снабдим GenericCustomer конструктором с параметрами, но исключим конструктор без параметров. В этом слу- случае компилятор не сгенерирует конструктор по умолчанию для GenericCustomer, поэтому, когда он попытается сгенерировать конструкторы для производных классов, он снова не сможет сделать этого, поскольку для производного класса отсутствует конст- конструктор без параметров базового класса, который можно вызвать. Обойти эту проблему можно, добавив свои собственные конструкторы в производные классы, даже если на са- самом деле ничего не надо делать в этих конструкторах, чтобы компилятор не пытался сгенерировать для них конструкторы по умолчанию. Итак, мы обсудили возможные проблемы с конструкторами и получили необходи- необходимые теоретические знания. Теперь можно перейти к практике. В следующем разделе мы добавим в пример Mortimer Phones конструкторы, которые принимают параметры. Объявление конструктора как private бывает полезно в некоторых случаях. Если у вас имеется класс, для которого никогда не потребуется создавать экземпляр, например класс, содержащий только статические члены, то можно предотвратить создание его экземпляров, сделав все конструкторы закрытыми. Однако очевидно, что это будет работать только в отсутствие производных классов. Это один из случаев, когда вы наверняка пожелаете объявить класс как sealed. Добавление конструкторов с параметрами в иерархию Добавим различные конструкторы в классы примера MortimerPhones. В результате бу- будет создан пример, который можно загрузить с сайта Wrox Press как MortimerPhones8. Обратимся к свойству Name класса GenericCustomer, которое хранит имя клиента. До сих пор это было свойство для чтения и записи, однако это не совсем разумно, поскольку обычно имена клиентов не должны меняться. (Мы не будем учитывать такие редкие слу- случаи, как женитьба клиента и т.п., и предположим, что имена остаются неизменными.) Мы хотим, чтобы имя клиента было чем-то, что устанавливается при создании экземпляра класса и затем остается неизменным. Уберем аксессор set у свойства Name — это сделает его доступным только для чтения. Можно усилить эффект, объявив соответствующее поле name как readonly. Затем потребуем, чтобы имя передавалось в конструктор, указав толь- только один конструктор, принимающий параметр. Код, реализующий эти изменения, будет выглядеть следующим образом: abstract class GenericCustomer { private., readonly..string name; :.. protected decimal balance,- " public GenericCustomer(string name) { this.дате = name; i public string Name { get { ' return name; Отметим, что в данном коде мы используем одно то же имя для параметра конструктора и для поля. Отличить эти две переменные позволяет ссылка this. Такой прием является общепринятым в С#и соответствует рекомендациям по выбору имен. Вообще говоря, имена параметров должны начинаться со строчной буквы. Соглашения по именованию обсуждаются в главе 8.
Объектно-ориентированный С# 151 Пока все было хорошо. Но это вызовет ошибку компиляции, когда компилятор по- попытается создать конструктор по умолчанию для производного класса, так как сгенери- сгенерированные компилятором конструкторы по умолчанию для Nevermore60Customer и PayAsYouGoCustomer будут пытаться вызвать конструктор без параметров GenericCus- tomer, однако в GenericCustomer такого конструктора нет. Следовательно, нам потре- потребуется реализовать свои собственные конструкторы в производных классах, чтобы избежать этого. Вот что необходимо добавить: class PayAsYouGoCustomer : GenericCustomer { '- "public. PayAsYuUGoCuetomer(stringn name) '■ :•' beu?e(name) . .« _ Л . ,,-. ' -■■ ' ■- •■ • // Здерь ничего Нет, но можно добавить код, есл?* необходимо * ' j,, ", .'"" Теперь экземпляры объектов PayAsYouGoCustomer будут создаваться только в том случае, если конструктору будет передаваться строка, содержащая имя пользователя,— это то, что нам нужно. Интересно, что делает наш конструктор PayAsYouGoCustomer со строкой. Он не может инициализировать поле name сам, так как не имеет доступа к за- закрытому полю в базовом классе. Поэтому он передает имя конструктору базового класса GenericCustomer. Он осуществляет это, указывая, что должен быть выполнен первым тот конструктор базового класса, который принимает имя в качестве параметра. Умно, да? Никаких других действий он не производит. В принципе, то же самое можно было бы сделать и с Nevermore60Customer, но для за- закрепления материала мы усложним пример. Допустим, что имеется несколько различных перегруженных конструкторов и применяется некоторая иерархия классов. Предположим, что клиенты Nevermore60Customer могли быть кем-то приведены в MortimerPhones ("приведи друга и получи скидки"). Следовательно, при конструирова- конструировании Nevermore60Customer нам может потребоваться передать имя направившего чело- человека. В реальности конструктор сделал бы с именем что- нибудь сложное, например, оформил бы скидки, но здесь мы просто сохраним имя направившего человека в другом поле readonly. Определение Nevermore60Customer будет выглядеть так: class Nevermore60Customer : GenericCustomer i ; public Nevermore6OCuStomer(string name, string referrerName) <. : base (name) this.referrerName - referrerName; ;■ } ■■ private readonly string referrerName; private uint highCostMinutesUsed; Конструктор берет имя и передает его в конструктор GenericCustomer для обработки. За переменную referrerName отвечаем мы с вами, поэтому конструктор работает с ней в своем теле. Однако не все клиенты Nevermore60Customer будут иметь имя направившего их че- человека, поэтому потребуется также конструктор, которому не нужен этот параметр (или, что одно и то же, конструктор, присваивающий ему значение по умолчанию). Вот как выглядит конструктор с одним параметром: ''; public ■SNevermore60Customer(string name) "<Nqne>") Теперь все наши конструкторы определены правильно. Проследим цепь событии, происходящих при выполнении строки: GenericCustomer Arabel = new Nevermore6OCustomer ( "Arabel Jones") ; Компилятор видит, что требуется конструктор с одним параметром, принимающий строку, поэтому он воспользуется последним из определенных нами конструкторов: public Nevermore60Customer(string name) : this(name, "<None>")
152 Глава 5 При создании экземпляра Arabel будет вызван этот конструктор. Он сразу передаст управление соответствующему конструктору Nevermore60Customer с двумя параметра- параметрами, передав ему значения Arabel Jones и <None>. Этот конструктор, в свою очередь, передает управление конструктору с одним параметром GenericCustomer, передавая ему в качестве параметра строку Arabel Jones, а тот передает управление конструктору по умолчанию System.Object. Теперь посмотрим, как будут выполнены конструкторы. Сначала исполняется конструктор System.Object; мы не можем сказать точно, что он делает, так как Microsoft, написавшая его, не предоставляет код. Затем выполняется конст- конструктор GenericCustomer — он инициализирует поле name. Затем получает управление кон- конструктор с двумя параметрами Nevermore60Customer, который устанавливает в <None> имя направившего человека. Наконец, выполняется конструктор Nevermore60Customer с одним параметром, который не делает ничего. Все это выглядит довольно сложно, но на самом деле это четкий и хорошо организо- организованный процесс. Каждый конструктор инициализирует переменные, за которые он от- отвечает. В результате экземпляр нашего класса будет корректно создан и подготовлен к использованию. Если вы будете следовать тем же принципам в процессе написания сво- своих собственных конструкторов классов, то обнаружите, что даже сложные классы ини- инициализируются ровно и без проблем. Перед завершением примера MortimerPhones8 мы должны добавить перекрываю- перекрывающий метод ToStringO в Nevermore60Customer, чтобы можно было посмотреть имя направившего человека: рцЬИс override string ToStringO -// в Nevermore60Customer 5 return base.ToString() + ", referrer: " + referrerName; Теперь можно проверить пример. Код для проверки, содержащийся в методе Main (), имеет вид: static, void MaiiHstrlngl} args) ': "GenericCustomer Arabel = new NevermoregOCustomer("Arabel Jones"); l,RecordCal-l(TypeOfCall.CallToLandline, 30); // Начало звонка 24 мая 2001, 13:15:34 !3DateTime Start = new, DateTime<2001, 5, 24, 13, 15, 24); /I Окончание авонка 24' щя- 2001, 13: №-.47 DateTirte'-End - new Datelime!2001, 5, 24, 13, 18, 47),- :Arabei.Eec,prdCaai-15ype0fCali.CaiiToLandline, Start, End)'; Cpnsole.WriteLinei'Arabel.'s details: Arabel.ToString()); Genericeustomer MrJones = new" PayAsYouGoCustomer("Mr. Jones"); ConsqlevWriteLine(MrJones.ToString() ) ; GenericCustomer- MrLeggit = new Nevermore60Customer("Mr. Leggit", MrLfe0git.RecordCall(TypeOfCall.CallToLandline, 30); Console.WriteLine(MrLeggit .ToString () ) ,- "Arabel") Код иллюстрирует вызов новых конструкторов с одним параметром и перегруженного метода GenericCustomer. RecordCal I (). При выполнении этого кода получим следующее: ГглЬеL' ^ (trt*\ils* Cir.t»*№"»r: fltMbe 1 Jon Cm ' uin v- fit1. Joiit*^., owitMj- И Custom i-1 Hi*. Ley</it , oiiincf: 0.6. vef e vetегч-еП <Hone>
Объектно-ориентированный С# 153 Уборка: деструкторы Мы знаем, что с помощью конструкторов указываются действия, которые должны быть выполнены при создании экземпляра класса. А нельзя ли делать то же самое при уничто- уничтожении экземпляра класса? Тем, кто знаком с C++, известно, что в C++ для этого применяет- применяется метод, который называется деструктором. Он вызывается в том случае, когда экземпляр класса выходит из зоны видимости или каким-то образом удаляется из памяти. Опытные разработчики на C++ широко используют деструкторы, причем не только для освобожде- освобождения ресурсов, но и для вывода отладочной информации и выполнения других задач. С# не поддерживает такой стиль программирования. Деструкторы существуют в С#, однако из-за применения системы сборки мусора невозможно предсказать, когда они бу- будут запущены, и их использование не приветствуется. Поскольку в С# за освобождение ресурсов отвечает сборщик мусора, обычно вообще не возникает необходимости в дест- деструкторах, т.е. программисту не требуется явно иметь дело с освобождением памяти. Как правило, нет нужды беспокоиться о том, что происходит при уничтожении экземпляра класса,— сборщик мусора незаметно для ваших глаз сам найдет выход из ситуации. Однако все же в ряде случаев может потребоваться выполнить некоторые действия перед тем, как переменные будут уничтожены (например, закрыть файлы), и С# предо- предоставляет для этого два способа: метод Dispose () или Close () и метод Finalize (). Эти методы созданы для совместной работы. FinalizeO Из всех вариантов освобождения ресурсов метод Finalize () наиболее близко соответст- соответствует концепции традиционного деструктора. Если вы объявите в классе метод Finalize (), он будет автоматически вызываться при уничтожении экземпляра класса. Однако имеется пара препятствий: О FinalizeO является недетерминированным. Это означает, что не возможно предсказать, когда экземпляр класса будет уничтожен, т.е. нет способа выяснить, когда будет вызван FinalizeO. Экземпляры будут уничтожаться тогда, когда сборщик мусора определит, что больше не существует ссылки на них. Однако сам сборщик мусора начинает действовать тогда, когда решит среда исполнения .NET,— и это является прерогативой .NET, а не вашей программы. Следователь- Следовательно, в метод Finalize () нельзя помещать код, который должен выполняться в ка- какое-то определенное время. Кроме того, не следует рассчитывать на то, что FinalizeO будут вызываться в каком-то определенном порядке для различных экземпляров класса. Запустить сборщика мусора в определенной точке программы можно с помощью метода System.GC.Collect (). System. GC является базовым классом .NET и представляет сборщика мусора, а метод Collect О вызывает сборщика мусора. Однако это делается в редких ситуациях, когда вы точно знаете, что полезно запустить сборщика мусора, например, если только что была закончена работа с очень большим числом объектов. Не станете же вы запускать процесс сборки мусора только для того, чтобы вызвать метод Finalized! О Не рекомендуется реализовывать метод Finalize (), если только класс действи- действительно не нуждается в нем. Если объект реализует Finalize (), это наносит силь- сильный удар по производительности при сборке мусора для данного объекта. Кроме того, задерживается окончательное удаление объекта из памяти. Сборка мусора работает таким образом, что объекты, для которых необходимо вызвать метод Finalize (), помещаются в специальную очередь для завершения работы с ними. При запуске сборщик мусора не уничтожает объекты, находящиеся в этой очере- очереди. Вместо этого вызываются их методы Finalize (), и только потом они удаля- удаляются из очереди, следовательно, они не будут уничтожены вплоть до следующего запуска сборщика мусора. Практически во всех случаях освобождение ресурсов средой исполнения .NET явля- является наилучшим решением, и метод FinalizeO реализовывать не требуется. В основ- основном метод Finalize () нужен тогда, когда объект работает с ресурсами, которыми .NET не управляет,— например, файлы и соединения с базами данных. В этом случае необхо- необходимо реализовать FinalizeO для того, чтобы закрыть эти соединения. Если вы дейст- действительно хотите определить метод FinalizeO, то используйте специальный синтаксис, который соответствует определению деструктора в C++: указывается имя класса с тильдой (-) перед ним:
154 class MyClass { ■-" -MyClass'C) ™. "* II выполняем очистку } // и т.д. Этот синтаксис похож на синтаксис статического конструктора. Нет параметров, нет возвращаемого значения и нет модификатора доступа. Очевидно, что необходимо заменить MyClass именем вашего класса. Не требуется явно вызывать метод Finalize О базового класса — компилятор учтет это и вызовет его неявно сам. Реализация метода Finalize () гарантирует, что указанные ресурсы в некоторый мо- момент будут освобождены. Однако это само по себе неудовлетворительно по причине от- отсутствия детерминизма и отрицательного воздействия на производительность сборщи- сборщика мусора. В частности, поскольку сборщик мусора не будет вызван в течение еще некоторого времени после завершения работы с объектом, ресурсы, использовавшиеся объектом, будут заняты дольше, чем это необходимо. Если ресурс включает, например, монопольную блокировку файла, это может превратиться в серьезную проблему. Здесь на сцену выходят Dispose () и Close (). DisposeO и CloseO Эти методы являются скорее соглашением, чем частью языка С#, хотя в С# имеется неко- некоторая встроенная поддержка Dispose (). Они работают таким образом, что при желании вы можете определить один из них для класса, понимая при этом, что клиентский код обязан явно вызвать этот метод для освобождения ресурсов. Ресурсы освобождаются сра- сразу после того, как отпадает надобность в них, что является большим преимуществом при наличии, например, монопольной блокировки файла. Недостаток же заключается в том, что вы полагаетесь на то, что клиентский код будет вести себя корректно и вызовет этот метод; однако этого может и не произойти. Типичное решение — использовать Dispose () и Close () совместно с Finalize (). Необходимо сделать следующее: public void Dispose 0 ■* '• v4 - '■ II освобождаем. ресурсы - GC.SuppressFinaiize(tbis); •\ ■». protected override void Finalize О ¥■ ' ■ '■■ , ■ ■ ■ • s!,j // освобождаем ресурсы ■ '■?',■■ ... ; .'*' base.Finalize(),; .'■..,..., В этом случае у вас появляется запасной вариант. Если клиентский код не забудет вы- вызвать Dispose (), ресурсы будут освобождены вовремя. Если же — забудет, то Finalize () будет вызван при сборке мусора. Обратите внимание на вызов GC. SuppressFinalize () в методе Dispose(). Метод SuppressFinalize () из класса System.GC информирует ере ду исполнения .NET о том, что для объекта, переданного в качестве параметра, не нужно больше вызывать Finalize (). Это означает, что сборщик мусора будет трактовать такой объект так, будто у него нет метода Finalize (). В результате Finalize () не будет вызван, что позволит избежать связанных с этим потерь производительности. Отличие CloseO от DisposeO Разница между CloseO и Di spose () состоит главным образом в соглашении по их при- применению. Close () предполагает, что ресурс может быть позже открыт снова, в то время как вызов Dispose () означает, что клиент закончил работу с этим конкретным ресурсом навсегда. Можно реализовать как один, так и оба метода, но не запутайте других разра- разработчиков. Вы можете реализовать Close () в ситуациях, в которых этот метод является традиционной практикой программирования или соответствует традиционной терми- терминологии — например, закрытие файла или соединения с базой данных. Dispose () следу- следует применять, например, для освобождения дескрипторов различных объектов GDI или Windows. Этот метод вам потребуется также в том случае, если вы пожелаете воспользоваться преимуществами архитектуры IDisposable.
Объектно-ориентированный С# 155 IDisposable С# предлагает синтаксис, который можно использовать для гарантии того, что Dispose () (но не Close ()) будет вызван для объекта автоматически, когда ссылка на него выйдет из области видимости. Синтаксис применяет ключевое слово using — хотя теперь в совер- совершенно ином контексте, который не имеет ничего общего с пространствами имен. Предпо- Предположим, что есть некий класс, назовем его ResourceGobbler, который полагается на использование некоторого внешнего ресурса, и мы хотим создать экземпляр этого класса. Мы могли бы сделать это следующим образом: :;ResourceC3qbbij3r, Thelnst&nce = pew ResourceGobbler (); ' . П обработка $- ■'-''-"' * e'iDispose () ; Код заключен в скобки. Они необязательны, однако таким образом мы подчеркива- подчеркиваем, что после вызова Thelnstance. Dispose О мы заканчиваем работу с Thelnstance, поэтому не остается больше мест, где ссылка на Thelnstance была бы видима. Приведенный код в точности соответствует следующему: using ,(ResourceGobbler Thelnstance <= ney» ResourceGobbler {=)) \ '. "■' '' ' * t' ft обработка if- .■ : . -. Выражение using, за которым в скобках следует определение переменной по ссыл- ссылке, поместит эту переменную в область видимости, задаваемую фигурными скобками. Кроме того, когда переменная выйдет из этой области видимости, будет автоматически вызван ее метод Dispose (). Синтаксис using имеет недостаток, который заключается в необходимости записи кода с отступами, и сам по себе не упрощает программирование. В главе 6 будет показано, что аналогичного эффекта можно достичь, поместив вызов Dispose!) в блок finally. Следовательно, можно не пользоваться этой конструкцией. В качестве альтернативы синтаксису using определим класс ResourceGobbler иным образом. Чтобы сделать это, забежим немного вперед, так как нам придется воспользо- воспользоваться интерфейсом. Ограничение заключается в том, что класс ResourceGobbler необ- необходимо унаследовать от интерфейса IDisposable, который определен в пространстве имен System: classfResouEceGofibler : IDinposabie i* iff. 'и ж-д., и public void Dispose() 'C It И, Т.Д. } T } Наследование интерфейса несколько отличается от наследования класса (см. ниже). Наследование от IDisposable вынуждает производный класс реализовать метод Dispo- Dispose () — если это не будет сделано, возникнет ошибка на этапе компиляции. Причина, по которой использование этого синтаксиса требует наследования от IDisposable, заклю- заключается в том, что в этом случае компилятор сможет убедиться в том, что объект, опреде- определенный в выражении using, имеет метод Dispose!), который он сможет вызвать автоматически. Наследование интерфейсов является обычным способом, с помощью ко- которого класс может указать, что он реализует определенные особенности. Пример: DestructorDemo На примере DestructorDemo мы покажем, как создать надежный деструктор. В примере используется класс DataStoreConnection, который служит для выполнения таких задач, как соединение с некоторым (не указанным) хранилищем данных. На самом деле мы не будем соединяться с каким-либо хранилищем данных, но смоделируем это действие с помощью поля DataStoreHandle, имеющего тип int. Оно принимает значе- значение 1, когда мы соединены с хранилищем, и 0 в противном случае. Также имеется поле
156 , Глава 5 CanOpen типа bool, первоначальное значение которого равно true. Оно устанавливает- устанавливается в false после вызова Dispose (), показывая тем самым, что объект больше не может использоваться для соединения с хранилищем данных. DataStoreConnection реализует и Close (), и Dispose (). Полное определение класса выглядит следующим образом: class DataStoreConnection : IDisposable private int DataStoreHandle = 0; private bool .CanOpen = true,- private readonly string naine'j public DataStoreConnection(string name) • this.name = name; public void Open() '( if (CanOpen == false) Console.WriteLine(name + ": Error; Attempt to Open after calling Dispose ()"); ri- fDataStoreHandle ==0) { - -. ^ PataStoreHandie, ~ 1; , j Console :writfeLiiie( name + ": Connected" to DataStpre") ; • i. ■-« •->*■.,,..■ •> ' ■ ,:■--■•■ tv - ■-,>-' " r : ■ Л--1" : . • - else ■Console .WflteLirie4narhe *-_ ' "': Error; Already connected to DataStore"); '}. public void Closed X DataStoreHandle' =, 0; } public void Dispose!' * ConsoXe^WrlteLihe<"Disposing1: " ♦ name); = false; .- ■ GC.SupJ>ressFinalize<this) ; -patastoreCOnnection() : Close E ; Конструктор этого класса принимает один параметр — строку. Это может быть имя хра- хранилища данных, с которым мы соединяемся. В нашем примере мы используем его в качест- качестве строки, которая отображается для идентификации экземпляра объекта при выводе сообщения на экран. Open () проверяет, можно ли открыть соединение, и открывает его в случае положи- положительного результата. Он выводит сообщение о том, что метод был вызван. С другой сто- стороны, Close () эмулирует закрытие соединения, устанавливая поле DataStoreHandle в нуль. Отметим, что, если этот метод вызвать дважды, ошибки не произойдет,— это реко- рекомендуемое поведение. Dispose () делает то же самое, что и Close (). Фактически он реа- реализован в виде вызова Closet). Однако он также отменяет финализацию, поскольку вызов Dispose () означает, что мы полностью закончили работу с объектом. Dispose () устанавливает поле CanOpen для того, чтобы показать, что, начиная с этого момента, бо- больше невозможно открыть соединение с источником данных из этого объекта. Это оправданно, так как мы отменили финализацию и, следовательно, потеряли защитный механизм, который учитывал возможность того, что клиентский код откроет соединение с источником данных и не закроет его явно. Наконец, метод Finalize О (деструктор) закрывает соединение. Отметим, что мы не используем метод Close () для отображения каких-либо сообщений. Дело в том, что он может быть вызван из Finalize (), а среда исполнения .NET не позволит выводить
Объектно-ориентированный С# 157 сообщения на консоль из этого метода. При попытке сделать это возникнет ошибка вре- времени исполнения, так как Finalize () запускается под управлением сборщика мусора, который не имеет доступа к окну консоли. Протестировать соединение можно с помощью кода: static void Main(string[J args) ? DataStoreCorinection Connection = new DataStoreConnectiont"FIRST"); { Conception.Open(); Connection.. Close (); j. Connect ion. Di epose О ; 9 ppnfeole^ WriteCine(); Connection. = new DataStoreConnectiont"SECOND"); J Connect ion.Close(); i Connect ion.Open(); , Connection.Open(}; Connection;Dispose(); ; Connection>Open(); using (DataStpreConneCtion Connection2 = new DataStoreConnectiont"THIRD")) Connection2.Open(); > > ■ J- GC.Collect О ; Отметим, что в этом коде мы вызвали сборщика мусора в конце программы для того, чтобы продемонстрировать, как это делается. На практике вы вряд ли будете так делать, если только вы не уверены в том, что имеется большое количество неиспользуемых объ- объектов, которые необходимо удалить из памяти. Вызывать сборщика мусора перед выхо- выходом из программы бессмысленно, так как все управляемые ресурсы, связанные с кодом, будут автоматически освобождены при этом в любом случае. Результат выполнения кода: Lrnt-rt Uintf u-. 7.1ЭЙ0 КЧ i": ion 5.00.219S) ■ > C,->,;,■ i.,1.1. 1V8S ?.-)«« Mi. ros-lt Ci.i-p. t.» DrttiKt 10 Ui-:iir!.'. ii у. ПНЬТ y Г i i>t,- ir.j: M УЯМ T'.l'Nn: firm-: ntt. n>it 1.1 Oprn лГ<п- cil'iin Pispo-rO 1 Л : C'tiiiciip.l tn Г)^(т^Г» or<; 14 11,0: OrnnfcneH to Dal:-."' - 11? Г 'in ;. .n;<= Till.-D Переменные по ссылке и по значению: что происходит в памяти Одно из преимуществ программирования на С# заключается в том, что программисту не требуется беспокоиться об управлении памятью,— сборщик мусора осуществляет очист- очистку памяти за вас. В результате вы получаете что-то близкое к эффективности языков типа C++, но без сложностей, связанных с ручным управлением памятью. Однако если вы же- желаете писать эффективный код, то все же должны понять, что творится за сценой. В дан- данном разделе мы посмотрим, что происходит в памяти компьютера при размещении в ней переменных. Необходимо подчеркнуть, что большая часть предлагаемого материала не документирована. Эта глава должна рассматриваться как упрощенное руководство, а не точное описание реализации схемы управления памятью.
158 Глава 5 Типы данных по значению Начнем с обсуждения переменной типа по значению. Для примера возьмем целое число. Посмотрим, что происходит в памяти при выполнении следующих строк кода: Л. i :, int NRacingCars = 1С; Rouble EngineSize = 3000.0; // производим расчеты Г }''■'** Напомним,, что к типам данных по значению относятся предопределенные типы данных, такие как int, float, bool и т.п., но не строки или объекты. Ниже мы рассмотрим структуры; они также являются типами по значению. Напротив, все классы и массивы - это типы данных по ссылке. В данном коде мы говорим компилятору, что требуется место в памяти для хранения целого числа и значения double и что доступ к этим областям памяти будет осуществля- осуществляться соответственно под именами NRacingCars и EngineSize. Строка, объявляющая переменную, показывает точку, с которой должен обеспечиваться доступ к перемен- переменной, а закрывающая фигурная скобка показывает точку, начиная с которой перемен- переменные нам больше не требуются. Выражаясь программистским языком, переменные попадают в область видимости и выходят из области видимости. Разумеется, имена NRacingCars и EngineSize записываются так. чтобы быть понятными человеку, а не компьютеру. (Дейст- (Действительно, имена настолько дружественны к пользователю, что даже сами по себе дают хорошее представление о том, что дела- делает программа!) Внутри компьютер не дает ячейкам памяти име- имена типа NRacingCars. Компьютеры думают исключительно в числах, поэтому с точки зрения компьютера доступная для кода память выглядит так: Позиция 0x0 Позиция 0x1 Позиция 0x2 и т.д. Позиция Oxffffffff Другими словами, мы работаем с большим числом ячеек памяти. Исходя из особенно- особенностей управления памятью в Windows, мы скажем, что это число составляет 4 гигабайта. (Или, если быть точным, 0x100 000 000 = 4 294 967 296 байт, где Ох означает, что далее сле- следует шестнадцатеричное число. Последняя ячейка имеет адрес, на единицу меньший это- этого значения, так как нумерация начинается с нуля.) Windows использует схему виртуальной адресации памяти, в которой соответствие адресов памяти, видимых вашей программе, адресам в аппаратной памяти и на диске полностью управляется Windows. На практике каждое приложение на 32-разрядном процессоре видит 4 Гбайт доступной памя- памяти вне зависимости от того, сколько физической памяти имеется на самом деле. Для 64-разрядных процессоров это число еще больше, но для простоты допустим, что мы име- имеем дело с 32-разрядным процессором. Эти 4 Гбайт памяти известны как виртуальное ад- адресное пространство. Эта память может быть корректно описана как виртуальная память, поскольку она представляет собой некоторое управляемое Windows отображение в физическую память, а не саму физическую память, но для удобства мы будем ссылаться на нее как на плоскую память. Каждая ячейка памяти из этих 4 Гбайт нумеруется с нуля и выше. Если требуется по- получить значение, хранящееся в определенной ячейке памяти, необходимо указать но- номер, который представляет собой эту ячейку памяти. В любом языке высокого уровня, будь то С#, VB, C++ или любой другой похожий язык, одно из действий, выполняемых компилятором, заключается в преобразовании имен, читаемых человеком, в адреса памя- памяти, воспринимаемые процессором. Эти 4 Гбайт памяти на самом деле содержат все, что составляет программу, включая исполняемый код и переменные, используемые при вы- выполнении программы. Все вызванные DLL будут загружены в то же самое адресное про- пространство — каждый элемент кода или данных будет иметь свое определенное положение. Где-то внутри этой памяти имеется область, известная как стек (см. главу 3). В стеке обычно хранятся типы данных по значению. При вызове метода стек используется так- также для копирования параметров, переданных методу. Для того чтобы понять, как рабо- работает стек, необходимо отметить следующий важный факт по поводу области видимости
Объектно-ориентированный С# 159 переменных в С#. Всегда происходит так, что если переменная А попадает в область ви- видимости раньше переменной В, то В выйдет из зоны видимости первой. Рассмотрим код: int A,- / / выполняем действия { int В; // выполняем другие действия В данном коде сначала объявляется А. Затем во внутреннем блоке кода объявляется В. После этого внутренний блок заканчивается, и В выходит из области видимости. Затем А выходит из области видимости. Таким образом, время жизни В полностью находится внутри времени жизни А. Так происходит всегда. Невозможно написать фрагмент кода на С#, в котором переменная В объявлена после А. но А выходит из области видимости первой. (Если не верите, попробуйте написать такой код. Вы обнаружите, что так сде- сделать нельзя. Этому препятствует тот способ, которым С# управляет областью видимости при помощи итоженных фигурных скобок.) Отметим, что если компилятор встречает строку типа int I, J, то порядок вхождения в область видимости выглядит неопределенным.: обе переменные объявляются одновременно и выходят из области видимости тоже одновременно. В данной ситуации д.пя нас неважно, в какой последовательности эти две переменные будут удалены из памяти. Компилятор всегда будет делать так, что переменная, размещенная в памяти первой, будет удалена из нее последней, сохраняя правило о не пересечении времен жизни переменных. Идея, заключающаяся в том, что переменные удаляются всегда в обратном порядке относительно их помещения в память, может показаться несколько абстрактной, однако она является ключевой для понимания работы стека. Посмотрим, что же на самом деле происходит при объявлении переменных NRacingCars и EngineSize в нашем примере. В некотором месте программы имеется так называемый указатель стека. Это пере- переменная (или адрес в памяти), которая содержит адрес следующей свободной ячейки в стеке (хотя из-за того что эта переменная используется довольно часто, она, скорее все- всего, будет храниться в специальной ячейке, обеспечивающей быстрый доступ к своему содержимому и известной как регистр центрального процессора). При запуске програм- программы указатель стека будет указывать в конец (да, в конец) участка памяти, отведенного под стек. По мере того как данные будут помещаться в стек, соответствующим образом будет изменяться и указатель стека, поэтому он всегда будет указывать на следующую свободную ячейку. Неизвестно, где точно в адресном пространстве расположен стек,— эта информация не нужна при работе в С#. Допустим, что непосредственно перед вы- выполнением приведенного выше кода, размещающего переменные, указатель стека содер- содержит значение 0xl2FA78 (десятичное 1243768). Эта ситуация представлена на рисунке. Полужирным выделено содержимое ячеек памяти, обычным шрифтом показан адрес или описание позиции: Указатель стека 0X12FA78 СТЕК Свободно 0X12FA76 Свободно 0X12FA77 Свободно 0x12FA78 Свободно Данные в стеке
160 Глава 5 Кстати, предложенное значение для указателя стека взято не наобум. Для того чтобы грубо определить, где в адресном пространстве расположен стек, пришлось немного поиграть с указателями, воспользовавшись небезопасным кодом (см. главу 6). Оказывается, стек заполняется данными от старшего адреса вниз, поэтому указатель стека должен декремептироватъся, а не инкрементироватыя. Вот почему было подчеркнуто, что первоначально указатель стека указывает в его конец. Теперь в область видимости попадает переменная NRacingCars, которой присваива- присваивается значение 10. Это значение помещается в ячейку 0xl2FA78, на которую указывает указатель стека. Строго говоря, так как int занимает 4 байта, это значение будет разме- размещено в 4 байтах от 0xl2FA75 до 0xl2FA78, а из значения указателя стека будет вычтено 4, поэтому он укажет на следующую свободную ячейку, 0xl2FA74. Следующая строка объявляет переменную EngineSize, имеющую тип double и зна- значение 3000.0. Тип double занимает 8 байтов, значение 3000.0 будет размещено в следую- следующих 8 байтах в стеке в том формате, который процессор использует для хранения 8-байтовых чисел с плавающей точкой, а указатель стека будет уменьшен на 8. Он снова будет указывать компьютеру, где располагается следующая свободная ячейка стека. Когда EngineSize выходит из области видимости, компьютер знает, что она больше не потребуется. Тот факт, что времена переменных всегда вложены друг в друга, гаранти- гарантирует: что бы ни случилось за тот период, пока EngineSize находится в области видимо- видимости, указатель стека будет указывать на ячейку непосредственно перед теми ячейками, в которых хранится EngineSize. Процесс удаления этой переменной из области видимо- видимости очень прост. Указатель стека инкрементируется на 8 и теперь показывает на ту пози- позицию, в которой раньше размещалась EngineSize. В данный момент мы находимся на закрывающей фигурной скобке, где NRacingCars также выходит из области видимости, поэтому указатель стека снова увеличивается — на этот раз на 4. Если в этот момент в об- область видимости попадает другая переменная, она перепишет собой участок памяти, идущий вниз от 0xl2FA78, в котором хранилась переменная NRacingCars. Мы подробно рассмотрели, каким образом переменные размещаются в памяти. Но мы не коснулись, например, того, как компилятор узнает адрес каждой переменной. Здесь необходимо отметить то, насколько быстрым и эффективным является процесс размещения переменных в стеке. По сути дела, кроме инкрементирования значения ука- указателя стека — простой арифметической операции, которая требует всего нескольких тактов,— никаких действий выполнять больше не требуется. Гиль/ данных по ссылке Стек обеспечивает высокую производительность, но он недостаточно гибок для приме- применения всеми переменными. Требование того, чтобы времена жизни переменных были вложены друг в друга, служит слишком большим ограничением в ряде случав. Нередко нужно использовать некий метод для выделения памяти под некоторые данные и иметь доступ к ним еще долго после выхода из метода. Такая возможность предоставляется, если память для хранения запрашивается с помощью оператора new — как в случае всех типов по ссылке. Здесь мы сталкиваемся с понятием кучи. Если в прошлом вам приходилось писать код, который требует низкоуровневого управления памятью, то вы знаете, как использовать стек и кучу для программ. Однако управляемая куча в .NET не то же самое, что и куча, используемая, например, классиче- классическим C++. Она работает под управлением сборщика мусора и обеспечивает более высокую производительность по сравнению с традиционными кучами. Управляемая куча (для простоты просто куча) является еще одной областью памяти, доступной в 4 Гбайт. Для пояснения того, как работает куча и как распределяется память для типов по ссылке, рассмотрим следующий фрагмент кода: void DoWork() { Customer Arabel; Arabel = new Customer(); Customer MrJones = new Nevermore60Customer (); } В этом коде используется наш пример Customer, но те же самые принципы относятся к любому классу (любому типу по ссылке). Мы объявляем ссылку на Customer с именем Arabel. Место для нее будет выделе- выделено в стеке, но вспомним, что это только ссылка, а не экземпляр Customer. Объем, за- занимаемый ссылкой Arabel, составит 4 байта (столько же, сколько занимает int или
Объектно-ориентированный С# 161 uint). Почему 4 байта? Они потребуются для хранения адреса, по которому располагает- располагается экземпляр Customer. Четыре байта также нужны для хранения числа в диапазоне от О до 4 Гбайт. (Отметим, что для 64- разрядного процессора адрес будет занимать 8 байтов.) Перейдем к следующей строке: Arabel = new Customer () ; Эта строка кода выполняет несколько действий. Во-первых, она выделяет память для хранения экземпляра Customer (именно экземпляра, а не адреса). Затем переменной Arabel присваивается адрес ячейки памяти, в которой располагается экземпляр. Также будет вызван подходящий конструктор для инициализации полей экземпляра класса (здесь нас это не интересует). Экземпляр Customer будет расположен не в стеке, а в об- области памяти, известной как куча. Мы не знаем точно, сколько памяти занимает экземп- экземпляр Customer, пусть это будет 32 байта. В этом объеме памяти содержатся поля экземпляра Customer, а также некоторая информация, включая vtable, которую .NET использует для определения и управления своими экземплярами классов. Среда исполнения .NET просмотрит кучу и захватит первый непрерывный блок дли- длиной 32 байта, который в данный момент не используется. Предположим, что это проис- происходит по адресу 0хС39Е7С A2818556) и что первая свободная ячейка в стеке непосредст- непосредственно перед выполнением этого выражения находилась, как и раньше, по адресу 0xl2FA74 (эта позиция не изменяется, так как данная строка кода сама по себе ничего не размещает в стеке, а ссылка Arabel уже существует). После выполнения этой строки содержимое памяти будет выглядеть следующим образом: Указатель стека 0X12FA74 СТЕК Свободно КУЧА 0x12FA74 Свободно 0x12FA75- 0x12FA78 содержит 0хС39Е7С (ссылка на Arabel) Указатель стека 0x12FA78 Данные в стеке СТЕК Свободно 0хС39Е7С- ОхСЗЭЕЭВ содержит экземпляр Arabel 0x12FA76 Свободно 0x12FA77 Свободно 0X12FA78 Свободно Данные в стеке Следующая выполняемая нами строка кода делает в принципе то же самое, за исклю- исключением того, что место в стеке под ссылку MrJones необходимо выделить одновременно с выделением места для MrJones в куче. Customer MrJones = new Nevermore60Customer () ; Отметим, что данные в куче содержат экземпляр Nevermore60Customer. Предполо- Предположим, что при выполнении этой строки среда исполнения .NET находит свободную па- память для MrJones по адресу 0хС39644 и что объекты Nevermore60Customer занимают
162 Глава 5 в сумме 36 байтов. На момент написания книги не было документировано, как хранятся объекты в куче, поэтому для примера мы будем считать, что более поздние объекты раз- размещаются в младших адресах, просто потому, что это упрощает наш рисунок! Указатель стека 0X12FA70 СТЕК Свободно 0x12FA71 - 0x12FA74 содержит 0хС39644 (ссылка на MrJones) 0x12FA75- 0x12FA78 содержит 0хС39Е7С (ссылка на Arabel) Данные в стеке ► КУЧА 0хС39644- 0хС39668 содержит экземпляр MrJones 0хС39Е7С- ОхСЗЭЕЭВ содержит экземпляр Arabel Указатель стека 0X12FA70 СТЕК Свободно 0x12FA71 - 0x12FA74 содержит 0хС39644 (ссылка на MrJones) 0x12FA75- 0x12FA78 содержит 0хС39Е7С (ссылка на Arabel) Данные в стеке ► КУЧА Свободно 0хС39644- 0хС39668 содержит экземпляр MrJones 0хС39Е7С - ОхСЗЭЕЭВ содержит экземпляр Arabel Данные в куче Видно, что процесс размещения переменной по ссылке является более сложным, чем аналогичный процесс для переменной по значению — и, безусловно, наблюдается потеря производительности. На самом деле мы упростили ситуацию, так как среде ис- исполнения .NET придется поддерживать информацию о свободных участках памяти в куче и обновлять ее по мере того, как в кучу будут добавляться новые данные. Однако те- теперь у нас имеется более гибкая схема поддержки времени жизни переменной. Посмот- Посмотрим, что происходит, когда ссылки Arabel и MrJones выходят из области видимости. В соответствии с логикой работы стека указатель стека будет декрементирован, а перемен- переменные перестанут существовать. Однако эти переменные хранили только адреса, а не сами экземпляры. Данные для этих экземпляров классов все еще расположены в куче, где они будут оставаться до тех пор, пока не завершится программа или не будет вызван сбор- сборщик мусора. Данные находятся в куче, но мы не имеем больше к ним доступа, поскольку переменные, которые на них ссылались, больше не существуют, и именно это условие проверяет сборщик мусора для определения того, что их можно удалить из кучи. Однако посмотрим, что будет, если изменить метод Dowork () ; следующим образом: Customer DoWork(out Customer myCustomer) Customer Arabel; Arabel = new Customer!);
Объектно-ориентированный С# 163 Customer MrJones = new Customer(); myCust'omer =: Srabel; r.etprn MrJone s; Код модифицирован для возврата значения ссылки на MrJones. Кроме того, ссылка на Arabel также передается в вызывающий код с помощью out-параметра. Вообще гово- говоря, возвращаемое значение и параметры метода хранятся в стеке вместе с локальными переменными и размещаются в стеке непосредственно перед выполнением метода. Это означает, что сразу после выхода из метода память может выглядеть следующим образом: Указатель стека 0x12FA70 Это значение, возвращаемое DoWork Это параметр myCustomer СТЕК Свободно /где располагался Arabel) 0x12FA71 - 0x12FA74 содержит 0хС39644 (ссылка на MrJones) 0x12FA75- 0x12FA78 содержит 0хС39Е7С (ссылка на Arabel) Данные в стеке КУЧА 0хС39644- 0хС39668 содержит экземпляр MrJones 0хС39Е7С - ОхСЗЭЕЭВ содержит экземпляр Arabel Два экземпляра класса по-прежнему существуют в куче, а клиентский код, которому было возвращено управление, имеет доступ к их содержимому. Это знакомо тем, кто ра- работает с C++ или с другим языком, использующим эквивалент оператора new для динами- динамического размещения памяти. Экземпляр класса был создан внутри метода, но остается доступным после завершения метода, обходя обычные правила, согласно которым пере- переменная перестает существовать в точке указания фигурной скобки, закрывающей блок, в котором была определена эта переменная. Более того, мы достигли этого, не делая ника- никакой копии данных. Это сильная сторона типов данных по ссылке, которая активно при- применяется в коде С#. У нас имеется хороший инструмент управления временем жизни данных, так как они гарантированно будут существовать до тех пор, пока имеется ссылка на них. Разумеется, как только необходимость в данных, хранящихся в ссылочном типе, от- отпадает, мы позволяем всем ссылкам на них выйти из области видимости. Данные, одна- однако, сохранятся в памяти, и если объем памяти станет слишком мал, запустится сборщик мусора и удалит их. Приведенные рассуждения и рисунки показывают, что работа управляемой кучи по- похожа на работу стека в том плане, что последующие объекты размещаются в памяти друг за другом. Это означает, что несложно вычислить, куда можно поместить следующий объект, используя указатель кучи, который указывает на следующую свободную позицию в памяти и который изменяется по мере добавления в кучу новых объектов. Однако здесь имеется небольшая проблема. Как говорилось выше, стек работает столь эффек- эффективно потому, что времена жизни стековых переменных вложены друг в друга. Времена жизни ссылок выходят за пределы области видимости. Однако куча действует так, будто они следуют указанному правилу. Как такое возможно? Это происходит благодаря сбор- сборщику мусора. Он убирает все объекты из кучи, ссылки на которые отсутствуют. После этого куча будет содержать разбросанные объекты, перемешенные со свободной памятью. Это будет выглядеть примерно так:
164 Глава 5 Используется Свободно Используется Используется Свободно Если бы управляемая куча оставалась в таком состоянии, то распределение в ней по- последующих объектов было бы неприятным процессом, так как компьютеру пришлось бы искать в ней участок памяти, достаточный для хранения каждого объекта. Однако сбор- сборщик мусора не оставляет кучу в таком состоянии. Освободив все объекты, какие только можно, он уплотняет остальные, перемещая их ближе к концу кучи для формирования одного непрерывного блока. Это означает, что куча может продолжать работать как стек, если рассматривать ее с точки зрения размещения новых объектов в памяти. Разу- Разумеется, после перемещения объектов в куче необходимо обновить также все ссылки на эти объекты, чтобы они содержали новые корректные адреса. Этим также занимается сборщик мусора. Действие по уплотнению, выполняемое сборщиком мусора, отличает управляемую кучу от старых неуправляемых куч. Неуправляемые кучи на самом деле содержали бы разбросанные данные, как показано на рисунке выше. Это означает, что при динамическом размещении переменной, например, в C++, среда исполнения проходит по связанному списку адресов в поисках места, где можно разместить данные, в то время как для управляемой кучи достаточно прочитать значение указателя кучи. По этой причине создание экземпляров типов по ссылке в .NET осуществляется намного быстрее, так как требуется меньшее количество перекачек страниц. Microsoft полагает, что это увеличение производительности компенсирует, если не перевесит потери производительности, связанные с необходимостью выполнения сборщиком мусора некоторой работы. Мы закончим этот раздел напоминанием о том, какие типы данных, помимо классов, будут располагаться в управляемой куче. Типы по ссылке включают в себя все классы, а также массивы. Например, строка: int [] Х= {3, 4, 5, б, 7); на самом деле является сокращенной записью для: int [] X = new int [5] {3, 4, 5, 6, 7}; При выполнении любой из этих строк содержимое массива будет размещено в куче, в то время как переменная X, которая содержит значение соответствующего адреса в куче, будет располагаться в стеке — точно так же, как и для экземпляров классов. Этот пример иллюстрирует одно из упрощений, сделанных в данном разделе. Ранее мы отметили, что типы по значению будут размещаться в стеке. Это не всегда так: если тип по значению определен как часть типа по ссылке (например, элемент массива или член класса), он будет располагаться в куче, среди данных соответствующего типа по ссылке. Отметим, что экземпляры классов могут сами содержать ссылки на другие классы, так же как элементы массива могут быть на самом деле типами по ссылке. В этом случае содержимое кучи будет включать в себя адреса других участков в куче, но пока нас это не интересует. Структуры Вы уже знаете, насколько удобными могут быть классы для инкапсуляции объектов в про- программе. Классы хранятся в куче, что дает большую гибкость в управлении временем жизни данных, но ценой некоторого снижения производительности. Потеря производительности невелика благодаря оптимизации управляемой кучи. Однако в ряде ситуаций класс может
Объектно-ориентированный С# 165 оказаться больше, чем требуется, и по причинам, связанным с производительностью, вы предпочли бы сохранить некоторую функциональность класса, но разместить данные в стеке. Наиболее очевидным примером является случай, когда требуется определить объект, чьей единственной целью будет объединение нескольких связанных элементов данных. Рассмотрим код: class Dimensions *■ { public double Length;, public double" Width; ? } " * Мы определили класс Dimensions, который содержит длину и ширину некоторого объекта. Возможно, мы пишем программу, которая позволит экспериментировать на компьютере с перестановкой мебели, и желаем сохранить размеры каждого элемента обстановки. Выглядит так, будто мы нарушаем правила хорошего стиля программирова- программирования, объявляя поля как public. Но дело в том, что нам не требуются все возможности класса. Все, что у нас есть,— это пара чисел, которые удобно рассматривать именно как пару, а не каждое число в отдельности. Не требуется ни большое число методов, ни воз- возможность наследования от этого класса. И, разумеется, мы не хотим, чтобы возникали потери производительности, связанные с размещением данных в куче, всего лишь ради двух значений double. Лучше объявить Dimensions как структуру. Единственное, что для этого нужно сделать,— заменить в коде ключевое слово class на struct: struct Dimensions { public double Length; public double Width; } Можно рассматривать структуры С# как упрощенный вариант классов. В целом они похожи на классы, однако предназначены больше для таких случаев, когда требуется сгруппировать вместе некоторые данные. От классов они отличаются следующим: О Структуры являются типами по значению, а не по ссылке. Они хранятся либо в стеке, либо внутри других объектов (если они содержаться внутри объектов, рас- расположенных в куче) и имеют те же самые ограничения на время жизни, что и про- простые типы данных. Это имеет различные последствия для производительности. О Структуры не поддерживают наследование. О Существуют некоторые отличия в том, как работают конструкторы для структур. В частности, компилятор всегда генерирует конструктор без параметров по умол- умолчанию, который вы не можете заменить. О При желании для структуры можно указать, как должны располагаться поля в памяти. Хотя мы представляем структуры как упрощенные классы, исторически концепция структуры как способа объединения данных появилась первой. Затем концепции классов и ООП развились как расширение структур. Например, язык С определяет тип данных, похожий на struct С#, но в нем отсутствуют классы. Поскольку структуры наподобие Dimensions предназначены для объединения эле- элементов данных, в большинстве случаев их поля объявляются как public. Для простых структур многие разработчики посчитают это приемлемой программистской практи- практикой. Но если структуры станут более сложными и у них появятся методы и конструкто- конструкторы, то будет лучше скрыть их внутреннюю реализацию точно так же, как и в случае классов. На самом деле в базовых классах .NET даже для таких простых структур, как в примере Dimensions, поля инкапсулируются свойствами. Ниже подробно описываются особенности структур. Однако мы не будем рассматри- рассматривать возможность управления размещением полей структуры в памяти. В С# это требует- требуется очень редко. Если вам действительно необходимо сделать это, поищите атрибут StructLayout в документации MSDN. Структуры как типы по значению Хотя структуры являются типами данных по значению, синтаксически к ним можно об- обращаться так же, как к классам. Например, используя вышеприведенное определение Dimensions, можно записать:
166 Глава 5 Dimensions Pt - new Dimensions(!; Pt.Length = 3; Pt.Width = 6; Отметим, что оператор new действует для структур иначе, нежели для классов и дру- других типов по ссылке. Вместо выделения памяти в куче оператор new вызывает конструк- конструктор по умолчанию, инициализируя все поля своими значениями по умолчанию в стеке (или внутри кучи). В самом деле, для структур можно записать: Dimensions Pt,- Pt.Length - 3; Pt.Width = 6; Если бы Dimensions был классом, эти строки вызвали бы ошибку компиляции, так как Pt содержал бы неинициализированную ссылку — адрес, указывающий в никуда, и мы не могли бы присвоить значения его полям. Однако для структуры объявление пере- переменной уже выделяет место для нее в стеке, поэтому можно присваивать значения ее по- полям. Отметим, что приведенный код не инициализирует структуру, поэтому строки: Dim^ns ons Pt; dout e D - Pt.Length;. вызовут ошибку КОМШИ.ЯЦ ш причем компилятор сообщит о том, что используется неи- неинициализированная переменная. Структура считается полностью инициализированной в том случае, если для нее был вызван оператор new либо если отдельно были присвоены значения всем ее полям. Тот факт, что структуры являются типами по значению, влияет на производитель- производительность, хотя в зависимости or того, как вы используете структуру, это влияние может быть положительным или отрицательным. С одной стороны, выделение памяти для структуры происходит очень быстро, так как это делается в стеке или внутри кучи. То же самое касается и удаления структур при их выходе из области видимости. С другой стороны, если структура передается в качестве параметра или присваивается другой структуре (например, А = В: где А и В — структуры), копируется все содержимое структу- структуры, в то время как для класса копируется только ссылка. Это ведет к потерям производи- производительности, которые зависят от размера структуры, т.е. следует помнить о том, что структуры предназначены для небольших объемов данных. Отметим, что при передаче структуры в качестве параметра в метод этих потерь производительности можно избе- избежать, если передать ее как параметр ref — в данном случае будет передан только адрес структуры в памяти, что осуществляется так же быстро, как и передача в случае класса. Однако, делая это, необходимо иметь в виду, что вызываемый метод может в принципе изменить значение структуры. Передавайте структуры как параметры ref для того, чтобы избежать потерь производительности, связанных с копированием данных. Структуры и наследование Структуры не предназначены для наследования. Это означает, что невозможно создать производную от структуры, а сама структура не может быть производной от класса. Един- Единственное исключение состоит в том, что структуры, так же как и другие типы в С#, явля- являются производными от класса System.Object. Следовательно, структуры имеют доступ к методам System.Object. Разрешается даже перекрывать методы System.Object в структурах. В качестве примера можно привести перекрытие метода ToString (). На самом деле наследование здесь не прямое: структуры являются производными от System. ValueType, который в свою очередь является производным от System. Object. ValueType не добавляет своих собственных методов, но перекрывает некоторые из методов Object более подходящими методами для типов по значению. Расширим структуру Dimensions и добавим для нее метод ToStringO: struct Dimensions { public double Length; public double Width; Dimensions (double l<=nfjth, doi ble w> iibi
Объектно-ориентированный С# 167 { Length _ength; Width ■- vjidth; } public override string ToStringO retirn " ( " + Length.ToStringO + " , " + Width.ToString0 + " )"; Здесь мы определяем метод так же, как для класса. Отметим, однако, что невозможно объявить члена структуры виртуальным. Это бы означало, что мы собираемся создавать производные классы от структуры, однако структуры не допускают этого. Аналогично, нельзя объявлять структуру абстрактной. Явное объявление структуры как sealed также вызовет ошибку компиляции. Структура и так неявно объявлена как sealed, поэтому нет необходимости делать это дополнительно. Конструкторы аля структур Конструкторы для структур определяют точно так же, как для классов, за исключением того, что нельзя определить конструктор, не принимающий параметров. Возможно, это нелогично, причина же кроется в реализации среды исполнения .NET. Редко, когда сре- среда исполнения .NET не способна вызвать объявленный вами конструктор без парамет- параметров. Microsoft пошла по легкому пути и запретила такие конструкторы для структур в С#. Это один из отрицательных аспектов структуры .NET, вызвавший споры в новостных группах, связанных с .NET. Таким образом, конструктор по умолчанию, который инициализирует все поля нуле- нулевыми значениями, всегда неявно присутствует, даже если имеются другие конструкторы, принимающие параметры. Невозможно обойти конструктор по умолчанию, присваивая полям начальные значения. Следующий код вызовет ошибку компиляции: struct Dimensions public double Length =1: // Ошибка. Начальные значения // не разрешаются, public double Width = 2; // Ошибка. Начальные значения // не разрешаются. Если бы Dimensions была объявлена как класс, приведенный код откомпилировался бы без проблем. Что касается деструкторов, то для структур можно определить метод Close () или Dispose () так же, как для класса. Однако деструктор Finalize () не поддерживается, компилятор сгенерирует ошибку при попытке его определения. Finalize () вызывается сборщиком мусора, но сборщик мусора имеет дело только с объектами по ссылке, а не со структурами, поэтому метод Fi nc Li zc () никогда не был бы вызван. Перегрузка операций Теперь поговорим о двух других типах членов, которые можно определить для класса или структуры: о перегруженных операциях и индексаторах. Начнем с рассмотрения перегрузки операций. Смысл перегрузки операций в том, что не всегда нужно вызывать методы или свойст- свойства для экземпляра класса. Часто требуется делать такие вещи, как сложение некоторых величин, их перемножение или логические операции, например сравнение объектов. Предположим, что вы написали класс, представляющий валютную величину для финан- финансовой программы. С# имеет тип decimal, который может представлять валюту, но в сложной программе может потребоваться более мощный класс, который не только хра- хранит значения в валюте, но и имеет методы для их конвертирования. Такой класс можно было бы использовать следующим образом: Currency А, В, С, D; // Инициализируем А, В и D С = А + В; D *=2.5; if (А==В && В>С) D = В + С; Возможно, этот фрагмент кода слишком перегружен математическими операциями, но идея ясна — вам хотелось бы выполнять некоторые арифметические действия и сравнивать экземпляры классов.
168 Глава 5 Проблема заключается в том, что это невозможно. До сих пор операции типа + и * предназначались строго для предопределенных типов данных — и по очень веской при- причине: компилятору известно, что означают эти операции для этих типов данных, напри- например, он знает, как сложить два значения long или разделить одно double на другое double, и способен сгенерировать соответствующий код на промежуточном языке. Од- Однако, определяя свои собственные классы или структуры, мы обязаны рассказать компи- компилятору обо всем: какие методы доступны для вызова, какие поля необходимо хранить для каждого экземпляра и т.д. Аналогично, если мы хотим записать что-то типа С=А+В; для одного из своих классов, мы должны сообщить ему, что означает соответствующая операция (в данном случае +) в контексте этого класса. Способ, с помощью которого это можно сделать, заключается в перегрузке операций. Назначение перегрузки операций Перегрузка операций во многом является исключительно синтаксическим удобством. В то время как многие темы, рассматриваемые до сих пор, относятся к фундаментальным аспектам объектно-ориентированного программирования и необходимы для того, что- чтобы надлежащим образом использовать классы, перегрузка операций не дает ничего, что нельзя было бы сделать с помощью одних только методов. Она предоставляет лишь более простой синтаксис, что само по себе является, большим плюсом. Например, для одного из классов нам нужно записать: С = (A+B)*D; Если бы отсутствовала перегрузка операций, то для выполнения этих действий при- пришлось бы определять методы. Результат, вероятно, был бы таким: A- D.Miltiply(A.Add(B) ) ; Должно быть совершенно ясно, какую из этих строк кода легче понять! Необходи- Необходимо также сказать, что для большого числа классов перегрузка операций бессмысленна. Зачем бы нам понадобилось, например, складывать экземпляры классов Customer и Authenticator? Перемножение двух клиентов MortimerPhones концептуально не имеет никакого смысла. Было бы глупо пытаться перегрузить операции + и * для класса Customer. Однако существуют классы, для которых, возможно, придется использовать пере- перегрузку операций. Среди них: О Практически любой объект из мира математики: координаты, векторы, матри- матрицы, тензоры, функции и т.д. Если вы пишете программу, которая осуществляет математическое или физическое моделирование, вам понадобятся классы, пред- представляющие эти объекты. О Графические программы, которые используют математические или координатные объекты для вычисления позиций на экране. О Класс, который представляет денежные суммы (например, в финансовой программе). О Программа обработки текста или его анализа может иметь классы, представляющие собой предложения, абзацы и т.д. Могут потребоваться операции для объединения предложений (более сложная версия конкатенации строк). Следует подчеркнуть, что перегрузка применима не только к арифметическим опера- операциям. Необходимо также иметь в виду операции сравнения: ==, <, >, !=, <=, >=, например, в операторе if (a==b). Для классов по умолчанию этот оператор будет срав- сравнивать ссылки а и Ь, проверяя, не указывают ли они на одно и то же место в памяти — здесь не проверяется, содержат ли экземпляры одинаковые данные. Для класса string это поведение перекрыто, и при сравнении строк на самом деле сравнивается их содер- содержимое. Вы можете сделать то же самое для своих классов. Для структур оператор == по умолчанию не выполняет ничего. Попытка сравнения двух структур вызовет ошибку на этапе компиляции, если только вы явно не перегрузите ==, чтобы показать компилятору, как произвести сравнение. Во многих ситуациях возможность перегрузки операций поможет написать читаемый, понятный код. Для иллюстрации перегрузки операций разработаем структуру под названием Vector, которая представляет трехмерный математический вектор. В мире математики векторы можно складывать или умножать на другие векторы или числа. В этом контексте мы будем использовать термин скаляр, который в математике означает простое число, а в терми- терминах С# — double. Но перед изучением перегрузки операций мы должны выяснить, как они работают.
Объектно-ориентированный С# __ 469 Тот факт, что в примере используется структура, а не класс, не важен. Перегрузка операций для классов и структур работает совершенно одинаково. Выполнение операций Для того чтобы понять, как перегружать операции, выясним, что происходит, когда ком- компилятор встречает в программе знак операции. В качестве примера возьмем операцию сложения (+). Допустим, что имеются следующие строки кода: dnt а=3-; Mint b=2; double d=4.0; long. 1 = a+b; double x =' d+a; Рассмотрим строку: long 1 = a+b; Можно сказать, что a+b — это понятный, удобный синтаксис для вызова метода, скла- складывающего два числа. Этот метод принимает два параметра, а и Ь, и возвращает их сум- сумму. Можно рассматривать а+b как более простой способ записи Add (a, b). Для целых чисел компилятор и JIT-компилятор на самом деле сделают так, что эта строка внутрен- внутренне будет реализована как высокоэффективный код, который осуществляет сложение с использованием аппаратного обеспечения. Он не станет вызывать программные алго- алгоритмы так, как это делается для большинства методов. В С# действует тот же принцип. Компилятор видит, что необходимо сложить два целых числа и вернуть long, поэто- поэтому он делает то же самое, что и для вызова метода: ищет наиболее подходящую из перегру- перегруженных версий + для заданных типов параметров. Сложение двух целых чисел проблем не вызывает — в современных процессорах существует специальная инструкция машинного кода для выполнения этой операции. Результат также будет иметь тип integer, его нужно привести к типу long, что разрешено в С#, поэтому проблем не возникает. Следующая строка double x = d+a,- является более сложной. Теперь необходимо прибавить integer к double. На большинстве машин это невозможно сделать напрямую, потребуется явно преобразовать int в double и сложить два значения double. Другими словами, наиболее подходящей перегруженной версией здесь будет версия операции +, принимающая в качестве параметров два double. Сложение двух double — более слож- сложная задача, чем сложение двух int. Напомним, что числа с плавающей точкой хранятся внутри процессора как мантисса и экспонента. При их сложении выполняется побито- побитовый сдвиг мантиссы одного из double так, чтобы две экспоненты имели одно значение, затем осуществляются сложение мантисс, сдвиг мантиссы результата и корректировка экспоненты для получения максимально возможной точности результата. В современных процессорах Pentium имеется аппаратная поддержка такой операции, но она отличается от сложения двух integer. Наконец, компилятору необходимо убедиться в том, что он сможет привести результат к требуемому типу, если это необходимо. В данном случае проблемы нет — сложение двух double дает еще одно double, т.е. тип переменной х. Теперь посмотрим, что происходит, если компилятор встречает такой код: Vector" Vectl, Vect2, Vect3; // инициализируем Vectl и Vect2 Vect3, ' V.ectl + Vect2; Vecti = Vectl*2; Vector — это структура, которую мы определим позже. Компилятор увидит, что ему необходимо сложить два вектора, Vectl и Vect2. Он будет искать перегруженную вер- версию операции +, которая принимает два вектора в качестве параметров. Эта операция также должна вернуть Vector или значение, которое может быть неявно преобразовано в Vector, чтобы можно было присвоить результат переменной Vector3. Другими слова- словами, для компиляции этого выражения компилятор должен найти определение операции, имеющее примерно такую сигнатуру: public static Vector operator + (Vector Ins, Vector rhs) Если он находит его, то вызывает реализацию этой операции. Если ему не удается найти такое определение, он будет искать другую перегруженную версию +, которую можно ис- использовать в качестве наиболее подходящей — возможно, принимающую два параметра другого типа данных, которые могут быть неявно преобразованы в экземпляры Vector. Если ничего подобного не обнаруживается, компилятор сгенерирует ошибку компиляции точно так же, как если бы он не нашел подходящий вариант перегруженного метода.
170 Глава 5 Аналогично, когда компилятор встречает строку Vectl = Vectl*2;, он видит, что ему необходимо умножить Vector на integer и сохранить результат в Vector. Он будет ис- искать перегруженную версию операции *, которая принимает в качестве параметра Vector и int. В нашем примере мы определим наиболее подходящую операцию, принимающую в качестве параметров Vector и double. Так как компилятор сможет неявно преобразо- преобразовать int в double, проблем не возникнет. Логические операции, операции сравнения и присвоения будут работать по тому же принципу, что и арифметические операции. Допустим, что компилятор встречает такую конструкцию (здесь Vector также является структурой): Vector Vectl, Vect2; // Инициализируем Vectl и Vect2 if (Vectl == Vect2) Компилятор будет искать перегруженную версию ==, принимающую два Vector в качестве параметров. Пример перегрузки операции сложения: структура Vector Создадим структуру Vector, представляющую трехмерный вектор. Трехмерный вектор — это набор из трех чисел (типа double), которые говорят о том, насколько далеко что-то перемещается. Переменные, представляющие эти числа, назы- называются х, у и z, и вы можете считать, что х показывает, как далеко что-то перемещается на восток, у — на север, a z — вверх (по высоте). Соединив вместе все три числа, мы полу- получим общее перемещение. Например, если х=3.0, у=3.0, z=1.0 (что мы обычно будем запи- записывать как C.0, 3.0, 1.0)), то вы перемещаетесь на 3 единицы на восток, на 3 единицы на север и на 1 единицу вверх. Если х=2.0, у=-4.0, z=-4.0 B.0, -4.0, -4.0), то вы перемещаетесь на 2 единицы на восток, на 4 единицы на юг (отрицательные числа означают противопо- противоположное направление) и на 4 единицы в землю. Здесь важна операция сложения. Если сначала вы перемещаетесь на вектор C.0, 3.0, 1.0), а потом на B.0, -4.0. -4.0), то общее движение может быть вычислено как сумма этих двух векторов. Сложение векторов означает сложение их индивидуальных компонентов, поэ- поэтому мы получим E.0, -1.0, -3.0). В этом контексте математики пишут с=а+Ь, где а и b — векторы, а с — результат. Мы хотели бы использовать нашу структуру Vector точно так же. Для начала напишем основу Vector, добавив к ней пару конструкторов и метод ToString () для отображения значения Vector. Пока мы реализуем принципы, которые уже были рассмотрены в этой главе: struct Vector public double x, у, z; public Vectortdouble x, double y, double z) this.x = X; this.у = y; this.z = z; public Vector(Vector rhs} , X' - rhs.x; у = rhs.y; z = rhs.z; public override string ToString() return " ( " + x + ■, " + у + ", " + z + " ) "; Этот код не требует пояснений. Отметим, что для упрощения поля оставлены как public. Мы могли бы сделать их закрытыми и создать соответствующие свойства для до- доступа к ним, но это никак не изменило бы пример, а его код стал бы гораздо сложнее. Кроме того, для такой простой структуры, как эта, являющейся по сути объединением компонентов х, у и z, вполне можно оставить поля открытыми. Отметим также, что от- отсутствует конструктор по умолчанию, так как он не допустим для структур. Вместо него
Объектно-ориентированный С# 171 используются два конструктора, требующих указания начального значения вектора либо путем передачи значений каждого компонента, либо путем передачи другого вектора, значение которого может быть скопировано. Конструкторы наподобие второго (public Vector (Vector rhs)) часто называют конструкторами копий, так как они позволяют инициализировать экземпляр класса или структуры, копируя в нее значение из другого экземпляра. С вектором, объявленным таким образом, можно делать следующее: Vector Vectl, Vect2, Vect3; Vectl = new VectorA.0, 1.5, 2.0); Vect2 = new Vector(Vectl); Однако нельзя записать так: Vect3 = Vectl + Vect2; Это приведет к ошибке компиляции, так как компилятор пока не знает, что означает + для вектора. В определение Vector необходимо добавить соответствующий код: struct Vector { public double x, у, z; / / Другие члены public static Vector operator + (Vector lbs. Vector rhs) { Vector Result = new Vector(lhs); Result ,x += rhs.x; Result.у +- rhs.y; Result.z += rhs.z; return Result; I Как это работает? Важным является синтаксис объявления оператора: public static Vector operator + (Vector lhs. Vector rhs) Операция объявляется практически так же, как метод, за исключением того, что с помощью ключевого слова operator, за которым следует знак операции, мы сообщаем компилятору, что определяем именно операцию. Тип возвращаемого значения пред- представляет собой тот тип, который получается при использовании этой операции. В на- нашем случае сложение двух векторов дает другой вектор, поэтому тип возвращаемого значения — Vector. Для данной перегруженной операции + тип возвращаемого значе- значения совпадает с типом содержащего его класса, но это не обязательно. Позже мы доба- добавим операции, которые будут возвращать другие типы. Здесь обрабатываются два параметра. Для операции, принимающей два параметра, такой как операция *, первый параметр представляет собой объект или величину, стоящую слева от знака +, а второй параметр — величину, стоящую справа от знака +. Другими словами, если записать: // а, Ь, с имеют тип Vector с = а + b; то вектор а передается в тело перегруженного + в качестве первого параметра, который мы назовем lhs, b передается как rhs, и возвращаемое операцией значение присваивает- присваивается с. Кстати говоря, lhs (left-hand side) и rhs (right- hand side) являются в данном случае общепринятыми сокращениями. Наконец отметим, что операция объявлена статической. Это означает, что она связа- связана со структурой или классом, а не с отдельным объектом, и поэтому не имеет доступа к указателю this. C# требует, чтобы перегруженные операции определялись именно так. Здесь все в порядке, поскольку lhs и rhs содержат все данные, необходимые операции для выполнения своей задачи. Изучив синтаксис объявления операции +, мы можем посмотреть, что происходит внутри нее: { Vector Result = new Vector(lhs); Result.x += rhs.x; Result.y += rhs.y; Result.z += rhs.z; return Result;
172 Глава 5 Эта часть кода является точно такой же, как если бы мы объявляли метод. Она действи- действительно вернет вектор, содержащий сумму lhs и rhs. Мы просто складываем между собой значения х, у и z. Этот фрагмент иллюстрирует все концепции, необходимые для перегрузки арифмети- арифметических операций. Перегрузим еще две арифметические операции для Vector: умножение на скаляр (или double в терминах С#) и перемножение двух векторов. Умножение вектора на скаляр означает простое умножение каждого его компонента на некоторое число. Например, 2*A.0, 2.5, 2.0) дает B.0, 5.0, 4.0). Соответствующая перегруженная операция имеет вид: public static. Vector operator * (double lhs. Vector rhs) I * return new Vector(lhs*rhs.x, lhE*rhs.y, lhs*rhs.z); } Этого, однако, недостаточно. Если а и b объявлены как Vector, можно записать: Ь= 2*а; Компилятор неявно преобразует целое число 2 в double для того, чтобы оно соответ- соответствовало сигнатуре перегруженной операции. Однако следующий код не будет компили- компилироваться: Ь = а*2; Проблема состоит в том, что компилятор трактует перегруженные операции точно так же, как и перегруженные методы. Он просматривает все доступные перегруженные операции в поисках наиболее подходящей. Приведенное выражение требует, чтобы первый параметр имел тип Vector, а второй — тип integer или такой тип, в который можно было бы неявно преобразовать integer. Но у нас нет такой перегруженной опера- операции. Компилятор не может менять порядок следования переменных исходя из того фак- факта, что у нас имеется перегруженный метод, принимающий сначала double, а затем Vector. Нам необходимо явно определить перегруженную операцию, которая принима- принимает сначала Vector, а потом double. Возможны два варианта реализации этого. Первый способ заключается в явном написании операции умножения вектора: public Static Vector operator * (Vector lhs, double rhs) : { * return new Vector(rhs*lhs.x, rhs*lhs-Y, rhs*lhs.z); Однако, учитывая, что мы уже написали код для реализации такой операции, можно повторно использовать его: public isfcatic Vector"operator * (Vector lhs, double rhs) { • • return rhs*lhs; ' Ф ' Этот код сообщает компилятору, что при встрече умножения Vector на double он может поменять порядок следования параметров и вызвать другую перегруженную опе- операцию. Какой вариант вы предпочтете — дело вкуса. Мы использовали второй вариант, так как он выглядит изящнее и демонстрирует идею в действии. Эта версия позволяет получить более управляемый код, так как не требует повтора кода для осуществления ум- умножения в двух отдельных перегруженных операциях. Однако отметим, что первая вер- версия может несколько улучшить производительность, поскольку в ней отсутствует один вложенный вызов метода. С другой стороны, существует большая вероятность того, что JIT-компилятор оптимизирует этот дополнительный вызов метода в любом случае. Следующий оператор, который мы перегрузим,— умножение векторов. В математике существует несколько способов перемножения векторов, но нас интересует тот, что но- носит название скалярного произведения и выдает в результате скаляр. Таким образом мы продемонстрируем, что арифметические операторы не обязаны возвращать тот же самый тип, что и тип класса, для которого они определены. В математике скалярное произведе- произведение двух векторов (х, у, z) и (X, Y, Z) определяется как величина х*Х + y*Y + z*Z. Этот спо- способ умножения, возможно, выглядит странно, но на самом деле он очень полезен, так как может использоваться для подсчета различных величин. Если вам придется писать код, который отображает сложную трехмерную графику, например, с применением Direct3D или DirectDraw, вы непременно обнаружите, что вашему коду требуется довольно часто вычислять скалярные произведения векторов в качестве промежуточного шага при
Объектно-ориентированный С# 173 расчете местоположения объектов на экране. В любом случае мы хотим, чтобы можно было написать double X = a*b, где а и Ь — векторы, и вычислить скалярное произведение. Соответствующая перегруженная операция выглядит так: public static double operator { return lhs.x*rhs.x + lhs.y*rhs.y } (Vector lhs, Vector rhs) lhs.z*rhs.z; Проверим работу операций с помощью простой программы: static void Main(string[] args) 0.0, // Демонстрация арифметических операций Vector Vectl, Vect2, Vect3,- Vectl = new Vector A.0, 1.5 Vect2 = new Vector @.0, Vect3 = Vectl + Vect2; Console .WriteLine (~"Vectl Console.WriteLine("Vect2 Console.writeLineCVect3 2.0); -10,0); " + Vectl) ; " + Vect2) ,- Vectl + Vect2' '=? + Vect3); Console.WriteLine(*Vect3 - " + 2*Vect3); Vect3 += Vect2; Console.WriteLineCVectS += Vect2 gives " + Vect3); Vect3 = Vectl*2; Console.WriteLinq("Setting Vect3 = Vectl*2 gives, " ч double dot = Vectl*Vect3; 'Console.WriteLine("Vectl*Veet3 = " + dot); Vect3); Этот код доступен как пример VectorDemo. При его запуске мы получим следующее: Jectl ,« <. 1 , 1.5 , 2 У Uect2 • < в . Й ; -10 > Uect3 " Uecei ♦ Mect2 '<= < i .„ l.S . -? > '«OectS * < 2 ■. 3 „ -16 > oct3*=Uect2 gives < 1 , l.S . -18 > >etciiig Uect3«Uectl-«2 gives < 2 , 3 k A > )ectl«Uect3 ■• 14;5 Перегруженные операции выдают корректные результаты. Однако, присмотрев- присмотревшись к тестовой программе, вы обратите внимание на то, как хитро мы использовали оператор, который не был перегружен,— оператор присваивания со сложением +=: Vect3 += Vect2,- Console.WriteLine("Vect3 += Vect2 gives + Vect3); Удивительно, но он выдал правильный результат! Что же произошло? Хотя += счита- считается одной операцией, на самом деле он может быть представлен в виде двух шагов — сложения и присвоения. В отличие от C++, С# не позволит вам перегрузить операцию =, но если вы перегружаете +, компилятор будет автоматически использовать перегружен- перегруженную операцию + для выполнения +=. Тот же самый принцип действует для операций ++, -=, —, *= и /= (очевидно, что для того, чтобы эти операции работали, потребуется пе- перегрузить соответственно операции -, * и /). Для структур С# всегда интерпретирует присвоение как копирование содержимого памяти, в котором хранится структура, в то время как для класса С# всегда копирует только ссылку. Перегрузка операторов сравнения В С# имеются шесть операторов сравнения, и они образуют три пары:
174 Глава 5 О == И ! = О > и >= О < И < = Объединение операций в пары отражает два важных момента. Во-первых, внутри каждой пары вторая операция должна всегда давать противоположный результат по сравнению с первой (если первая выдает true, то вторая выдает false, и наоборот). Во-вторых, С# требует, чтобы эти операции всегда перегружались парами. Если вы пере- перегружаете ==, то обязаны перегрузить и !=, иначе возникнет ошибка компиляции. В случае перегрузки == и .'= необходимо перегрузить также метод Equals О, который наследуется от System, object всеми классами и структурами. Если не сделать этого, компилятор выдаст предупреждение. System.Object рассматривается в главе 7. Еще одним ограничением является то, что операции сравнения должны возвращать bool. Это фундаментальное отличие операций сравнения от арифметических опера- операций. Результат сложения или вычитания двух величин теоретически может быть любого типа в зависимости от величин. Мы видели, что перемножение двух векторов может да- давать скаляр. Другим примером является базовый класс .NET System.DateTime: можно вычесть из одного DateTime другой, однако результат будет не DateTime, а экземпляр System. Time Span. Напротив, для операции сравнения не имеет смысла возвращать что-либо, кроме bool. В остальном перегрузка операций сравнения следует тем же принципам, что и пере- перегрузка арифметических операций. Сравнение величин не всегда так просто, как кажет- кажется, и мы покажем это на примере. Переопределим для нашего класса Vector операции == и !=. Начнем с ==. Можно предположить, что хорошо было бы сделать так: public static bool operator == (Vector lhs. Vector rhs) // Неверно { if (lhs.x == rhs.x && lhs.у == rhs.у && lhs.z == rhs.z) return true ; else return false; } Такой подход является синтаксически корректным, но он, скорее всего, будет посто- постоянно выдавать неправильные результаты. Дело в том, что величины с плавающей точ- точкой нельзя сравнивать между собой на равенство так же, как вы сравниваете целые числа, поскольку числа с плавающей точкой могут отличаться на очень малые величи- величины. Для целых чисел очевидно, что они равны, если оба содержат значение 1, и не рав- равны, если одно из них содержит 1, а другое — 3. Для чисел с плавающей точкой этот вопрос более тонкий. Например, если два double содержат соответственно 3.0000000 и 3.0000001, отличаются ли они на самом деле? Простая проверка, которую мы осуществ- осуществляем выше, скажет, что не равны, хотя на самом деле такая маленькая разница вполне могла возникнуть из-за ошибок округления в некоторых вычислениях, и эти два числа на самом деле одинаковы. Для double и float операция сравнения выполняет проверку на точное равенство, но во многих случаях требуется более сложная проверка, чем эта. Для нашего класса Vector мы будем более осторожны при определении понятия ра- равенства. Наше решение будет зависеть от использования класса. Какие виды значений будет содержать класс Vector? Готовы ли мы пожертвовать производительностью ради того, чтобы получить более сложную и точную проверку двух векторов на равенство? Быть может, лучше вообще не определять == и ! = для класса Vector, а в документации отметить, что этот класс не поддерживает сравнение? В качестве одного из решений можно предложить следующий вариант перегружен- перегруженной операции ==. Оно вполне приемлемо по производительности и будет выдавать по- понятные результаты при условии, что компоненты вектора не слишком малы. Более сложное решение будет учитывать величину вектора, но тем самым еще больше снижать производительность: private const double Epsilon = 0.0000001; public static .bool operator == (Vector lhs, Vector rhs) { if (System.Math.Abs(lhs.x - rhs.x) < Epsilon && System.Math.Abs(lhs.у - rhs.у) < Epsilon && System.Math.Abs(lhs.z - rhs.z) < Epsilon)
Объектно-ориентированный С# 175 return true; else return false; Операция сравнения определяет очень маленькую величинO, 'рь Llon, и полагает, что векторы равны, если ни один из их компонентов не отличается друг от друга более чем на эту величину. Метод использует статический метод Abs, определенный в базовом классе .NET System.Math. Этот метод возвращает абсолютное значение числа, убирая знак Необходимо также перекрыть операцию ! =. Наиболее простой способ таков: public static bool operator \= (Vector lhs. Vector rhsl return (lhs == rhs); Однако для повышения производительности можно расширить реализацию, а не просто вызывать операцию ==. В результате мы уменьшим на единицу число вызываемых во время исполнения методов. Проверим работу операций с помощью тестовой программы. На этот раз определим три вектора, два из которых имеют близкие значения и при сравнении должны оказаться равными: Vector Vectl = Vect2 = Vect3 = Console. Console. Console. Console. Console. Console. Console. Vectl, Vect2, Vect3 ; new VectorC.0, 3.0, -10.0); new VectorC.0, 3.0000001, -10 new VectorB.0, 3.0, WriteLine("Vectl==Vect2 WriteLine("Vectl==Vect3 WriteLine C'vect2==vect3 WriteLine() ; WriteLine("Vectl!=vect2 WriteLine("Vectl!=Vect3 WriteLine("Vect2<=vect3 В результате выполнения этого ко; Ucctl« UectZ returns True frctl »<*Ueet3 returns Fals* Uuct2»bUi;ct3 r*!t<n<ns Palso Vrctlf"Uect2 returns Falstf Lfecti* Uect3 ruitirn» True Щшч* & 'Л9 tti*t* & *Л wa k1 ai 1>аъ*< V ktai a> 3 " ' ' 6.0) ; returns returns returns returns returns returns ja получим .0); " * (Vectl =- " + (Vect L =- " + (VecL^ -- " + (Vectl ! = " + (Vectl != 11 + (Vect2 ! = следующее: Vect2)). Vect3)); Vect3)). Vect2)); Vect3)); Vect3)); xf ~~1 1 j Операции, допускающие перегрузку В С# существует большое число операторов, одни из них можно перегрузить, а другие нет. Разрешается перегружать следующие операции: Категория Арифметические бинарные Арифметические унарные Битовые бинарные Битовые унарные Сравнения Операции +, *, /, -, % +, -, ++, — &, 1 , Л, «, >> ! , ~, true, false ==, !=, >=, <, <=, > Ограничения Нет Нет Нет Нет Должны перег попарно
176 Глава 5 В этом списке отсутствует ряд операций, однако этому есть логическое объяснение. Некоторые операции не могут быть перегружены явно, они вычисляются с использовани- использованием других перегруженных операций. Это операции арифметического и побитового при- присваивания +=, -=, *=, /=, »=, «=, %=, &=, I = и Л=, а также условные логические операции && и I I (они вычисляются с помощью & и I). Разработчики на C++ будут удивлены тем, что операторы [ ] и () не могут быть перегружены. Дело в том, что С# добивается того же результата другими способами — используя индексаторы вместо перегрузки [ ] и опреде- определенные пользователем приведения вместо перегрузки (). Определенные пользователем приведения рассматриваются в главе 6. Индексаторы Индексаторы, как и свойства, не относятся к необходимым средствам объектно-ориенти- объектно-ориентированного программирования. Скорее, они обеспечивают некоторое синтаксическое удобство, облегчающее применение определенных классов. Индексаторы позволяют осуществлять доступ к объекту так, будто это массив. Добавление индексатора в Vector В качестве примера мы вновь используем структуру vector. Как и в случае перегрузки операций, индексаторы работают одинаково для структур и классов, поэтому тот факт, что мы применяем структуру, не является важным. До сих пор мы ссылались на компоненты структуры vector с помощью их имен х, у и z. Но это не единственный способ обращения к ним. Математики находят полезным рас- рассматривать векторы так, будто это массивы, где х является первым элементом, у — вто- вторым, a z — третьим. Другими словами, для того чтобы установить значение компонента х, они могут записать: MyVector[0] р 3.6; Если мы будем рассматривать структуру Vector как массив, то нам понадобится способ для выполнения таких вещей, как проход по компонентам: for (int i=Q; I<3; I++) { :-' Vect2[I] = I; }' При текущем определении Vector эти фрагменты кода будут вызывать ошибку на этапе компиляции, так как компилятор не поймет, что подразумевается под первым эле- элементом Vector. Индексаторы позволяют решить эту проблему. Если определить для класса индексатор, это укажет компилятору, что делать, если он встретит код, в котором экземпляр класса рассматривается так, будто это массив. Индексаторы определяются примерно так же, как свойства,— с использованием фун- функций get и set. Основное отличие состоит в том, что в качестве имени индексатора применяется ключевое слово this. Чтобы определить индексатор для класса Vector, изменим его описание следующим образом: struct Vector { public double x, у, z; public double this [int i] { get { switch (i) { case 0: return x; case 1: return у ; •case 2: t return z ; "!> default: : thr6w new IndexOutOfRangeExceptionf "Attempt to retrieve Vector element " + i);
Объектно-ориентированный С# 177 set {. switch (i) { case 0: x ** value; break; case 1: у » value,- break; < case 2: ' z = value; ; break; * default: throw new IndexOutOfRangeException( "Attempt to set Vector element " + i); Л > // и т.д. В этом коде есть кое-что новое. Для начала рассмотрим строку, которая объявляет индексатор: public double this [int i] Эта строка говорит, что мы хотим рассматривать каждый экземпляр Vector как од- одномерный массив с int в качестве индекса (или, что в данном случае эквивалентно, па- параметра) и что тип возвращаемого значения при этом double. Индексаторы предоставляют большую свободу — в качестве индекса можно использовать любой тип данных, хотя наиболее часто применяются целочисленные типы uint, int, ushort, short и т.п. и string. В качестве возвращаемого типа можно использовать любой тип данных, который, по вашему мнению, является наиболее подходящим. В теле индексатора имеются те же самые аксессоры get и set, что и у свойств. Можно сделать индексатор только для чтения или только для записи, убрав соответствующий ак- сессор. Синтаксис аксессоров соответствует тому, что используется при объявлении свойств, за исключением того, что теперь мы имеем аксессоры для переменных, объяв- объявленных параметрами индексатора. (Напомним, что аксессоры get и set для свойств ни- никогда не принимают явных параметров.) Аксессор get должен возвращать тип, который объявлен в качестве возвращаемого для индексатора, в то время как аксессор set не дол- должен возвращать ничего. Он имеет доступ к дополнительному неявному параметру value, чей тип данных соответствует типу, объявленному для индексатора (в нашем случае — double). Параметр value инициализируется как величина с правой стороны оператора присваивания, который используется в выражении индексатора. Мы используем передаваемый параметр для определения того, к какому из компонен- компонентов Vector должен осуществляться доступ, и либо устанавливаем, либо возвращаем соот- соответствующее поле. Заметим, что выражения switch имеют метку def au] t для обработки ситуации, когда индексатор вызывается с неверным параметром. В этом случае генериру- генерируется исключение: throw new IndexOutOfRangeExceptionl "Attempt to retrieve VecLor element " + i) ,- Исключения являются тем механизмом, с помощью которого обрабатываются нео- неожиданные ошибочные состояния в С#. Мы еще не рассматривали исключения, поэтому не проводили проверок на ошибки в примерах. Однако в случае индексатора нужно обя- обязательно проверить, находится ли переданный индекс в соответствующих пределах — для нашего класса Vector это интервал от 0 до 2. Приведенная строка кода как раз и за- занимается обработкой выхода за границы диапазона. На самом деле должен осуществля- осуществляться переход в специальный участок кода, который вы должны в идеале написать и пометить как ответственный за обработку этой ошибочной ситуации. Мы пока не на- написали такой блок кода, так что этот оператор вызовет завершение программы. Теперь попробуем воспользоваться индексатором: / Vector Vectl = new VectorA.0, -5.0, 4.6); Vector Vect2 = new Vector(); Console.WriteLine<"Vectl = " + Vectl); Console.WriteHriel "Vectl [1] = ■' + Vectl ГШ; for Unt I = 0; КЗ; 1++)
178 Глава 5 Vect2{I] = I; Console.WriteLine("Vect2 - vecL2), Отметим, что в данном примере для индексирования компонентов vector применяется цикл for. Для индексаторов можно использовать циклы far, do и while, но нельзя написать цикл for each. Оператор fern -ach работает несколько иначе, он рассматривает элемент как коллекцию, а не массив. Класс или структуру можно заставить работать как коллекцию, однако это требует реализации определенных интерфейсов, а не индексаторов (см. главу 7). В результате получим: Можно также убедиться в том, что код обработки исключений отслеживает выход за границы диапазона. Если попытаться получить доступ к Vector так: double TryTh^s = Vect . ] ,- то консольное приложение немедленно завершится, а на экране появится диалоговое окно: An unhanded exception of type 'System.IndexOutOfRangeExcepticn' occur'ed in Vector Derno.exe Additional information: Attempt to retrieve Vector elements Break Continue Help Выполняя обработку исключений, вы сможете предпринять определенные действия, чтобы избежать завершения программы (см. главу 6). Другие примеры индексаторов Индексаторы — чрезвычайно гибкое средство. Например, они не ограничены одномер- одномерными массивами. Классы и структуры можно рассматривать как многомерные массивы, добавив для этого дополнительный параметр в квадратные скобки. Индексаторы можно перегружать — структура или класс может иметь неограниченное число индексаторов при условии, что все они имеют различное число или типы параметров. Например, если бы мы хотели написать класс Matrix, то, вероятно, пожелали бы рассматривать его как двумерный массив double. Помимо этого математики трактовали бы каждую строку матрицы как вектор. Мы не будем рассматривать здесь такой пример, но в принципе этого можно было бы достичь, определив два индексатора:
Объектно-ориентированный С# 179 // для структуры Matrix public double this [uint i, uint j] public vector this [uint i] Другое применение индексаторов — доступ к части класса или структуры по строке. Это означает, что может присутствовать массив или некоторая структура данных, которая имеет именованные элементы: // для класса ListOfCustomers public Customer this [string Name] // в клиентском коде Customer NextCustomer = CustomerLisc["Simon Robinson"] Полезной вариацией этого является доступ к элементам с использованием перечис- перечислимого значения. На самом деле индексаторы широко используются для классов, представляющих неко- некоторую структуру данных, массив, список, отображение и т.д., и определяются в базовых классах .NET, которые представляют эти структуры (см. главу 7). Интерфейсы Для получения представления об интерфейсах в С# вернемся к примерам наследования в реальном мире, приведенным в главе 4. Мы рассмотрели два примера: машины Форд и джемперы/рубашки. (Напомним, что джемпер — это английское название свитера, а рубашка — легкая спортивная куртка.) В примере с машинами Форд мы продемонстрировали наследование реализаций, т.е. реализация некоторой функциональности (описание составляющих частей машин) была общей для разных типов машин. До сих пор мы имели дело именно с этим типом наследования. В примере с джемперами и рубашками использован несколько иной тип наследова- наследования. Он заключается в том, что общим для различных объектов является только пользо- пользовательский интерфейс, в то время как собственно реализация (материал, из которого сделаны вещи) различна для каждого объекта. Этот тип наследования поддерживается в С* с помощью интерфейсов. В терминах программирования интерфейс похож на класс, за исключением того, что ни один из его членов не реализован. Интерфейс группирует методы, свойства, события и индексаторы. Для него нельзя даже создать экземпляр; все, что он содержит,— это сиг- сигнатуры своих членов, причем таковыми могут быть только указанные четыре типа чле- членов. Интерфейс не имеет конструкторов (как можно сконструировать то, для чего нельзя создать экземпляр) и полей (это подразумевало бы некоторую внутреннюю реа- реализацию). Интерфейс не может иметь перегруженных операций, хотя это вызвано не принципиальными проблемами, связанными с их объявлением,— таких проблем нет, а возможной несовместимостью с другими языками .NET, например с VB.NET, который не поддерживает перегрузку операций. Интерфейс объявляется почти так же, как класс, за исключением того, что его члены не имеют реализаций (наподобие абстрактных методов). Для разнообразия рассмотрим синтаксис интерфейса одного из базовых классов .NET — IEr ш-ierator из пространства имен System.Collections. Интерфейс выглядит следующим образом: interface ISnamerator { // Свойства object Current {get; } / / Методы bool MoveNextl); void Reset(); } Интерфейс lEnumerator имеет два метода и одно свойство. Этот интерфейс важен для реализации коллекций и разработан для инкапсуляции функций перемещения по элемен- элементам коллекции. MoveNext () перемещается к следующему элементу. Reset () возвращается к первому элементу, a Current получает ссылку на текущий элемент.
180 Глава 5 Помимо отсутствия реализаций, необходимо отметить также отсутствие модификато- модификаторов доступа для членов. Члены интерфейсов всегда открыты и не могут быть объявлены виртуальными или статическими. Это прерогатива классов, а не интерфейсов. Зачем нужны интерфейсы? Основная причина в том, что они эффективно исполня- исполняют роль соглашений. До сих пор мы рассматривали классы, имеющие различные члены, и не беспокоились о том, чтобы сгруппировать эти члены, — классы просто содержали список различных методов, полей, свойств и т.п. Часто возникают ситуации, в которых для использования класса определенным образом необходимо знать, реализует ли класс те или иные особенности. Этого добиваются путем объявления того, что класс реализу- реализует один или несколько интерфейсов. Класс реализует интерфейс, наследуя его и обеспе- обеспечивая реализации всех членов, объявленных в этом интерфейсе. Это аналогично использованию в VB6 ключевого слова Implements, которое показывает, что объект реа- реализует интерфейс. Выше приводился пример класса, использующего интерфейс для объявления того, что он реализует определенные особенности. Классы могут наследовать IDisposable, чтобы показать, что они поддерживают метод Disposed- IDisposable определяется следующим образом: public interface' IDisposable { void Dispose() ; } Класс, реализующий IDisposable, обязан реализовывать метод Dispose (). Другим хорошим примером является цикл foreach. В принципе можно использо- использовать f oreach для итерации по экземплярам класса при условии, что класс способен вес- вести себя как коллекция. Как среда исполнения .NET может сообщить о том, что экземпляр класса представляет собой коллекцию? Она опрашивает экземпляр и выясня- выясняет, реализует ли тот интерфейс System.Collections. IEnumerable. Если да, то среда исполнения использует методы этого интерфейса для итерации по членам коллекции. Если экземпляр не реализует интерфейс, цикл foreach вызовет исключение. Вы можете спросить, почему в данном случае нельзя напрямую выяснить, реализует ли класс требуемые методы и свойства? Дело в том, что это очень ненадежный способ про- проверки. Например, существует огромное число причин, по которым класс может реализо- реализовать метод MoveNext () или Reset (), не имеющий ничего общего с коллекциями; класс может инкапсулировать доступ к файлу, a MoveNext () — перемещаться к следующему бай- байту в файле. Если же класс объявляет, что он использует интерфейсы, необходимые для коллекций, то можно быть уверенным в том, что он действительно является коллекцией. Вторая причина, по которой необходимы интерфейсы,— совместимость с СОМ. До появления .NET среда СОМ, а позднее DCOM и СОМ+, обеспечивала основной способ, с помощью которого приложения могли общаться друг с другом на платформе Windows, а конкретная объектная модель, используемая СОМ, сильно зависела от интерфейсов. Именно с появлением СОМ концепция интерфейсов стала широкоизвестной. Необхо- Необходимо отметить, что интерфейсы С# не то же самое, что интерфейсы СОМ. Интерфейсы СОМ имеют строгие конкретные требования, например, они обязаны применять GUID в качестве идентификаторов, которые не являются обязательными в интерфейсах С#. Однако с помощью атрибутов (особенность С#, которая будет рассмотрена в следую- следующей главе) можно сделать так, чтобы интерфейс С# действовал как СОМ-интерфейс, и, следовательно, обеспечить совместимость с СОМ (см. главу 19). Реализация интерфейсов: пример с джемпером и рубашкой Закончим наши рассуждения об интерфейсах коротким примером, который продемон- продемонстрирует, как можно использовать интерфейсы для разработки объекта, следующего парадигме наследования интерфейсов. Напишем пример с джемпером. Нам необходим интерфейс, который инкапсулирует свойства джемперов: interface IJumper { void PutOn№ool SleevesRolledUp) ; void Takeoff.)) ,- int Temperaturelncrea.se { get;
Объектно-ориентированный С# 181 Этот интерфейс позволяет снять или надеть джемпер, а также выяснить, на сколько он повышает температуру владельца — как эффективно он согревает вас! Метод PutOn () принимает параметр типа boolean, который позволяет указать, закатаны ли рукава. Обратите внимание на имя: Uumper. Имеется соглашение, по которому имена ин- интерфейсов традиционно начинаются с буквы I — сразу становится понятно, что это интерфейс. В большинстве случаев рекомендации .NET no именованию не одобряют использование так называемого "венгерского" стиля именования объектов, в котором первые буквы имен указывают тип определяемого объекта. Интерфейсы являются одним из редких случаев, для которых предпочтительным является "венгерский" стиль именования объектов. Рекомендации по именованию рассматриваются в главе 8. Теперь можно написать классы, представляющие джемперы. Это могут быть совер- совершенно разные классы, не связанные друг с другом. Однако, реализуя интерфейс I jumper, они сообщат миру, что представляют собой джемперы. Для примера создадим только один класс, который назовем PolyesterJumper: class PolyestersTuriiper : IJumper private- int temperaturelncrease; public virtual void, PutOn(bool sleevesRolledUp) ■"*'""■ ' . * . '"' ■ " ' * ■if: .{SleevesRolledUp} v ■•.".'■'.. *temgej;a6urelrjcrg?ise, =• 10.;' -, , ; /'else ,o . temper,afcurelncreasje в 15> ,, - ,^ . -. public void- Takeoff (), ■ ' ' ' "v * temperattireiiicrease1 = 0; • puBllc. int Temperacurelncrease1 У ■ ■ -- ?;-override1'- s(trii^ #6'Stringt) "Jumper: ,is" off ",- case ipj >. ' .returt "Jumper is onV, but sleeves are rolled- up" ; case ■ 1§:"•'•-, - return "Jumper is on"; itfT ~ Г' : " ' retufcn " *;. А/ Этого не должно произойти } .* Реализация этого класса не требует пояснений. Предполагается, что джемпер из полиэстера способен поднять температуру на 10 градусов, если рукава закатаны, и на 15 градусов, если они опущены. Единственная интересная строка в этом коде — объявление класса: class PolyesterJumper : IJumper
182 Глава 5 Объявлено, что Polyester Jumper наследует только один интерфейс, I Jumper, а другие классы не указаны. Базовый класс также не указан, поэтому Polyest °r Jumper будет унасле- унаследован от System. Obj ect. Правило состоит в том, что класс может быть производным толь- только от одного класса (если родительский класс не указан, то в качестве него принимается System. Obj ect) и может наследовать любое количество интерфейсов. Наследование класса означает, что Polyester Jumper автоматически получает всех членов базового класса и может перекрыть или скрыть их. В данном случае мы реализо- реализовали свой собственный перекрывающий метод ToString () для отображения состояния нашего класса. Наследование от IJumper означает, что PolyesterJumper получает всех его членов. Но так как интерфейс не реализует ни один из методов, PolyesterJumper обязан обеспечить реализации для них. Если хотя бы одна реализация будет отсутствовать, компилятор сгене- сгенерирует ошибку. Напомним, что интерфейс лишь показывает присутствие своих членов. Класс решает, объявлять их виртуальными или абстрактными (хотя последние допустимы только в том случае, если сам класс является абстрактным и от него будет производиться наследование). В данном случае мы решили, что метод VutOnO является подходящим для перекрытия, поэтому он объявлен виртуальным. Любой класс имеет не только унаследованных членов. Он может добавить своих соб- собственных членов. В данном случае мы добавили закрытое поле для хранения увеличения температуры и открытую константу, которая указывает производителя данного класса джемперов. Теперь протестируем класс: static void Main(string[] args) { PolyesterJumper MyJumper = new PolyesterJumper(); MyJumper.PutOn(true); Console.WriteLine(MyJumper.ToString()) ; MyJumper.TakeOff(); Console.WriteLine(MyJumper.ToString()); MyJumper.PutOn(false); Console.WriteLine(MyJumper.ToString()); Console.WriteLineC Made by: " + PolyesterJumper.Manufacturer); Console. P.eadLine () ,- В результате получим: Однако эта проверка PolyesterJumper не показала, что он реализует интерфейс IJumper. В приведенном клиентском коде нет ничего, что основывалось бы на том фак- факте, что представлен этот интерфейс. Как нам убедиться в том, что джемпер реализует этот интерфейс и, следовательно, является джемпером, придерживаясь "договорной" спецификации для джемперов? Ответ заключается в том, что доступ к джемперу можно было бы получить с помощью ссылки на этот интерфейс: static void Main(string[] args) { IJumper MyJumper = new PolyesterJumper(); MyJumper.PutOn(true);
Объектно-ориентированный С# 183 По-другому можно было бы записать: static void Main(string[] args) { PolyesterJumper TheJumper = new PolyesterJumper(); IJumper MyJumper; MyJumper = TheJumper; MyJumper.PutOn(true); Отметим, что использование ссылки IJumper не запрещает нам получать доступ ко всем открытым полям, реализованным в PolyesterJumper, вне зависимости от того, яв- являются ли эти члены частью интерфейса или нет, но действительно дает клиентскому коду подтверждение, что наш класс является настоящим джемпером. Наследование интерфейсов Интерфейсы могут наследовать друг друга точно гак же, как классы. Предположим, что необходимо определить соглашение для курток — набор методов, которые обязан реализо- вывать класс, чтобы считаться курткой. Куртки должны делать то же самое, что и джемпе- джемперы, а также позволять надевать капюшон. Подходящий интерфейс мог бы выглядеть следующим образом: interface Д, vestshx-г _ "umper boc' Hooi Jp i get; s :t; мы } Теперь Sweat - г.: . содержит свойство HuouUp и всех членов IJumper. Если бы определи.™ класс курток как: cla.= s Cottoi Sweatshirt : ISweatshirt { то' L _. . ;олжен бы был реализовать все методы, определенные в ISweatshirt и [ .■: - РазраЫ. i'kv i tact.а < '< ' t л ^vulj -r мы оставляем вам п качестве упражнения. Заключение В данной главе были приведены некоторые особенности, предлагаемые С#, которые упрощают написание классов и объектов и делают их синтаксис более понятным. Мы уз- узнали, как С# работает с памятью - эта информация полезна для написания высокопроиз- высокопроизводительного кода. Было рассмотрено, как конструкторы и деструкторы позволяют указывать, каким образом должны инициализироваться объекты и какие действия необ- необходимы для освобождения ресурсов после уничтожения объектов. Мы показали, как можно в некоторых случаях увеличить производительность путем определения структур вместо классов, а также описали удобный синтаксис, предлагаемый перегрузкой операций, индексаторами и перегрузкой методов.
п Л 7 \N а в а 7 ч> Дополнительные возможности С# В предыдущих главах мы рассмотрели основы синтаксиса С# и принципы объектно-ориен- объектно-ориентированного программирования, узнали, как применять их при объектно-ориентированном программировании в С#. Мы изучили достаточно для того, чтобы использовать С# для напи- написания хорошо продуманных объектно-ориентированных программ. Однако С# также пред- предлагает ряд дополнительных возможностей, которые могут оказаться чрезвычайно полезными в определенных обстоятельствах. Язык С# работает не в изоляции, он взаимодействует с платформой .NET и с базовы- базовыми классами. В данной главе мы рассмотрим те дополнительные возможности, которые являются частью самого языка. В главе 7 будут описаны возможности, поддержка которых осуществляется в основном посредством базовых классов, с минимальной помощью со стороны синтаксиса С#. Однако это деление не является четким, базовые классы будут появляться и в этой главе, а в главе 7 мы изучим новые аспекты синтаксиса языка. В этой главе рассматриваются следующие темы: О Ошибки и обработка исключений. С# имеет мощный механизм обработки оши- ошибочных ситуаций, использующий методику, известную как обработка исключе- исключений. Можно создать свой обработчик для каждого типа ошибочного состояния, а также четко разделить код, который обнаруживает ошибки, и код, который их обрабатывает. Это означает, например, что библиотечный код может взять на себя ответственность за определение ошибок, оставляя клиентскому коду принятие решения по их обработке. О Приведение типов. В главе 3 рассматривалось преобразование различных типов данных с помощью явного и неявного приведения типов. Здесь будет показано, как расширить действие этого процесса путем определения приведения типов для ваших собственных классов. О Делегаты. Это способ, с помощью которого С# позволяет коду ссылаться до нача- начала исполнения программы на метод без указания последнего. Делегаты похожи на указатели функций в C++, но являются безопасными по типу и объектно-ориен- объектно-ориентированными. О События. В программировании бывает нужно сделать так, чтобы определенный код вызывался при каком-то действии, например, при нажатии пользователем кнопки мыши. С# содержит встроенную поддержку таких сценариев с использованием событий. D Препроцессор С#. При компиляции код сначала проходит стадию, известную как предварительная обработка. Она позволяет сделать в коде различные тек- текстовые подстановки и может быть полезна для создания отличающихся версий программы, соответствующих различным ситуациям. О Атрибуты. Позволяют отмечать в коде те элементы, которые требуют какого-то внимания. Они полезны для документирования и могут использоваться для управ- управления компиляцией кода.
486 Глава 6 О Небезопасный код. С# обычно скрывает от программиста детали доступа к памя- памяти с целью упрощения процесса программирования, однако в ряде ситуаций тре- требуется прямой доступ к памяти. С# позволяет осуществлять его в блоках кода, специально для этой цели объявляемых небезопасными. Ошибки и обработка исключений Как бы хорошо вы ни программировали, ваши программы всегда должны быть готовы к встрече с ошибками. Например, в самый пик расчетов программа может обнаружить, что она не обладает достаточными правами для чтения требуемого файла или при посыл- посылке запросов сеть оказывается недоступной. В подобной ситуации недостаточно вернуть соответствующий код ошибки. Мы говорим об исключительных проблемах. Ошибка мо- может возникнуть в момент, когда вы находитесь в самой середине сложного процесса об- обработки, возможно, в методе, имеющем 15^гый или 2(Мый уровень вложенности, и здесь ваш код обнаружит, что он не может завершить свое действие. Вряд ли вы захотите про- просто так вернуться из выполняемого в данный момент метода в вызвавший его метод. На самом деле, требуется немедленно пройти обратно эти 15 — 20 вложенных вызовов ме- методов, чтобы полностью выйти из задачи и разобраться с возникшим беспорядком. Как можно ожидать от современного объектно-ориентированного языка, С# имеет хоро- хорошие средства для обработки подобных ситуаций. Он использует механизм, известный как обработка исключений. Исключение указывает на возникновение некоторого исключительного ошибочного со- состояния и содержит информацию о возникшей проблеме. Более формально, в терминах классов .NET, исключение является экземпляром класса, который был явно или неявно унаследован от базового класса System.Exception. Этот раздел будет полезен разработчикам на Visual Basic. Возможности обработки ошибок в VB весьма ограничены и по существу сводятся к выражению On Error goto. Если вы имеете опыт программирования на VB, то обнаружите, что исключения С# открывают для ваших программ новый мир обработки ошибок. С другой стороны, разработчики на Java и C++ знакомы с принципами использования исключений, так как эти языки обрабатывают ошибки аналогичным образом. Имеется лишь несколько отличий: в С# есть блок finally, предназначенный для освобождения ресурсов и отсутствующий в C++; С# требует, чтобы классы исключений наследовались от System.Exception, в то время как в C++ в качестве исключения может выступать все что угодно. Разработчики на C++ должны отметить, что благодаря безопасности типов в С# использование исключений в нем не приводит к потерям производительности, связанным с необходимостью поддержки дополнительной информации о типе, которая требуется в C++. Программирование исключений В этом разделе мы опишем идею, лежащую в основе исключений. Для того чтобы обеспечить возможность обработки ошибочных состояний, соответ- соответствующая часть программы обычно делится на блоки трех типов: О Блоки try содержат код, который составляет часть программы, но в котором могут встретиться серьезные ошибочные состояния. О Блоки catch содержат код, который обрабатывает различные ошибочные состояния. О Блоки finally содержат код, который освобождает ресурсы или предпринимает другие действия, обычно выполняемые в конце блока try. Понять ситуацию будет легче, если представить себе, что блок try концептуально со- содержит в себе связанные блоки catch и Чье 1у, как показано на рисунке. (Когда дело дой- дойдет до программирования, мы увидим, что синтаксически блоки catch и finally следуют за блоком try, а не содержатся в нем. Кроме того, переменные, объявленные в блоке try, выходят из области видимости, когда управление передается в блок catch или finally.) На рисунке стрелками показаны возможные пути исполнения программы:
Дополнительные возможности С# 187 г БЛОК TRY 1 L Ч| f 1У Вложенные вызовы методов Н Г 1 Возникла ошибка 1 Возникла ошибка 2 Нет ol БЛОК CATCH F БЛОК CATCH г иибок г БЛОК FINALLY 1 Управление передается 3£ пределы блока try Управление передается в блок l- . Это блок кода, отмеченный ключевым словом try и ограниченный фигурными скобками. Если ошибок не возникает, исполнение осу- осуществляется обычным образом. По окончании блока try, даже если ничего не произош- произошло, управление автоматически передается в блок ^ га ■п у, который должен содержать инструкции для освобождения ресурсов. Однако если в каком-то месте блока try обнару- обнаруживается ошибочное состояние, управление немедленно передается из блока try в соот- соответствующий блок rnrt , который отмечен как обрабатывающий этот тип ошибки. По завершении блока cat. ' управление автоматически передается в блок finally. Таков принцип работы. Синтаксис для описанного выше процесса выглядит сле- следующим образом (он пока не совсем точен — в блок catch необходимо добавить ряд параметров): try { // код для нормальной работы } catch { // обработка ошибок } finally { // освобождение ресурсов } Это не единственный способ реализации указанной схемы, однако он дает представ- представление об общих принципах. Например, можно опустить блок finally. Можно реализо- реализовать сколько угодно блоков catch для обработки различных типов ошибок. Можно даже опустить все блоки cat с в этом случае синтаксис будет служить не для определения исключений, а как гарантия того, чго по окончании блока try будет выполнен код в блоке finally, это может быть полезно в том случае, если в блоке try имеется несколько точек выхода. Возникает вопрос: если кол выполняется в блоке try, как он узнает, что необходимо перейти в блок с при возникновении ошибки? Здесь на сцену выходят исключения. При обнаружении ошибки код осуществляет так называемую генерацию исключения: он создает экземпляр объекта исключения — экземпляр класса, производного от базово- базового класса System.Exo ' on (пусть это будет, например, класс MyException), и выдает его следующим образом: throw new ""yExceptior ' ; Как только компилятор встречаеч отратор throw внутри блока try, он немедленно ищет соответствующий блок i at ch, связанный с этим блоком try, чтобы передать ему управление. Для блока try можно определить любое число блоков catch, но компьютер найдет подходящий, используя класс MyException: он будет искать такой блок catch, который указывает на экземпляр того же класса (или базового класса). В приведенном выше коде оператор catch оставлен пустым, однако обычно он записывается так: catch (MyException e) ... что соответствует обработчику catch, отвечающему за любой оператор throw, который выдает экземпляр "" r,xt:< ш - ( п.
488 Глава 6 Предположим, что в блоке t ry могут возникнуть две серьезные ошибки: переполне- переполнение (например, преобразование из отрицательного long в ulong) и выход за границы массива (возможно, вы пытаетесь получить доступ к элементу 10 в массиве, имеющем размерность 5). Microsoft уже написала классы исключений для этих состояний. Они из- известны соответственно как OverflowException и IndexOutOfRangeException, и оба определены в пространстве имен System. Теперь наш блок try будет выглядеть следующим образом: try { // обычный код // допустим, что имеются две переменные типа bool: Overflow и OutOfBounds, // которые устанавливаются в true в случае возникновения исключения if (Overflow == true) throw new OverflowException(J; / / другая обработка <t if (OutOf Bounds == true} throw new IndexOutOfRarigeExceptionO ; ' It иначе * продолжаем нормальное исполнение J - " .'- catch (OverflowException e) { // обработка состояния переполнения } catch (IndexOutOfRangeException e) { // обработка состояния выхода за границы массива } finally { ' " /: / / освобождение ресурсов } На первый взгляд кажется, что все то же самое можно было бы сделать с помощью On Error GoTo, и мы лишь более четко разделили различные части кода. На самом деле здесь представлен более мощный и гибкий механизм обработки ошибок. Основная осо- особенность заключается в том, что операторы throw не обязательно должны находиться в том же методе, что и блок try. Как только в программе встречается блок try, он продол- продолжает действовать даже при передаче управления другим методам. Если компилятору по- попадается оператор throw, он немедленно проходит сквозь все вызовы методов через стек в поисках окончания блока try и начала соответствующего блока catch. Во время этого процесса все локальные переменные в промежуточных вызовах методов выходят из области видимости. Это делает архитектуру try. . .catch прекрасно приспособлен- приспособленной к ситуации, описанной в начале раздела, когда в середине процесса обработки воз- возникает ошибочная ситуация в методе, находящемся на 15-ом или 20-ом уровне вложенности, и вся обработка должна быть немедленно остановлена. I Оператор throw может находиться в любом методе, вызванном во время выполнения блока try,— оно не обязано располагаться в том же самом методе, в котором определен блок try. Блоки try играют важную роль в управлении ходом выполнения программы. Одна- Однако необходимо понимать, что исключения предназначены для исключительных ситуа- ситуаций — отсюда их название. Плохим стилем программирования считается, например, использование исключений как способа управления выходом из цикла do. . .while. Теперь посмотрим, как это действует на практике. Начнем с простого примера кода, ко- который корректно обрабатывает пару возможных ошибочных состояний. Затем обсудим бо- более сложные особенности исключений и приведем пример, который продемонстрирует довольно сложный процесс обработки исключений. Пример SimpleExceptions Первый пример называется SimpleExceptions. Он периодически просит пользовате- пользователя ввести число и производит определенную обработку этого числа. В данном примере
Дополнительные возможности С# 189 обработка заключается в отображении числа. Однако предположим, что по некоторым сооб- соображениям число должно находиться в пределах от 0 до 5. Профамма не сможет выполнить действия с числом, если оно выйдет за границы этого диапазона. Поэтому, если пользователь введет число больше пяти или отрицательное значение, мы сгенерируем исключение. Программа продолжает запрашивать числа до тех пор, пока пользователь не нажмет Enter без ввода значения. Необходимо отметить, что этот код не относится к хорошим примерам использования обработки исключений. Как уже было отмечено, исключения предназначены для исключительных ситуаций. Пользователи всегда вводят не то, что нужно, поэтому данный случай не считается. Обычно программа сразу же проверяет введенное пользователем значение и просит заново ввести его, если возникает какая-то проблема. Однако генерация исключительных состояний в небольшом примере затруднительна. Поэтому мы позволим себе эту плохую практику, чтобы продемонстрировать работу исключений. В последующих примерах будут показаны более реальные ситуации. Код для SimpleExceptions выглядит следующим образом: using'1 System; namespace Wrox.ProfessionalCSharp.Chapter6.SimpleExceptioris { public class- MainEntryPoint { public static void Main(string[] args) { string userlnput; while (true) { try { Console.Write("Input a number between 0 and 5 " + "(or just hit return to exit)> "); userlnput = Console.ReadLineO; if (userlnput == "") * break; int index = Convert.Tolnt32(userlnput); if (index < 0 II index > 5) throw new IndexOutOfRangeExceptionl"You typed in " + userlnput); Console.WriteLine("Your number was " + index); } catch (IndexOutOfRangeException e) { ' Console.WriteLine("Exception: " + "Number should be between 0 and 5. " + e.Message); } catch (Exception e) { Console.WriteLinet" An exception was thrown. Message was: * + e.Message); } catch { Console.WriteLinel"Some other exception has occurred"); )■ finally { Console.WriteLinel"Thank you"); ■ } :} Основой этого кода является цикл while, который использует Console.ReadLine () для получения значения пользователя. ReadLine () принимает строку, поэтому первое, что мы делаем,— преобразуем ее в int с помощью метода System. Convert. Tolnt32 (). Класс System.Convert содержит полезные методы для выполнения преобразования
190 Глава 6 данных, и здесь необходимо вспомнить, что компилятор С# приводит int к экземпля- экземплярам базового класса System. Int32. Мы проверяем, не является ли введенная строка пус- пустой,— это условие выхода из цикла while. Отметим, что выражение break передает управление не только за пределы цикла while, но и за пределы блока try. Как способ управления ходом исполнения программы try работает независимо от всех остальных операторов С#, способных менять ход исполнения программы, поэтому с выходом за пределы блока try нет никаких проблем. Разумеется, как только управление передается за пределы блока try, выполняется оператор Console.WriteLine () в блоке finally. Теперь рассмотрим условие для исключения: if (index < l II index > 5) throw r.ew IndexOutOfRange£xception("You typed in " + user Input) ; При генерации исключения необходимо решить, какой тип исключения должен пе- передаваться. Нам доступен класс Si stem.Exception, но это базовый класс, и считается плохой практикой программирования генерация в качестве исключения экземпляра этого класса, так как он не переносит информации о природе исключительного состоя- состояния. Microsoft определила множество других классов исключений, являющихся произ- производными от System.T xcepticr . Каждый из них соответствует конкретному типу исклю- исключительного состояния, кроме того, вы можете определять свои собственные классы. Идея состоит в том, чтобы предоставить о конкретном ошибочном состоянии настоль- настолько много информации, насколько возможно, путем передачи экземпляра класса, кото- который соответствует конкретному ошибочному состоянию. В данном случае лучшим вариан- вариантом является Syste™ "^ "exOutOfRargefcixception. Он имеет несколько перегруженных конструкторов. Мы выбрали тот, что принимает строку с описанием ошибки. Теперь посмотрим, что происходит при выполнении этого примера. Программа за- запустится и попросит пользователя ввести число. Если введенное число корректно, вы- выражение в операторе if примет значение false, и throw не будет выполнен. Компьютер выведет сообщение, показывающее, какое число было введено. В этот мо- момент мы оказываемся на закрывающей фигурной скобке, которая отмечает окончание блока try. В этой точке компьютер будет искать соответствующий блок finally для вы- выполнения заключительной стадии обработки. В блоке finally выводится сообщение "Thank you". Обычно в этом блоке выполняются такие вещи, как закрытие файловых деск- дескрипторов и вызов методов Disposed различных объектов для освобождения ресурсов. После выхода из блока f Lnally компьютер передает управление следующему оператору. В данном случае мы возвращаемся к началу цикла while и снова входим в блок try. Допустим, что теперь пользователь вводит число, не попадающее в диапазон от 0 до 5. Это будет обнаружено в операторе if, и будет создан экземпляр объекта IndexOutOfRan- geExcci.h : а. В этой точке компьютер немедленно выйдет из блока try (поэтому опера- оператор ' ч „..Writ. " bne("Your number was " ^ index); не будет исполнен). Затем компьютер начнет поиск блока catch, обрабатывающего IndexOutOfRangeException. (Вообще говоря, это не совсем верно. Компилятор* в этот момент уже знает, какой обра- обработчик catch необходимо вызвать, но для упрощения предположим, что компьютер определяет это во время исполнения кода.) Первым встретится блок - tch: i~"i -_- -idexOutOfKangeExc-. ". .on e) { '".wrineL.neCExcer: -on: " J- " № .. =r she be between 0 and 5." + e.Message); } Так как этот блок catch принимает параметр подходящего класса, он будет выпол- выполнен, и на экране появится сообщение об ошибке. Передаваемое исключение е является тем самым исключением которое было сгенерировано ранее. Следовательно, мы имеем доступ к любой информации, содержащейся в этом экземпляре исключения. В данном случае мы используем свойство Excepti on.Message. Оно возвращает строку, которую мы передали в конструктор этого экземпляра исключения. После исполнения блока catch управление передается в блок finally, как если бы исключения не было. Отметим, что мы дополнительно реализовали еще один блок catch: catch (Exception e) { Console.WriteLine("An exception was thrown. Message was: " + e.Message);
Дополнительные возможности С# 191 Этот блок catch тоже мог бы обрабатывать iKdexOutOfRangeException, если бы эти исключения не отлавливались предыдущим блоком catch — напомним, что ссылка на класс В может также указывать на любой производный от В класс (см. главу 4). Так как IndexOutOfRangeException является производным от Exception, то передаваемый здесь параметр е мог бы вполне быть экземпляром IndexOutOfRangeException. Тогда почему не исполняется этот блок catch? Дело в том, что компилятор выполняет только первый подходящий блок catch. Как только он находит блок, способный принять пере- переданный экземпляр исключения, он останавливается, игнорируя оставшиеся блоки catch. Вполне логичен вопрос: зачем мы реализовали этот блок catch, если он никогда не исполняется? Он предусмотрен на тот случай, если будет передано любое другое иск- исключение. В блоке try находится не только наш код. В нем осуществляются три отдель- отдельных вызова функций в пространстве имен System — мы вызываем Console. ReadLine (), Console.Write () и Convert .Tolnt32 (). Что произойдет, если один из этих методов сгенерирует исключение? Такая ситуация вполне возможна. Если набрать что-нибудь, не являющееся числом, на- например "а" или "привет", то метод Convert .Tolnt32 () передаст исключение класса Sys- System. FormatException, показывая, что переданная в Tolnt32 О строка не может быть преобразована в int. Компилятор пройдет обратно сквозь вызовы методов в поисках под- подходящего обработчика. Первый блок catch, принимающий IndexOutOfRangtEx^eplion, не сможет это сделать. Тогда компьютер обратится ко второму блоку catch. Этот блок способен обработать исключение: FormatException является производным от Exception, поэтому экземпляр FormatException может быть передан в качестве параметра. Такая структура является типичной. Мы начинаем с блоков cat -h, которые служат для обработки конкретных ошибочных состояний. Заканчивается все более общими блоками, обрабатывающими остальные ошибки, для которых не было написано конк- конкретных обработчиков. Порядок следования блоков catch имеет значение. Если бы мы расположили два следующих блока в указанной последовательности, код не был бы от- откомпилирован: // Неверно] Не будет компилироваться! catch (Exception e) { Console.WriteLinel"An exception was thrown. Message was: " + e.Message); } catch 'IndexOutOfRangeException e) { Console.WriteLine("Exception: " + "Number should be between 0 and 5. " + e.Message) ; } Дело в том, что код во втором блоке catch никогда не будет выполняться. Если гене- генерируется исключение IndexOutOfRangeException, то оно обрабатывается первым бло- блоком catch. Компилятор обнаружит это и откажется компилировать код. Правило состоит в том, что обработчики исключений для производного класса должны идти раньше, чем для базового. Наконец, рассмотрим третий блок catch: catch { Console.WriteLinef"Some other exception has occurred"); } Это наиболее общий блок catch, он не принимает никаких параметров. Причина, по которой этот блок catch расположен здесь, состоит в том, что исключение может быть сгенерировано другим кодом, написанным не на С#, или даже неуправляемым кодом. То, что в качестве исключений могут быть переданы только экземпляры класса, произ- производного от System.Exception, является требованием языка С#. Другие языки могут не иметь такого ограничения. Например, C++ позволяет передавать в качестве исключения любую переменную. Если ваш код использует библиотеки или сборки, написанные с по- помощью других языков, то он может встретить исключение, не являющееся производным от System. Exception. Наш заключительный блок catch выловит любое из таких иск- исключений. Однако мы не сможем с ним ничего сделать — мы не знаем, какой класс может представлять данное исключение, соответственно не существует типа параметра, с по- помощью которого его можно представить. Даже object не подойдет, так как исключение может быть сгенерировано не .NET-кодом, т.е. оно не будет производным от System.Object. У нас просто нет доступа к объекту исключения, чтобы выяснить, что произошло. 69
192 Глава 6 В нашем примере, возможно, не имело особого смысла добавлять этот блок catch. Это полезно в том случае, если вы напрямую вызываете методы из библиотек, которые разработаны не для .NET и которые способны генерировать исключения. Однако мы включили его в пример для иллюстрации общего принципа. Теперь запустим наш пример. На экране видно, что происходит при вводе различ- различных значений. Показана генерация как исключения IndexOutOfRangeException, так и FormatException: icrosoft Windoua 20В0 [Uersion S.ИИ.21951 <O Copyright 1V8S-2(W>0 Microsoft C6r#- Input л miftbcc berueen И and 5 Хлг ^just bit i*eturn to dxit>> 4 Sfonv number ii&ii.'-q ' .-,,,,—-. , ' ' ' ?:■* • Than К you " i д -JP, , ,& Irtput a nu^be»vb«t< Л ' ^ * *^" 4^ Vmtr nunbct* was" 0 ,. ^ ■ - «■. Thrmk you _ , . .. ■ Input a number between 0 ann S (Sr Just; hit ir'eturn to' «ciO.j» Hxccrtion: Nuhher slioul'l be between В and S» 4<n typed In 10 DtaOb you' ' . , : ' '" * VA Input 41 пи^Ь«л>ю'Сие«1> Э «пД ^ 4J^ jui» »iit IV^titfn tie tt*ie>. fin exception us thrownw fi^csayei,uas: Input string was n t ' rnenb v«u ■ !,w•.. M. .: - . v(< •; Input d nunher Mtwftftn в т<п<тй? Cor Jirft Id' г»»»!1*» «•<!. t»lt)S Обработка исключений из другого кода В примере мы продемонстрировали обработку двух исключений. Одно из них — IndexOutOfRangeException — было сгенерировано нашим собственным кодом. Дру- Другое — FormatException — было передано из одного из базовых классов. Это общая пара- парадигма. Для библиотечного кода является типичной генерация исключений в том случае, если он обнаруживает некоторую проблему или если один из методов вызван некоррект- некорректно, с передачей неправильных параметров. Однако библиотечный код редко пытается поймать сгенерированные таким образом исключения. Как поступать с подобными проб- проблемами, должен решать клиентский код. Нередко во время отладки кода исключения генерируются в библиотеках базовых классов, и происходит это из-за ошибок в вашем собственном коде, приводящих к пере- передаче неверных параметров в методы базового класса. Процесс отладки в некотором смысле включает в себя определение того, почему были сгенерированы исключения, и устранение этих причин. Вашей целью должно стать то, чтобы к моменту рассылки кода клиентам исключения действительно возникали бы только в исюпочительных обстоятель- обстоятельствах и по возможности обрабатывались соответствующим образом в вашей программе. Другие свойства и методы System.Exception В примере мы использовали только одно свойство объекта исключения — Message. Однако необходимо отметить, что в System. Exception существует ряд других доступных свойств: i ——— Свойство Описание HelpLink Ссылка на файл помощи, который предоставляет дополнительную информацию об исключении. Message Текст, который обычно передается конструктору исключения. Описывает ошибочное состояние. Source Имя приложения или объекта, вызвавшего исключение. StackTrace Подробности о вызовах методов в стеке. Это помогает отследить метод, передавший исключение. TargetSite Имя метода, сгенерировавшего исключение. Два из этих свойств, StackTrace и TargetSite, автоматически устанавливаются сре- средой исполнения .NET. В зависимости от ключей компиляции TargetSite может быть не- недоступно, в этом случае возвращается null. Свойства Sc. ce. Message и HelpLink должны заполняться кодом, генерирующим исключение, путем установки этих свойств непосред- непосредственно до генерации исключения. Код, выполняющий эти действия, может выглядеть примерно так:
Дополнительные возможности С# 193 if { (ErrorCondition == true) Exception MyException = new ClassMyException("Help!!!!"); MyException.Source = "Название приложения"; MyExceptiori.HelpLink = "MyHelpFile.txt"; throw MyException; ClassMyException — имя конкретного класса исключения. Отметим, что обычной практикой является указание в именах классов исключений слова 'Exception1. Поскольку этот класс обязан быть производным от System.Exception, для ссылки на него удобно использовать ссылку на System.Exception. Действия, производимые при отсутствии обработки исключений Могут иметь место ситуации, когда существует возможность генерации исключения, а блок try, способный обработать такой вид исключения, отсутствует в программе. Для ил- иллюстрации вернемся к примеру SimpleExceptions. Предположим, что мы опустили бло- блоки catch для FormatException и всех остальных исключений, а оставили только блок, реагирующий на исключение indexOutOfRangeException. (Возможно, разработчик не подумал о том, что базовые классы могут генерировать свои собственные исключения.) Что произойдет, если будет сгенерировано исключение FormatException? Его выловит среда исполнения .NET. Позже будет показано, как блоки try вкладывают- вкладываются друг в друга. В примере это делается неявно. Блок try не является в нашем коде единст- единственным. Среда исполнения .NET помещает всю программу в один большой блок try — это делается для каждой программы .NET. Этот блок try имеет обработчик catch, который способен обработать любой тип исключения. Если возникает исключение, не обрабатывае- обрабатываемое вашим кодом, управление передается из программы в этот блок catch среды исполне- исполнения .NET. Однако результат, вероятно, вас не устроит. Дело в том, что выполнение программы будет прервано, а пользователь увидит диалоговое окно с сообщением о том, что код не обработал исключение, и со сведениями, которые .NET удастся получить об этом исключении. Но, по крайней мере, исключение будет обработано! Именно это и произошло в примере Vector, когда программа сгенерировала исключение (см. главу 5)! Вообще, при создании исполняемого файла следует обрабатывать максимальное чис- число исключений. Если же вы пишете библиотеку, то обычно лучше не обрабатывать иск- исключения (если только конкретное исключение не представляет собой то, что ваш код сможет обработать), а предположить, что их обработает вызывающий код. Исключения базовых классов В этом разделе мы коротко рассмотрим некоторые из исключений, доступных в базовых классах. Затем перейдем к более сложному примеру и покажем, как вкладывать блоки try друг в друга и определять собственные классы исключений. Microsoft определила большое число классов, и привести здесь их полный список вряд ли возможно. Иерархия классов на рисунке дает лишь представление об общей структуре: Object Exception I SystemException | ArgumentException] I ArgumentNullException | N (ArgumentOutOfRangeException | | UnauthorizedAccessException | ApplicationException | 7 Наследуйте \свои классы исключений от этого класса | IQException | | StackOverflowException] | FileLoadException | 1 FileNotFoundException | | DirectoryNotFoundException | | OverflowException | |EndOfStreamException|
194 Глава 6 Здесь показаны некоторые из исключений System и часть исключений из пространст- пространства имен System. 10. Названия пространств имен не указаны на рисунке. Однако отметим, что все классы на рисунке расположены в пространстве имен System, за исключением класса IOException и производных от него классов. Они находятся в пространстве имен System. 10, которое связано с чтением и записью данных в файлы. Вообще говоря, для исключений не существует особого пространства имен: классы исключений должны раз- размещаться в том пространстве имен, которое соответствует классам, способным генериро- генерировать эти исключения. Поэтому исключения, связанные с вводом/выводом, находятся в пространстве имен System. 10. Мы не будем подробно рассматривать классы исключений, показанные на рисунке. Назначение большинства из них должно быть понятно из их названий. Нашей целью яв- является получение общего представления об иерархии. Отметим лишь, что: О Исключение StackOverflowException возникает тогда, когда участок памяти, отведенный под стек, заполняется до отказа. Стек используется для хранения па- параметров методов и типов данных по значению (см. главу 5). Переполнение стека может возникнуть в том случае, если, например, метод начинает рекурсивно вы- вызывать сам себя. Обычно это фатальная ошибка, поскольку приложение, как пра- правило, не способно сделать ничего, кроме как завершиться, и в этом случае маловероятно, что будет выполнен даже блок finally. Обычно обработка подобных ошибок в собственном коде ie имеет особого смысла. □ Потоки будут рассмотрены в главе 14. Поток представляет собой движение данных между их источниками. Обычной причиной появления EndOfStreamException является попытка чтения за границами файла. О Переполнение Overf lowException возникает, например, тогда, когда вы пытаетесь привести тип int. содержащий значение -40, к типу uint в контексте checked. Начнем изучение иерархии исключений с общего объекта исключения System.Ex- System.Exception. Как и любой класс .NET, он унаследован от System.Object. В своем коде вы вообще не должны генерировать экземпляры этого класса. Это считается плохой практи- практикой программирования, так как не дает представления о природе ошибочного состояния. В иерархии существуют два важных класса, порожденных от System. Exception. О System.SystemException — предназначен для исключений, обычно генерируе- генерируемых средой исполнения .NET, или исключений, которые имеют общую природу и могут быть сгенерированы практически любым приложением. Например, сре- среда исполнения .NET может сгенерировать StackOverflowException, если обна- обнаружит, что стек переполнен. С другой стороны, вы можете сгенерировать Агз_ .ti Exception или его подклассы в своем коде, если обнаружите, что метод вызван с несоответствующими аргументами. Подклассы System. SystemExcepti on представляют как фатальные, так и некритичные ошибки. О System.ApplicationException — этот класс является важным, поскольку пред- представляет собой базу, предназначенную для любого класса исключений, опреде- определенного третьими лицами (другими словами, не Microsoft). Поэтому, если вы определяете свои собственные исключения, которые охватывают специфичные для вашей программы ошибочные состояния, их необходимо явно или неявно наследовать от System.ApplicationException. Два этих класса исключений и их подклассы охватывают все ошибочные состояния, специфичные для приложений сторонних производителей или генерируемые базовыми классами .NET либо средой исполнения .NET и характерные для .NET. Сюда не относят- относятся исключения, которые генерируются некоторыми базовыми классами, но тем не ме- менее являются специфичными для определенных типов действий. Хорошим примером этого могут быть классы, порожденные от System. 10. IOException. Эти исключения представляют некоторые из ошибочных состояний, которые могут возникать при по- попытке чтения файлов — область, охватываемая базовыми классами в пространстве имен System. 10. Иерархия классов для исключений в некотором роде необычна, большинство этих классов не добавляет функциональности в свои соответствующие базовые классы. Обыч- Обычно наследование одного класса от другого осуществляется с целью перекрытия некото- некоторых методов (или свойств и т.п.) или добавления новых возможностей с помощью но- новых методов. Однако в случае обработки исключительных ситуаций причиной для добавления унаследованных классов является указание более специфических ошибоч- ошибочных состояний, и, как правило, при этом не возникает необходимости в перекрытии или добавлении новых методов. Основная идея этой иерархии заключается в том.
Дополнительные возможности С# 195 чтобы использовать различные блоки catch для сужения или расширения диапазона об- обрабатываемых в программе ошибочных состояний. Например, у вас может быть метод, для которого необходимо отлавливать все попытки его вызова с несоответствующими значениями аргументов. При возникновении подобной ситуации может потребоваться выполнить некоторое общее действие. Однако действие должно быть иным в том слу- случае, если аргумент, который обязан содержать ссылку на экземпляр объекта, в действи- действительности содержит null. С такой ситуацией можно справиться следующим образом: catch- (ArgumentNullException e) £ // обработка ошибки, когда аргумент равен null } catch (ArgumentException e) £ // обработка всех остальных ошибок, связанных с аргументами } Здесь гарантируется, что при генерации ArgumentNullException оно будет обрабо- обработано соответствующим кодом. Однако если возникнет другая проблема с аргументами, не попадающая в эту категорию, например, код обнаружит выход аргумента за допусти- допустимый диапазон и сгенерирует ArgumertOutOfRangeException, то среда исполнения про- проигнорирует первый блок catch, так как он не принимает в качестве аргумента соответствующий класс. Тем не менее второй блок catch успешно выловит и обработает это исключение. Вложенные блоки try Одной из приятных особенностей исключений является возможность вложения блоков try друг в друга. Так, можно записать: try { // обычный код // Точка А try / / ОбыЧНЫЙ КОД: • // Точка В ;- } catch { // обработка ошибок У/ Точка С } finally { // освобождение ресурсов } // обычный код // Точка* D } catch { /У обработка ошибок } finally { / / освобождение ресурсов } Для простоты в коде используется только один блок catch для каждого из блоков try. Но в обоих случаях можно вместе расположить несколько блоков catch. Вложенные блоки try работают следующим образом. Предположим, что исключе- исключение генерируется или в точке А, или в точке D кода. Эти точки находятся вне внутренне- внутреннего блока try, и поэтому исключение будет обработано так, как было описано выше,— внешним блоком catch при условии, что он содержит обработчик для соответствующе- соответствующего типа исключения. Ситуация рассматривается так, как если бы внутреннего блока try не было вообще.
196 Глава 6 Допустим, что исключение генерируется в точке В. Мы находимся во внутреннем блоке try, поэтому среда исполнения .NET обратится к внутреннему блоку catch в поис- поисках соответствующего обработчика. Если среда найдет его, этот обработчик будет вы- выполнен, а затем, как обычно, будет исполнен внутренний блок finally, после чего управление перейдет в точку D. Другими словами, вся обработка происходит во внутрен- внутреннем блоке try, а внешний блок try не задействуется. Все опять обстоит так, как будто бы в программе только один блок try. Теперь предположим, что исключение сгенерировано в точке В, но для класса данно- данного исключения во внутреннем блоке catch обработчика нет. У среды исполнения .NET не остается другого выбора, кроме как выйти из внутреннего блока try в поисках соот- соответствующего обработчика. Поскольку мы покидаем внутренний блок try, код в этом блоке try выполняется обычным образом. (Напомним, что код в блоке finally выпол- выполняется всегда, независимо от того, что вызвало передачу управления за пределы соот- соответствующего блока try.) Однако код в точке D не будет исполнен, так как после выполнения внутреннего блока try система продолжит поиск подходящего обработчи- обработчика исключения. Следующее очевидное место для поиска — внешний блок catch. Если си- система обнаруживает обработчик, он исполняется, после чего выполняется внешний блок finally. Если же подходящий обработчик не найден, его поиск продолжится. Это означает, что будет выполнен внешний блок finally, а затем, поскольку больше блоков catch нет, управление будет передано среде исполнения .NET, которая завершит работу программы и выведет привычное диалоговое окно, сообщающее о необработанном исключении. В данном случае два вложенных блока try действуют совместно в поисках наилучшего обработчика ошибочного состояния. Еще более интересный случай наблюдается тогда, когда исключение генерируется в точке С. Если программа находится в точке С, значит, она обрабатывает исключение, сгенерированное в точке В. Однако внутри блока catch совершенно законной является генерация другого исключения (см. ниже). В данном случае исключение расценивается как сгенерированное внешним блоком try, поэтому управление немедленно покинет внутренний блок catch, будет выполнен внутренний блок finally, и система обратится к внешнему блоку catch в поисках соответствующего обработчика. Аналогично, если исключение генерируется во внутреннем блоке finally, управление немедленно пере- передается подходящему обработчику во внешнем блоке catch. Мы рассмотрели ситуацию с двумя блоками try, но те же самые принципы действу- действуют для любого числа вложенных блоков try. На каждом этапе среда исполнения .NET будет передавать управление вверх по цепочке блоков try в поисках соответствующего обработчика. На каждом этапе, как только завершается исполнение блока catch, испол- исполняется код в соответствующем блоке finally, но до тех пор пока не будет найден и вы- выполнен соответствующий обработчик catch, никакой другой код за пределами блока finally исполняться не будет. Вложенные блоки try используются по двум причинам: О С целью изменения типа сгенерированного исключения О Для обработки различных типов исключений в различных участках кода Рассмотрим это более подробно. Изменение типа исключения Изменение типа исключения может быть полезно в том случае, если первоначально сгене- сгенерированное исключение неадекватно описывает проблему. Обычно происходит следую- следующее. Допустим, что среда исполнения .NET генерирует низкоуровневое исключение, которое сообщает о том, что произошло переполнение (Overf lowException) или аргу- аргумент, переданный в метод, был некорректным (класс, порожденный от ArgumentExcep- tion). Однако, зная, в каком контексте произошло исключение, вы уверены, что в его, основе лежит другая проблема (например, переполнение могло возникнуть только в том случае, если файл, читаемый в данный момент, содержит некорректные данные). Наибо- Наиболее подходящим, что может сделать обработчик первого исключения, является генерация другого исключения, которое точнее описывает проблему,— чтобы блок catch, идущий следом, смог отреагировать на нее более адекватно. В данном случае он может также пере- передать и первоначальное исключение с помощью реализованного в System. Exception свой- свойства InnerException. InnerException содержит ссылку на любое сгенерированное исключение на тот случай, если основному обработчику потребуется эта дополнительная информация.
Дополнительные возможности С# 197 Необходимо отметить, что исключение может произойти внутри блока catch. Напри- Например, вы пытаетесь прочитать файл, содержащий указания по обработке исключения, а он отсутствует. Обработка различных исключений в разных участках кода Вторая причина использования вложенных блоков try заключается в том, что в этом случае различные типы исключений могут обрабатываться в разных участках кода. Хоро- Хорошим примером является цикл. Внутри него могут возникать различные исключительные ситуации. Некоторые из них могут оказаться довольно серьезными и потребуют выхода из цикла, в то время как другие могут быть менее серьезными, и в этом случае будет до- достаточно перейти к следующей итерации цикла. Этого можно достичь, имея один блок try, обрабатывающий менее серьезные исключительные ситуации, внутри цикла, и внеш- внешний блок try, обрабатывающий более серьезные ситуации, вне цикла. Как это работает, будет показано в примере ниже. Пример использования исключений: MortimerColdCall Пример MortimerColdCal 1 будет содержать два вложенных блока t ry. Он продемонст- продемонстрирует практику определения своих собственных классов исключений и генерацию исключения внутри блока try. В этом примере мы вновь обратимся к компании сотовой связи Mortimer Phones (см. главу 4). Предположим, что Mortimer Phones требуются новые клиенты. Их отдел про- продаж собирается обзвонить людей по списку, чтобы предложить им стать клиентами, или, используя торговый жаргон, сделать "холодные" звонки. У нас есть текстовый файл, содержащий имена жертв — прошу прощения, клиентов,— которых будут обзвани- обзванивать. Файл должен иметь определенный формат, в котором первая строка содержит ко- количество людей в файле, а каждая последующая — имя очередной персоны. Другими словами, правильно сформированный файл имен выглядит примерно так: File Edit Format Help 3 _J Zbigniew Harlequin Avon from 'Blake's T Karii watson Поскольку это лишь пример, мы не собираемся на самом деле обзванивать этих людей. Наша версия будет отображать на экране имя человека (возможно, для того, чтобы этой информацией воспользовался сотрудник отдела продаж). Именно поэтому в файле со- содержатся только имена. В данном примере мы не будем считывать телефонные номера. Программа попросит пользователя ввести имя файла, а затем прочитает его и отобразит на экране содержащиеся в нем имена. Задача кажется простой, однако даже здесь имеются два момента, способных вы- вызвать проблемы и заставить нас прервать всю работу: 1. Пользователь может ввести имя несуществующего файла. Это будет отслеживаться с помощью исключения FileNotFound. 2. Файл может иметь некорректный формат. Здесь также могут возникнуть две проб- проблемы. Во-первых, в первой строке файла может находиться не целое число. Во-вто- Во-вторых, в файле может оказаться не то число имен, которое указано в первой строке. Для отслеживания этих проблем мы напишем свое собственное исключение — ColdCallFileFormatException. Также может возникнуть ситуация, которая не потребует завершения всего процесса, но придется перейти к следующему человеку по списку в файле (т.е. эту ошибку необходи- необходимо обрабатывать во внутреннем блоке try). Среди людей в списке могут оказаться шпио- шпионы, работающие на конкурирующие телефонные компании, и мы не желаем извещать их о своем существовании, случайно позвонив одному из них. Наше исследование показало, что имена шпионов начинаются на Z. Они должны быть отсеяны еще при подготовке файла данных, однако на тот случай, если кто-то из них просочится, нужно проверять каждое имя в файле и генерировать LandLineSpyFoundException при обнаружении шпиона. Разумеется, это еще один наш собственный объект исключения.
198 Глава 6 Мы реализуем этот пример, создав класс ColdCallFileReader, который связывается с файлом и извлекает из него данные. Этот класс будет написан безопасным способом; все его методы будут генерировать исключения в случае некорректного вызова — например, если метод, требующий чтения файла, вызывается до того, как файл был открыт. С этой целью мы создадим еще один класс исключений — UnexpectedException. Определение собственных классов исключений Нам необходимо определить три исключения. Определение собственных исключений довольно простая задача, так как редко требуется добавлять в них свои собственные мето- методы. Необходимо только реализовать конструктор для того, чтобы обеспечить корректный вызов конструктора базового класса. Приведем полную реализацию LandLineSpy- FoundExcepti on: class LandLineSpyFoundExceptic.i : AppJ icationException , { public LandLineSpyFoundExce^tion(stii"g spyName) : baseCLandLine r py f - ~d, with name " ■•■ spyName) public Larra..i_.nc !i->v u. aExcepcLur ' sLri ig spyName, Exception inuerExcepcion) : base( "La.idL ne spy foi ' with name " ^ "pyNarne, innerException) Отметим, что мы унаследовали его от ApplicaLionException. На самом деле, если бы мы подошли к вопросу более формально, то, возможно, ввели бы промежуточный класс, что-то наподобие Сп_ ICal] FileException, производный от ApplicationExcep- tlon, и унаследовали бы оба наших класса исключений от этого класса — с целью сделать так, чтобы обрабатывающий код имел дополнительную степень контроля над тем, какие обработчики исключений какое исключение обрабатывают. Но, не желая усложнять пример, мы не стали этого делать. LandLineSpyFoundFxception выполняет совсем небольшую работу. Мы предполо- предположили, что передаваемое в его конструктор "сообщение" является именем найденного шпиона, и поэтому конвертируем эту строку в более осмысленное сообщение об ошибке. Мы также реализовали два конструктора, один из которых принимает сообщение, а другой принимает внутреннее исключение как параметр. При определении своих собственных классов исключений лучше определять как минимум два таких конструктора (хотя в данном примере второй конструктор Landl n< SpyF<- urdException использоваться не будет). Теперь определим Col allFil -ormatException. Оно основывается на тех же принципах, за исключением того, что обработка сообщения не производится: class ColdCallFileFormatbxcept.lon : ApplicationException { public ColdCallFileFormatExc-Bption (string Message) : base(Message) public ColdCallFileFormatException (string Message, Exception innerException) : base(Message, innerException) И, наконец, UnexpectedException, которое выглядит точно так же, как ColdCallFile- ColdCallFileFormatException: class UnexpectedException : ApplicationException { public, UnexpectedException (string Message) : base(Message) public UnexpectedException (string Message, E:vt~er>t"ion inner Exception)
Дополнительные возможности С# 199 : base(Message, innerException) Метод Main() Метод Main()B примере MortimerroldCall выглядит следующим образом. Поскольку мы будем работать с файлом, нам придется вызывать методы не только из пространства имен System, но и из пространства имен System. ТО. Вопросы чтения и записи в файлы рассматриваются в главе 14. В данном примере обработка (файла является очень простой, а код достаточно понятным. using System; using System.10; namespace Wrox.Professiona'CSharp.Chapter6 .MortimerColdCall { class MainEntryPoint { static void Main(stiing[] args) { string fileName; Console.WriteC'Please type in the name of the file " + "\ncontaining the names if the people to be cold-called > "); fileName = Console.ReadI.no; ColdCallFileReader pe^vl ;ToRing = new ColdCallFileReader(); try { peopleToRing.Oiien(f ileName) ; for (int I - 0; I<peopleToRing.NPeopleToRing; I + +) { peopleToRing.ProcessNextPerson() ; } Console.Wri teLir = ( "All cal~Lees processed correctly") ,- } catch (FileNotFoundExceptioi e) { Console.WriteLineI"The file {0} does not exist", fileName); } catch (ColdCallFileFormatException e) { Console.WriteLineC'The file {0} appears to have been corrupted", fileName); Console.WriteLine("DetajIs of problem are: {0}", e.Message); if (e.InnerException != null) Console.WriteLine("Inner exception was: {0}", e.InnerException.Message); } catch (Exception e) { Console.WriteLine("Exception occurred:\n", e.Message); } finally { peopleToRing.Dispose () ,- Этот код представляет собой цикл для обработки имен из файла. Сначала мы запраши- запрашиваем у пользователя имя файла. Затем создаем экземпляр объекта класса ColdCallFile- ColdCallFileReader. Этот класс служит для чтения данных из файла. Отметим, что это делается вне начального блока try, поскольку переменные, для которых создаются экземпляры клас- классов, должны быть доступны в последующих блоках catch и finally. Если бы они были объявлены внутри блока t ry- то они вышли бы из области видимости сразу после закры- закрывающей фигурной скобки блока .- г ,. В блоке try открывается файл (метод ColdCallFileReader .Open ()) и в цикле считы- ваются все имена из этого файла. Метод ColdCallFileReader. ProcessNextPerson (), который будет определен позже, читает и отображает имя следующего человека в файле,
200 Глава 6 а свойство ColdCallFileReader.NPeopleToRing сообщает, сколько человек должно быть в файле (как указано в первой строке файла). Имеются три блока catch: первый для исключения FileNotFoundException, второй для ColdCallFileFormatException, и третий блок catch будет обрабатывать все осталь- остальные ошибки .NET, которые будут появляться, если что-то еще будет работать неправильно. Именно этот блок обработает, например, все UnexpectedException. В случае возникновения FileNotFoundException отображается соответствующее сообщение. Отметим, что в этом блоке catch мы вообще не используем экземпляр иск- исключения. Дело в том, что в этом блоке мы решили продемонстрировать дружествен- дружественность нашего приложения по отношению к пользователю. Объекты исключений, как правило, содержат техническую информацию, полезную для разработчиков. Нет необ- необходимости выдавать ее конечным пользователям. Поэтому в данном случае мы создаем свое собственное простое сообщение. В случае ColdCallFileFormatException мы поступаем прямо противоположно и показываем, как предоставить более подробную техническую информацию, включая детали внутреннего исключения, если таковое имеется. Наконец, мы обрабатываем остальные общие исключения, производные от Sys- System. Exception. Это делается для того, чтобы на экран выводилось дружественное по от- отношению к пользователю сообщение, а не стандартный диалог .NET, сообщающий о необработанном исключении. Однако мы не берем во внимание исключения, не являю- являющиеся производными от System.Exception. Здесь мы не вызываем напрямую код, со- созданный до появления .NET, поэтому любая такая ошибка покажет, что произошло что-то серьезное и, возможно, непоправимое — в любом случае мы не сможем ее обработать. Блок finally освобождает ресурсы. В данном случае это означает закрытие всех открытых файлов. Метод ColdCallFileReader. Dispose () делает то же самое. Класс ColdCallFileReader Теперь определим класс, который занимается чтением файла. Так как этот класс устанав- устанавливает соединение с внешним файлом, мы должны быть уверены в том, что он коррект- корректно освобождается в соответствии с принципами, указанными в главе 5. Поэтому класс является производным от I Disposable. Во-первых, объявим две переменные: class ColdCallFileReader : .[Disposable i '• * ■FileStream fs; j_ StreamReader sr; .uint nPeopueToRing; bool isDispbsefl '- false; bool isOpen = false; Классы FileStream и StreamReader находятся в пространстве имен System. 10 и яв- являются базовыми классами, используемыми для чтения файла. FileStream позволяет установить соединение с файлом, a StreamReader специально предназначен для чтения текстовых файлов и реализует метод StreamReader (), читающий из файла строку текста. Поле isDisposed связано с процессом деструкции (финализации). Оно показыва- показывает, был ли вызван метод Dispose О . Поле isOpen используется для проверки ошибок — в данном случае для проверки того, что StreamReader действительно имеет соединение с открытым файлом. Процесс открытия файла и чтения его первой строки, содержащей количество имен в файле, осуществляется в методе Open (): public void. Open(string fileName) { fs = new .FileStreamffileName, FileMode.'Open) ; sr = new StreamReader(fs); if (isDisposed) , throw new UnexpectedException( "Attempt to open cold-call file after Disposed called."); try { Г. string FirstLine - sr.ReadLine(); ■: -.-,■■. ■» nPeopleToRing *>* uint. ! . . isOpen = true; ' 4■■'■■* f'.£?b;catch:,;.('FormatException e)
Дополнительные возможности С# 201 throw new ColdCallFileFprmatExceptionf "First line isnVt an integer", e) ; Этот метод включает в себя первый из двух внутренних блоков try. Целью данного бло- блока является обработка ошибок, возникающих из-за того, что первая строка файла не содер- содержит целое число. В этом случае среда исполнения .NET генерирует FormatException, которое мы обрабатываем и преобразуем в более осмысленное исключение, информи- информирующее о проблеме с форматом файла. (Отметим, что System.FormatException пред- предназначено для указания проблем, связанных с основными типами данных, а не с файла- файлами, поэтому передавать это исключение вызывающей функции не слишком полезно.) Новое сгенерированное исключение будет обработано во внешнем блоке try. Поскольку в данном случае не требуется освобождать ресурсы, отсутствует блок finally. Метод Open () также проверяет поле isDisposed для выяснения того, не вызывался ли уже метод Dispose (). Так как вызов Dispose () означает, что вызывающий его код завершил свою работу с объектом, попытка создать соединение с файлом после вызова Dispose () расценивается как ошибка. Если все в порядке, поле isOpen принимает значение true, показывающее, что уста- установлено соединение с файлом, из которого теперь можно читать данные. Метод ProcessNextPerson () также содержит внутренний блок try: public void ProcessNextPersonО { if (lisOpen) throw new UnexpectedException( "Attempt to access cold-call file that is not open"); ■ try , { •" string- name; '- name ? sf. ReadLinel); if (name. Length == 0) throw new CpldCallFileFormatExceptionC'Not enough names"); if (nametO] == !Z4 ; { .throw new LanaLineSpyFoundException(name); i, ') Console.WriteLine!name); : } catch (LandXiineSpyFoundException e) Consdle-.WriteLine (e.Message) ,- Здесь с файлом могут возникнуть две проблемы (предполагается, что файл открыт,— метод ProcessNextPerson () проверяет это в первую очередь). Во-первых, прочитав следующее имя, можно обнаружить, что это шпион. В этом случае исключение обраба- обрабатывается первым блоком catch в данном методе. Так как исключение обрабатывается здесь, внутри цикла, значит, продолжится исполнение метода Main () программы, и бу- будут обрабатываться последующие строки в файле. Во-вторых, при попытке чтения следующего имени может быть обнаружен конец файла. Метод ReadLine () класса StreamReader работает таким образом, что при обнару- обнаружении конца файла он не генерирует исключение, а возвращает пустую строку. Поэтому при получении пустой строки мы будем знать, что формат файла является неправильным, и в первой строке указано большее число записей, чем на самом деле присутствует в фай- файле. В этом случае генерируется исключение ColdCallFileFonnatException, которое бу- будет обработано внешним обработчиком исключений (это приведет к прекращению выполнения программы). Здесь также не нужен блок finally, поскольку не требуется освобождать ресурсы. В данном случае показан пустой блок finally для иллюстрации того, что так тоже можно делать. Нам осталось рассмотреть три члена ColdCallFileReader: свойство NPeopleTo- Ring, которое содержит предполагаемое количество имен в файле, метод Dispose () и
202 Глава 6 деструктор, которые закрывают файл, если он открыт. Отметим, что метод Dispose () проверяет, действительно ли необходимо закрыть файловый поток, и затем устанавли- устанавливает в null ссылку на файловый поток. Это гарантирует, что не возникнет проблем, если по какой-то причине клиент попытается вызвать Dispose () несколько раз. public ,uint NPeopleToRing { get if (lisOpen) throw new UnexpectedException( Attempt to access cold-call return nPeopleToRing; file that is not open" public void Disposed; { if (fs != null) fs.Close(); fs = null; GC.SuppressFinalize(this) isDisposed = true; isOpen = false; -ColdCallFileReader(); I if- (fs != i-ull) fs.Close(); Теперь можно протестировать программу. Сначала запустите ее с файлом, содержи- содержимое которого было показано ранее. В нем записаны три имени, что соответствует числу, указанному в первой строке. Затем используйте файл people2 . txt, который содержит очевидную ошибку: File Format Help 49 Zbigniew Harlequin Avon from 'Blake's 7' Karii Watson Lj Наконец, попробуйте открыть несуществующий файл people3.txt. Исполнение программы три раза для трех файлов дает следующие результаты: Tr'OMortinerColdCall . ■ „."■' Please type in the name of tm file , containing the natoes of the people to be! cold-called > people.txt LandLine spy found, with nane Zbignieu Harlequin Avon from 'Blake's 7* Karli. Wat у on All callees processed correctly F:\>MortinerColdCall , Please type in the name of the file containing the names of the people to be cold-called > people2.txt LatidLine spy found, with nane Zbigniew Harlequin . Avon fron 'Blake's' 7' • ■■"■' Karli Watson ,., ■ The file people2.txt appears to bawe been corrupted Details of pi-oblqn ai-p: Not enougli names P:4>MortinerColdCall Please type in the nane of the file ; , ., ■■#■*' The file people3^t>tt дЛйёв not exist. :i: ' ■•
Дополнительные возможности С# 203 Определенные пользователем приведения типов В главе 3 говорилось о том, как конвертировать значения между различными предопре- предопределенными типами данных. Это осуществляется с помощью процесса приведения типов. С# допускает два различных вида приведения типов: явное и неявное. Разница состоит в том, что явное преобразование необходимо явно отметить в коде, заключив тип данных назначения в круглые скобки: int 1 = 3; long L =5 I; // неявное , .short S = (short) I; // явное Для предопределенных типов данных явное приведение типа требуется в том случае, если при приведении типа существует риск ошибки или потери данных — например, при преобразовании из int в short, так как short может не вместить в себя значение int. Кроме того, преобразование типов данных со знаком в типы данных без знака мо- может привести к ошибке в том случае, если переменная со знаком содержит отрицатель- отрицательное число, а при преобразовании чисел с плавающей точкой в целочисленные типы данных теряется их дробная часть. Идея явного указания таких приведений заключается в том, что вы тем самым сообщаете компилятору о своем понимании риска потери данных и о том, что вы написали свой код с учетом такой возможности. Так как С# позволяет определять свои собственные структуры и классы, что означает создание собственных типов данных, можно предположить, что существует возможность приведения и для собственных типов данных. С# действительно позволяет делать это. Me ханизм заключается в том, что можно объявить приведение как операцию одного из клас- классов. Приведение должно быть отмечено как явное или неявное в зависимости от того, как оно должно использоваться. Ожидается, что вы будете следовать тем же рекоменда- рекомендациям, что и для предопределенных приведений типов: если вы знаете, что приведение типа всегда будет безопасным, какое бы значение ни содержалось в исходной перемен- переменной, его можно объявить неявным. Если же существует риск того, что для тех или иных значений что-то будет происходить не так — возможно, потеря данных или генерация исключения,— тогда необходимо определить преобразование как явное. Собственные приведения типов необходимо объявлять явными в том случае, если возможно возникновение ошибки приведения или генерация исключения для некоторых значений исходной переменной. Синтаксис для определения приведения типа аналогичен тому, что применяется для перегрузки операций. Это не случайное совпадение, так как приведение типа может рас- рассматриваться как операция, чье действие заключается в преобразовании типа данных источника в тип данных назначения. Для пояснения приведем фрагмент кода: public static implicit operator float (Curency value) >г; // и т.д. Код является частью структуры Currency, предназначенной для хранения денежных сумм. Объявленное здесь приведение типа позволяет неявно преобразовать величину Currency в float. Отметим, что если приведение объявлено неявным, компилятор по- позволит использовать его как в явной, так и в неявной форме. Если же преобразование объявлено явным, компилятор разрешит использовать его только таким образом. Здесь преобразование объявлено статическим. Как и в случае перегрузки операций, С# требует, чтобы приведения типов были статическими. Это означает, что каждое при- приведение типа принимает один параметр, который является типом данных источника. Разработчики на C++ отметят, что это прямая противоположность C++, в котором приведения типов являются членами экземпляров классов. Пример: структура Currency Рассмотрим применение определяемых пользователем явных и неявных преобразова- преобразований на примере SimpleCurrency (который можно загрузить с сайта). В этом примере объявляется структура Currency, которая содержит положительную сумму в долларах США. Вообще говоря, С# для таких целей предлагает тип decimal, но вы можете создать свой собственный класс или структуру для представления денежных сумм, если требует- требуется выполнять сложные финансовые вычисления и реализовывать для этого специальные методы.
204 Глава 6 Синтаксис преобразования типов одинаков для структур и классов. В нашем примере применяется структура. Однако все то же самое было бы справедливо и в случае, если бы мы объявили Currency классом. Определение структуры Currency: struct Currency i public uint Dollars; public ushort Cents; public Currency(uint Dollars, ushort Cents) { this.Dollars = Dollars; this.Cents = Cents; public override string ToString() { return string.Format("${0}.{1,-2:00}", Dollars, .Cents); Применение типов данных без знака для Dollars и Cents гарантирует то, что экзем- экземпляр Currency может содержать только положительные значения. Мы ограничиваем здесь допустимые значения для того, чтобы позже показать некоторые особенности яв- явных приведений типов. Этот класс можно использовать, например, для хранения сведе- сведений о зарплате сотрудников компании. (Зарплаты не бывают отрицательными!) Для упрощения класса его поля объявлены открытыми, но обычно они объявляются закрытыми и для каждого определяется соответствующее свойство. Начнем с того, что нам нужно преобразовывать экземпляры Currency в значения float, где целая часть float представляет доллары. Другими словами, мы хотели бы записать код следующим образом: ■_•■ Currency Balance = new Currency A0, 50); float D = Balance; / / Нужно, чтобы D было присвоено 10.5 Чтобы иметь такую возможность, необходимо определить приведение типа. К опре- определению Currency добавим следующее: struct Currency { // и т.д. public static implicit operator float (Currency value) { return value.Dollars + (value.Cents/100.Of); J Это неявное приведение типа. В данном случае это допустимо, так как из опреде- определения Currency ясно, что любое значение, способное в нем храниться, может также храниться и в float. Преобразование всегда будет работать правильно. Здесь мы немного лукавим - нагие определение "всегда будет работать правильно" является слишком широким: на самом деле при преобразовании из uint в float может происходить потеря точности, но Microsoft посчитала эту ошибку незначительной и объявила приведение int-e-float неявным. А как насчет обратного преобразования? У нас есть величина с плавающей точкой — float, и ее требуется преобразовать в Currency. В данном случае не гарантируется, что преобразование будет работать, так как float способен хранить отрицательные значе- значения, которые не могут храниться в экземплярах Currency, и, кроме того, float может хранить числа гораздо большей величины, чем поле (uint) Dollar в Currency. В том случае, если float содержит неподходящее значение, приведение его к Currency может дать непредсказуемый результат. Из-за этого риска преобразование из float в Currency должно быть объявлено явным. Вот наша первая попытка: public static explicit operator Currency (float value) { uint Dollars = (uint)value; ushort Cents = (ushort)((value-Dollars)*100); return new Currency(Dollars, CentsJ; "■-.-} . . . ' - • . .. - - . ■ . ..•-..-
Дополнительные возможности С# 205 В данном коде мы сначала получаем число долларов как целую часть переданной вели- величины. Допустим, что передано значение 45.63f. Преобразование его в uint даст 45 — число долларов. Разница между этим числом и оригинальным float равна 0.63. Умножив ее на 100, мы получим число центов. Этот код будет успешно откомпилирован: float Amount = 45.63f; Currency Amount2 = (Currency)Amount; Однако следующий код сгенерирует ошибку компиляции, так как делается попытка неявно использовать явное приведение типа: float Amount = 45.63f; Currency Amount2 = Amount; // неверно Хорошо ли это? Помечая приведение типа как явное, вы предупреждаете разработ- разработчика о том, что может произойти потеря данных. Но в нашем случае это, вероятно, не то, что требуется. Напишем и запустим тестовый пример. Метод Main () создает экземп- экземпляр структуры Currency и пытается выполнить несколько приведений типов. В начале кода величина bal ance записана двумя разными способами (это понадобится нам позже): static void Main(string[] args) try Currency balance = new Cu". ■• cyE0, 35); Console.WriteLine("balance le " + balance); Console.WriteLine("bdlance is (using ToStringO) " + balance.ToString()); float balance2 = balance; Console.WriteLine("After converting to float, = " + balance2); balance = (Currency)balance2; Console.WriteLinef"After converting back to Currency, = " + balance); -Console.WriteLineC'Kow attempt to convert <put of .range value of " + "-$100.00 to a Currency;"); checked I balance = (Currency)(-50.5); Console.WriteLine("Result is " + balance.ToString()); 3 catch (Exception e) Console.WriteLinef"Exception occurred: " + e.Message); Отметим, что весь код размещен в блоке try для обработки любых исключений, кото- которые могут возникнуть во время преобразований. Мы также поместили строки, преобразу- преобразующие неверное значение в Currency, в блок cnecked, поэтому отрицательные значения должны обрабатываться. А будут ли? При исполнении этого кода получаем результат: icj-osoft l/in-'ous- 2009 CUei-eion S .80*21951 Щ№$Ы'8520Ш V (\tt brt^ti ftftetex»ftve»t1ng Ь&ск«й Curienfcii, ^.З} Han attenlpt to csnueicc <iut.of гап&едеОме ofi .^SlH&.ftB tS a Fedult is $4294967246.60486 ' ' ,, ¥ I ■ K& '^ Щ r ,C , _, t -v S ■" ч " I""
206 Глава 6 Большая часть кода работает нормально, однако исключение не было сгенерирова- сгенерировано. Проблему легко обнаружить. Место, в котором происходит переполнение, находит- находится не в методе Main () — оно внутри кода для операции приведения, которая вызывается из метода Main (). А этот код не был отмечен как checked. Одно из решений заключается в обеспечении того, что само приведение должно производиться в контексте checked: public static explicit operator Currency (float value) { checked { uint Dollars = (uintlvalue,- // при отрицательном значении / / происходит переполнение ushort Cents - (ushort)((value-Dollars)*100); return new Currency(Dollars, Cents); Это гарантирует, что исключение будет корректно сгенерировано (даже если вызы- вызывающий код не является checked). В качестве альтернативы можно сгенерировать свое собственное исключение внутри операции приведения. Для этого потребуется написать дополнительный код, но вы сможете передать более детальную информацию об ошибке. Здесь мы не будем рассматривать этот вариант. Разумеется, при объявлении приведения типов, которое будет часто использоваться и для которого весьма важна производительность, вы можете предпочесть вообще не осуществлять проверку на ошибки. Это вполне приемлемое решение при условии, что поведение преобразования и отсутствие обработки ошибок будет хорошо документировано. В нашем примере осуществляется преобразование из или в float, однако для дру- других типов данных синтаксис аналогичен. Кроме того, тем же синтаксисом вы будете пользоваться, если потребуется объявить приведение типа для двух классов или струк- структур. Преобразование типа не обязано применять один из предопределенных типов данных. Приведение типов между классами В примере Currency использовались только классы, которые преобразовывались в или из float — один из предопределенных типов данных. Однако применение простых ти- типов данных не является жестким требованием. Можно объявить преобразование типов для приведения типов классов или структур, объявленных в коде. Однако существует несколько ограничений: О Нельзя осуществлять приведение типов между классами, один из которых явно или неявно является производным от другого. □ Приведение типа должно быть объявлено внутри определения либо типа- источ- источника, либо типа-приемника. Для иллюстрации этих требований рассмотрим ситуацию: class А { / / реализация } class В : public А { / / реализация } class С : public В £ / / реализация } class p : public В { // реализация
Дополнительные возможности С# 207 Другими словами, имеется следующая иерархия классов: В этой ситуации совершенно законным будет объявление приведения типов для пре- преобразования между С и D, так как эти классы не наследуются друг от друга. Код для этого мог бы быть таким (если требуется, чтобы приведения типов были неявными): public static implicit opeisator D (С value) // и т.д. public static, implicit operator С (D value) II и т.д. Каждое из этих приведений можно поместить либо внутри класса С, либо внутри класса D, но нигде более. С# требует, чтобы определение приведения типа помещалось либо в класс-источник (или структуру-источник), либо в класс-приемник. Побочным эф- эффектом является то, что невозможно определить приведение типов для двух классов, если нельзя получить доступ к исходному коду хотя бы одного из них. Это существенно, так как не позволяет сторонним лицам объявлять приведения типов для ваших классов. Другим моментом является то, что как только приведение типов объявлено в одном из классов, его уже невозможно объявить в другом классе. Совершенно очевидно, что для каждого преобразования должно существовать только одно определение, иначе ком- компилятор не сможет решить, какое из определений выбрать. Для нашей иерархии можно объявить только приведения из С в D и из D в С. Все остальное (например, из А в С или из D в В) вызовет ошибку компиляции, поскольку С# не разрешает пользовательские приведения типов для производных друг от друта клас- классов. Причина этого проста: такие приведения уже существуют. Всегда можно неявно при- привести производный класс к базовому, а базовый явно к производному. Как это сделать, описывается ниже. Приведения типов между базовыми и производными классами Начнем со случая, когда источник и приемник являются типами по ссылке, и рассмотрим два класса: My Base и My Derived, где My Derived явно или неявно унаследован от My Base. Что касается преобразования из MyDerived в MyBase, всегда можно записать (пред- (предполагая, что доступны конструкторы): MyDerived DerivedObject = ,new MyDerivedO; MyBase BaseCopy '* DerivedObject; В данном случае выполняется неявное приведение MyDerived в MyBase, что допусти- допустимо, так как существует правило, по которому ссылка на тип MyBase может ссылаться на объекты класса MyBase или любые производные от него объекты. Это возможно, поско- поскольку экземпляры производного класса представляют собой экземпляры базового класса плюс некоторая добавка (см. главу 5). Все функции и поля, объявленные в базовом классе, определены и в производном классе. Далее можно записать: Myliase DerivedOb'ject = new MyDerivedO; MyBase BaseObject = new MyBaseО; MyDerived DerfvedCopyl = (MyDerived! DerivedObject; // верно MyDerived DerivedCopy2 = (MyDerived) BaseObject; // вызовет исключение Этот код является допустимым в С#, он иллюстрирует приведение типа базового клас- класса к типу производного класса. Однако при выполнении последнего оператора возникнет исключение. В этом случае приводимый объект сначала проверяется. Так как ссылка на
208 Глава 6 базовый класс может в принципе ссылаться на экземпляр производного класса, вполне возможно, что этот объект на самом деле является экземпляром производного класса, для которого мы пытаемся осуществить приведение. В этом случае приведение выполняется успешно — ссылка на производный объект будет ссылаться на этот объект. Однако если объект не является экземпляром производного класса (или любого класса, являющегося производным от него), преобразования типа не происходит и генерируется исключение. Отметим, что приведения типов, осуществляемые компилятором между базовым и производным классами, на самом деле не выполняют никаких преобразований данных. Все, что они делают,— устанавливают новую ссылку на объект, если преобразование воз- возможно. В этом смысле они отличаются от тех приведений типов, которые вы определяете самостоятельно. Так, в примере SimpleCurrency (см. выше) мы определили приведения, которые осуществляют преобразование между структурой Currency и float. В приведе- приведении float-Currency мы создали новый экземпляр структуры Currency и инициализиро- инициализировали его требуемыми значениями. Аналогично, в примере иерархии классов, если бы мы определили приведение типов из С в D, то реализация выглядела бы так: public static implicit operator D(C value) { D Result = new DO; // Код для инициализации D соответствующими значениями // в зависимости от содержимого С return D; } Здесь опять создается экземпляр нового объекта со значениями, основанными на значениях старого объекта. Предопределенные приведения типов между базовым и про- производным классом не делают этого. Если вам необходим такой вид функциональности, т.е. действительно требуется преобразовать экземпляр MyBase в объект MyDerived со значениями, основанными на содержимом экземпляра MyBase, этот синтаксис приведения использовать нельзя — придется написать метод для такого преобразования: class Base I public Derived T->Derived() { Derived Result = new Derived!); // инициализируем Result значениями на основе Base rerutn Result; } Упаковывающие и распаковывающие приведения типов Выше мы рассматривали приведение типов между базовым и производным классами, ко- которые оба являлись типами по ссылке. Аналогичные принципы применимы при приве- приведении типов по значению, хотя в этом случае недостаточно скопировать ссылки — необходимо также скопировать некоторые данные. Разумеется, невозможно осуществлять наследование от структур или примитивных типов по значению. Поэтому приведение типа между базовой и производной структу- структурами неизменно означает приведение типов между примитивным типом или структу- структурой и System.Object. (Теоретически можно приводить типы между структурой и System.ValueType, хотя трудно сказать, зачем это нужно.) Приведение любой структуры (или примитивного типа) к obj ect всегда доступно в ка- качестве неявного приведения — так как это приведение от производного типа к базовому — и является знакомым процессом упаковки, который был коротко рассмотрен в главе 3. Например, для нашей структуры Currency: Currency Balance = new CurrencyD0, 0) ; object BaseCopy = Balance; При выполнении неявного приведения содержимое Balance копируется в кучу, а ссылка объекта BaseCopy будет указывать на него. На самом деле происходит следую- следующее: когда мы первоначально определяем структуру Currency, платформа .NET неявно создает другой (скрытый) класс — упакованный класс Currency, который содержит те же самые поля, что и структура Currency, но при этом является типом данных по ссыл- ссылке, хранящимся в куче. Это происходит при определении любого типа по значению — не важно, структуры или перечисления,— и аналогичные упакованные типы по ссылке су- существуют соответственно для всех примитивных типов по значению: int, double, uint и т.п. К этим упакованным классам в исходном коде невозможно получить прямой
Дополнительные возможности С# 209 доступ никакими способами, но именно эти объекты используются тогда, когда тип по ссылке приводится к object. Когда мы явно приводим Currency к object, создается эк- экземпляр ограниченного класса Currency, который инициализируется данными из струк- структуры Currency. Именно на этот упакованный экземпляр Currency будет ссылаться BaseCopy. Это означает-, что синтаксически приведение производного типа к базовому для типов по значению работает так же, как и для типов по ссылке. Обратное приведение известно как распаковка. Так же, как приведение типов между базовым типом по ссылке и производным типом по ссылке, оно является явным. Если объект имеет недопустимый тип, будет сгенерировано исключение: Object Derivedbbject = new CurrencyD0, 0); object BaseObject = new objectf); Currency DerivedCopyl = (Currency)DerivedObject; // все нормально Currency DerivedCopy2 = (Currency)BaseObject; // генерируется исключение Этот код работает аналогично коду, показанному ранее для типов по ссылке. Приведе- Приведение DerivedObject к Currency выполняется без проблем, так как DerivedObject на са- самом деле ссылается на упакованный экземпляр Currency — приведение будет осуществлено путем простого копирования полей из упакованного объекта Currency в новую структуру Currency. Второе приведение не срабатывает, поскольку BaseObject не ссылается на упакованный экземпляр Currency. Множественное приведение типов Одна из ситуаций, которую необходимо отслеживать при определении приведений типов, заключается в том, что если не будет существовать прямого приведения типов для выпол- выполнения требуемого преобразования, компилятор С# попытается найти способ совмещения нескольких приведений. Например, обращаясь к структуре Currency, предположим, что компилятор встречает- такие строки кода: Curency Balance * new CurrencydO, 50); long Amount = (JongJBalance; double AmountD = Balance; Сначала инициализируется экземпляр Currency, затем мы пытаемся привести его к long. Проблема заключается в том, что такой тип преобразования не определен. Однако код будет успешно компилироваться. Произойдет следующее: компилятор поймет, что вы определили неявное приведение Currency в float, а он знает, как явно привести float к long. Он откомпилирует- эту строку в код на промежуточном языке, который сначала преобразует balance в float, а затем преобразует результат в long. To же самое происходит и в последней строке кода при преобразовании Balance в double. Однако так как приведения Currency в f l^at и float в double являются неявными, то и в коде это преобразование можно записать неявно. При желании последовательность приведений можно указать явно: Currency Balance = new Currency'\0, 50); 3.ong Amount = (long) (float)Balance; double AmountD = (double)(float)Balance; Однако в большинстве случаев это лишь усложнит ваш код. Следующий код приведет к ошибке компиляции: Currency Balance - new CurrencydO, 50); long. Amount = Balance; Дело в том, что наилучшим путем преобразования, который удается найти компилятору, по-прежнему является преобразование сначала в float, а затем в long. Но преобразование float в long должно быть указано явно. Даже такой код вызовет ошибку: Currency Balance = new_ CurrencydO, 50); long Amount = (float)Balance; В данном случае мы (бесполезно) явно осуществили первую часть преобразования, но не сделали этого для преобразования float-long. Само по себе это не должно создавать особых проблем. Эти правила понятны и пред- предназначены для того, чтобы предотвратить потери данных, о которых разработчик мо- может и не знать. Однако если вы не будете осторожны при определении своих собственных приведений типов, компилятор может осуществить такое приведение, что вы получите неожиданные результаты. Допустим, что кому-то еще в группе, занимаю- занимающейся разработкой класса Currency, приходит в голову мысль, что было бы полезно
210 Глава 6 иметь возможность преобразования u; aL, содержащего общее количество центов, в Currency (центов, а не долларов, так как нельзя терять доли доллара). Поэтому может быть написано преобразование: // не делайте этого! public static implicic operator Currency (uint value) { return new Currency (value/lOOu, (ushort) (value%100) ) ,- } Отметим, что u, идущее за первым числом 100, гарантирует, что value/100u будет интерпретироваться как uint. Если бы мы написали value/100, компилятор интерпре- интерпретировал бы это выражение как int. В коде мы явно указали "Не делайте этого", и вот почему. Посмотрите на следующий фрагмент кода: все, что мы делаем,— преобразуем uint, содержащее 350, в Currency и обратно. Что, по-вашему, будет находиться в Вап_2 после выполнения кода? uint Bal = 350; Currency Balance = Bal,- uint Bal2 = (uint)Balance; Ответом будет не 350, a 3! И здесь все логично. Мы явно преобразуем 350 в Curren- Currency, получая в результате р з1 an - .е -2 э г„ 3, Balance.Cents=50. Затем компилятор как обычно определяет наилучший путь для обратного преобразования. В результате Balance неявно преобразуется в float (значение 3.5), и уже это значение явно преобра- преобразуется в uint — получаем 3. Имеются и другие типы данных, для которых преобразование из одного типа данных в другой, а затем обратно вызывает потерю данных. Например, преобразование float, содержащего 5.8 в ' .^, а затем обратно в float приведет к потере дробной части, резу- результатом будет 5. Однако существует принципиальная разница между потерей дробной части и "случайным" делением числа более чем на 100! Currency неожиданно стал опасным классом, который вытворяет странные вещи с целыми числами! Проблема состоит в том, что имеется конфликт в интерпретации целых чисел наши- нашими приведениями. Приведения между Cur re.. и float интерпретируют целое число 1 как один доллар, а последнее привс дение i ',-Currency интерпретирует это значение как один цент. Это пример плохой ризработки. Если вы хотите, чтобы ваши классы были пригодны для использования, вам придется убедиться в том, что все ваши приведе- приведения типов взаимозаменяемы в гом смысле, что они приводят к одинаковым результатам. В данном случае совершенно очевидно, что необходимо переписать приведение uint-? ._ . так. чтобы оно интерпретировало целое число 1 как один доллар: // Верно public static implicit operator Currency (uint value) { return new Currency (value, 0i ,- } В связи с этим возникает вопрос: зачем вообще нужно такое приведение? Дело в том, что без этого приведения единственным способом для компилятора осуществить приве- приведение uint-CXrr^r су было бы приведение через float. В данном случае прямое преоб- преобразование является более эффективным, поэтому дополнительное приведение увеличивает производительность. Однако необходимо убедиться в том, что оно дает те же результаты, которые могут быть получены при использовании приведения через float. Вы можете встретить ситуации, в которых отдельное объявление преобразова- преобразований для различных предопределенных типов данных позволяет больше преобразований делать неявно. Для проверки совместимости приведения типов следует выяснить, приводят ли преоб- преобразования к одним и тем же результатам (за исключением, возможно, потери точности, как в преобразовании float-i.-t), независимо от того, каким способом они осуществляются. Хорошим примером может быть класс Currency. Рассмотрим код: Currency Balance = new CurrencyE0, 35); ulong Bal = (ulong) Balance; В настоящий момент существует только один способ, которым компилятор может выполнить это действие: преобразовав Currency неявно в float, а затем явно в ulong. Преобразование f loat-ulong требует явного приведения, в данном случае мы его указали.
Дополнительные возможности С# 2ЛЛ Однако допустим, что мы добавляем другое приведение для неявного преобразования Currency в uint-. Мы сделаем это, изменив структуру Currency и добавив преобразова- преобразования как в, так и из uint. Этот код содержится в примере SimpleCurrency2: / / Верно! pub) ic static imp j.cit cpeirtur Currency (ui.nt value) { return new Currency(value, Oj, } public static implicit operator uint (Currency value) t return value.Dollars; } Теперь у компилятора имеется другая возможность преобразования из Currency в ulong: преобразовать неявно Currency в uint, а затем неявно в u_ong. Какой из этих двух путей он выберет? Разумеется, в С# существуют четкие правила (которые мы не бу- будем описывать в этой книге, их можно найти в документации MSDN), определяющие, как компилятор выбирает наилучший путь из нескольких возможных. Вы должны разра- разрабатывать свои приведения типов так, чтобы все маршруты приведения давали один и тот же результат (за исключением возможной потери точности), тогда будет не важно, какой из них выберет- компилятор. (В данном случае компилятор выбирает маршрут Currency->bint->ulong, а не Currer -с i ■ очд.) Для проверки примера Simpler .г гопсу^ добавим следующий код в тестовую программу: try { Currency balance = r-iw Currency 3 >) ; Console.WriteLine (bale :< v ,- Console.Writelir.t "ta_ai-c_ _ ■*■ La_a... ; Console.WriteLl..e( "balance is ;using ToScr-.i'' ' balance.ToSt- ring()); uint balance3 = (uint) balance; Console.WriteLine("Converting to uint gives " * balance3); Выполнение этого примера дает следующие результаты: icrosoft Ittndous 2вВ0 fUersion 5.00.21951 <C> Copyright. 198Е-20Й0 Microsoft' Corp. G:N>SinpleCurretlcy2 , balance'"is SSB.3S . balance is (using ToStringO> $50.3S Conuertinsr to uint giues 50 '. after converting to float. » 50.35 flfter converting bach to Cui-rcncy» * $50.34 Mow attempt tro convert out of i-дпэй ualuc of --$100.00 to a Curi-encys Exception occurred: Exception of type Rynten.OuerflouException was tlirmm. Преобразование в uint было успешным, хотя, как и ожидалось, мы потеряли при этом центы. Приведение отрицательного значения float к Currency вызвало ожидае- ожидаемое исключение переполнения, так как теперь само преобразование float-Currency осуществляется в checked-контексте. Однако результат показывает- одну потенциальную проблему, которую следует учиты- учитывать при приведении типов. Самая первая строка некорректно вывела значение balance, показав 50 вместо $50.35. Из этих строк: Console.WriteLine(balance); Console.WriteLine("balance is " + balance); Console. WriteL^ef "balance is (using ToStringO) " + feaiSRce.ToStringt) );
212 Глава 6 только две последние строки правильно отобразили Currency как строку. Что же произошло? Проблема заключается в том, что совмещение приведений типов с перегрузками методов может быть непредсказуемым. Рассмотрим эти три строки в обратном порядке. Третий оператор Console.Write- Line (' явно вызывает метод ил re: су. Г- String A. гарантируя, что Currency отобра- отобразится как строка. Второй оператор не делает этого. Однако строковый литерал "balance is ", который передается в Console.WrittLine (), ясно показывает компилятору, что па- параметр должен интерпретироваться как строка. Поэтому метод Currency .ToSt ring () будет вызван неявно. Самый первый метод Console.WriteLine () передает структуру Currency в Conso- Console .WriueLine () . Conso Le .WriteLine (> имеет большое число перегруженных версий, но ни одна из них не принимает структуру Currency. Поэтому компилятор начнет выяс- выяснять, как можно привести Currency, чтобы она соответствовапа одному из перегружен- перегруженных методов Console.WriteL^ne (). Оказывается, что один из перегруженных методов Console .Wi iueLine () разработан специально для быстрого и эффективного отображения uint и принимает uint в качестве параметра. А мы только что определили приведение, которое неявно преобразует Currency в uint. Об остальном вы можете догадаться сами. Кстати говоря, Console.Wr teLineO имеет- еще один перегруженный метод, кото- который принимает в качестве параметра double и отображает его значение. Если вы внима- внимательно посмотрите на результаты первого примера SimpleCurrency, то обнаружите, что первая строка отобразила Currency как doubl e, используя этот перегруженный метод. В том примере отсутствовало прямое приведение Currency в uint, поэтому компилятор выбрал путь Currency >f oql.- ^di iuble как предпочтительный при поиске подходящего перегруженного метода Console.WriteLir.e (). Но сейчас, имея возможность прямого преобразования в uint, компилятор предпочел использовать его Результатом является то. что если у вас имеется метод, для которого существует неско- несколько перегруженных вариантов, и вы пытаетесь передать в него параметр, чей тип дан- данных не соответствует точно типу данных ни в одном из этих перегруженных методов, то вы заставляете компилятор не только выбирать приведения типов для преобразования данных но и какой перегруженный метод — а, следовательно, и какие преобразования — использовать. Разумеется, компилятор всегда поступает логично и в соответствии со строгими правилами. Однако результат может оказаться не тем, который ожидается. Если существуют какие-то сомнения, лучше явно указывать приведения типов. Делегаты Делегаты лучше всего рассматривать как новый тип объекта в С#, в некотором роде они похожи на классы. Они предназначены для ситуаций, когда требуется передать методы другим методам. Например: int. I - int. Parse ("99") ; Здесь вызывается статический метод Parso() из класса System. Int32. Этот метод принимает один параметр — для данного перегруженного варианта — строку. Можно рассматривать ситуацию следующим образом. Метод Parse () производит какие-то дей- действия точно так же, как и другие методы, а именно, он преобразует некоторые данные в 1] t. Однако для того, чтобы использовать этот метод, необходимо указать, что пред- представляют собой значимые данные. Мы делаем это путем передачи параметра (или аргу- аргумента). Вот для чего в основном используются аргументы методов — метод берет данные, передаваемые ему п качестве аргументов, и выполняет над ними действия. Мы так часто применяем передачу данных в качестве параметров, что даже не заду- задумываемся об этом, и потому идея передачи методов в качестве параметров может зву- звучать несколько странно. Однако возможны случаи, когда метод помимо обработки данных должен делать что-то еще с другим методом, и — усложним ситуацию — во время компиляции неизвестно, что это за метод. Эта информация доступна только во время исполнения, и, следовательно, ее нужно передавать в качестве параметра первому методу. Для пояснения разберем примеры: О Запуск потоков. Потоки рассматриваются в главе 7. Здесь скажем только, что по- поток — это последовательность исполнения. Когда приложение начинает работу, оно последовательно выполняет инструкции в статическом методе MainO. В С# можно запустить новую последовательность исполнения, которая будет работать параллельно с той, что уже исполняется. Запускается поток при помощи метода
Дополнительные возможности С# 213 Start О экземпляра одного из базовых классов, System.Threading.Thread. Приложение должно с чего-то начать, и точкой отсчета является Main (). Анало- Аналогично, если вы желаете, чтобы компьютер начал новую последовательность ис- исполнения, нужно указать ему, где начать эту последовательность. Необходимо передать ему подробную информацию о методе, в котором начнется исполнение, т.е. Thread. Start () должен принимать параметр, содержащий информацию для этого метода. О Общие библиотечные классы. Разумеется, существует большое число библио- библиотек, содержащих код для выполнения различных стандартных задач. Обычно библиотеки самодостаточны в том смысле, что вы точно знаете, как выполнить задачу на этапе написания библиотеки. Однако иногда задача содержит в себе не- некоторую дополнительную задачу, и только клиентский код знает, как ее выпол- выполнить. Ниже мы рассмотрим пример сортировки массива объектов. Мы напишем класс, который принимает- массив объектов и сортирует их в порядке возраста- возрастания. Часть процесса сортировки заключается во взятии двух объектов из массива и их сравнении с целью определения, какой из них должен идти первым. Так как наш класс должен уметь сортировать массивы объектов любого типа, не сущест- существует способа, с помощью которого он сможет заранее определить, как произво- производить это сравнение. Клиентский код, который передает нашему классу массив объектов, должен также сообщить классу, как выполнять сравнение для конкрет- конкретных объектов, которые необходимо отсортировать. Другими словами, клиент- клиентский код должен будет передать нашему классу сведения о методе, который можно вызвать для выполнения сравнения. □ События. События играют важную роль в программировании Windows GUI. Идея состоит в том, что часто требуется информировать некоторый код о том, что про- произошли те или иные события. Например, при наборе текста в Microsoft Word каж- каждый раз, когда нажимается клавиша, Word должен быть информирован об этом, чтобы он смог выполнить соответствующее действие. (Обычно это означает добав- добавление соответствующей буквы в документ и отображение ее на экране.) Каждый раз при нажатии на клавишу Windows вызывает- конкретный метод внутри Word, который осуществляет соответствующее действие, т.е. обрабатывает событие. Для того чтобы Windows могла сделать это, Word в некоторый момент должен со- сообщить Windows о том, какой метод необходимо вызвать в ответ на нажатие кла- клавиши. Поэтому Word должен вызвать функцию API и передать ей подробности об этом методе. В очередной раз мы видим, что сведения о методе должны быть пе- переданы в качестве параметра. Очевидно, что, программируя на С#, мы будем иметь дело со средой исполнения .NET, а не с Windows, но когда мы приступим к программированию GUI с помощью Windows Forms в главе 9, то используем те же самые принципы: код должен будет сообщать среде исполнения .NET о том, какие методы обрабатывают какие события. Чтобы сделать это, необходимо бу- будет передавать подробную информацию о методах в качестве параметров другим методам. Итак, в ряде случаев методы должны принимать сведения о других методах в качестве параметров. Теперь выясним, как это сделать. Наиболее простой способ — передача име- имени метода в качестве параметра. Возвращаясь к примеру с потоками, допустим, что мы со- собираемся запустить новый поток и что имеется метод EntryPoint, в котором должно начаться исполнение нового потока: void EntryPoint (f) i •■■ ff выполнение, того, что должен делать новый поток } Могли бы мы начать новый поток, используя следующий код? Thread, NewThread = new Thread () ; Thread.Start{EntryPoint); /У Неверно Это наиболее простой способ ведения дел, и некоторые языки, например С и C++, именно так и поступают в подобной ситуации (в С и C++ параметр EntryPoint является указателем функции). Кстати говоря, что-то подобное происходит за кулисами в Visual Basic при добавлении обработчиков событий, однако среда исполнения Visual Basic настолько хорошо ограждает вас от деталей происходящего, что вы никогда не догадывались об этом.
214 Глава 6 К сожалению, такой прямой подход вызывает некоторые проблемы с безопасностью типов, а также игнорирует тот факт, что в объектно-ориентированном программирова- программировании методы редко существуют в изоляции и обычно должны быть ассоциированы с экзем- экземпляром класса перед вызовом. Из-за этих проблем С# не разрешает использовать этот подход. Если требуется передавать методы в качестве параметров, сведения о методе не- необходимо поместить в новый тип объекта — делегат. Делегаты являются особым типом объекта — особым в том смысле, что они содержат не данные, а информацию о методе. Использование делегатов в С# Для того чтобы можно было использовать класс в С#, нужно выполнить два действия. Во-первых, необходимо определить класс, т.е. сказать компилятору, какие поля и методы образуют класс. Затем (если только мы не используем статические методы) необходимо создать экземпляр класса, т.е. создать объект этого класса. То же самое справедливо для делегатов. Сначала требуется определить делегатов, которые будут применяться. Опре- Определение делегата означает указание компилятору того, какой тип методов будут пред- представлять делегаты этого типа. Затем нужно создать один или несколько экземпляров делегатов. Синтаксис для определения делегатов выглядит следующим образом: delegate void VoidOperationfuint X); В данном случае мы определили делегата VoidOperation и указали, что каждый эк- экземпляр этого делегата может содержать сведения о методе, принимающем один пара- параметр uint и возвращающем void. Ключевым моментом является то, что делегаты безопасны по типу. При определении делегата вы должны указать все подробности сигнатуры метода, который он будет представлять. Удобно рассматривать делегата как способ именования сигнатуры метода. Допустим, что мы хотим определить делегата TwoLongsOp, который будет представ- представлять функцию, принимающую в качестве параметров два значения long и возвращающую double. Мы могли бы сделать это следующим образом: delegate double TwoLongOp(long LI, long L2); Делегата, который представляет- собой метод без параметров, возвращающий строку, можно определить так: delegate string GetAString (),- Синтаксис аналогичен тому, который используется при определении методов, за исключением того, что отсутствует тело метода, а определение предваряется ключевым словом delegate. Поскольку то, что мы делаем, представляет- собой по сути определение класса, делегата можно определять там же, где и класс, т.е. либо внутри другого класса, либо вне всякого класса в пространстве имен как объект наивысшего уровня. В зависимо- зависимости от того, насколько доступным должно быть определение делегата, можно применить один из обычных модификаторов доступа — public, pi ivate, protected и т.п.: public delegate string GetAString(); Когда мы говорим "определение делегата", это буквально означает "определение нового класса". Делегаты реализуются как экземпляры классов, производных от базового класса System. Delegate. Компилятору С# известно об этом классе, и он использует его синтаксис для делегатов, чтобы скрыть от нас детали его работы. Это еще один хороший пример того, как С# работает совместно с базовыми классами для того, чтобы сделать программирование настолько простым, насколько это возможно. Определив делегата, мы можем создать его экземпляр и использовать его для хранения сведений о конкретном методе. К сожалению, здесь имеется проблема с терминологией. Для классов существуют два отдельных термина: "класс", который означает определение, и "объект", который означает экземпляр класса. К сожалению, для делегатов используется только один термин. То, что получается при создании экземпляра делегата, также называется делегатом. Поэтому когда мы говорим о делегатах, необходимо по контексту определять, о чем конкретно идет речь.
Дополнительные возможности С# 215 Следующий фрагмент кода демонстрирует использование делегатов. Это довольно долгий путь вызова метода ToString() для int: private delegate string GetAString(); static void Main(stringП args) { int X = 40; GetAString FirstStringMethod = new GetAString(X.ToS'tring); <, Console.WriteLine("String is " + FirstStringMethod()); // С FirstStringMethod инициализированным X.ToStringO // приведенный выше оператор эквивалентен // Console.WriteLine("String is " + X.ToStringO); В этом коде мы создали экземпляр делегата типа GetAString и инициализировали его таким образом, что он стал ссылаться на метод ToString () целочисленной пере- переменной X. Делегаты в С# всегда синтаксически принимают конструктор с одним пара- параметром, в качестве параметра передается метод, на который будет ссылаться делегат. Сигнатура данного метода должна соответствовать сигнатуре указанной при объявле- объявлении делегата. Поэтому в данном случае, если бы мы попытались инициализировать FirstSt.ringKeti. H любым другим методом, сигнатура которого отличается от сигнату- сигнатуры То£ !-r j tig (), возникла бы ошибка компиляции. Так как int. ToString () яшмется ме- методом экземпляра (а не статическим), для корректной инициализации ддаегата требуется вместе с именем метода указать экземпляр (XV Следующая строка кода использует делегата для отображения строкового значения. В любом коде указание имени делегата с последующими скобками, в которых перечисле- перечислены параметры, имеет точно такое же действие, что и вызов метода, для которого деле- делегат служит оболочкой. Поэтому в приведенном коде оператор Console.writeLine () эквивалентен тому, что записано в комментарии. Одна из особенностей делегатов заключается в том, что они безопасны по типу в том смысле, что сигнатура вызываемого метода является корректной. Однако их совершен- совершенно не интересует, для какого типа объекта вызывается метод и является ли метод стати- статическим или методом экземпляра. Экземпляр делегата может ссылаться на любой статический метод или метод экземпляра любого объекта любого типа — при условии, что сигнатура этого метода совпадает с сигнатурой делегата. Для демонстрации этого расширим приведенный выше пример кода, в котором ис- используется делегат FirdtStringMet'jod, чтобы он вызывал пару других методов для дру- другого объекта: метод экземпляра и статический метод. Вновь обратимся к структуре Currency (см. выше). Напомним, что структура Cuirency уже имеет свою собственную перегруженную версию ToString (). Добавим в Currency статический метод с такой же сигнатурой: struct Currency . { public static string GetCurrencyUnit(); { return "Dollar"; } Теперь можно использовать экземпляр GetAString следующим образом: private delegate string GetAStiing(); static void Main(string[] args) { int X = 40; GetAString FirstStringMethod = new GetAString(X.ToString); Console.WriteLine("String is " + FirstStringMethodО); Currency Balance = new CurrencyC4, 50); FirstStringMethod = new GetAString(Balance.ToString); Console.WriteLine("String is " + FirstStringMethod()); FirstStringMethod = new GetAString(Currency.GetCurrencyUnit); Console.WriteLine("String is " + FirstStringMethod{));
216 Глава 6 Этот код показывает, как можно вызвать метод при помощи делегата, и последователь- последовательно присваивает делегату ссылки на различные методы разных экземпляров классов: ста- статические методы или методы экземпляров разных типов классов, при условии, что сигнатура метода совпадает с определением делегата. Однако мы все еще не продемонстрировали процесс собственно передачи делегата другому методу и не сделали ничего полезного. Метод ToStringt) для объектов int и currency можно вызывать более простым способом, без использования делегатов. Од- Однако такова природа делегатов: чтобы оценить их полезность, необходимо рассмотреть сложный пример. Мы приведем два примера применения делегатов. Первый иллюстри- иллюстрирует, как передавать делегатов в методы и как можно использовать массивы делегатов — но в целом он не выполняет ничего такого, что нельзя было бы сделать без делегатов. Более сложный пример — класс BubbleSorter — реализует метод для сортировки масси- массивов объектов в порядке возрастания. Этот класс было бы трудно написать без применения делегатов. Простой пример использования делегатов Определим класс MathOperations, который имеет пару статических методов для выполне- выполнения действий над числами double. Затем используем делегатов для вызова этих методов. Математический класс выглядит следующим образом: .class MathOperations public static double MultiplyByTwo(double value) return value*2; } public static double Square(double value) Лгеййрп. value*value; } Вызываются эти методы так: using System; namespace Wrox.ProfessionalCSharp.Chepter6.SimpleDelegate { delegate" double BoubieOp(double x) ; class MainEntryPoint { static void Main (seeing [] args) {■ , DoubleOp; [J, operations ~ { new DoUbleOpWathOperations.MultiplyByTwo) , .new Doubl^pp(MathOperalions.Square) for (int 1=0; I < operations. Length; I++) { Console.WriteLine("Using operations[{0}]:", I) ; ProcessAndDisplayNumber(operations[I], 2.0); ProcesSAndDisplayNumber(operations[I], 7.94); ProcessAndDisplayNumber(operations[I], 1.414); Console.WriteLineО; i , ' static void ProcessAndDisplayNumber(DoubleOp action, double value) { double result = action(value); Console.WriteLine("Value is {0}, result of operation is Cl}", value, result);
Дополнительные возможности С# 217 В этом коде мы создаем массив делегатов DoubleOp. (Напомним, что, определив класс делегата, мы можем создавать его экземпляры точно так же, как и для обычных классов, поэтому их помещение в массив не является проблемой.) Каждый элемент массива иници- инициализируется ссылкой на отдельные операции, реализованные в классе MathOperat Lons. Затем мы в цикле проходим по массиву, применяя каждую операцию к трем отдельным значениям. Этим мы показываем один из способов применения делегатов: их можно группировать в массив, а несколько методов можно вызывать в цикле. Ключевыми в этом коде являются строки, где мы передаем каждого делегата в метод ProcessAndDisplayNunib ;r (), например: ProcessAndDisplayNumber(operat-ons[I], 2.0); Здесь передается имя делегата, но без параметров. При условии, что opervv. ions [I] является делегатом, синтаксически: "operations [I] " означает "делегат", т.е. метод, представляемый делегатом "operations [ II [2.0.1 •• означает "вызвать этот метод" Метод ProcossAndDi^pi "yNumber ) принимает делегата в качестве своего первого параметра: static void Process.AndDisplay'tfu.nbo1' ' b" - .. r , r'~" ble ve.1' О Затем, находясь внутри srioi"o метода, мы вызываем: double Result = actLon(value); Это приводит к вызову метода, обернутого экземпляром делегата action, а возвра- возвращаемый результат сохраняется в переменной Result. Выполнение примера приводит к следующему результату: Using Operations[01: Ualue is 2, result of operation is 4 Ualue is 7.94,..result of operation is 15.88 Ualue is 1.414, result of operation is 2.-828 Using Operationstl1:. Ualue is 2, result of, operation is 4 Ualue is 7.94, result of operation is 63.0436 -,i. 1)аДие is 1.414, result of operation .is 1-999390 ,■ Пример BubbleSorter Теперь мы приведем пример, в котором делегаты чрезвычайно полезны. Напишем класс BubbleSorter. Он реализует статический метод Sort (), который в качестве первого параметра принимает массив объектов и сортирует его в возрастающем порядке. Напри- Например, если мы передадим массив int {0. 5, 6, 2,1}, метод отсортирует его так: @,1,2, 5, 6}. Алгоритм пузырьковой сортировки является довольно известным и очень простым (хотя неэффективным) способом сортировки чисел. Он работает, проходя по всему мас- массиву, сравнивая каждую пару чисел и меняя их в случае необходимости, в результате чего большие числа постепенно перемещаются в конец массива. Для сортировки int метод пузырьковой сортировки выглядит так: -*■ // Это не часть примера for (int i=0; i<sortArray.Length; i++) { for (int j=0; j<i; j++) { if (j > i) // проблема с этой проверкой
218 Глава 6 int tenip = sortArray [ i 1; // меняем sort Array [ i] = sortArray [ j ],- sortArray{j] = temp; Все хорошо для чисел int, но требуется, чтобы метод Sort О мог сортировать лю- любые объекты. Другими словами, если клиентский код передает массив структур Curren- Currency или любых других классов и структур, которые он смог определить, мы должны суметь отсортировать его. Это приводит к появлению проблемы в строке if (j>i) кода, так как она требует сравнения двух объектов массива для определения того, какой из них больше. Мы можем выполнить это сравнение для чисел int, но как мы сделаем это для класса, который ранее нам не встречался? Ответ заключается в том, что клиент- клиентский код, которому известно о существовании этого класса, передаст в наш код делегата, который оборачивает метод, выполняющий сравнение. Определим этого делегата как: delegate bool CompareOp(object Ihs, obiecl rhs) ; А метод Sort () будет иметь сигнатуру: static public void Sort(object i] sortArray, CompareOp rhsIsGreater) В документации для этого метода будет отмечено, что rhsIsGreater должен ссылать- ссылаться на статический метод, принимающий два аргумента и возвращающий true в том слу- случае, если значение второго аргумента "больше" (т.е. он должен следовать в массиве позже), чем значение первого. Теперь определим класс ВчЪЫ ^Sorter: class BubbleSorter { static public void Sortfobject [.] sortArray, CompareOp rhsIsGreater) ; { for (int i=0; i<sort Array. Length,- i++) i for (int j=0; j<i; j*+) { if (rhsIsGreater(sortArray[i], sortArray[j])) { object temp = sortArray[i]; sortArray[i] = sortArray[j]; sortArray[3J = temp; J } Для того чтобы использовать этот класс, необходимо определить другой класс, в ко- котором можно создать массив, требующий сортировки. Предположим, что наша компа- компания сотовой связи Mortimer Phones имеет список работников и желает отсортировать его по их зарплатам. Каждый из работников представлен экземпляром класса Employee, который выглядит следующим образом: class Employee { private string name; private decima" salary,- public Employee (strl/ig name, J. icimal salary) { ■this, name = nrme,- .this, salary - salary,- } public override strinq ToStr:ng() { return sri'-"" 'or -*■(--.-. » ,0-^1", ■" ); } public static bool KhplRGreater tn\ >-i■_ .-4. ..'л r rht:)
Дополнительные возможности С# 219 Employee Lhs = (Employee) Lhs; Employee Rhs = (Employee)rhs; return (Rhs.Salary > Lhs.Salary) «? true : false; Для того чтобы добиться соответствия сигнатуре делегата Compci <=Юр, нам пришлось определить Rhsj"sGreater в этом классе как принимающий в качестве параметров две ссылки на object, а не на Employee. Это означает, что для выполнения сравнения мы должны привести параметры к ссылкам Emp Loyee Теперь можно написать клиентский код, который будет выполнять сортировку. using System; namespace Bubbi_^"i ex { delegate bnol Compar ( i с l *- >iect rhs) ; class Cassl { static void Main(sfrlng[" яг^,.= ) { Employee [] employees - { new Employee("Karx. Wat!_:~", 20000), new Employee ("Bi'1'' Gates", -GCOO) , new Employee("Simon Robinson", 2500C) , new Employee ("Mortimer", (decimal)' r,^ ^00 .38) , new Employee("Arabel Jones", 23С <uj , new Employee ("Avon from 'Blake's 1' ", 50000) } ; CompareOp EmployeeCompareUp = л_w CompareOp(Employee.RhsIsGreater); BubbleSorter.Sort(employees, EmployeeCompareOp); for (int 1=0; I<employees. Length; I++) Console.WriteLine (employees [IJ .ToStringO ) ; Результат выполнения кода показывает, что работники отсортированы по зарплатам: J}EL ill Gates, £1в.вИИ.И ai-li IMtson, £20.000.00 l-dbel Jones, £23.000.вв inon Robinson, £25,000.00 ftuon fi-on 'Blake's 7', £50,000.00 riortinei-. £1,000,000.38 Зарплаты показаны в £, так как я живу в Великобритании, и мои региональные на- настройки установлены в UK. При форматировании каждой зарплаты в виде строки струк- структура decimal выбирает фунты. Если вы загрузите и запустите этот пример, то £ будет заменен знаком вашей местной валюты. В результате зарплата может оказаться слишком маленькой A0000 итальянских лир не совсем равны GBE10000!). Многообъектные делегаты До сих пор каждый из использовавшихся делегатов служил оболочкой только для одного вызова метода. Если требуется вызывать более одного метода, необходимо явно вызывать делегата несколько раз. Однако делегаты могут вмещать в себя более одного метода. Такой
220 Глава 6 делегат называется многообъектным. При вызове многообъектный делегат по порядку вызывает каждый метод. Рассмотрим код, который был взят из примера SimpleDelegate: delegate viud DoubieOp(double value); // delegate double DoubleOp(double value); // теперь мы не можем так сделать class MainEntryPoint { static void Main(string[] args) { DoubleOp operations = new DoubleOp(MathOperations.Mult'iplyByTwo) ; operations += new DoubleOp(MathOperations.Square); В данном случае создан экземпляр делегата operations. В более раннем примере мы хотели хранить ссылки на два метода, поэтому создавали массив делегатов. Здесь же мы помещаем обе операции в один делегат. Делегаты понимают операции + и +=. При необ- необходимости можно было бы развернуть приведенные выше две строки кода — результат был бы таким же: DoubleOp operationl = new DoubleOp(MathOperations.MultiplyByTwo); DoubleOp operation2 = new DoubleOp(MathOperations.Square); DoubleOp operations = operationl + operation2; Делегаты понимают также операции - и -= для удаления вызовов методов из делегата. Однако существует ограничение. Отметим, что мы изменили определение делегата DoubleOp таким образом, что теперь он ссылается на метод, возвращающий void. Ком- Компилятор не позволит применить операции + и += для совмещения нескольких методов в одном делегате в том случае, если метод, на который он ссылается, возвращает не значение void. Предположим, что, используя предыдущее определение DoubleOp, вы написали: double result .='operationsB.0); // неверно где operations содержит более одного метода. Эта строка приведет к вызову всех мето- методов в operations с параметром 2.0. Что произойдет с возвращаемыми результатами? Возвращаемая величина result примет только один из них, а все остальные будут поте- потеряны. Чтобы избежать этого, компилятор сообщит об ошибке в том случае, если вы по- попытаетесь поместить более одного метода в делегат, который возвращает не void. По той же самой причине при использовании многообъектных делегатов не следует приме- применять out-параметры в методе. Компилятор позволит сделать так, однако вы потеряете все выходные значения, кроме последнего. Кстати говоря, за кулисами происходит следующее. Когда компилятор видит, что объявленный вами делегат представляет собой метод, возвращающий void, он автоматически предполагает, что вам требуется многообъектный делегат, и создает объект базового класса System.MulticastDelegate, который является производным от System.Delegate. System.MulticastDelegate имеет дополнительных членов, которые позволяют объединять методы в список ■для последовательного вызова. Для демонстрации использования многообъектных делегатов переделаем пример SimpleDelegate в новый пример MulticastDelegate. Так как теперь требуется, чтобы делегат ссылался на методы, возвращающие void, мы должны переписать методы в классе MathOperatior.s — теперь они будут отображать свои результаты, а не возвращать их: class MathOperations { public static void MultiplyByTwo(double value) I double result = value*2; Console.WriteLine("Multiplying by 2: {0} gives {1}", value, result); } public static void Square(double value) t ■„ double result = value*value; .Console.WriteLine("Squaring: {0} gives {1}", value, result); В соответствии с этим мы переписали ProcessAndDisplayNumber:
Дополнительные возможности С# 221 static void ProcessAndDisplayNulnber(DoubleOp action, double value) i Console.WriteLine("\nProcessAndDisplayNumber called with value = " + value); action (value) ,- } Теперь протестируем наш многообъектный делегат следующим образом: static vo.'u Main(str—iaN ara;-) ( DoubleOp operations = new DoubleOp(MathOperations.MultiplyByTwo); operations += new DoubleOp(MathOperations.Square); ProcessAndDisplayNulnber (operations, 2.0) ; ProcessAndDisplayNumber(operations, 7.94) ; ProcessAndDisplayNumber(operations, 1.414); Console.WriteLir.eO ; } Теперь кажлый раз при вызове ' >с( ssAr л1 ' ;_р__г " muf r он будет выводить сооб- сообщение о том, что он вызван. После этого оператор: action (vali' приведет к последовательному выполнению каждого метода из делегата action. Запуск кода даст следующие результаты. ProcessflndDiSpirtyNuribei* called uitb uciluf Multiplying by 2: 2 gives 4 Squaring: 2 gives 4. ' . ProcesSftndDisplayNunher cblled with value ■ ~ "" flultiplyintf by 2: V.94 giues 15.88 Squaring: V.94 giues 63.0436 ' PfocessAndDisplayNunbep called uitli ид1ке - 1.414 Hiiltiplyina by 2: 1.414 gives 2.826 Squaring: 1.414 gives 1.V99396 Pf:\C# Proiects\Pro C# Code\Ch5AdvC5h\Multicast Oelegate\bin\Debi События События являются тп 1лчным способом, с помощью которого приложения Windows полу- получают уведомления о том. что что-то произошло. При выполнении Windows генерируется огромное число собьтн . Например, при нажатии на кнопку мыши приложение, в окне которого вы произвели ото (ействие, uvkt уведомлено об этом событии. То же самое про- происходит, когда вы делаете ч го-го с помчи ыо клавиатуры. Аналогично, при минимизации, восстановлении и максимизации окон соответствующие приложения будут уведомлены об этом, если они желают производить какие-либо действия в ответ на эти события. Необходимо отметить, что сказанное выше не совсем точно. Строго говоря, события вообще не являются частью операционной системы Windows. Windows использует так называемые сообщения Windows, которые уведомляют приложения о происходящем. Однако сообщения Windows относятся к низкоуровневым структурам С, с которыми тяжело работать, поэтому языки высокого уровня, такие как VB, традиционно оборачивают сообщения высокоуровневым каркасом, в котором события являются основными объектами, чтобы упростить задачу программиста. С# и .NET делают то же самое, т.е. можно работать исключительно с событиями, хотя основную работу на самом деле выполняют сообщения Windows. В терминологии С# события являются особой формой делегатов.
222 Глава 6 Microsoft разработала события С# таким образом, что для понимания их работы практически не требуется знать, как действуют лежащие в их основе делегаты. Поэтому для начала рассмотрим события с точки зрения клиентского кода. Выясним, какой код необходимо написать для того, чтобы получать уведомления о событиях, не задумываясь о том, что происходит за сценой. Затем мы создадим пример, генерирующий события, и посмотрим, как работает стоящий за всем этим каркас делегатов. В этом контексте мы используем термин "событие" в двух значениях: как нечто, что произошло, и как объект, определенный в языке С#,- объект, который занимается обработкой события. Имея в виду последнее, мы обычно будем говорить "событие С#" или, если это очевидно из контекста, просто "событие". Событие с точки зрения клиента Клиентом здесь является любое приложение, которое необходимо информировать о происходящем. Предположим, что нажимается кнопка мыши, хотя событие может быть каким угодно другим. Кроме того, существует некоторое программное обеспечение (по- (помимо операционной системы Windows и платформы .NET), которое представляет собой то, что отслеживает возникновение события и уведомляет об этом наше приложение. Это программное обеспечение будет называться "генератором событий". Схема выглядит следующим образом: -- Клиент Обработчик события OnClick 'Я хочу, чтобы меня уведомляли' Объект EventArgs произошло событие Генератор событий Мышь Где-то внутри клиента существует метод, который должен вызываться в том случае, если происходит событие MouseClick. Этот метод известен как обработчик данного события, и мы назовем его OnClick. (Это имя не случайно; в главе 21 мы увидим, что ба- базовые классы Windows Forms действительно имеют для этой цели обработчик с именем OnClick.) Несколько ранее, скорее всего при запуске, клиентское приложение проин- проинформировало генератор событий о своем интересе к нажатиям кнопок мыши и о том, что при возникновении такого события необходимо вызывать метод OnClick (). Здесь на сцену выходят делегаты. В терминологии С#, для того чтобы предоставить генерато- генератору событий эту информацию, клиент должен разместить ссылку на OnClick () внутри де- делегата. Как только генератор событий получит эту информацию, при обнаружении нажатия на кнопку мыши он может использовать этого делегата для вызова клиентского метода OnClick (). Теперь обратимся к коду. Платформа .NET требует точной сигнатуры для любого об- обработчика событий. OnClick () и все остальные обработчики событий обязаны выглядеть следующим образом: void OnClick(object sender, EventArgs e) // e также может быть // производным от EventArgs { // код для обработки события Обработчики событий не могут возвращать ничего, кроме void. В них отсутствует точка, которая могла бы служить для возврата значения. Все, что желает сделать генера- генератор событий,— вызвать метод. Он ничего не хочет знать о том, что делает в ответ кли- клиент. Обработчики должны принимать два параметра. Первый параметр является ссыл- ссылкой на объект, который сгенерировал событие; другими словами, генератор событий передает ссылку на самого себя. Второй параметр должен быть ссылкой либо на базо- базовый класс .NET System. EventArgs, либо на производный класс. Можно рассматривать класс EventArgs как общий базовый класс для всех уведомлений о произошедших собы- событиях. В некоторых случаях генератор событий отметит, что в ответ на определенные со- события он пошлет ссылку на производный класс. Производный класс может содержать дополнительную информацию о событии, например местоположение мыши или клавишу, нажатую на клавиатуре.
Дополнительные возможности С# 223 С обработчиком события мы разобрались. Теперь нужно уведомить генератор собы- событий о том, что некоторое событие интересует нас в первую очередь. Для этого необхо- необходимо, чтобы генератор имел некий элемент, способный принимать запросы на уведомления. Этот элемент будет открытым членом класса .NET, который представляет генератор событий и является членом типа event. Этот член представляет собой собы- событие С# и является специализированной формой многообъектного делегата. Допустим, что клиент имеет ссылку на генератор событий в ссылочной переменной Generator и что член, представляющий событие, называется Mousedick. В коде для класса клиента может быть следующее: EventGenerator Generator = GetAReferenceToTheEventGeneratorSomehowl); Предполагается, что EventGenerator представляет собой класс генератора событий. Клиент может уведомить генератор событий о том, что он желает принимать сооб- сообщения о нажатиях кнопки мыши, с помощью такой строки кода: // Generator — это ссылка на генератор событий Generator. Moused ick += OndickO; И это все, что необходимо сделать клиенту. Мы знаем, что событие Mousedick представляет собой многообъектный делегат. Следовательно, оно будет содержать ссылки на все обработчики событий, зарегистриро- зарегистрированные различными клиентами для получения уведомлений, а эта строка кода лишь до- добавляет в список наш собственный обработчик. Кроме того, если клиент решит впослед- впоследствии, что он больше не заинтересован в этом событии, то он сможет сообщить об этом генератору событий следующим образом: Generator.Mousedick -= OnClickO; Таким образом, объем кода, который необходимо написать в клиенте, невелик. Не- Необходимо также иметь в виду, что вы будете писать код клиентов событий гораздо чаще, чем код их генераторов. По крайней мере, что касается пользовательского интерфейса, то Microsoft уже написала все генераторы событий, которые могут вам потребоваться (они располагаются в базовых классах .NET, в пространстве имен windows. Forms; см. главу 9). Обратим внимание на следующий момент. Мы лишь вскользь упомянули о том, как клиент вначале получает ссылку на генератор событий. Это не является частью общей системы событий. О получении ссылки должно говориться в документации к конкретно- конкретному генератору событий. В случае Windows Forms можно унаследовать клиента от класса генератора. Нередко вы будете создавать экземпляр генератора событий одного из базовых классов .NET. Эта архитектура является чрезвычайно гибкой. Клиент может запрашивать уведомле- уведомления о стольких событиях, о скольких пожелает; он даже может запрашивать уведомления из различных источников. Именно по этой причине в обработчик события передается первый параметр — ссылка sender. Клиент имеет ссылку на объект, сгенерировавший со- событие. Следовательно, если существует более одного возможного источника события, об- обработчик события сможет легко определить, кто его сгенерировал. Например, ваше приложение может быть формой Windows, имеющей несколько кнопок. Каждая из этих кнопок может уведомить вас, что она была нажата, и по ссылке sender удастся опреде- определить, какая кнопка послала уведомление. Помимо этого существует большое число различ- различных клиентов, которые могут уведомляться об одном и том же событии. Каждый из них добавляет свой обработчик события для этих событий, и благодаря специфике .работы многообъектных делегатов при возникновении события о нем будут уведомлены все его обработчики. Пример использования событий: консольные уведомления Напишем пример UserlnputNotify, который генерирует события. Мы снова возвраща- возвращаемся к компании сотовой связи Mortimer Phones. Будет создано небольшое консольное приложение для сотрудников Mortimer Phones, которое выв'одит-сообщение пользовате- пользователю. Пользователь может на выбор просмотреть личное послание от Мортимера (прези- (президента компании) или общее рекламное объявление. Программа продолжает спрашивать у пользователя, какое сообщение он желает просмотреть, до'тех пор, пока пользователь не нажмет X для выхода из программы. Однако мы собираемся структурировать програм- программу таким образом, чтобы она использовала события. Она будет следовать классической архитектуре уведомления о событиях, которая имеет широкое применение и похожа на архитектуру, используемую в Windows Forms. Зак. 69
224 Глава 6 В коде нас интересуют два объекта: О UserlnputMonitor. Этот объект имеет дело с пользовательским вводом. Он опра- опрашивает пользователей насчет того, какие сообщения они желают просмотреть. О MessageDisplayer. Этот объект отвечает за отображение соответствующего сообщения. Поскольку MessageDisplayer не связан с пользовательским вводом, он не может знать, когда и какое сообщение ему необходимо отображать; в этом ему приходится пола- полагаться на UserlnputMonitor. UserlnputMonitor будет уведомлять его, генерируя собы- события. MessageDisplayer сообщит UserlnputMonitor, что он желает получать уведомле- уведомления о том, когда необходимо отобразить сообщение для пользователя. Как только поступит соответствующий запрос от пользователя, UserlnputMonitor сгенерирует соот- соответствующее событие, что приведет к вызову обработчика событий в MessageDisplayer. На этом этапе наша программа прекрасно иллюстрирует применение событий, но не делает ничего, что нельзя было бы сделать с помощью простого цикла внутри одного ме- метода в одном классе, используя знания о С#, полученные в трех первых главах! Однако мы добавим то, что продемонстрирует гибкость архитектуры событий. Дело в том, что Мортимер желает получать уведомление, когда его сотрудники выбирают для просмот- просмотра его личное сообщение. Это позволяет ему чувствовать себя популярным и согревает его душу. Он хотел бы, чтобы его уведомляли о том, что пользователь просматривает его личное сообщение. С этой целью ManagersStaf fMonitor будет отображать диалоговое окно с сообщением "Карр" (Мортимер любит говорить "Карр", когда чувствует себя сча- счастливым). На этом примере мы продемонстрируем вам красоту архитектуры событий, так как все, что требуется сделать для того, чтобы Мортимер узнал о событии,— доба- добавить собственный обработчик события ManagersStaf fMonitor. Когда пользователь по- попросит отобразить сообщение, возникающее в результате событие вызовет оба обработчика. Если какое-нибудь другое программное обеспечение захочет получать уведомления об этом событии, то нужно будет сделать то же самое — написать обработчик события и добавить его к событиям в UserlnputMonitor. В этом случае обработчики событий будут вызываться по цепочке. Структура нашего приложения выглядит следующим образом: Клиент: MessageDisplayer Обработчик события: UserRequestHandler Клиент: ManagersStaffMonitor Обработчик события: UserRequestHandler ,. UserRequestEventArgs Генератор событий: UserlnputMonitor Событие: OnUserRequest / UserRequestEventArgs На рисунке показаны обработчики событий для двух классов-клиентов, Message- MessageDisplayer и ManagersStaffMonitor, а также событие в классе-генераторе событий, UserlnputMonitor. Для передачи сведений о событии мы используем новый класс UserRequestEventArgs, который наследуется от System. EventArgs и который реали- реализует одно свойство, Request. Оно будет показывать, какой и.ч двух возможных запросов сделал пользователь. Для начала необходимо написать класс UserRequestEventArgs, который будет пере- передавать сведения о событии. Для представления списка возможных событий используем перечисление: ?^Щ,{AdRequest, PersonaiMessageRequest}; iasa'4BS?r
Дополнительные возможности С# 225 privacy "RequestType request; public UserRequestEventArgs(RequestType request) : base() { this.request = request; public- RequestType Request get, { return request; Этот код не требует пояснений. Мы добавляем поле и соответствующее свойство, со- сообщающее о выборе пользователя. Наш конструктор принимает параметр, содержащий это значение, а затем вызывает конструктор базового класса. Теперь генератор событий UserlnputMonitor: eg userInputMom tor '"■ • public delegate void UserRequest(object 'sender, UserRequestEventArgs e) ; if ' . - -, , -•■ " public event User-Request .OnUserRequest; i " public -void RunO . ь { , while (true) Console.WriteLine("Select preferred option"); "- Console.WriteLineC Request advertisement - 'hi't *A then return"); Console.WriteLine(" Request personal message from Mortimer - " + "hit P then return");,. Console.WriteLinel"Exit - hit X then return"); string response = Console.ReadLineO; char responseChar = (response ==*")? : char.ToUpper(response[0)); switch (responseChar) { case 'A1 : OnUserRequest(this, new UserRequestEvencArgs(RequestType.AdRequest)); break; case 'P': OnUserRequest(this, new UserRequestEventArgs.(Request.Type.PersonalMessageRequest) ) ; break; case 'X' : -v return; ■ ) } Этот класс содержит лишь одну строку кода, в которой действительно используется ключевое слово event для объявления события. Нас интересуют строки: public delegate void UserRequest(object sender, UserRequestEventArgs e) ; public event UserRequest; OnUserRequest,- Схема практически та же самая, что и при определении делегатов (см. выше). Снача- Сначала мы объявляем делегата, а затем — его экземпляр. Отметим, что сигнатура делегата со- соответствует той, что требует .NET для обработчиков событий: он возвращает void, а в качестве параметров принимает object и объект, производный от EventArgs. Следующая строка более интересная — здесь мы сообщаем компилятору, что класс со- содержит член-событие типа, указанного делегатом. Эту строку можно рассматривать как:
226 Глава 6 // Неверно, приводится для сравнения public UserRequest OnUserRequest = new UserReguest(// метод); за исключением того, что синтаксис события позволяет компилятору убедиться в том, что делегат имеет корректную сигнатуру. Это также подразумевает, что мы создаем объ- объект, но не указываем для него методы, на которые он может ссылаться,— то, что мы не стали бы делать с использованием альтернативного синтаксиса для делегатов. Оставшейся частью класса UserlnputMonitor является метод RunO, который в цикле опрашивает пользователя. Обратите внимание на то, как происходит генерация событий: switch (responseChar) { case 'A' : OnUserRequest(this, new UserRequestEventArgs(RequestType.AdRequest)); break; case ' P' : OnUserRequest(this, new UserRequestEventArgs(KequestType.PersonaJMessageRequest)); break; case "X1 : return; } Мы вызываем событие, что автоматически вызывает все методы, на которые оно в дан- данный момент ссылается (это те самые обработчики событий, о которых сообщили клиен- клиенты). В каждом случае мы создаем экземпляр нового класса, унаследованного от EventArgs, чтобы передать его в обработчик события. Теперь рассмотрим классы клиента. Сначала MessageDisplayer: class MessageDieplayer * ■■",-. ■* ' ' - '..».;. г -. -*' public MessageDisplayer(OserInputMonitor monitor) *'■ "*■ ."' '::... 'i monitor ."OnUserRequest += new UserlnputMonitor.UserRequest(UserRequestHandler); ) "■ « Л- protected void UsexRequestHandler(object sender, UserRequestEventArgs e) ' ^ ,-■ - switch (e.Request) { case Reques.t'I'ype.AdRequest: ■ ^ Console.Writeljinef "Mortimer phones Is better than' anyone else " + * "because1'\nall- oxix software is written in C#!\n"l; break; ti '. case Requestiype.PersonalMessageRequest: : * Console.WriteLinel"Today Mortimer issued ithe following " + "statement: \n Nevermore!\n"); ■break;- .. >.. В конструкторе MessageDisplayer мы сообщаем классу UserlnputMonitor о том, что желаем получать уведомления о событиях. Мы осуществляем это путем добавления ссылки на обработчика событий для события UserRequest. Обработчиком событий яв- является метод UserRequestHandler, он проверяет параметр EventArgs, чтобы посмот- посмотреть, какой тип сообщения запросил пользователь, и отображает соответствующее сообщение. Отметим, что, следуя общей практике, мы объявляем обработчика события как protected. Вообще говоря, обработчики событий должны вызываться через собы- события, т.е. через делегатов. Обычно нет причин вызывать их напрямую, поэтому нет необ- необходимости и в том, чтобы их видели внешние классы. (Тот факт, что метод невидим внешним классам, не запрещает этим классам вызывать его с помощью делегата, если делегату присвоена ссылка на этот метод.) Личный класс Мортимера ManagersStaf f Monitor делает практически то же самое, за исключением того, что он отображает диалоговое окно, а не выводит строку на консоль: class ManagersStaffMonitor
Дополнительные возможности С# 227 public ;ManagersStaffMonitor(UserlnputMonitor monitor) monitor. OntlserRequest += new, UserlnputMonitor.UserRequest(UserRequestHandler); - v ' ' ' "protected void UserRequestHandlerfobject sender, UserRequestEventArgs e) i ' ' if (e.Request == RequestType.PersonalMessageRequest) { MeesageBox.Show("Kaark!", "Mort imer says..."); Наконец, мы можем посмотреть, как работает весь набор классов. Приведем код для метода Main: using System; using System.Windows. Forms; namespace Wrox.ProfessionalCSharp.Chapter6.UserlnputNotify { class MainProgramClass { ■£ static void Main (string [] 'args) ,"' Hj -;bser|nputMpn±4:or inputMonitor = new UserlnputMonitor (); f'i MessageDisprayer inpetProcessor .= new MessageDisplayer (inputMonitor) ; .Maua_gersStaffMonitor mortimer = new ManagersStaffMonitor<inputMonitor) ; inputMonitor.Run(); // код для других классов Это классический пример объектно-ориентированного программирования. Главная точка входа сама по себе не делает практически ничего. Она лишь создает экземпляры различных объектов, которые составляют приложение, и заставляет их начать взаимо- взаимодействие друг с другом. В данном примере создается и соответствующим образом иници- инициализируется экземпляр для каждого из трех классов. Затем вызывается метод Run () класса InputMonitor, который входит в главный цикл программы, запрашивая ввод по- пользователя. Приложение Windows Forms имеет очень похожую структуру (см. главу 9). Для этого типа приложений создаются экземпляры объектов, а затем вызывается статиче- статический метод Run () базового класса .NET Windows. Forms. Application, который запускает весь процесс обработки событий. Запустив этот пример, мы получим следующий результат: F:\C#Projects\JPno(>»Cotfay* Kt»lpt\r: preiers'ed option Rt;<H' >^t Publicity Messa*fi! hit P then .i-ct«Wi lieiiuesi; Statenenc fron Hm-tlnci» - tilt M tfienv t< Ыи bit X l Mit hit. H Iloi't iitei* Hitine all out* softua ReqiiA&c Public tltert return ■ - . ■'$ a is bKttel* ibArt лпмопс pise bccAusr i'k i uritfc.n an Ctt? ity Ноайп«й bit'I* then vel'tifti. Request Statfenent fron ItortiMr bit H their In-'tulrf txi<- .-» Me X tfieh reltu'ti 1 I issued thf» fulluiHnd "b*«(jc^rtcnt J i <•, • л, .■■.„-., V'1 :r * ■ 'I.. '■ >-
228 Глава 6 Директивы препроцессора С# Помимо обычных ключевых слов, С# содержит команды, известные как директивы пре- препроцессора. Эти команды никогда не транслируются в команды исполняемого кода, но оказывают влияние на процесс компиляции. Например, директивы препроцессора мож- можно использовать для предотвращения компиляции некоторых блоков кода. Скажем, пла- планируется выпуск двух версий: базовой версии и полной версии, которая будет иметь больше возможностей. Директивы препроцессора помогут исключить компиляцию до- дополнительных возможностей при компиляции базовой версии программного продукта. Другой пример: вы написали большой объем кода, который должен снабдить вас отла- отладочной информацией. Вероятно, этот код не требуется компилировать при создании финальной версии продукта. Директивы препроцессора можно отличить по стоящему перед ними символу #. Директивы препроцессора играют важную роль в С и C++. Однако в СМ не так много директив препроцессора, и они используются не слишком часто. В С# существуют другие механизмы, позволяющие достигать того же эффекта. Также отметим, что в СМ отсутствует отдельный препроцессор. Так называемые директивы препроцессора обрабатываются компилятором. Тем не менее С# сохранил название "директивы препроцессора", поскольку эти команды создают эффект наличия препроцессора. Кратко рассмотрим директивы препроцессора. #define и #undef #def ine используется следующим образом: ftdefine DEBUG Это сообщает компилятору, что существует символ с указанным именем, в данном случае DEBUG. Это похоже на объявление переменной, только эта переменная на самом деле не имеет значения — она просто существует. Этот символ не является частью кода: он присутствует во время компиляции кода. #undef выполняет обратное действие — удаляет определение символа: ttundef DEBUG Если символ не существует, #undef не имеет эффекта. Аналогично, ttdef ine не дейст- действует в том случае, если символ уже существует. Команды #define и #undef необходимо размещать в начале исходного файла С#, перед тем, как будет компилироваться код, объявляющий какие-либо объекты. Директива #def ine сама по себе бесполезна, но становится мощным средством при совместном использовании ее с другими директивами, особенно с #if. Кстати, вы можете заметить некоторые особенности по сравнению с обычным синтаксисом С#. Директивы препроцессора не оканчиваются точкой с запятой и обычно являются единственной командой в строке. Дело в том, что для директив препроцессора С# не требует использования точки с запятой для разделения команд. Если он видит директиву препроцессора, то предполагает, что следующая команда расположена на следующей строке. #if, #elif, #else и #endif Эти директивы сообщают компилятору, компилировать или нет блок кода. Рассмотрим метод: .; int DoSomeWork{d6uble X) " ( "' '*•" II какие-то действия fit DEBDG Console.WriteLin^CX = " +' X) ; ftendif } Этот код будет компилироваться как обычно, за исключением команды Conso- Console. WriteLine, расположенной внутри блока #if. Эта строка выполняется только в том случае, если был определен символ debug с помощью предшествующей команды #def i- пе. Обнаружив выражение #if, компилятор смотрит, существует ли указанный символ,
Дополнительные возможности С# 229 и компилирует код внутри блока #if только в том случае, если символ существует. В про- противном случае компилятор игнорирует весь код, записанный до соответствующей дирек- директивы tendif. Общей практикой является определение символа DEBUG при отладке и размещение фрагментов отладочного кода в блоках #if. Перед тем как компилировать окончательную версию программы, вы комментируете выражение #def ine, и весь отладоч- отладочный код исчезает, размер исполняемого файла становится меньше, а конечные пользовате- пользователи не получат отладочной информации. (Очевидно, что придется дополнительно протестировать код и убедиться в том, что он работает без символа DEBUG.) Эта методика весьма распространена в программировании на С и C++ и называется условной компиляцией. Директивы telif (=else if) и #else могут использоваться в блоках #if. Кроме того, можно вкладывать блоки #if: Bdefiiie- ENTERPRISE ftdefine W2K / I текст программы ENTERPRISE' // какие-то действия #If W2K // действия, выполняемые только в enterprise edition, работающей под W2K jftelif PROFESSIONAL ' // какие-то, другие действия #eise II код для базовой версии «ehdif Отметим, что использование #i f является не единственным способом условной компиляции кода. С# предоставляет альтернативный механизм с применением атрибута Conditional (см. ниже). #if и #elif поддерживают ограниченный набор логических операций, используя операторы !, ==, != и I I. Считается, что символ равен true, если он существует, и false, если нет. Например: #if W2K && (ENTERPRISE==false) II если определен W2K и %е определен-ENTERPRISE. #warning и #error Когда компилятор встречает директиву препроцессора «warning или terror, он генери- генерирует соответственно предупреждение или ошибку. Если компилятор видит директиву ^warning, он выводит пользователю текст, расположенный после ttwarning, после чего компиляция будет продолжена. При наличии # error пользователь увидит на экране со- соответствующее сообщение об ошибке, компиляция при этом будет остановлена, и IL-код не будет сгенерирован. Эти директивы можно использовать для проверки того, не было ли совершено нечто недопустимое с помощью операторов ttdefine. Директиву #warning удобно также при- применять для того, чтобы напомнить себе о необходимости сделать что- то: #if DEBUG && RELEASE #error "Одновременно объявлены DEBUG и RELEASE" #endif «warning "He забудь убрать эту строку перед тем, как отдать код на проверку начальству!" Console.WriteLine("'Ненавижу эту работу*"); #region и #endregion Директивы tfregion и tendregion используются для отметки того, что определенный фрагмент кода должен рассматриваться как единый блок с заданным именем, например: 'Member- fieid: Declarations - -int Xj ■;, * double JD-; '• Currency Balance; #eridre'gion Само по себе это не представляется полезным — на процесс компиляции эти коман- команды не оказывают никакого влияния. Однако их достоинство заключается в том, что они
230 Глава 6 распознаются некоторыми редакторами, включая Visual Studio.NET. Редакторы могут использовать эти директивы для лучшего размещения кода на экране (см. главу 8). #line Директива tline применяется для изменения информации об имени файла и номере строки, которая выводится компилятором в предупреждениях и сообщениях об ошиб- ошибках. Эта директива требуется обычно в том случае, если при написании программы вы используете пакет, который изменяет набранный текст программы перед тем, как по- послать его компилятору, и в результате номера строк или даже имена файлов, возвращае- возвращаемые компилятором, не соответствуют номерам строк в файлах или именам файлов, которые вы редактируете. Директива tline позволяет восстановить это соответствие. Для восстановления нумерации строк по умолчанию можно использовать синтаксис #line default: #line 164 "Core.cs" // Известно, что после обработки этого файла // промежуточным пакетом это будет // строка 164 в файле Core.cs / / какие-то действия #line default // Восстанавливаем нумерацию строк по умолчанию Атрибуты Атрибуты аналогичны директивам препроцессора в том смысле, что они не транслируются в операторы в откомпилированном коде, но служат директивами компилятору. Однако в то время как директивы препроцессора специфичны для компилятора С#, атрибуты составля- составляют часть платформы .NET и даже представляются классами .NET. Кроме того, число дирек- директив препроцессора фиксировано, а число атрибутов теоретически не ограничено, поскольку платформа .NET предлагает механизм для определения собственных атрибутов. По сути дела, атрибут является маркером, которым можно пометить элемент кода, например метод или класс, или даже отдельный аргумент метода и который содержит дополнительную информацию об этом элементе. Например, атрибут Conditional может использоваться для пометки метода как отладочного: [Conditional ("DEBUG")] public void DoSorneDebugStuff() { // какие-то действия } Имя атрибута указывается в квадратных скобках непосредственно перед определени- определением элемента. Некоторые атрибуты принимают параметры — они передаются в круглых скобках сразу после имени атрибута. В наиболее простом случае связывание атрибута с элементом может означать, что в от- откомпилированной сборке оставлена дополнительная информация об этом элементе, кото- которая может использоваться для дополнительного документирования (с помощью технологии отражения (см. главу 7), к этой информации можно получить программный до- доступ из кода С#). Это справедливо для любого пользовательского атрибута. Однако некото- некоторые атрибуты, объявленные в базовых классах, явно распознаются компилятором С#. Для этих атрибутов компилятор предпринимает определенные действия, которые могут от- отразиться на сгенерированном коде. Это касается и атрибута Conditional, который ука- указывает компилятору, что код для элемента необходимо компилировать только в том слу- случае, если ранее был определен соответствующий символ с помощью оператора #def ine. При наличии такого большого разнообразия атрибутов практически невозможно ска- сказать, для чего предназначен каждый из них. Мы дадим представление о том, что может быть достигнуто с помощью атрибутов, рассмотрев три часто используемых атрибута обще- общего назначения, которые определены в базовых классах и которые распознаются компилято- компилятором. В главе 7 будет показано, как определять свои собственные атрибуты и зачем это нужно. В последующих главах вы также будете встречаться с атрибутами базовых классов .NET. Познакомимся с атрибутами: О Conditional. Любой метод можно пометить атрибутом Conditional. Компиля- Компилятор не будет компилировать этот метод и любые операторы, которые ссылаются на него, если определен соответствующий символ. Это может быть полезно при условной компиляции (например, для отладочных версий программы).
Дополнительные возможности С# 231 О DllImport. Несмотря на обширные возможности библиотеки базовых классов .NET, иногда возникают ситуации, когда требуется получить доступ к Windows API или к другим функциям, реализованным в DLL. Для этой цели существует ат- атрибут Dllimport. Он помечает метод как определенный во внешней DLL, а не в какой-либо сборке. О Obsolete. Этот атрибут применяется для пометки метода, который считается устаревшим. В зависимости от настройки атрибут может генерировать либо предупреждение, либо ошибку компиляции, если компилятор встретит код, пытающийся использовать этот метод. Отметим, что можно указать точное расположение полей структуры в памяти, исполь- используя атрибут StructLayout. Продемонстрируем использование атрибутов на примере небольшой программы под названием attributes. Этот код отображает MessageBox с помощью функции Windows API MessageBox, а также выводит два отладочных сообщения, если компилируется отладочная версия. Одно из отладочных сообщений выводится с применением устаревшего метода. Приведем код примера: ((define, DEBDG using System; using System.Runtime. jnteropServices; using System.Diagnostics; namespace Wrox.ProfessionalCSharp.Chapter6.Attributes { i •■ MainFrogramClass [DllImport (-• User32.dll") ] public static extern int MessageBox(int hParent, string Message, '• string Caption, int Type) ; static void Main(string[] args) { '.'DisplayRunningMessage () ; fDisplayDebugMessage(); MessageBox{Q, "Hello", "Message", 0); Console.ReadLine(); } (Conditional("DEBUG"I private static void DisplayRunningMessageO * Console.WriteLine("Starting Main routine. Current time is DateTime.Now); (Conditional("DEBUG">] {(Obsolete <) 1 „private- static void DisplayPebugMessage() V, , .Console.WriteLineCStarting Main routine"); V ''■> ■ Мы начали с указания дополнительных пространств имен. Дело в том, что атрибут Dllimport объявлен в пространстве имен System. Runtime. InteropServices, а атрибут Conditional — в пространстве имен System.Diagnostics. Внутри основного класса программы мы определяем метод, который будет вызывать- вызываться из внешней DLL. Функция API MessageBox () определена в файле User32 .dll, поэто- поэтому его имя передается в атрибут Dllimport в качестве параметра. Отметим, что этот метод также пришлось объявить как extern. MessageBox () была написана на С, поэто- поэтому четыре параметра, которые она принимает, строго говоря определены типами дан- данных, доступными в С, но для типов данных С# они соответствуют int (используется для хранения дескриптора Windows, который можно указать равным нулю), двум строкам, содержащим сообщение и заголовок окна, и int, показывающему, какие кнопки необхо- необходимо вывести в диалоговом окне. Указывая в качестве последнего параметра нуль, мы га- гарантируем, что будет отображена только одна кнопка ОК.
232 Глава 6 Далее определены отладочные функции. DisplayDebugMessage О выводит на кон- консоль строку, в которой говорится о том, что программа запущена. Этот метод должен выполняться только в том случае, если создается отладочная версия, поэтому мы поме- помечаем его как условный при наличии символа DEBUG. Метод отмечен также как устарев- устаревший, поскольку была написана новая версия, DisplayRunningMessageО, которая до- дополнительно отображает текущую дату и время. Больше не следует применять DisplayDebugMessage (), поэтому мы помечаем его с помощью атрибута Obsolete, что- чтобы компилятор выводил предупреждение при использовании данного метода. Существует несколько перегруженных версий этого атрибута: [Obsolete("Метод DisplayDebugMessage является устаревшим. " + , "Используйте метод DisplayRunningMessage.")] При использовании метода будет сгенерировано предупреждение, но вместо сообщения по умолчанию будет выведено указанное сообщение. Другая версия: [Obsolete.{'Метод DisplayDebugMessage является устаревшим. * + ■ -'" "Используйте метод DisplayRunningMessage."), true] вызовет ошибку компиляции в случае использования этого метода. Второй необязатель- необязательный параметр имеет тип bool и указывает компилятору, что он должен рассматривать применение устаревшего метода в коде как ошибку, а не выдавать предупреждение. Перед запуском кода скажем несколько слов об атрибуте Conditional. Этот атрибут довольно сложен. В том случае, если условие не будет выполнено, компилятор не только не откомпилирует код метода, но и автоматически проигнорирует все строки кода, вызы- вызывающие этот метод, во всей остальной части исходного файла. (Чтобы компилятор мог делать это, метод должен возвращать void.) Атрибут Conditional более удобный способ условной компиляции, чем директивы препроцессора #if ... #endif (см. выше), так как эти директивы необходимо указывать для каждого участка кода, требующего условной компиляции. С другой стороны, директивы препроцессора более гибкие, поскольку могут вычислять некоторые логические выражения и, кроме того, их можно применять к любой части кода. Атрибут Conditional можно указывать только для отдельного метода. Компиляция и запуск примера Attributes с определенным символом DEBUG приводит к следующим результатам: Небезопасный код С# скрывает от разработчика большую часть схемы управления памятью за счет исполь- использования сборщика мусора и ссылок. Однако в ряде случаев необходим прямой доступ к памяти. Это может понадобиться для обеспечения высокой производительности или для обращения к функции во внешней (не .NET) DLL, которая требует передачи указателя в качестве параметра (например, некоторые функции Windows API). Иногда нужно исследо- исследовать содержимое памяти в целях отладки, или же вы пишете приложение, например отлад- отладчик, которое анализирует другие процессы, и пользователю требуется прямой доступ к памяти. В этом разделе будут рассмотрены особенности С#, позволяющие выполнять эти действия. Необходимо отметить, что ситуации, в которых может потребоваться использование указателей, на самом деле не так уж часты. Не рекомендуется применять указатели без надобности, так как в этом случае код не только будет
Дополнительные возможности С# 233 сложнее отлаживать, но он еще и не пройдет проверки на безопасность по типу памяти, осуществляемой CLR (см. главу 1). Привлекайте указатели, только если вы полагаете, что существует веская причина для их использования в конкретном приложении (наиболее вероятно для обеспечения обратной совместимости с существующим кодом, с которым придется работать приложению). Этот совет касается, в частности, бывших разработчиков на C++. В С# имеются указатели, но они не используются в нем так же часто, как в C++. Указатели В примерах выше мы активно использовали ссылки, которые являются "переодетыми" указателями. Мы уже видели, что переменные, представляющие классы и массивы, со- содержат адрес памяти, по которому в действительности хранятся соответствующие дан- данные. Указатель представляет собой переменную, которая точно так же, как и ссылка, содержит в себе адрес объекта в памяти. Разница состоит в том, что синтаксис С# для ссылок не позволяет осуществлять программный доступ по этому адресу. В случае ссылки переменная синтаксически трактуется так, как если бы она хранила собственно содержи- содержимое объекта. Если бы, к примеру, экземпляр класса, на который ссылается переменная, хранился бы в памяти по адресу 0х334Ь38, то ссылку нельзя было бы использовать для прямого обращения к ячейке памяти, расположенной по адресу 0х334Ь38. Ссылки С# разработаны таким образом, чтобы упростить использование языка и оградить вас от ошибок, связанных с нечаянной порчей памяти и даже с нарушением работы сборщика мусора. При использовании указателей вам доступен конкретный адрес в памяти. Это дает возможность выполнять самые разные операции. Например, к адресу можно доба- добавить 4 байта и посмотреть или даже изменить то, что содержится в памяти четырьмя байтами дальше. Применение указателей дает три преимущества: О Производительность. При условии, что вы отвечаете за свои действия, можно га- гарантировать, что доступ к данным или их обработка осуществляются максималь- максимально эффективно — вот почему такие языки, как С и C++, всегда допускали использование указателей. О Обратная совместимость. Несмотря на все возможности, предлагаемые средой исполнения .NET, разрешается вызывать старые функции Windows API, а в ряде случаев это может быть единственный путь достижения цели. API-функции напи- написаны на С, языке, интенсивно использующем указатели, т.е. многие из этих функ- функций принимают указатели в качестве параметров. Сторонние производители также могли в прошлом создать DLL, функции которых принимают параметры- указатели. О Иногда требуется сделать адрес в памяти доступным для пользователя (например, если создается приложение, обеспечивающее прямой интерфейс пользователя с памятью, такое как отладчик). Однако низкоуровневый доступ к памяти имеет свою цену. В частности:^ О Синтаксис, требуемый для достижения этой функциональности, является более сложным. О Указатели труднее применять. Необходимо обладать хорошими навыками про- программирования и четко понимать, что делает код, для того, чтобы успешно испо- использовать их. При работе с указателями легко внести в программу неуловимые ошибки. О В частности, если вы не будете осторожны, можно невзначай переписать другие переменные, вызвать переполнение стека, получить доступ к области памяти, ко- которая вообще не хранит переменные, или даже затереть информацию о вашем коде, которая требуется среде исполнения .NET. В результате программа будет аварийно завершена. Кстати, всего несколько десятилетий назад неаккуратное использование указателей могло привести к аварийному завершению не только вашей программы, но и других программ, запущенных в системе, а в исключительных случаях - к краху всей операционной системы. В наши дни операционные системы имеют гораздо больше встроенных средств безопасности, которые не позволяют переписать память, принадлежащую другому процессу или самой операционной системе, и поэтому от беспечного применения указателей теперь может пострадать только ваш процесс.
234 Глава $ Несмотря на эти риски, указатели остаются мощным и гибким инструментом для написания эффективного кода. С# разрешает использовать указатели только в специально помеченных блоках кода. Ключевое слово для этого — unsafe. (Microsoft специально выбрала такое ключевое слово, чтобы подчеркнуть опасность применения указателей.) Отдельный метод можно пометить как unsafe: unsafe int GefcSomeNumber() II код, использующий указатели Небезопасным может быть объявлен любой метод — независимо от того, какие дру- другие модификаторы были для него указаны (например, статические методы, виртуальные методы). Можно также пометить целый класс или структуру как unsafe: unsafe class MyClass // теперь любой метод в этом классе может использовать указатели Если вы отметите класс или структуру как небезопасную, то все ее члены также будут считаться небезопасными. Аналогично, можно пометить поле как unsafe: class MyClass unsafe int *pX; // объявление поля-указателя В' классе Можно также отметить блок кода внутри метода как unsafe: unsafe / // небезопасный код Однако локальная переменная не может быть unsafe: int MyMethodO - unsafe vint *pX,- / / Неверно Если требуется небезопасная локальная переменная, нужно объявить и использовать ее внутри небезопасного метода или блока кода. Следует обратить внимание еще на один момент. Компилятор С# откажется компилировать небезопасный код, если вы не сообщите ему явно о том, что код включает в себя небезопасные блоки. Флаг, применяе- применяемый для этого,— unsafe. Следовательно, для компиляции файла MySource. cs, который содержит блоки небезопасного кода, должна использоваться команда (при условии, что не требуется других опций компиляции): esc /unsafe MySource.cs Или: esc -unsafe MySource.cs Синтаксис указателей Сделав блок кода небезопасным, можно объявить указатели, используя синтаксис: int «pWidth, pHeight; double *pResult; Разработчики на C++ должны обратить внимание на то, что в С# синтаксис отличается: оператор С# int *px, pY; соответствует оператору C++ int *px, *pY;. Код объявляет три переменные: pwidth и pHeight являются указателями на int, a pResult — на double. При использовании в объявлениях переменных символ * говорит о том, что определяется указатель, т.е. то, что хранит адрес переменной заданного типа.
Дополнительные возможности С# 235 Общепризнанной практикой является использование префикса р в именах переменных, служащих указателями. После объявления можно использовать переменные-указатели точно так же, как обычные переменные. Однако для начала необходимо изучить две новые операции: О & означает "взять адрес от". Эта операция преобразует тип данных по значению в указатель, например int в *int. Она носит название адресной операции. О * означает "взять содержимое по этому адресу". Эта операция преобразует указа- указатель в тип данных по значению (например, * float в float). Она известна как операция разыменования. Из этих определений видно, что & и * имеют прямо противоположное действие. Вы, наверное, помните, что символы & и * представляют также операции побитового and <&) и умножения (*). Однако на практике не возникает никаких проблем: из контекста всегда можно понять, что означают эти символы в каждом конкретном случае. Для указателей они всегда используются как унарные операции - они действуют только на одну переменную и в коде расположены перед ней. С другой стороны, побитовое and и умножение являются бинарными операциями - они требуют наличия двух переменных, и поэтому в данном значении символы & и * должны быть расположены между двумя переменными. В качестве примера использования этих операций рассмотрим код: pX ■= SX; .?. PX = PX;' *pY ft 20V Мы объявляем переменную X как целое число, за ней следуют два указателя на цело- целочисленные переменные: рХ и pY. Затем мы устанавливаем рХ таким образом, чтобы он указывал на X (другими словами, содержимое рХ является адресом X). Значение рХ при- присваивается pY, в результате чего pY также будет указывать на X. Наконец, значение X из- изменяется на 20. Последнее, вероятно, понять труднее всего. Напомним, что оператор * означает "взять содержимое по этому адресу". Так как pY указывает на X, то *pY даст X. Поэтому выражение *pY = 20; приведет к тому, что значение X изменится на 20. Отме- Отметим, что здесь нет прямой связи между переменными pY и X. Просто в данный момент pY указывает на участок в памяти, в котором содержится X. Для пояснения происходящего предположим, что X хранится в ячейках памяти 0xl2FA78 - 0xl2FA7B в стеке (т.е. 4 позиции, так как int занимает 4 байта). Поскольку стек распределяет память вниз, переменная рХ будет храниться в позициях от 0xl2FA74 до 0xl2FA77, a pY расположится в позициях от 0xl2FA70 до 0xl2FA73. Отметим, что рХ и pY также занимают по 4 байта. Это происходит не потому, что int требует 4 байта, а потому, что на 32-разрядных процессорах для хранения адреса необходимо 4 байта. Если бы рХ был указателем на double (8 байт) или byte A байт), он все равно занимал бы 4 байта. Таким образом, после исполнения приведенного выше кода, стек мог бы выглядеть так: Указатель стека 0x12FA6C 0x12FA70flO 0x12FA73 0x12FA74w> 0x12FA77 0x12FA78flO 0x12FA7B) pY = 0x12FA78 pX = 0x12FA78 X = 20 @x14) Значения int будут храниться последовательно в стеке 32-разрядного процессора. Однако этого нельзя сказать о других типах данных. Дело в том, что 32-разрядные про- процессоры работают эффективно, получая данные из памяти блоками по 4 байта. Память в таких машинах делится на 4-байтовые блоки, иногда называемые DWORD (до .NET так
236 Глава 6 назывался 32-разрядный int без знака). Из памяти лучше всего получать значения DWORD — хранение данных, выровненных по границам DWORD, как правило, приводит к увеличению производительности. По этой причине среда исполнения .NET обычно вы- выравнивает типы данных в памяти так, что они занимают число байтов, кратное четы- четырем. Например, short занимает 2 байта, но при размещении short в стеке указатель стека будет декрементирован на 4, а не 2, поэтому следующая переменная в стеке снова начнется с границы DWORD. Можно объявить указатель на любой тип по значению, т.е. на любой из предопреде- предопределенных типов uint, int, byte и т.д. или на структуру. Однако невозможно объявить указатель на класс или массив. Дело в том, что поступая таким образом, можно вызвать проблемы со сборщиком мусора. Чтобы работать корректно, сборщик мусора должен точно знать, какие экземпляры класса были созданы в куче и где они располагаются. Однако если код начнет манипулировать классами с использованием указателей, то можно невзначай повредить информацию о классах в куче, которую среда исполнения .NET хранит для сборщика мусора. В этом контексте любой тип данных, к которому мо- может получить доступ сборщик мусора, известен как управляемый тип. Указатели могут быть объявлены только как неуправляемые типы, поскольку сборщик мусора не может работать с ними. Приведение указателей к целочисленным типам Поскольку указатель на самом деле хранит целое число, представляющее адрес, то ад- адрес в любом указателе может быть явно приведен к целому типу, и наоборот. Например, совершенно законно написать так: int X = 10; int *рХ, pY; рХ = &Х; pY = рХ; *pY = 20; uint У = uint (pX) ; int. -*pD,; = .-..<int*JY; В результате У будет иметь тип uint и содержать значение 1243768 (=0xl2FA78). За- Затем эта величина преобразуется обратно в int* и сохраняется в новой переменной pD. Следовательно, pD также указывает на оригинальное значение X. Перевод указателя в целочисленный тип нужен, например, для отображения его на экране. Методы Console.Writei) и Console.WriteLineO не имеют перегруженных методов, принимающих указатели, но примут и отобразят значения указателей, приве- приведенные к целочисленным типам: Console.WriteLine("Адрес = * + рХ); // Неверно Console.WriteLine( "Адрес = " + {uint) pXj ; // Верно Отметим, что указатель можно приводить к любому целочисленному типу. Но посколь- поскольку в 32-разрядных системах адрес занимает 4 байта, приведение указателя к чему-либо, кроме uint, long или ulong, практически всегда приводит к ошибкам переполнения, (int тоже может вызвать проблемы, так как его диапазон составляет грубо от -2 миллиар- миллиардов до 2 миллиардов, в то время как адрес находится в диапазоне от 0 до примерно 4 мил- миллиардов.) Когда С# будет выпущен для 64-битовых процессоров, адрес будет занимать 8 байтов, поэтому в таких системах приведение указателя к чему-либо, кроме ulong, при- приведет к ошибкам переполнения. Необходимо помнить и о том, что ключевое слово checked не применимо к преобразованиям, использующим указатели. Для таких преоб- преобразований исключение не будет сгенерировано даже в checked-контексте. Среда испол- исполнения .NET предполагает, что если вы используете указатели, то, видимо, знаете, что делаете, и учитываете возможность переполнения! Преобразования указатель-целый тип должны быть явными. Неявные преобразования не допускаются. Приведение типов для разных типов указателей Можно производить явные преобразования между указателями, указывающими на разные типы. Например: t>yte Byte s= 8} S>yte *pByte ~ &Byte; doubief- *pPouble = (double*)pByte; , . , „ , ,
Дополнительные возможности С# 237 Это допустимый код, но в подобных случаях вы должны точно знать, что делаете! При обращении к double, на которую указывает pDouble, на самом деле будет рассмат- рассматриваться память, содержащая byte, и трактоваться она будет так, будто это double, что не даст осмысленных результатов. Однако может потребоваться преобразовать типы для реализации объединения или привести указатели на другие типы данных к указателю типа sbyte для того, чтобы исследовать отдельные байты памяти. Указатели void Если требуется указатель, но не требуется задавать, на какой тип данных он указывает, можно объявить его как void: void *PointerToVoid; PointerToVoid * (void*)PointerToInt; Основное назначение такого указателя состоит в осуществлении вызовов функций API, требующих передачи параметров void*. В частности, компилятор сообщит об ошибке в том случае, если вы попытаетесь разыменовать указатель void с помощью оператора *. Оператор sizeof Если необходимо явно использовать размер типа в коде, можно применить оператор sizeof, который принимает имя типа данных в качестве параметра и возвращает число байтов, занимаемых этим типом. Например: int X i sizeof (double); X будет присвоено значение 8. Достоинство sizeof состоит в том, что не требуется запоминать размеры конкрет- конкретных типов, и вы можете быть уверены в том, что значение, используемое в программе, является корректным. Для предопределенных типов данных sizeof возвращает следующие значения: sizeof(sbyte) sizeof(short) sizeof(int) = sizeof(long) = sizeof(char) = sizeof(double) = 1; = 2; 4; 8; 2; = 8; sizeof(byte) = sizeof(ushort) sizeof(uint) = sizeof(ulong) = sizeof(float) = sizeof(bool) = 1; = 2; 4; 8; 4; 1; Sizeof можно также применять к структурам, определенным пользователем, однако в этом случае результат будет зависеть от того, какие поля имеются в структуре. Нельзя использовать sizeof для классов. Пример PointerPlayaround Теперь рассмотрим пример использования указателей. Код называется PointerPlaya- PointerPlayaround. Он манипулирует указателями и отображает результаты, показывая, что происходит в памяти и где хранятся переменные: ' static, unsafe void: Main(string[l args) { int X = 1,6; t short *Y = -1; '> double Z = 1.5; int *pX = &X; •■'* 'Short *pY' = ;&Y; > double *pz = &Z; ^ Consdle.WriteLiiief"Address of X,- is {0}, size is {1}., value is B)", 'i -1 ,(uint)&X, sizeof (tnt-fr X); '-> 'Console;WriteLine{"Address of Y is {0}, size is (I), value is {2}", (uijit)&Y, sizeof (shprt),, Y) ; ': 'Console.WriteLinet"Address of Z is {0), size is {1), value is Ш". (uint)bZ, sizeof(double), Z) ; Console.WriteLine("Address of pX=&X is @), size is {1), :'ч!'..-". .. » ..', _. . . » . ..... . ■ value is {2)*,
238 Глава 6 (uint)bpX, sizeof(int*) , (uint)pX); Console.WriteLinel "Address of pY=&Y is {0}, size is {:•. , value is {2}", (uint)&pY, sizeof(short*) , (uint)pY); Console.WriteLinel "Address of pZ=&Z is {0}, size is {1}, value is {2}", (uint)&pZ, sizeof(double*) , (uint)pZ); *pX = ,20; Console.WriteLine("After setting *pX, X = {0}", X); Console.WriteLine("*px = {0}", *pX) ; pZ = (double*)pX; Console.WriteLineC'X treated as a double = {0}", *pZ); Этот код объявляет три переменные по значению: int x, short Y и double Z, а также указатели на эти значения. Затем выводятся значения всех этих переменных, их размеры и адреса. Отметим, что, беря адреса рх, pY и pZ, мы в действительности рассматриваем указатель-на-указатель — адрес адреса значения! Наконец, мы используем указатель рХ для изменения значения X на 20, а также произ- производим приведение указателя, чтобы посмотреть, какой результат мы получим, если будем рассматривать содержимое х как double! Выполнение этого кода дает следующие результаты: Полученные результаты соответствуют нашему описанию работы стека (см. главу 5). Он размещает более поздние переменные в младших адресах памяти. Подтверждается и то, что блоки памяти в стеке всегда имеют размер, кратный 4 байтам. Например, Y име- имеет тип short B байта), а адрес 1243464 показывает, что для нее зарезервированы ячей- ячейки памяти 1243464 — 1243467. Если бы среда исполнения .NET располагала переменные строго друг за другом, то Y занимала бы только две позиции 1243466 — 1243467. Арифметика указателей К указателям можно добавлять или вычитать целочисленные значения. Однако компиля- компилятор подходит к этому с умом. Допустим, что у вас имеется указатель на int и вы пытае- пытаетесь добавить к его значению 1. Компилятор предположит, что необходимо рассмотреть ячейку памяти, размещенную сразу за int, и поэтому увеличит значение указателя на 4 — размер int. Если бы указатель ссылался на тип double, то добавление к нему 1 увеличило бы его значение на 8 байтов — размер double. И только если бы указатель ссылался на byte или sbyte (каждый по 1 байту), то добавление к нему 1 действительно изменило бы его значение на 1. С указателями можно использовать операции +, -, +=, —, ++ и —, где переменная с пра- правой стороны этих операторов будет long или ulong. Отметим, что для void-указателей арифметика указателей невозможна. Например, имеются следующие определения: uint 'о 9 3; byte В = 8;
Дополнительные возможности С# 239 double D = 10.0; uint *pUint = &U; // размер uint равен 4 byte *pByte = &B,- // размер byte равен 1 double *pDouble = &D; // размер double равен 8 И предположим, что эти указатели указывают на адреса: pUint — 1243468, pByte 1243464, pDouble — 1243456. После исполнения кода: ++pUint; // добавляет к pUnit 1, т.е. 4 байта pByte -= 3; // вычитает из pByte 3, т.е. 3 байта double *pDouble2 = pDouble - 4,- // pDouble2 = pDouble - 32 байта // D*8 байтов) Указатели будут содержать: pUint - 1243472, pByte - 1243461, pDouble - 1243424. и«ш.<ж,чи_миия—ь.чя ни iinii —пиит imini питии и ятшашак. , т._эяи " Общее правило заключается в том, что добавление числа X к указателю на тип Т со значением Р дает в результате Р + X*(sizeof (T)). Необходимо быть осторожным с этим правилом. Если последовательные значения данного типа хранятся в памяти последовательно, то сложение указателей позволяет перемещать указатели между ячейками памяти. Но если вы имеете дело с такими типами данных, как byte или char, чьи размеры не кратны 4, то последовательные значения не будут по умолчанию храниться в последовательных ячейках памяти. Также можно вычесть один указатель из другого при условии, что оба указывают на один и тот же тип данных. В данном случае результатом будет long, чье значение вычис- вычисляется как разность значений указателей, поделенная на размер типа, который они представляют: double *pDl = (double*I243424; // такая инициализация // указателя совершенно корректна double "pD2 = (double*) 124340.0; // такая инициализация // указателя совершенно корректна long. L = pDl - pD2; // в результате получаем 3 > II (=24/sizeof(double)) Указатели на структуры: оператор доступа к компоненту структурированного объекта Указатели на структуры работают точно так же, как и указатели на предопределенные типы по ссылке. Однако существует одно условие: структура не должна содержать типов по ссылке. Это связано с упомянутым выше ограничением, что указатели не могут указы- указывать на ссылочные типы. Если бы указателю разрешалось указывать на структуру, кото- которая имеет внутри ссылочный тип, это ограничение можно было бы обойти! Во избежание этого компилятор выдаст ошибку при попытке создания указателя на структу- структуру, содержащую типы по ссылке. Допустим, что имеется структура, определенная следующим образом: Struct MyGroovyStruct { public long X; public float F; ■)■ Тогда указатель на нее можно определить так: MyGroovyStruct *pStruct; И инициализировать его так: MyGroovyStruct Struct = new MyGroovyStruct О; pStruct. = &Struct ; Доступ к членам структуры можно осуществлять с помощью указателя: (*pStruct).X * 4; (*pStruct).F « 3.4f; Однако такой синтаксис выглядит немного сложным. Поэтому С# определяем другую операцию для доступа к членам структур через указатели. Она известна как операция до- доступа к компоненту структурированного объекта и обозначается тире с последующим знаком больше, что похоже на стрелку: ->
240 Глава 6 С помощью операции доступа к компоненту структурированного объекта приведенный выше код может быть записан так: pStruct->X = 4; pStruct->F = 3.4f; Можно также напрямую установить указатели соответствующего типа на поля в структуре: long *pL = &(Struct.X) ; float *pF = &<Struct.F) ;i Или, что то же самое: long *pL = & (pStruct->X) ; float *pF = &(pStruct->F) ; Эти выражения также синтаксически довольно сложные, но в данном случае не сущест- существует эквивалента операции ->. Указатели на члены класса Выше было сказано, что невозможно создать указатель на класс. Это связано с тем, что сборщик мусора не поддерживает информацию об указателях, а только о ссылках, поэтому создание указателей на классы может привести к тому, что сборка мусора будет работать неправильно. Однако большинство классов содержит члены, которые сами по себе являются типами по значению, и вы можете создать на них указатели. Однако это требует использования специального синтаксиса. Например, перепишем структуру из предыдущего примера в виде класса: class MyGroovyClass { public long X; ;': pittjlic float F; } Теперь вы, возможно, пожелаете создать указатели на ее поля х и F точно так же, как прежде. К сожалению, это приведет к ошибке компиляции: MyGrodvyClass Class = new MyGroovyClass(); long *pL = s-'(Class.X); // неверно float *pF = &(Class.F); // неверно В чем проблема? Мы же объявляем здесь указатели на вполне законные типы — long* и float*. Дело в том, что хотя X и F.caMH по себе являются неуправляемыми типами, они включены в состав класса, который располагается в куче. Это означает, что они неявно находятся под управлением сборщика мусора. В частности, сборщик мусора может в лю- любой момент активизироваться и переместить MyGroovyClass на новое место в памяти для того, чтобы привести кучу в порядок. Если он сделает это, то, разумеется, обновит все ссылки, так что переменная Class по-прежнему будет указывать на корректное местопо- местоположение. Однако сборщик мусора не знает о том, что применяются указатели, поэтому если он переместит Class, то pL и pF не изменятся и будут указывать на неверные ячей- ячейки памяти. Из-за опасности возникновения этой проблемы компилятор не позволит присвоить указателям адреса членов управляемых типов таким образом. Обойти это можно с помощью ключевого слова fixed, которое говорит сборщику мусора о том, что могут существовать указатели, указывающие на члены определенных экземпляров классов, и поэтому эти экземпляры не нужно перемещать. Синтаксис исполь- использования fixed в случае одного указателя таков: MyGroovyClass Class = new MyGroovyClass(); / / что-то делаем fixed (long *pClass = & (Class.D) { // какие-то действия } Другими словами, мы отмечаем блок кода как fixed. Блок кода ограничен фигурными скобками, в то время как в круглых скобках объявляется и инициализируется указатель, который будет указывать на член класса. Область видимости этой переменной (в данном примере — pClass) будет ограничена блоком fixed, а сборщик мусора будет знать, что экземпляр Class класса MyGroovyClass нельзя перемещать, пока исполняется код внутри блока fixed.
Дополнительные возможности С# 241 Если требуется объявить несколько таких указателей, можно разместить несколько выражений fixed до соответствующего блока кода: MyGroovyClass class = new MyGroovyClass(); gixed (ldng *pb = & (Class.!,)) fixed (float fpF = &(Class.F)) { // какие-то действия } Блоки fixed можно вкладывать друг в друга, если требуется использовать несколько указателей: MyGroovyClass Class = new MyGroovyClass(); fixed (long *pL - &(Class.L)) { //■ какие-то, действия с pL fixed {float *pp = &(Class.?)) ■ { //> какие-то действия с pF 3 * Внутри одного выражения fixed можно инициализировать несколько переменных при условии, что они имеют один и тот же тип: MyGroovyClass Class = new MyGroovyClass() ; MyGroovyClass Class2 = new MyGroovyClass(); fixed (long *pL = ■ & (Class. L) , :pL2 = &(Class2.L)) { // И, Т..Д. Во всех этих случаях неважно, указывают ли указатели на поля в одном и том же либо в разных экземплярах классов или на статические поля, не связанные ни с одним экземп- экземпляром класса. Добавление в пример классов и структур В этом разделе приводится пример PointerPlayaround2, демонстрирующий арифмети- арифметику указателей и применение указателей на структуры и классы. Для начала вернемся к структуре Currency и определим класс и структуру, которые будут представлять Currency. Эти типы аналогичны структуре Currency, объявленной ранее, но являются более простыми и имеют несколько иные поля: ^truct .CurrencyStruct _ J-skv { ' ■' public" .long' Dollars; public toyte Cents; .*;■ public override string ToStringO » ireturn "$" + Dollars + "." + Cents; class CurreneyCiass С , public JLong Dollars; public' byte Cents; < public override string ToStringO ({■'., > return "$" +■ Dollars + "." + Cents; , } У Мы выбрали Currency в качестве структуры и класса с той лишь целью, чтобы наш пример был менее абстрактным и более реальным. Отметим также, что CurrencySt- CurrencyStruct и CurreneyCiass идентичны, за исключением того, что CurrencyStruct является структурой, a CurreneyCiass — классом. Таким образом мы продемонстрируем исполь- использования указателей для обоих типов объектов. После определения структуры и класса можно применить к ним указатели. Приведем новый пример. Так как код довольно большой, он сопровождается подробными
242 Глава 6 комментариями. Начнем с отображения размера структуры Currency, создав пару ее эк- экземпляров и несколько указателей, причем эти указатели будут использоваться для ини- инициализации одной из структур — Amountl. Адреса переменных выводятся на экран: public static unsafe void Main(string!] args) Console.WriteLine:("Size of Currency struct Is '■" + , ' sizeof (CurirencyStruct):) ; CurrericyStruct Amountl, Amount2; CurrencyStruct *pAmount = &Amountl; long *pDollars =• &(pAmouht->Dollarsj; byte *pCents = &(pAmount->Cents); Console.WriteLine("Address of Amount!- is " + (uint)&Amountl).- Console.WriteLine("Address- of Ampunt2 is " * (uint)&Amount2); Consoie..Wri;teLine( "Address of pAmount is ■" b. (uint)&pAmount); Console.WriteLinet1!Address of jjDollaxs is " + (uintJ&pDollars); Console.WriteLinet"Address of pCents is ". +. (uine)&pCents) i pAmount->Dollars = 20; ' ! *pCents = 50; Console.WriteLine( "Amountl contains " + Amountl); ■* Теперь произведем манипуляции с указателями, основываясь на знании того, как ра- работает стек. В соответствии с порядком объявления переменных Amount2 будет располо- расположен по адресу сразу перед адресом Amountl. sizeof (CurrencyStruct) возвращает 16 (как показано на рисунке ниже), поэтому CurrencyStruct занимает кратное четырем число байтов. Следовательно, после декрементирования указателя на Currency он будет указывать на Amount 2: -~pAmqunt; // теперь указатель должен указывать на Amount2 Consble".WriteLine("Amount2 has address @) and contains' {!}", (uintlpAmount, *pAmount); Этот оператор Console. WriteLine () довольно интересен. Мы отобразили содержи- содержимое Amount2, но перед этим не инициализировали его! На экран будет выведен мусор — то, что хранилось в памяти по данному адресу до запуска примера. В обычной ситуации компилятор С# предотвратит применение неинициализированного значения, но при использовании указателей можно обойти все проверки на этапе компиляции. Именно так и получилось, потому что компилятор не мог знать, что мы на самом деле отобража- отображаем содержимое Amount 2. Только нам это известно — учитывая принцип работы стека, мы можем предсказать эффект от декрементирования pAmount. Начав использовать арифметику указателей, вы обнаружите, что можно получить доступ ко всем видам пере- переменных и к любой области памяти, к которым компилятор в обычной ситуации не по- позволил бы обратиться, отсюда и описание арифметики указателей как небезопасной. Теперь поработаем с указателем pCents. Сейчас pCents указывает на Amountl .Cents, но мы хотим сделать так, чтобы он указывал на Amount2 .Cents, и мы воспользуемся опе- операциями с указателями вместо прямой команды компилятору. Для этого необходимо уме- уменьшить содержащийся в pCents адрес на sizeof (Currency), но чтобы арифметика работала, нужно осуществить приведение типов: // осуществляем приведение типов для того, чтобы // pCents указывал на Cents внутри Amount2 CurrencyStruct *pTempCurrency = (CurrencyStruct*)pCents; pCents = (byte*) ( -pTempCurrency ); Console. WriteLine ("Address of pCents' is now " + (uint)kpCents) ; Наконец, применим ключевое слово fixed для создания указателей, которые указы- указывают на поля в экземпляре класса, и используем эти указатели для установки значений полей экземпляра. Отметим, что мы впервые смогли посмотреть адрес объекта, который хранится в куче, а не в стеке: Console.WriteLinet"\nNow with classes"); // Теперь попробуем сделать то же самое для классов CuirencyClass Ajtiount3 = new. CurrencyClass (') ; t fixed (long *pDollars2 = & (Amounts .Dollars )'<) "и fixed (byte *pCents2 = &(Amounts.Cents)) { Console.WriteLine( "Amount3.Dollars has address " + (uint)pDollars2); Console.WriteLine("Amounts.Cents has' address.- ",,*-. <uint)pCents2); ,,.
Дополнительные возможности С# 243 *pDollars2 = -100; Console.WiiteLine("Amount3 contains " + Amount3); } Запуск этого кода дает следующие результаты: Отметим в результатах неинициализированное значение Amount 2, а также то, что раз- размер структуры Currency равен 16 — несколько больше, чем можно ожидать, зная размеры ее полей A long=8 + 1 byte=l). Очевидно, что здесь осуществляется выравнивание на бо- большее число слов. Из этого кода можно также видеть, что типичное значение для адре- адресов в куче — 12819868 = 0xC39D9C. В сравнении со стеком, куча существует в совершенно иной области виртуального адресного пространства. Использование указателей для повышения производительности Мы потратили немало времени на изучение того, что можно делать с помощью указателей. Однако все, чем мы до сих пор занимались,— это игра с памятью, которая, вероятно, инте- интересна тем, кто хочет знать, как это работает внутри, но она не помогла написать нам дейст- действительно полезный код! Теперь рассмотрим пример, в котором разумное использование указателей приведет к значительному увеличению производительности. Создание стековых массивов В этом разделе мы рассмотрим одну из основных областей, где указатели могут оказаться весьма полезными: создание высокопроизводительных массивов в стеке. В главе 3 было показано, что С# имеет богатую поддержку массивов. Он позволяет работать как с одно- одномерными, так и с прямоугольными и неровными многомерными массивами, но страдает от того, что эти массивы на самом деле являются объектами, а именно экземплярами System. Array. Это означает, что массивы хранятся в куче, и им сопутствуют все связан- связанные с этим потери производительности. Однако возможны ситуации, когда нужно со- создать массив на короткий промежуток времени, и при этом нежелательны потери производительности, вызываемые тем, что массивы являются объектами по ссылке. Это можно сделать с помощью указателей (хотя только для одномерных массивов). Для создания высокопроизводительного массива служит ключевое слово stackalloc. Команда stackalloc сообщает среде исполнения .NET о том, что нужно выделить опре- определенное количество памяти в стеке. При вызове необходимо передать этой команде два параметра: тип переменной, которую требуется сохранить, и число сохраняемых пе- переменных. Например, чтобы выделить память для хранения 10 значений decimal, необ- необходимо написать: decimal *pDecimals = stackalloc decimal Г10]; Отметим, что эта команда лишь выделяет память. Она не пытается заполнить ее каки- какими-либо значениями — вы должны сделать это сами. Идея состоит в том, что это высокопро- высокопроизводительный массив, и установка начальных значений снизит его производительность, Аналогично, для хранения 20 значений double можно записать: double *pDoubles = stackalloc double [203; Идея ясна?
244 Глава 6 Здесь в качестве числа переменных для хранения указывается константа, однако эта величина может рассчитываться и во время выполнения программы. Поэтому второй пример можно записать следующим образом: int Size; Size- = 20; // или какая-то другая величина, вычисленная во время У/ выполнения программы double *pDoubles = stackalloc double. [Size]; Из этих фрагментов кода видно, что синтаксис stackalloc несколько необычен. Вслед за ключевым словом сразу идет имя типа данных, который необходимо хранить (и это должен быть тип по значению), а затем в квадратных скобках указывается число пе- переменных, для которых требуется выделить место. Умножив это число на size- of(тип_данных), мы получим количество выделенных байтов. Использование квадрат- квадратных скобок предполагает наличие массива, что не удивительно: раз выделяется место для, скажем, 20 double, то очевидно, что вам требуется массив из 20 double. Наиболее простой тип массива, который можно получить: блок памяти, хранящий элементы один за другим. Например: Последовательное выделение Указатель, возвращенный stackalloc Элемент массива 0 Элемент массива 1 Элемент массива 2 и т.д. На рисунке приведен указатель, возвращаемый stackalloc. Эта команда всегда воз- возвращает указатель на размещаемый тип данных, причем указатель показывает на начало выделенной области памяти. Следующий вопрос: как использовать полученную память? Поскольку значение, воз- возвращаемое stackalloc, указывает на начало памяти, к первой ячейке выделенной обла- области можно обратиться, разыменовав указатель. Например, для выделения места под double и затем для установки первого элемента (т.е. элемента 0 в массиве) в значение 3.0 можно записать так: double *pDoubles = *pDoub!es = 3.0; stackalloc double [20]; А как насчет следующего элемента? Здесь на сцену выходит арифметика указателей. Напомним, что если добавить к указателю 1, то на самом деле его значение будет увели- увеличено на размер типа данных, на который он указывает. В данном случае этого достаточ- достаточно для перехода к следующей свободной ячейке памяти в выделенном блоке. Поэтому второй элемент блока (т.е. элемент массива номер 1, так как отсчет элементов массива всегда ведется с нуля) можно установить следующим образом: double *pDoubles = stackalloc double [20]; *pDoubles = 3.0; *(pDoubles+l) = 8.4; Применяя этот принцип, элемент массива с индексом X можно получить с помощью выражения *(pDoubles+X). Этот способ по-своему хорош — он обеспечивает доступ к элементам массива. Однако использование такого синтаксиса для получения элементов массива не приобретет боль- большого числа поклонников! К счастью, С# определяет альтернативный синтаксис. С# дает четкое определение квадратным скобкам, когда они применяются по отношению к ука- указателям. Если переменная р имеет тип указателя, а х является любым числовым типом, то в С# выражение р [х] всегда интерпретируется как * (р+х). Это справедливо в любом случае — указатель р не требуется инициализировать с помощью stackalloc. Эта
Дополнительные возможности С# 245 сокращенная нотация дает нам удобный синтаксис для доступа к массиву. На самом деле это означает, что для доступа к стековым массивам используется точно такой же синтак- синтаксис, что и для доступа к массивам в куче, которые представлены классом System. Array: double *pDoubles = stackalloc double [20] ; pDoublesiO) = 3.0; //'- pDoubles[0]- —■ это' то же самое, что и "pDoubles pDoublesIl] = 8.4; // px>6ubles[l] — это то же самое,, что'-и *(pDoubles+l) Очевидно, что идея применения синтаксиса массивов к указателям не нова. Она являлась фундаментальной частью языков С и C++ с того самого момента, как они были изобретены. В самом деле, разработчики на C++ узнают в стековых массивах, которые можно получить с помощью stackalloc, классические стековые массивы С и C++. Именно этот синтаксис и способ, посредством которого связываются указатели и массивы, были одной из причин популярности С в 70-х гг. и основной причиной того, что использование указателей стало столь распространенной методикой программирования в С и C++. Доступ к нашему высокопроизводительному массиву осуществляется точно так же, как к обычному массиву С#. Однако необходимо сделать одно предупреждение. Следую- Следующий код в С# вызовет исключение: douWe [] MypoutjleArray = new double .120]; MyDtfubleArrayiSOJ =3.0,- Исключение возникает по естественной причине: мы пытаемся получить доступ к массиву, используя индекс, находящийся за допустимыми пределами (индекс 50, в то время как максимально допустимое его значение 19). Однако, если объявить эквивалент- эквивалентный массив с помощью stackalloc, то для массива уже не будет существовать объек- объекта-оболочки, который смог бы выполнить проверку на выход за границы массива. Поэтому следующий код не вызовет исключение: double *pDpubles = stackalloc double [20] ;. ppQubles[50] ,=s~ '3.0; В этом коде мы выделяем память, достаточную для хранения 20 значений double. За- Затем мы присваиваем ячейке в памяти, занимающей sizeof (double) и отстоящей от на- начала выделенной памяти на 50*sizeof (double), значение double 3.0. Эта ячейка памяти находится за пределами области, выделенной для массива double. Какие данные хранятся в этих ячейках, неизвестно. В лучшем случае мы лишь испортим значение дру- другой переменной. Но также возможно, что мы перепишем некоторые позиции в стеке, которые использовались для хранения адреса возврата из метода, исполняемого в дан- данный момент,— адреса, который говорит компьютеру, куда передать управление при вы- выходе из метода. В этом случае дальнейший путь исполнения программы будет совершенно другим! Мы в очередной раз видим, что высокая производительность, кото- которой позволяют добиться указатели, имеет свою цену: вы должны быть совершенно уве- уверены в том, что делаете, в противном случае вы будете получать странные ошибки во время исполнения. Пример QuickArray В данном примере программа спрашивает пользователя, какое число элементов необхо- необходимо выделить для массива. Затем код использует stackalloc для размещения массива, состоящего из элементов long и имеющего указанный размер. Элементы массива запол- заполняются с помощью квадратных скобок, внутри которых стоят целые числа, начиная с 0, а результаты выводятся на консоль: using System; namespace Wrox.ProfessionalCSharp.Chapter6.QuickArray { class MainEntryPoint { , static unsafe void Main(string!] args) { ,, s Console.Write("How big an array do you want? \n> ") ; string userlnput = Console.ReadLineO,• uint fei'ize -• uint.Parse(userInput); ; long *pArray =, sta'Ckal-lpc long I (int)size] ;
246 Глава 6 for (int 1=0; I<size; I++) pArray[I] = 1*1; * for (int 1=0; I<size; I++) Console.WriteLinet "Element {0;} = {!}",, I, * (pArray+I)) ; В результате выполнения примера получим следующее: tienont 1 Cl&(Mefit Я Elenont,3 Eleneftf -1 *» .*« 't i« &*_*н"т№ 'P... > ь 4 ■ * Заключение В этой главе были рассмотрены: мощный механизм, с помощью которого С# обрабатыва- обрабатывает ошибочные состояния посредством исключений; приведение типов для разных клас- классов и структур; передача методов в качестве параметров с помощью делегатов и уведомление приложений о происходящем с помощью событий. Были представлены два способа управления процессом компиляции кода в С#: с помощью директив препроцес- препроцессора и атрибутов. И в конце было показано, как с помощью указателей можно достичь высокого уровня производительности. Из первых глав мы узнали, что С# является мощным объектно-ориентированным языком, позволяющим писать хорошо структурированный код, который следует самым последним объектно-ориентированным методологиям. В этой главе мы увидели, что С# на самом деле идет дальше, предлагая возможности для решения большого числа важных программистских задач. В следующей главе рассматривается, как С# взаимодействует с базовыми классами, обеспечивая доступ к мощным методам программирования в таких областях, как обработ- обработка строк, пользовательские атрибуты и потоки. Мы также увидим, как с помощью мето- методики отражения в программе С# можно получить доступ к метаданным, описывающим ее собственные объекты, а также к метаданным других приложений.
п ■У NS Л я % а // \> в а С# а базовые классы В этой главе подробно рассматриваются базовые классы и их взаимодействие с языком С#. В частности, обсуждаются следующие темы: О Строки и регулярные выражения О Коллекции О Пользовательские атрибуты О Отражение О Потоки Более подробно будет также изучен System. Obj ect — класс, от которого порождаются все остальные объекты. В одной главе мы охватываем довольно разные темы, однако на это есть свои причины. С самого начала отмечалось, что язык С# нельзя рассматривать в изоляции. Язык тесно взаимодействует как с платформой .NET, так и со связанной с ней библиотекой классов — с базовыми классами .NET. Например, все ключевые слова С#, представляющие такие типы данных, как int, long и string, подставляются компилятором напрямую в соответствую- соответствующие базовые классы (в данном случае — System, j'^1 ~\? , System. Int64 и System. String). Другим примером является то, что в случае исключений выражения throw и catch будут работать, только если объект исключения унаследован от Р/ item.Exception. Взаимодействие С# и базовых классов .NET требует их совместного изучения. Одна- Однако до сих пор акцент делался на те особенности С#, в которых синтаксис языка играет первостепенную роль, а базовые классы рассматривались лишь в качестве помощников. В этой главе будут представлены особенности С#, реализуемые в основном посредством использования определенных базовых классов, а не при помощи синтаксиса С#. System.Object System.Object — это универсальный базовый класс, от которого наследуются все осталь- остальные объекты. Основные его методы были перечислены в главе 4, однако подробно рассмот- рассмотреть мы смогли только методы ToString () и Finalize (). В этой главе рассказывается об остальных методах System.Object. Кроме того, рассматриваются вопросы сравнения объектов на равенство. Для напоминания воспроизведем таблицу, приведенную в главе 4, в которой пере- перечислены методы, доступные в System.Object: Метод Доступ Назначение string ToString() int GetHashCode() public virtual public virtual Возвращает строковое представление объекта. Возвращает хэш объекта для обеспечения эффективного поиска экземпляров объекта в словарях.
248 Глава 7 Метод bool Equals(object obj) bool Equals(object objA, object objB) bool ReferenceEquals(object objA, object objB) Type GetType() Object MemberwiseClone() void Finalize() Доступ public virtual public static public static public protected protected virtual Назначение Сравнивает данный экземпляр объекта с другим экземпляром класса на равенство. Сравнивает экземпляры объекта на равенство. Проверяет, указывают ли две ссылки на один и тот же объект. Возвращает объект, унаследованный от System.Type, который содержит сведения о типе данных. Создает ограниченную копию объекта. (Другими словами, копирует в объект только данные, но не другие объекты, на которые сылаются его поля.) Может использоваться в некоторых ситуациях для освобождения ресурсов. Четыре метода объявлены виртуальными и, следовательно, могут быть перекрыты. Однако во всех случаях практика хорошего программирования накладывает ограничения на реализацию перекрывающих методов. Что касается членов System.Object, то: О ToString () предназначен для быстрого и легкого получения строкового представ- представления и используется в ситуациях, когда требуется получить содержимое объекта, возможно, в целях отладки. Именно так он и должен быть реализован. Если необхо- необходимо более сложное строковое представление, которое, например, учитывает гео- географические местоположение, а также требования клиентского кода по поводу конкретного формата объекта, то нужно реализовать интерфейс Iformattable (см. ниже). Например, даты могут быть выражены с помощью огромного числа форматов — метод DateTime. ToString () вообще не позволяет выбирать форматы. О GetHashCode () применяется, если объекты размещаются в структуре данных, изве- известной как отображение (хэш-таблица или словарь). Член используется классами, ко- которые работают с такими структурами, для определения того, куда в структуре поместить объект. Если класс должен применяться в качестве ключа для словаря, то для него потребуется перекрыть GetHashCode (). Существуют жесткие ограничения на реализацию перегруженного метода (см. ниже). О Equals () (обе версии) и ReferenceEquals (). Между использованием этих трех методов и операции сравнения (==) существует едва заметная разница. На перекрытие виртуального метода Equals () с одним параметром накладывается ряд ограничений, так как некоторые базовые классы в пространстве имен System.Collections вы- вызывают этот метод и ожидают, что он будет вести себя определенным образом. О Finalized исполняет роль деструктора и вызывается тогда, когда ссылочный объект захватывается сборщиком мусора для освобождения ресурсов (см. главу 4). Его следует перекрывать только тогда, когда это действительно необходимо, на- например, если объект использует внешние ресурсы, такие как файлы или соедине- соединения с базами данных. При перекрытии этого метода было бы неплохо реализовать также метод Close () или Dispose (). Отметим, что типы по значению не обрабатываются сборщиком мусора, поэтому перекрывать для них Finalize О бессмысленно. О GetType() возвращает экземпляр класса, порожденного от System.Type. Этот объект может предоставить обширную информацию о классе, членом которого является ваш объект, включая базовый тип, методы, свойства и т.п. System.туре является также точкой входа для технологии отражения .NET (см. ниже). О MemberwiseClone (). Это единственный метод System.Object, который не будет подробно рассматриваться в книге — в этом нет необходимости, поскольку его концепция очень проста. Он создает копию объекта и возвращает ссылку (в слу- случае типа по значению упакованную ссылку) на копию. Отметим, что при этом по- получается ограниченная копия, т.е. копируются только типы по значению класса. Если класс содержит внутри ссылки, то скопированы будут только эти ссылки, но не объекты, на которые они ссылаются.
С# и базовые классы 249 Сравнение объектов по ссылке на равенство Одной из особенностей System.Object, которая может на первый взгляд показаться не- необычной, является то, что он определяет три различных способа для сравнения объек- объектов на равенство. Если прибавить к этому операцию сравнения, то мы получим уже четыре способа. Зачем это нужно? На самом деле между этими методами существуют едва заметные различия. Начнем знакомство с ними со сравнения типов по ссылке. ReferenceEqualsO Метод ReferenceEquals () проверяет, ссылаются ли две ссылки на один и тот же экзем- экземпляр класса, иными словами, содержат ли ссылки один и тот же адрес памяти. Так как это статический метод, перекрыть его невозможно, поэтому его реализация в Sys- System. Object является единственно доступной. Метод ReferenceEquals () всегда возвращает true, если ему переданы две ссылки на один и тот же экземпляр объекта, и false в противном случае. Однако он считает, что null равен null: p X, У,-- .. .„'■■' X » nfew.-SomeClassO ; У *=" new SomeClass (); t>ool -В1 - ReferenceEquals (null, null).; // возвращает true booi B2 = ReferenceEquais (null*, X);~ ..-,'* It возвращает false booZ B3. = ReferenceEquals(X, Y); // возвращает false, так как Х и У ■" _ i.-^W II указдеа1йг 'На различные объекты Виртуальный метод EqualsO Виртуальная версия EqualsO для конкретного экземпляра может рассматриваться как прямая противоположность ReferenceEqualsO. Хотя реализация EqualsO в Sys- System. Object сравнивает ссылки, этот метод предназначен для случая, когда требуется пе- перекрыть его для сравнения значений экземпляров объекта. В частности, если нужно использовать экземпляры класса как ключи в словаре, то придется перекрыть этот метод для сравнения значений. В противном случае в зависимости от того, как будет перекрыт метод GetHashCode (), класс словаря, содержащий этот метод, либо вообще не будет ра- работать, либо будет работать неэффективно. Перекрытый метод ни в коем случае не дол- должен генерировать исключения. Дело в том, что это может вызвать проблемы для классов словарей и возможно даже для некоторых других базовых классов .NET, которые внутри себя вызывают этот метод. Вариант перекрытия этого метода будет показан ниже в примере MortimerPhonesEmployees. Статический метод EqualsO Статическая версия Equals () делает практически то же самое, что и виртуальная. Разни- Разница в том, что статическая версия метода способна работать в ситуации, когда один из объектов представляет собой null, и, следовательно, обеспечивает дополнительный уро- уровень безопасности на случай возникновения исключений. Статический метод прежде всего проверяет, равны ли null переданные ему ссылки. Если обе ссылки равны null, метод возвращает true (так как null считается равным null). Если null равна только одна из них, метод возвращает false. Если же обе ссылки на что-то ссылаются, вызывается виртуаль- виртуальная версия Equals (). Это означает, что при перекрытии виртуальной версии Equals () эффект будет таким же, как если бы дополнительно перекрывалась статическая версия метода. Операция сравнения (==) Операцию сравнения следует рассматривать как промежуточную версию между строгим сравнением по значению и строгим сравнением по ссылке. Предполагается, что в боль- большинстве случаев запись: bool'В - (Х=-У); // X и Y объекты должна означать, что сравниваются ссылки. Однако допускается, что могут существовать классы, смысл которых становится понятнее, если рассматривать их значения. В этом случае лучше перекрыть операцию сравнения для выполнения сравнения по значению. Очевидный пример — строки. Для них Microsoft уже перекрыла этот оператор, посколь- поскольку, сравнивая строки, разработчик думает, скорее всего, о сравнении их содержимого, а не указывающих на него ссылок.
250 Глава 7 Теперь должно быть ясно, почему компилятор выдает предупреждение, если опера- операция сравнения перегружена, а метод Equals () — нет. Так как Equals () предназначен для сравнения по значению, а == служит для сравнения по ссылке, было бы странно, если бы мы перегрузили ==, но оставили бы Equals () выполнять сравнение ссылок. Кстати, не пытайтесь перегрузить операцию сравнения путем вызова версии Equals О с одним параметром. Если вы сделаете это, а где-то в коде будет предпринята попытка вычисления (objA == objB), причем objA окажется равным null, то будет немедленно сгенерировано исключение, поскольку среда исполнения .NET попытается вычислить null. Equals (objB)! Использование противоположного подхода - перегрузки Equals () для вызова операции сравнения - должно быть безопасным. Сравнение на равенство типов по значению При сравнении на равенство типов по значению действуют те же самые принципы, что и для типов по ссылке: Ref erenceEquals () используется для сравнения ссылок, Equals () предназначен для сравнения значений, а операция сравнения рассматривается как проме- промежуточный случай. Однако важным отличием является то, что типы по значению должны быть упакованы для преобразования их в ссылки, и то, что Microsoft уже перегрузила метод экземпляра Equals () в классе System.ValueType для того, чтобы он осуществлял более подходящее для типов по значению сравнение. Если вызвать sA. Equals (sB), где sA и sB яв- являются экземплярами некоторой структуры, то будет возвращаться true или false в зави- зависимости от того, содержат ли sA и sB одинаковые данные во всех своих полях. С другой стороны, по умолчанию перегруженная версия == для ваших собственных структур недо- недоступна. Запись (sA == sB) в любом выражении приведет к ошибке компиляции, если только для рассматриваемой структуры в коде не была определена перегруженная операция ==. Другим моментом является то, что Ref erenceEquals () будет всегда возвращать false для типов по значению, так как для того, чтобы вызвать этот метод, типы по значению необходимо будет упаковать в объекты. Даже если записать: bool В = ReferenceEquals(V, V); II V является переменной некоторого типа // по значению то в результате все равно будет получено false, поскольку V будут отдельно упакованы при преобразовании каждого параметра и в результате будут получены разные ссылки! Вызов Ref erenceEquals () для сравнения типов по значению не имеет смысла. Хотя перегруженный по умолчанию метод Equals (), предоставляемый System. Value- Type, подходит для большинства определяемых структур, возможно, вы пожелаете снова перекрыть его для своих собственных структур с целью увеличения производительности. Кроме того, если тип по значению содержит в качестве полей ссылочные типы, то может понадобиться перекрыть Equals () для обеспечения соответствующей семантики этих полей, поскольку метод Equals () по умолчанию будет лишь сравнивать их адреса. Работа со строками Ключевое слово С# string на самом деле ссылается на базовый класс .NET Sys- System. String. System. String является мощным и надежным классом, однако это не един- единственный класс в арсенале .NET, связанный со строками. Мы рассмотрим свойства System.String, а также ряд интересных возможностей, которые связаны со строками, но доступны при использовании других классов .NET, в частности, из пространств имен System.Text и System.Text .RegularExpressions. Мы обсудим следующие темы: О Создание строк. Для выполнения повторяющихся изменений строки, например, с целью получения длинной строки перед ее отображением или передачей в другой метод или программное обеспечение, класс string неэффективен. В этой ситуации больше подходит другой класс, System. Text .StringBuilder. □ Выражения форматирования. Мы подробно рассмотрим выражения форматирова- форматирования, которые использовались в методе Console. WriteLine () на протяжении послед- последних нескольких глав. Оказывается, что выражения форматирования обрабатываются с помощью двух простых интерфейсов — IFormatProvider и IFormattable, и что реализация этих интерфейсов для собственных классов позволит определять свои последовательности форматирования, в результате чего Console.WriteLine О и аналогичные классы будут отображать значения ваших классов так, как вы укажете.
С# и базовые классы 251 О Регулярные выражения. .NET содержит довольно сложные классы, которые обрабатывают ситуации, связанные с поиском или извлечением из длинной строки подстрок, удовлетворяющих определенному сложному критерию. Под "сложными" в данном случае понимаются ситуации, когда требуется найти все позиции в строке, в которых повторяется символ или последовательность сим- символов, или когда необходимо найти все слова, начинающиеся с "s" и содержащие по крайней мере одну "п". Соответствующие методы можно написать с помощью строкового класса, но они оказываются громоздкими. Лучше использовать классы из System.Text.RegularExpressions, специально созданные для такого типа обработки. System.String Прежде всего познакомимся с некоторыми из доступных методов в классе String. System.String представляет собой класс, специально предназначенный для хране- хранения строки и выполнения над ней различных действий. Этот тип данных довольно ва- важен, поэтому С# имеет для него собственное ключевое слово и связанный с ним синтаксис, упрощающий работу со строками. Например, можно объединять строки с помощью перегруженных операций: string, Message = "Привет"; Message *=.'" -, мир",-,, String Messaged = Messagel + ■!"; Можно извлечь конкретный символ строки с помощью синтаксиса, подобного индек- индексаторам: char ess. ?= '.Message [2 ] ; Имеется большое число методов для выполнения общих задач, таких как замена сим- символов, удаление пробелов, преобразование строчных букв в прописные. К этим методам относятся: Метод Compare CompareOrdinal Format IndexOf IndexOfAny LastlndexOf LastIndexOfAny PadLeft PadRight Replace Split Substring ToLower ToUpper Trim Назначение Сравнивает содержимое строк, принимая во внимание региональные настройки при сравнении определенных символов. То же, что и Compare, но не учитывает региональные настройки. Форматирует строку, содержащую различные значения, и указывает, как должна быть отформатирована каждая величина. Определяет первое вхождение данной подстроки или символа в строке. Определяет первое вхождение любого из набора заданных символов в строке. То же, что и IndexOf, только ищет последнее вхождение. То же, что и IndexOfAny, только ищет последнее вхождение. Выравнивает строку, добавляя несколько указанных символов в начало строки. Выравнивает строку, добавляя несколько указанных символов в конец строки. Заменяет вхождения данного символа или подстроки в строке другим символом или подстрокой. Разделяет строку на массив строк, используя заданный символ для определения границ подстрок. Получает подстроку, начинающуюся с указанной позиции в строке. Преобразует все символы строки в строчные. Преобразует все символы строки в прописные. Удаляет пробелы в начале и в конце строки.
252 Глава 7 Отметим, что эта таблица не является полной. Она приведена для того, чтобы вы получили представление о возможностях, предлагаемых строками. Создание строк String — чрезвычайно мощный класс, реализующий множество полезных методов. Од- Однако string имеет один недостаток, который делает этот класс неэффективным для осу- осуществления повторяющихся изменений строки: это на самом деле неизменяемый тип данных, т.е. после инициализации объекта string он больше не может изменяться. Ме- Методы и операторы, которые изменяют содержимое строки, в действительности создают но- новые строки, копируя в них содержимое старых строк в случае необходимости. Например, приведем код: String GreetingText = "Hello from all the people at Wrox Press. " ; GreetingText += "We do hope you enjoy this book as much as we enjoyed writing it"; При исполнении этого кода происходит следующее. Во-первых, создается объект типа System. String, который инициализируется текстом "Hello from all the people at Wrox Press. ". При этом среда исполнения .NET выделяет ровно столько памяти, сколько необ- необходимо для хранения строки C9 символов), а переменная GreetingText будет ссылаться на этот экземпляр строки. Следующая строка кода синтаксически выглядит так, будто мы добавляем к строке текст. Однако на самом деле создается новый экземпляр строки, под который выделяется ровно столько памяти, сколько требуется для объединенного текста — в сумме 103 симво- символа. Первоначальный текст "Hello from all the people at Wrox Press. " копируется в новую строку вместе с дополнительным текстом "We do hope you enjoy this book as much as we en- enjoyed writing it". После этого обновляется адрес, хранящийся в переменной Greeting- Text, и переменная теперь будет указывать на новый строковый объект. С этого момента старый строковый объект выходит из области видимости — ни одна переменная на него не ссылается, поэтому он будет удален при очередном запуске сборщика мусора. Само по себе это выглядит безобидно, но допустим, что требуется закодировать стро- строку, заменив каждую букву (кроме знаков пунктуации) символом ASCII с кодом на единицу больше, чем текущий. В результате строка превратится в нечитаемую последователь- последовательность символов. Существует несколько способов решения этой задачи, но наиболее про- простой и, в случае, если необходимо использовать только класс String, наиболее эффективный способ — использовать метод String. Replace О, который заменяет все вхождения заданной подстроки в строке другой подстрокой. При использовании Replace () код будет выглядеть так: String." GreetingText = "Hello from all the people at Wrox Press. " ; GreetingText += "We. do hope you enjoy this book as much as we enjoyed writing it"; fpr- (int I •*=. (int)'г1; I>=(intIa1; I-) ^ char Old = (char)I- char New, = (char) A+1) ; GreetingText ч= GreetingText.Replace(Old, New); } for (int i '= (inti'Z1; I>=4(int)'A-; J-) char Old = (chair) I; char New = (char) A+1) ; GreetingText = GreetingText.Replace(Old, New) ; ) Console. WriteLine("Encoded:\n * + GreetingText); Для простоты этот код не меняет 'Z' на 'А' или V на 'а1. Эти буквы заменяются соот- соответственно символами '[' и '{'• Как вы думаете, сколько памяти необходимо выделить в сумме для того, чтобы произве- произвести это кодирование? Replace () работает очень аккуратно в том смысле, что этот метод не станет создавать новую строку, если только действительно не произведет изменений в ста- старой строке. Первоначальная строка содержит 23 различных строчных символа и 2 пропис- прописных. Следовательно, Replace () выделит память для новой строки 25 раз, каждый раз по 103 символа. Это означает, что по завершении процесса кодирования в куче, ожидая сбора мусора, будут находиться строковые объекты, способные в сумме хранить 2575 символов! Очевидно, что при интенсивном использовании строк для обработки текстов ваше прило- приложение очень скоро столкнется с серьезными проблемами по части производительности .
С# и базовые классы 253 Для решения этой проблемы Microsoft создала класс System.Text.StringBuilder. Действия, которые можно выполнять с помощью StringBuilder, ограничены заменой, добавлением и удалением текста. Однако он работает более эффективно. В то время как при создании String выделяется ровно столько памяти, сколько требу- требуется для хранения строки, при создании StringBuilder обычно выделяется больше па- памяти. Можно явно указать объем памяти, который необходимо выделить. Если же не сделать этого, то в зависимости от размера строки, переданной при инициализации StringBuilder, будет отведено определенное количество памяти по умолчанию. В классе присутствуют два свойства: Length, которое содержит длину хранящейся строки, и Capa- Capacity, которое показывает, сколько памяти было выделено в целом. Все изменения строки происходят в этом блоке памяти, т.е. добавление строк и замена символов выполняются эффективно. Однако удаление и вставка подстрок по-прежнему являются неэффективны- неэффективными операциями, поскольку при этом часть строки должна быть перемещена. Только в том случае, если при выполнении той или иной операции превышается объем отведенной под строку памяти, выделяется новый участок памяти и в него перемещается строка. На момент написания книги Microsoft не документировала, насколько увеличивается объем буфера, но из экспериментов следует, что StringBuilder примерно удваивает свой объем, если обнаруживает, что старого объема уже недостаточно, а новое его значение не было установлено явно. Используя StringBuilder для создания строки приветствия можно написать код: StringBuiider GreetingBuilder = new StringBuilder ("Hello from all the people at Wrox Press. ", 150); GreetirigBuilder.AppendC'We do hope you enjoy this book as much as we enjoyed writing it"); В этом коде начальная емкость StringBuilder установлена в 150. Всегда лучше ука- указать емкость, которая несколько больше максимально возможной длины строки, для га- гарантии того, что StringBuilder не придется перераспределять память в случае ее исчерпания. Теоретически в качестве емкости можно указать любое число типа int, од- однако если вы попытаетесь выделить максимум — 2 миллиарда символов — скорее всего система пожалуется на отсутствие такого объема памяти! При исполнении приведенного кода сначала создается объект StringBuilder: - < Hello from all the people at Wrox Press. <не инициализировано - 39 символов ►-« 111 символов- После вызова метода Append () остальной текст помещается в пустое пространство, при этом выделять новую память не требуется. Однако реальные выгоды от использова- использования StringBuilder проявляются при выполнении повторных замен текста. Например, если попытаться закодировать текст (см. выше), то весь процесс кодирования можно произвести без выделения дополнительной памяти: StringBuilder GreetingBuilder = new StringBuilder ("Hello from all the people at Wrox Press. ", 150); GreetingBuilder. Append ("We do hope you enjoy this book as much as we enjoyed writing ic"); for (int I = (int)'z1; I>=(int)'a'; I-) .;''Char Old. = (char) I; «■ char New, = (char) A+1) ; - GjreeXmgBuilder = GreetingBuilder.Replace (Old, Mew); .}- , ■; tor Jlnt I = (int)'Z1; I>=(int)'A'; I-) char Old = (char)I; char -New = ,|char) A+1) ; " GreetingBuilder =. GreetingBuilder.Replace(Old, New); } Console..WriteLine (."Encoded: \n " + GreetingBul lder. ToString ()) ; Этот код использует метод StringBuilder. Replace (), который делает то же самое, что и String. Replace (), но без копирования строки. Общий объем памяти, выделяемый в данном случае для хранения строк, составляет 200 для StringBuilder и для строковых операций, выполняемых в заключительном операторе Console.WriteLine().
254 • Глава 7 Обычно для манипуляции со строками используется StringBuilder, а для хранения и вывода на экран окончательного результата — string. Члены StringBuilder Выше продемонстрирован только один конструктор StringBuilder, принимающий начальные строку и объем в качестве параметров. Существует также несколько других. В частности, можно передать только строку: StringBuilder sb = new StringBuilder("Привет"); Или создать пустой StringBuilder заданной емкости: StringBuilder sb = new StringBuilderB0); Помимо упомянутых свойств Length и Capacity, имеется также свойство только для чтения MaxCapacity, которое показывает, до какого предела может расти данный экземп- экземпляр StringBuilder. По умолчанию он определяется int.MaxValue (примерно 2 млрд.), однако это значение можно указать и меньшим при конструировании объекта: // Обе строки устанавливают -Начальную емкость б 100, а максимум равен 500. // Поэтому объем обоих StringBu-1 Lder никогда не превысит 500 символов. // При попытке сделать это будет сгенерировано искпючение. StringBuilder sb = new SningBu' Jer ("Призе г", 100, 500); StringBuilder sb = new StiingBuilderA00, 500); Объем StringBuilder можно в любое время указать явно, хотя в том случае, если он будет установлен в значение, меньшее текущей длины строки или превышающее макси- максимальную емкость, будет сгенерировано исключение: StringBuilder Lsb = new StringBuilder("Привет"); sb.Capacity = 100; Перечислим основные методы StringBuilder: Имя Назначение Append Добавляет строку к текущей строке. AppendFormat ' Добавляет строку, полученную при помощи спецификатора формата. Insert () Вставляет подстроку в текущую строку. Remove () Удаляет символы из текущей строки. Replace \) Заменяет в текущей строке все вхождения символа или подстроки соответственно другим символом или подстрокой. ToString () Возвращает текущую строку, приведенную к объекту System. St ring (перекрыт метод System.Object). Для многих из этих методов существует несколько перегруженных вариантов. Метод AppendFormat <) является тем самым методом, который вызывается при вызове Console.WriteLine(). Он отвечает за замену выражений форматирования типа iC:D} тем, что должно содержаться вместо них (см. ниже). На момент написания книги не существовало приведения (явного или неявного) StringBuilder к string. Если необходимо вывести содержимое StringBuilder как string, единственный способ сделать это - использовать метод ToStringO. Форматирование строк Выше приводились примеры классов и структур, для которых был реализован метод ToString () с целью быстрого отображения содержимого переменной. Однако может существовать несколько различных способов отображения переменной, которые часто зависят от конкретной местности. Наиболее очевидным примером является базовый класс .NET System. DateTime. Одну и ту же дату можно записать множеством способов: 14 February 2001, 14 Feb 2001, 2/14/01 в США; в Великобритании это было бы 14/2/01; в Германии — 14 Feb ruar 2001 и т.д.
С# и базовые классы 255 Аналогично, для структуры Vector (см. главу 5) бьи реализован метод Vector. ToString (), который отображает вектор в формате D, 56, 8). Однако существует еще один способ за- записи векторов: тот же самый вектор может быть представлен как 4i+56j+8k. Если ваши классы должны быть дружественными по отношению к пользователю, то они обязаны поддерживать возможность отображения строковых представлений в любом формате, который потребуется пользователю. Среда исполнения .NET определяет стандартный способ, с помощью которого это следует делать: применение интерфейса IFormattable. Ниже будет показано, как добавить его к вашим классам и структурам. Наиболее очевидным моментом, когда необходимо указывать формат для отображе- отображения переменной, является вызов Console. WriteLine (). Мы будем использовать этот ме- метод в качестве примера, хотя большинство рассуждений подходит и для других ситуаций, в которых требуется форматировать строку. Если, например, нужно отобразить значение переменной в списке или текстовом окне, то для получения подходящего строкового представления переменной, как правило, будет использоваться метод St с i.rg. Format (), а спецификаторы формата в этом случае являются такими же, как и передаваемые в Console. WriteLine (). Кроме того, действует один и тот же внутренний механизм. Нач- Начнем с рассмотрения того, что происходит при указании строки форматирования для при- примитивного типа, а затем выясним, как можно указать свои собственные спецификаторы формата для пользовательских классов и структур. Напомним, что форматирование строк в Console.Write() и Console. WriteLine () производится следующим образом (см. главу 3): double D = 13-. 45; Int I = 45; Console.WriteLine("The double is {0:10:E} and the i-" conta -is {I D, I) ; Строка форматирования состоит преимущественно из текста, который необходимо отобразить, а в том месте, где должна располагаться отформатированная переменная, в скобках помещается индекс, показывающий ее положение в списке параметров. В скобках может находиться и другая информация, касающаяся формата элемента: О Число позиций, которые может занимать представление элемента. Эта информация отделяется запятой. Отрицательное число показывает, что элемент должен быть выровнен по левому краю, а положительное — по правому. Если элемент занимает большее число позиций, чем было указано, он все равно отображается полностью. О Может быть указан также спецификатор формата. Он отделяется запятой и по- показывает, как должен быть отформатирован элемент — например, требуется ли отобразить число в виде валюты или вывести его в научной нотации? Общие спецификаторы формата для числовых типов: Спецификатор С D Е F G N Р X Применяется к Числовые типы Только целые типы Числовые типы Числовые типы Числовые типы Числовые типы Числовые типы Только целочис- целочисленные типы Значение Местное денежное значение Общий целочисленный формат Научная запись Десятичное число с фиксированной точкой Общий целочислен- целочисленный формат Местный числовой формат Процентная запись Шестнадцатеричный формат Пример $4834.50 (США) £4834.50 (Великобритания) 4834 4.834Е+003 4384.50 4384.5 4,384.50 (Великобритания/США) 4 384,50 (континентальная Европа) 432,000.00% 1120 (Примечание: если необходимо отобразить 0x1120, то <0х> необходимо вывести отдельно.) Зак. 69
256 Глава 7 Если требуется, чтобы целое число было дополнено нулями, можно использовать спецификатор формата 0 (нуль), повторив его заданное число раз. Например, специфика- спецификатор формата 0000 приведет к тому, что 3 будет показано как 0003, а 99 — как 0099, и т.д. Невозможно привести полный список спецификаторов, поскольку другие типы дан- данных могут добавлять свои собственные спецификаторы. Здесь главной задачей является показать, как определять собственные спецификаторы для своих классов и структур. Форматирование строки В качестве примера того, что происходит при форматировании строк, посмотрим, как выполняется оператор: Console. WriteLinef "The double is @,10:E) and the int contains {1}", D, I); На самом деле Console.WriteLine О передает весь набор параметров непосредст- непосредственно в статический метод String.Format () — тот же метод, который служит для фор матирования значений в строках, используемых иным образом, например, выводимых в текстовую область. Мы не имеем возможности посмотреть исходный код метода, но реа- реализация перегруженного метода WriteLinef), принимающего три параметра, делает примерно следующее: II Вероятная реализация Console.WriteLineО public void WriteLine(string format, object argO, object argl) { WriteLine(string.Format(format, argO, argl)); } Вызываемая выше версия метода, принимающая один параметр, просто выводит содержимое переданной ей строки, никак не форматируя ее дополнительно. String.Format () должен сконструировать окончательную строку, заменив каждый спецификатор формата подходящим строковым представлением соответствующего объ- объекта. Однако, как было отмечено ранее, эта ситуация требует использования экземпляра Str ingBuilder, а не string, что на самом деле и происходит. Для данного примера будет создан экземпляр StringBuil^ эг, который будет инициализирован первой известной ча- частью строки — текстом "The double is ". После этого будет вызван метод StringBuil- der.AppendFormat (), в который будет передан первый спецификатор формата, ■'{0,10:Е}",и связанный с ним объект double для того, чтобы добавить строковое пред- представление этого объекта к формируемой строке, и этот процесс будет продолжаться че- чередующимися вызовами StringBuilder.Append!) и StringBuilder.AppendFormat() до тех пор, пока не будет получена вся строка. Теперь StringBuilder .AppendFormat () должен выяснить, как отформатировать объ- объект. Первое, что он сделает,— проверит, реализует ли объект интерфейс iFormattable из пространства имен System. Это можно сделать, попытавшись привести объект к это- этому интерфейсу и проверив успешность приведения, или можно использовать ключевое слово С# is. Если эта проверка окончится неудачей, AppendFormat () вызовет метод объекта ToString (). Все объекты либо наследуют этот метод от System.Object, либо перекрывают его. В примерах, рассмотренных выше, именно это и происходит, поско- поскольку ни один из определенных нами классов или структур не поддерживает этот интер- интерфейс. Вот почему для того, чтобы такие структуры и классы, как Vector, Customer, Nevermore60Customer, отображались с помощью метода Console. WriteLine (), необ- необходимо перекрывать метод Object.ToStringO . Однако все предопределенные примитивные числовые типы реализуют этот интер- интерфейс. Следовательно, для этих типов, в частности для double и int из нашего примера, метод ToString (), унаследованный от System.Object, не будет вызван. Чтобы понять, что же происходит вместо этого, рассмотрим интерфейс IFormattable. IFormattable определяет всего один метод, который также называется ToString (). Однако этот метод в противоположность версии System.Object, не принимающей параметров, ожидает два параметра. Определение Iformattible таково: interface IFormattable { string ToString(string format, IFormatProvider formatProvider); 5 Первый параметр, ожидаемый перегруженной версией ToString (, ),— строка, ука- указывающая запрашиваемый формат. Другими словами, это часть спецификатора, кото- которая располагается в {} в строке, первоначально переданной в Console. WriteLine() или String.Format (). Например, сначала оператор в нашем примере был таким:
С# и базовые классы 257 Console. WriteLine( "The double is {O,1C:E} and the int contains {1}", D, I); Следовательно, при вычислении первого спецификатора, {0,10:Е}, перегруженный метод будет вызван для переменной double D, а первым переданным ему параметром бу- будет "Е". Здесь StringBuilder. AppendFormat О будет всегда передавать тот текст, кото- который следует после двоеточия в соответствующем спецификаторе формата из начальной строки. Второй параметр ToString () — это ссылка на объект, который реализует интерфейс IFormatProvider. Этот интерфейс предоставляет дополнительную информацию, кото- которая может потребоваться ToString () при форматировании объекта, включая сведения о географическом положении (напомним, что географическое положение .NET анало- аналогично региональным настройкам Windows — если форматируются даты и валюты, необ- необходима эта информация). Такой объект может понадобиться при вызове перегруженной версии ToString () непосредственно из исходного кода. Однако StringBuilder .Append- .AppendFormat О передает в качестве этого параметра null. Если formatProvider равен null, то ожидается, что ToString () будет использовать географическое положение, указанное в системных настройках. В примере первый элемент, который необходимо отформатировать,— double, для него запрашивается экспоненциальная запись при помощи спецификатора формата "Е". Метод StringBuilder .AppendFormat () определит, что double поддерживает интер- интерфейс IFormattable, и, следовательно, вызовет перегруженный метод ToStr ing () с дву- двумя параметрами, передавая "Е" в качестве первого параметра и null в качестве второго. Теперь уже от реализации этого метода для double зависит, как будет сформировано строковое представление double с учегом требуемого формата и текущего географического положения. Затем при необходимости StringBuilder .AppendFormat () добавит в возвращен- возвращенную строку пробелы для того, чтобы заполнить 10 позиций, указанных в данном случае в строке форматирования. Следующий объект, который необходимо отформатировать, это int. Для него не за- запрашивается какой-то конкретный формат (спецификатор формата {1}). Если формат не указан, StringBuilder .AppendFormat () передаст в качестве строки форматирования ссылку null. Соответственно снова ожидается, что перегруженный метод int. ToString () с двумя параметрами поведет себя должным образом. Так как формат не был указан явно, скорее всего, будет вызван непараметризированный метод ToStringO. Весь процесс можно представить на рисунке: Console.WriteLinefThe double is @,10:E}and the int contains {1}", D, I) StringFormat("The double is {0,10:E}and the int contains {1}", D, i) StringBuilder ("The double is") StringBuilder.AppendFormat П0,10:ЕГ, D) StringBuilder.Append (" and the int contains * StringBuilder.AppendFormat (", D)
258 Глава 7 Пример FormatfableVector Теперь, когда мы выяснили, как конструируются строки форматирования, расширим пример Vector (см. главу 5), чтобы можно было форматировать векторы разнообразны- разнообразными способами. Исходный код для этого примера носит название FormattableVector. Знание изученных принципов упрощает программирование. Все, что необходимо,— реа- реализовать интерфейс IFormattable и перегруженный метод ToString (), определенный этим интерфейсом. Спецификаторы формата, которые будут поддерживаться, таковы: □ N должен интерпретироваться как запрос значения, известного как норма векто- вектора. Это сумма квадратов его компонентов, которая для математиков представляет собой квадрат длины вектора и обычно отображается так: II 34.5 II. О VE должен интерпретироваться как запрос на отображение каждого компонента в научном формате точно так же, как в случае спецификатора Е для double: B.3Е+01, 4.5Е)-02, 1.0Е+00). о UK должен интерпретироваться как запрос на отображение вектора в форме 23i + 450j ^ Ik. О Все остальное должно возвращать представление Vector по умолчанию: B3, 450, 1). Для упрощения примера мы не будем реализовывать возможность отображения век- вектора совместно в формате IJK и в научном формате. Однако будем считать специфика- гор формата нечувствительным к регистру, позволяя запись ijk вместо IJK. Отметим, что для указания спецификаторов можно использовать любые строки. Для начала изменим объявление Vector с целью реализации IFormattable: struct Vector : IFormattabxe { public :\ rn - x, y, z; Теперь добавим реализацию перегруженного метода ToString () с двумя параметрами: public string ToString(string format, IFormatProvider formatProvider) { if format =-' null) returr. ToString (); string format"pper s format.ToUpper(); switch {forma "pperi { case "N": return "|| " + "SormO .ToStringO + " I I" ; case "VE": return String.FormatC( {0:E}, Q:E}, {2:E} )", x, y, z) ; case "UK": StringBuilder sb = new StrirgBuilder(x.ToStringl), 30); sb. Append (" -. + "); sb.Appir.d y.ToStringO ) ; sb.Append(" j + "); sb.Append(y.ToStringO ) ,- sb.Appendl" k") ; retur: sb. ToString (', ; default: return ToStringi)■ Это все, что нужно сделать! Отметим, что перед вызовом любых методов с парамет- параметром format он проверяется на равенство null — в этом методе не должно возникать иск- исключений! Спецификаторы форматов для примитивных типов не чувствительны к регистру, поэтому- другие разработчики ожидают от ваших классов такого же поведения. При использовании спецификатора формата VE требуется, чтобы каждый компонент был отформатирован в научной записи, поэтому снова применяется String.Format (). Поля х, у и z имеют тип double. Для случая IJK в строку необходимо добавить несколь- несколько подстрок, поэтому для улучшения производительности используется StringBuilder. Для полноты представления приведем непараметризированный метод ToString (), разработанный в главе 5: pnb'ic override ♦ i Lg roStr;ng()
С# и базовые классы 259 return Наконец, необходимо добавить метод Norm (), он не был приведен п первом опреде- определении структуры Vector в главе 5: public double Norm О ■}-' return х*х + у*у + z*z,' Теперь можно протестировать возможности форматирования Vector при помощи кода: Static, void Ma,in,(str:Uig>n arg:s) ( ' - Vector VI = vnew VectorA, '32, 5); Vector V2 =; hew Vector(845-4, 54.3, -7.8k; " Console-.WriEeLinepXnln xjk formatj\nVl is {P, 30:IJK}\nV2 is tl-> 30:UK}", " " " VI, V2); Console. WriteLine (" Xriln default format, \nVl is {0, 30}\nV2 is {1, 30}", VI, V2); Console'.WriteLineC^ifiln VE format,\nVl is {0, 30:VE.}AnV2 is {1, 30:VE}", -: VI* V2); Console. WriteLin1e("\nNorms are:\nvl is {0, 20:N)\nV2 is {I/ 20:N}", 4 ,V1, V2) ; Результат выполнения кода таков: ,« t:\Cw.uments and «etbngiVAni.Mn Щ щ iKtes ui is .<>1.ввв№йг«Aвв>э.гевв U2 'iii< JB.4S*МВД«Ы2, 5.43И0 <k\ ?*■ -i-f> » ..., ,.tj<«. ,•..■■ «topns дга: n, ■-..■•-'.■ ','J| U2 is „J^M^e.^i'- 8вЕ«ИИ1/. -I'ibi'M <•:* ra rj." '■■• l;.4V-.r, » ,t:,.4 ' •' ' : I , v /' .- - .. J ■ ' -. '-V*ti* Таким образом, наши спецификаторы формата были обработаны корректно. Регулярные выражения Регулярные выражения формируют одну из тех небольших технологических областей, ко- которые чрезвычайно полезны для широкого диапазона программ. Однако эта технология малоизвестна среди разработчиков. Ее можно рассматривать практически как миниязык программирования, который имеет одну цель: поиск строк в большом строковом выраже- выражении. Эта технология ненова — она вышла из среды UNIX и широко применяется в Perl. Microsoft портировала технологию на платформу Windows, где до сих пор она преимуще- преимущественно использовалась в языках сценариев. Регулярные выражения поддерживаются некоторыми классами .NET из пространства имен System. Тех^. RegularExpress ' ons. В этом разделе даются базовые сведения о регулярных выражениях и связанных с ними классах .NET. Если регулярные выражения вам уже хорошо знакомы, пролистай- пролистайте этот раздел и посмотрите ссылки на базовые классы .NET. Возможно, вы заметите, что механизм регулярных выражений .NET разработан с целью обеспечения совмести- совместимости с регулярными выражениями Perl 5, однако имеется несколько дополнительных возможностей. Введение в регулярные выражения Сейчас практически все текстовые редакторы предоставляют те или иные возможности поиска. Обычно на экран выводится диалоговое окно, в котором набирается строка для поиска в блоке текста, а если требуется произвести операцию замены, то можно указать также и строку замены. В простейшем случае можно, например, запросить замену всех
260 Глава 7 слов "MS" в документе на слово "Microsoft". Такие задачи легко решаются с помощью клас- класса string, с использованием метода string. Replace (). Но что если необходимо найти все повторяющиеся слова в документе? Написание процедуры, которая будет выбирать повторяющиеся слова из класса string, может быть довольно сложным. Это именно тот случай, когда используется язык регулярных выражений. Язык регулярных выражений позволяет записывать выражения для поиска. Текст для поиска в документе совмещается с esc-последовательностями и другими символами, имею- имеющими специальное назначение. Например, последовательность \Ь означает начало или конец слова (границу слова), поэтому если необходимо указать, что осуществляется поиск слова с символами th в начале, то соответствующее регулярное выражение будет \bth (т.е. последовательность "граница слова - t - h"). Если бы требовалось найти все вхож- вхождения th в конце слова, то мы записали бы th\b (последовательность "t - h - граница слова"). Однако регулярные выражения могут быть более сложными. Они обеспечивают, например, возможность сохранения участков текста, найденных в процессе поиска. В качестве другого примера, предположим, что приложение должно преобразовать телефонные номера Великобритании из национального в международный формат. В Ве- Великобритании национальный формат представляет собой что-то типа 0233 345532, что иногда может быть записано как @233) 345432. В международном формате число дол- должно быть записано как +44 233 345532, т.е. ведущий нуль должен быть заменен на +44, а лишние скобки убраны. Среди возможных операций поиска и замены эта не слишком сложная, однако она потребует определенных усилий по написанию кода, если для ее реализации будет использоваться класс string и, следовательно, понадобятся методы, доступные в System.String: необходимо найти все нули, которые идут в начале числа или сразу за левой скобкой. Язык регулярных выражений позволяет записать для этого короткую строку поиска и замены. Рассмотрим простой пример, принимая во внимание только поиск строк, но не их замену. Пример RegularExpressionsPlayaround В этом коротком примере мы продемонстрируем некоторые особенности регулярных выражений и то, как .NET использует механизм регулярных выражений в С#, осуществляя поиск и отображая результат. Для поиска нам понадобится текст, скажем, такой: string Text = @"XML has made a major impact in almost ■every aspect of software development Designed as an open, extensible, self-describing language, it has become the standard for data and document delivery on the web. The panoply of- XML- related technologies continues to develop at breakneck speed,' to enable 'validation, navigation, transformation, linking, querying, description, and messaging of data."; Отметим, что это корректный код на С# - он хорошо иллюстрирует дословные строки, которые имеют префикс @. В дальнейшем мы оудем называть этот текст входной строкой. Начнем с простого поиска текста, который не требует указания esc-последовательностей и применения команд регулярных выражений. Допустим, что требуется найти все вхождения строки ion. Эту строку мы будем называть шаблоном. Используя регулярные выражения и объ- объявленную выше переменную Text, можно сделать так: string. Pattern = "ion"; MatchCollection Matches = Regex.Matches(Text, Pattern, RegexOptions.); foreach (Match NextMatch in Matches) { Console.WriteLine(NextMatch.Index) ,- } Здесь используется статический метод Matches () класса Regex из пространства имен System.Text. RegularExpressions. Этот метод в качестве параметров принимает некоторый входной текст, шаблон и набор дополнительных флагов из перечисления RegexOptions. В данном случае указано, что поиск должен быть нечувствителен к реги- регистру. Метод Matches () возвращает ссылку на объект MatchCollection. Техническим термином для результатов поиска в выражении по шаблону является термин "совпаде- "совпадение". Оно представлено классом System.Text .RegularExpressions.Match. Мы можем обнаружить любое число совпадений, поэтому возвращаем MatchCollection, содержа- содержащую все совпадения, каждое из которых представлено объектом Match. В приведенном коде осуществляется проход по всей коллекции с помощью свойства Index класса Match, которое возвращает индекс местоположения во входном тексте, где было обнаружено совпадение. При запуске этого кода будет найдено 4 совпадения.
С# и базовые классы 261 Вся мощь регулярных выражений заключается в строке-шаблоне. Помимо текста шаблон может содержать так называемые метасимволы, которые представляют собой специальные символы, передающие команды, а также esc-последовательности, которые работают аналогично esc-последовательностям С#. Эти символы помечаются знаком \. Допустим, что необходимо найти слова, начинающиеся с п. Для этого можно исполь- использовать управляющую последовательность \Ь, которая показывает границу слова (грани- (граница слова — это то место, где алфавитно-цифровой символ предшествует или следует за пробелом или знаком пунктуации). Тогда можно записать: string Pattern = @"\bn"; MatchCollection Matches = Regex.Matches(Text, Pattern, RegexOptions.IgnoreCase I RegexOptions.ExplicitCapture); Обратите внимание на символ @ в начале строки. Нам необходимо, чтобы \Ь было передано в механизм регулярных выражений .NET во время исполнения — мы не хотим, чтобы обратная косая черта была перехвачена компилятором С#, который предполага- предполагает, что это управляющее выражение для него самого. Если требуется найти слова, закан- заканчивающиеся на ion, можно записать так: string Pattern = @"ion\b"; А как найти все слова, которые начинаются с буквы п и заканчиваются ion? (Из при- приведенного текста будет выбрано только одно слово navigation.) Очевидно, что нужен шаблон, начинающийся с \bn и заканчивающийся ion\b. Но что должно быть в середи- середине? Необходимо как-то указать компилятору, что между п и i on может находиться любое число символов, но главное, чтобы это не были пробелы. Корректный шаблон выглядит так: string pattern = @"\bn\S*ion\b"; В регулярных выражениях такие жуткие последовательности символов встречаются по- постоянно, но они подчинены строгой логике. Управляющая последовательность \S означает любой символ, кроме пробела. Знак * иногда называют квантификатором. Он означает, что предшествующий символ может повторяться любое число раз, в том числе и ни разу. По- Последовательность \s* говорит "любое число символов, за исключением пробелов". Та- Таким образом, приведенный шаблон будет совпадать с любым словом, начинающимся на п и заканчивающимся ion. В таблице приведены некоторые основные специальные символы и управляющие последовательности, используемые в регулярных выражениях: Значение л Начало входного текста $ Конец входного текста Любой единичный символ, за исключением перевода каретки (\п) * Предшествующий символ может повторяться 0 и более раз + Предшествующий символ может повторяться 1 и более раз ? Предшествующий символ может повторяться 0 или 1 раз \s Любой пустой символ Пример ~В х$ i.ation ra*t ra+t ra?t \sa Примеры, которые приведут к совпадению В, но только в случае, если ^ он будет являться первым символом в тексте X, но только в случае, если он будет являться последним символом в тексте isation, ization rt, rat, raat, raaat и т.п. rat, raat, raaat и т.п. (Но не rt) Совпадение будет только с rt и rat [пробел]a. \ta. \na (\t и \п имеют те же самые значения, что и в С#)
262 Глава 7 Значение Пример Примеры, которые приведут к совпадению I \S Любой символ, I не являющийся пустым \Ь Граница слова \В Любая позиция не на границе слови \SF aF, therefore, cF но не \tF io*i\b любое слово, закапчивающееся на ion \BX\3 Любое X в середине слова Если требуется выполнить поиск одного из метасимволов, то нужно поставить пе- перед этим метасимволом обратную косую черт). Например, . означает любой символ, за исключением символа перевода каретки, в то время как \ . означает точку. Можно запросить проверку на совпадение с различными символами, заключив их в квадратные скобки. Например, [1 I с | означает один символ, который может представ- представлять собой 1 или с. Для поиска всех вхождений слое тар и man можно применить после- последовательность та~р|л|. В квадратных скобках можно также указать диапазон, например: La-zj для обозначения любого строчного буквенного символа, [А-Е] для обозначения прописных 6v>.b от А до j ] для представления цифры. Если требуется найти целое число (т.е. последовательность символов, содержащую только цифры от О до 9), то можно записать . (знак - указываем что должна быть по крайней мере одна цифра, но их может быть и несколько; совпадениями для этого шаблона будут: 9, 83, 854 и т.п.). Отображение результатов Теперь напишем простой пример Regu?'. Kxpressiui.sPlayaround. Мы укажем неско- несколько регулярных выражений и отобразим результаты, чтобы дать представление о том, как работают регулярные выражения. Центральной частью примера является метод WriteMatches О , который выводит все совпадения из I" '.^ в более летальном формате. Для каждого совпаде- совпадения отображаются: индекс места ei > обнаружения, совпавшая строка и более длинная строка, состоящая из найденного совпадения и нескольких символов до и после него в тексте — до 5 символов в начале и кон- е i не > менее 5 символов, если совпадение прои- произошло в первых пяти позициях в начале или в конце исходного текста). Другими слова- словами, совпадение со словом messaging в конце текста даст " =md messaging of d" — пять символов до и после совпадения, а совпадение с последним словом data покажет строку "g of data." — только один символ после совпавшего слова, так как на нем строка за- заканчивается. Более длинная строка покажет нам, где регулярные выражения обнаружили совпадение: static void WriteMatchep,spring Text, Mi tchCollection Matches) Console.WriteLinef"Original text was: Console.WriteLine("No. of matches: " foreach (Match NextMatch in Mai rhes) run" + Text + "\n"); - Matches.Count); int Index = NextMatch.Tr string Result ■= NextMatch.ToStringO ; int CharsBefore = (Index < 5) ? Index : 5; int FromEnd = Text.Length - Index - Result.Length; int CharsAfter = (FromEnd <5) ? FromEnd : 5; int CharsToDisplay = CharsBefore + CharsAfter + Result.Length; Console.WriteLineC1 Index: @), \tString: A), \t{2}". Index, Result, ■ Text.Substring(Zndex-CharsBefore, CharsToDisplay)); Большая часть работы в этом методе заключается в определении того, сколько сим- символов в более длинной подстроке можно отобразить, не выходя за границы исходного текста. Отметим, что здесь используется еще одно свойство объекта Match — Value, со- содержащее совпавшую строку. Помимо этого в RegularExpressionsPlayaround присут- присутствуют методы Findl; Find2 и т.д., которые производят поиск на основе примеров, приведенных выше. Например, FA nd2 ищет все слова, начинающиеся с п:
С# и базовые классы 263 static void FindlO { string Text = @"XML has made a major impact in almost every aspect of software development Designed as an open, extensible, self-describing language, ic has become the standard for data and document delivery on the web. The panoply of XML- related technologies continues to develop at breakneck speed, to enable validation, navigation, transformation, linking, querying, description, arid messaging of data."; string Pattern = @"\bn\S*ion\b"; MatchCollection Matches = Regex.Matches(Text, Pattern, RegexOptions.IgnoreCase RegexCptions.IgnorePatternWhitespace I RegexOptions.ExplicitCapture); WriteMatches(Text, Matches); } Далее следует простой метод Ка : г (), который можно редактировать для выбора одного из методов Fir1d<Ti>'': static void Main(string[] args) { Findl(); Код также использует пространство имен RegularExpress ".. s: using System; using System.Text .Regtu г £* m ; Запуск примера с использованием метода F ^ i дает следующие результаты: Совпадения, группы и выборки Одной из особенностей регулярных выражений является то, что символы можно группи- группировать вместе. Это действует точно так же, как составные выражения в С#. Напомним, что в С# можно группировать любое число выражений, помещая их в скобки, и результат будет расцениваться как одно составное выражение. В шаблонах регулярных выражений разрешается группировать вместе любые символы (включая метасимволы и управляю- управляющие последовательности), а результат рассматривается как один символ. Единственное отличие состоит в использовании круглых, а не фигурных скобок. Результирующая последовательность носит название группы. Например, шаблон ) ^ будет искать все последовательности an. Квантификатор + действует только для предыдущего символа, но мы сгруппировали символы, и теперь он действует для повторяющихся an, рассматриваемых как один элемент. Это означает, что применение выражения ,аг)+ктекст\ "bananas :ame to Europe late in the an- annals of history" даст последовательность anan из слова bananas. С другой стороны, если бы мы написали an1, то были бы найдены аг.г в annals и две последовательности an в bananas. Может возникнуть вопрос: почему an) + дает anan, но не определяет индивидуа- индивидуальные вхождения an? Дело в том, что совпадения не должны перекрываться. Если существует несколько возможных перекрывающихся вариантов, то по умолчанию будет выбрана самая длинная последовательность. Группы позволяют решать сложные задачи. По умолчанию объединение части шаб- шаблона в группу указывает механизму регулярных выражений, что необходимо запомнить все совпадения именно для этой группы, помимо совпадений для всего шаблона. Други- Другими словами, группа считается отдельным шаблоном, для которого будут возвращены свои собственные совпадения. Это может быть полезно в том случае, если необходимо разбить строку на составные части.
264 Глава 7 Например, URI имеют формат <протокол>://<адрес>:<порт>, где порт является необязательным. В качестве примера можно привести http://www.wrox.com:4355. Допус- Допустим, что необходимо извлечь из URI протокол, адрес и порт, причем сразу за URI может быть (а может и не быть) пустое пространство, но будут отсутствовать знаки пунктуации. Это можно сделать с помощью выражения: \b(\S+)://(\S+)(?::(\S+))?\b Выражение работает следующим образом. Прежде всего первая и последняя после- последовательности \Ь гарантируют, что рассматриваются только части текста, являющиеся целыми словами. Внутри шаблона первая группа (\S+) : // выберет один или несколько символов, не являющихся пробелами, за которыми следует : //. Например, этому будет соответствовать http:// для HTTP URI. Скобки приведут к тому, что http будет сохра- сохранена как группа. Последующая группа (\S+) выберет в приведенном URI такие выраже- выражения, как www.wrox.com. Эта группа завершится при достижении либо конца слова (закрывающая \Ь), либо двоеточия (:), указанного в следующей группе. Следующая группа должна выбрать порт (в нашем случае :4355). Символ ? показыва- показывает, что эта группа является необязательной для совпадения. Если выражение : хххх будет отсутствовать, это не воспрепятствует нахождению совпадения. Это важно, так как номер порта не всегда указывается в URI — в большинстве случаев он отсутствует. Мы хотим показать, что двоеточие также может присутствовать или от- отсутствовать, но мы не желаем хранить его в группе. Это достигается путем использова- использования двух вложенных групп: внутренняя (\S+) выберет все, что следует после двоеточия (в нашем примере — 4355). Внешняя группа содержит внутреннюю, перед которой сто- стоит двоеточие, а перед ним — последовательность символов ?:. Эта последовательность указывает, что данную группу сохранять не требуется (необходимо сохранить только 43 55 — нам не нужно и : 43 55!). Не удивляйтесь двум следующим друг за другом символам двоеточия — первое является частью ?:, что говорит "не сохранять эту группу", а вто- второе — текстом, который следует искать. Если запустить этот шаблон для строки: Привет, я только что нашел этот замечательный URI no http://. Что это было? - Ах, да, http://www.wrox.com то будет найдено только одно совпадение http: //www.wrox.com. Внутри этого совпаде- совпадения имеются три описанные выше группы, а также четвертая группа, представляющая собой само совпадение. Теоретически каждая группа может содержать ни одного, одно или более одного совпадения. Каждое из этих индивидуальных совпадений известно как выборка. Поэтому первая группа, (\S+), имеет одну выборку — http. Вторая группа также имеет одну выборку —www.wrox.com, а третья группа не имеет выборок, поскольку в URI отсутствует номер порта. Обратите внимание на строку, содержащую второе выражение http: //. Оно совпа- совпадает с первой группой, но не будет обработано поисковым механизмом, так как полное выражение поиска не совпадает с этим элементом текста. К сожалению, у нас нет возможности показать примеры кода С#, использующего группы и выборки. Отметим лишь, что классы RegularExpressions .NET поддерживают группы и выборки с помощью классов, известных как Group и Capture. Также существуют классы GroupCollection и CaptureCollection, ко- которые представляют соответственно коллек- коллекции групп и выборок. Класс Match имеет метод Groups (), возвращающий соответству- соответствующий объект GroupCollection. Класс Group реализует метод Captures (), который воз- возвращает CaptureCollection. Взаимоотноше- Взаимоотношение между объектами показано на рисунке: RegEx Метод Метод ~^~* Match() MatchCollection Match i GroupCollection Group CaptureCollection Capture
С# и базовые классы 265 Возвращение объекта для группы каждый раз, когда требуется только сгруппировать символы, может быть нежелательным. В том случае, если нужно лишь сгруппировать символы в шаблоне, это будет приводить к дополнительным потерям производительно- производительности. Возвращение выборки можно отключить для конкретной группы, начав эту группу символов в последовательности с ?:, как сделано в примере с URI, или сразу для всех групп, указав флаг RegExOptions.ExplicitCaptures в методе RegEx. Matches О, как делалось в примерах выше. Группы объектов Теперь рассмотрим поддержку, предлагаемую базовыми классами .NET для структур дан- данных, в которых сгруппировано некоторое число одинаковых объектов. Наиболее простой такой структурой данных является обычный массив (см. главу 3). Массив является экземп- экземпляром класса System. Array, но С# оборачивает этот класс своим собственным синтакси- синтаксисом. System. Array имеет то преимущество, что он обеспечивает эффективный доступ к отдельному элементу по его индексу, а синтаксис С# делает программирование с использо- использованием этого класса более понятным. Однако Array обладает большим недостатком, заключающимся в необходимости указания его размера при создании экземпляра класса. После его создания не существует возможности добавлять, вставлять или удалять элемен- элементы. Кроме того, для доступа к элементу требуется указать числовой индекс. Это неудобно в том случае, если, например, обрабатывается набор записей Employee и необходимо просмотреть запись по имени служащего. .NET поддерживает множество других структур данных, которые полезны в различ- различных обстоятельствах. Помимо этого существует большое число интерфейсов, реализовав которые, классы могут заявлять о своей поддержке всей функциональности конкретного типа структуры данных. Рассмотрим три типа структур: списки массивов, коллекции и словари (известны еще как отображения). За исключением System.Array, все классы структур данных располагаются в про- пространстве имен System.Collections. Имя System.collections отражает одну из неоднозначностей, присущих компьютерной терминологии. Слово Collection часто применяется для обозначения любой структуры данных. Однако это также и конкретный класс, который реализует IEnumerable или IColle zc~on - конкретный тип структуры данных (см. ниже). В этой главе термин "коллекция" используется для обозначения конкретного класса, за исключением тех случаев, в которых имена базовых классов .NET вынуждают применять его в более общем смысле. Списки массивов Список массивов похож на массив, за исключением того, что список способен расти. Он представлен классом System.Collections.ArrayList. Класс ArrayList имеет некоторые сходства с классом StringBuilder (см. выше). Аналогично тому, как StringBuilder выделяет достаточный объем памяти для хране- хранения определенного числа символов и для последующей работы с ними в выделенном пространстве, ArrayList выделяет достаточное количество памяти для хранения ссы- ссылок на объекты. Впоследствии с этими объектными ссылками можно работать как угод- угодно в пределах выделенной памяти. При добавлении объектов в ArrayList после его заполнения он автоматически увеличит свою емкость в два раза по сравнению с текущей. Список массивов можно создать, указав его начальную емкость. Например, создадим список векторов: ArrayList Vectors = new ArrayListB0); Если начальный размер не указан, по умолчанию он устанавливается равным 16: krrayList Vectors = new ArrayList(); //по умолчанию емкость равна 16 После этого в список можно добавлять элементы при помоши метода Add (): Vectors.Add(new VectorB, 2, 2)); Vectors.Add(riew VectorO, 5, 6)); ArrayList рассматривает все свои элементы как ссылки на объекты. Это означает, что в ArrayList можно хранить любые объекты, но при обращении к ним необходимо привести их к соответствующему типу данных:
266 Глава 7 vector Element2 =*■ (vector)Vectors [1,] ; Этот пример показывает также, что Array List определяет индексатор, поэтому к его элементам можно осуществлять доступ как к элементам массива. Разрешается также вставлять элементы в ArrayList: Vectors.InsertA, new VectorC, 2, 2)); // вставка.в позицию 1 Существует также полезная перегруженная версия Insert (), служащая для вставки в ArrayList всех элементов коллекции при наличии ссылок на интерфейс ICollection. Элементы можно удалять: Vectors. RemoveAt A) ; / / удаляет объект в позиции 1. Можно также передать ссылку на объект методу Remove () . Однако удаление элемента в этом случае займет больше времени, так как ArrayList будет производить линейный поиск по массиву. Емкость можно прочитать или изменить с помощью свойства Capacity: Vectors. Capacity = 30; *i Отметим, что изменение емкости приведет к тому, что весь ArrayList будет перемещен в новый блок памяти с заданной емкостью. Число элементов, в действительности содержащихся в ArrayList, может быть полу- получено с помощью свойства Count: int nVectors = Vectors.Count; Список массивов полезен в том случае, если необходимо создать массив объектов, раз- размер которого заранее неизвестен. Можно создать "массив" в ArrayList, а затем преобра- преобразовать ArrayList в массив, если данные должны быть представлены в виде массива (это требуется, например, если нужно передать массив в метод, который ожидает массив в качестве параметра). Взаимоотношение между ArrayList и Array во многом похоже на взаимоотношение StringBuilder и String. К сожалению, в отличие от класса StringBuilder, не существует метода для преоб- преобразования списка массивов в массив. Приходится использовать цикл для ручного копи- копирования ссылок. Отметим, однако, что копируются только ссылки, а не объекты, так что это не должно сильно повлиять на производительность: Vector [] VectorsArray = new Vector[Vectors.Count]; for (int 1=0; I< Vectors.Count; I++) VecrcrsArray[I] = (Vector)Vectors[I]; Коллекции Коллекция представляет собой набор объектов, доступ к которым осуществляется путем последовательной обработки каждого элемента. В частности, такой набор объектов ис- используется в цикле for each. Другими словами, если записать что-то типа: £breach (string NextMessage in MessageSet) { DoSomechir.3(NextMessage) ,- } то предполагается, что переменная MessageSet является коллекцией. Главная цель кол- коллекций — возможность использования цикла f oreach. По части дополнительных функций они предлагают совсем немного. Ниже мы более подробно рассмотрим коллекции и реализуем свою собственную кол- коллекцию, изменив соответствующим образом пример vector. Основные понятия коллек- коллекций не новы для .NET. В течение многих лет коллекции были частью СОМ, а также использовались в Visual Basic с помощью удобного синтаксиса For Each. Скрывающаяся же за этим архитектура похожа на архитектуру коллекций .NET. Общее представление о коллекциях Внутренне объект является коллекцией, если он может предоставить ссылку на связанный объект, известный как перечислитель, который способен перемещаться по элементам кол- коллекции. Коллекция должна реализовывать интерфейс System. Collect ions. IEnumerable. IEnumerable определяет только один метод и выглядит следующим образом: interface IEnumerable
С# и базовые классы 267 TEnumerator GetEnumerator () ; } Цель GetEnumerator () — вернуть объект-перечислитель. Как видно из приведенного кода, объект-перечислитель должен реализовывать интерфейс System.Collections. 1Ег umerator. Существует также дополнительный интерфейс для коллекций, icolleci-ivn, который является производным от lEnumerable. Более сложные коллекции будут реализовывать и этот интерфейс. Помимо GetEnumerator {/ он предлагает свойство, которое напрямую возвращает число элементов в коллекции. Он также поддерживает копирование коллекции в массив и может передавать информацию о том, является ли он потоковобезопасным. Однако здесь мы рассмотрим лишь интерфейс коллекций lEnumerable. IEnumerator выглядит следующим образом: interface IEnlomerator С object Current { get; } bool MoveNext() ; v void Reset () ,- •} Предполагается, что IEnumerator работает следующим образом: реализующий его объект должен быть связан с одной конкретной коллекцией. Когда объект инициализи- инициализируется в первый раз, он не ссылается на какие-либо элементы в коллекции, и для того чтобы перечислитель стал указывать на первый элемент коллекции, необходимо вы- вызвать MoveNext (). После этого текущий элемент можно получить с помощью свойства Current. Current возвращает ссылку на object, поэтому ее необходимо привести к типу объектов, хранящихся в коллекции. С этим объектом можно выполнить какие угод- угодно действия, а затем переместиться к следующему элементу коллекции, снова вызвав ме- метод MoveNext (). Этот процесс повторяется до тех пор, пока не будут исчерпаны все элементы коллекции — в этом случае Current вернет null. При желании можно в любой момент вернуться к началу коллекции, вызвав метод ю в< с . Отметим, что Peset () на самом деле возвращается не к первому элементу, а в позицию перед первым элементом, поэтому после вызова Reset () необходимо снова вызвать метод MoveNext () Для перехода к первому элементу коллекции. Очевидно, что цель коллекций — обеспечить способ прохода по всем элементам в том случае, когда вы не желаете указывать индекс и полагаетесь на то, что коллекция сама вы- выберет порядок выдачи элементов. Это означает, что вы не беспокоитесь о порядке, в кото- котором извлекаются элементы (главное, что извлекаются все элементы), хотя в ряде случаев конкретная коллекция может возвращать элементы в определенном порядке. В некото- некотором смысле коллекция является очень простым типом группы объектов, так как она не по- позволяет добавлять или удалять элементы из группы. Все, что можно сделать, — получить элементы в порядке, определяемом самой коллекцией, и поработать с ними. Невозможно даже заместить или изменить элементы коллекции, поскольку свойство Carre rt допуска- допускает только чтение. Наиболее частое применение коллекций — использование удобного синтаксиса цикла f oreach. Массивы также являются коллекциями — команда f огеас" успешно работает с ними. В случае массивов перечислитель, реализуемый классом System.Array, проходит по всем элементам по индексу массива от 0 и далее. Из вышесказанного следует, что приведенный цикл f oreach в С# является синтакси- синтаксическим сокращением следующего фрагмента: t Enumerator IEnumerator .= MessageSet.GetEnumerator() ,- string NextMessage; Enumerator.MoveNext(); while ( ■SSfextMessage * Enumerator. Current) != null) : { DoSomething(NextMessage); // NextMessage служит только для чтения Enumerator.MoveNext(); Заметьте, что фрагмент кода заключен в фигурные скобки. Они поставлены для того, чтобы этот код в точности соответствовал коду, приведенному ранее для цикла f oreach. При отсутствии скобок код отличался бы, в частности, тем, что переменные NextMessage и Enumerator остались бы в области видимости за пределами цикла.
268 Глава 7 Важной особенностью коллекций является то, что перечислитель возвращается в виде отдельного объекта. Он не должен представлять собой то же, что и сама коллекция. Это сделано по той причине, что к одной коллекции одновременно может быть приме- применено несколько перечислителей. Добавление поддержки коллекций в структуру Vector Структура vector (см. главу 5), в которую ранее была добавлена поддержка форматиро- форматирования, получит очередное расширение — поддержку коллекций! На данный момент экземпляр Vector содержит три компонента: х, у и z. В главе 5 был определен индексатор, так что экземпляр Vector можно рассматривать как массив, и к ком- компоненту х можно обратиться как к SomeVector [0], к компоненту у как к SomeVector [1], а к компоненту z как к SomeVector [2]. Теперь преобразуем структуру Vector в новый проект VectorAsCollection, в котором можно осуществлять итерацию по компонентам Vector с помощью кода: foreach (double Component in SomeVector) Console.WriteLinef"Компонент " + Component); Первое, что необходимо сделать,— отметить Vector как коллекцию, заставив его ре- реализовать интерфейс IEnumerable. Начнем с изменения объявления структуры Vector: struct Vector -: IFormattable, IEnumerable { public double x, y, z; Отметим, что интерфейс IFormattable представлен в связи с тем, что ранее была добавлена поддержка спецификаторов формата строки. Теперь необходимо реализовать интерфейс IEnumerable: struct Vector : IFormattabie, IEnumerable ( public IEnumerator GetEnumeratorO { return new VectorEnumerator(this); } Реализация GetFnumerator () зависит от существования нового класса, VectorEnu- merator, который нам необходимо определить. Так как vectorEnumerator не является классом, который должен быть виден внешнему коду напрямую, мы объявляем его как закрытый класс внутри структуры Vector. Его определение выглядит так: private class VectorEnumerator : IEnumerator < Vector thevector; // объект Vector, на который ссылается этот / / перечислитель *"''■■ int location; // элемент theVector, на который в данный момент // ссылается перечислитель 1 public VectorEnumerator(Vector theVector) С this.theVector = theVector; location = -1,- : } '" public bool MoveNextO { +tlocation; return (location>2) ? false : true; } public object Current { get { if (locartion < 0 I I location >2) throw new InvalidOperationExceptionl "Перечислитель находится либо перед первым элементом, пибо " + "после последнего элемента Vector"); return theVectort(uint)location]; } } i. public void Reset () t • i location ■'= -1;
С# и базовые классы 269 Как требует перечислитель, VectorEnumerator реализует интерфейс IEnumerator. Он содержит два поля: theVector, являющееся ссылкой на Vector (коллекцию), с кото- которой связан этот перечислитель, и location — переменную int, которая задает, на что в коллекции должен показывать перечислитель, другими словами, должно ли свойство Current извлекать х, у или z-компонент вектора. В данном случае location рассматривается как индекс, а внутренняя реализация пе- перечислителя осуществляет доступ к vector как к массиву. При этом корректными индек- индексами будут 0, 1 и 2, причем мы расширим их значением -1, показывающим, что перечислитель находится до начала коллекции, и 3, показывающим, что он находится после окончания коллекции. Именно поэтому данное поле устанавливается в -1 в конст- конструкторе VectorEnumerator: public VectorEnumerator(Vector theVector) { this.theVector = theVector; .location = -1; } Отметим, что конструктор также принимает ссылку на экземпляр vector, который необходимо обработать. Она была получена в методе Vector .GetEnumerator (): public IEnumerator GetEnumerator() { return new VectorEnumerator[this); Словари Словари представляют собой сложную структуру данных, обеспечивающую доступ к эле- элементу на основе некоторого ключа, который может быть любого типа данных. Они так- также известны как отображения или хэш-таблицы. Словари полезны в том случае, если необходимо хранить некоторые объекты так, будто это массив, но для индексирования структуры использовать не числовой, а другой тип данных. Они также позволяют свободно добавлять и удалять элементы, примерно как ArrayList. Рассмотрим использование словарей на примере MortimerPhonesEmployees. Пред- Предполагается, что Мог timer Phones (компания сотовой связи, см. главу 4) обладает некото- некоторым программным обеспечением, которое обрабатывает данные о своих сотрудниках. С этой целью нам потребуется структура данных — что-то типа массива,— содержащая дан- данные о работниках. Предположим, что каждый работник в MortimerPhones идентифици- идентифицируется с помощью идентификатора, представляющего собой набор символов, например В342 или W435, и хранящегося в виде объекта EmployeelD. Сведения о работнике хра- хранятся в виде объекта EmployeeData; в нашем примере к ним относятся идентификатор служащего, его имя и заработная плата. Допустим, что имеется такой идентификатор служащего: EmployeelD id = new EmployeelD("W435"); А также есть переменная employees, которая синтаксически может рассматриваться как массив объектов EmployeeData. Однако на самом деле это не массив, а словарь, и поэто- поэтому данные о работнике можно получить с помощью объявленного выше идентификатора, например: EmployeeData theEmployee = employees [ id] ; ■* Вот в чем заключается сила словаря. Он выглядит, как массив (на самом деле он бо- более похож на ArrayList, так как позволяет динамически менять объем, а также добав- добавлять и удалять элементы), но для его индексации не требуется использовать целые типы: допустим любой тип данных. В случае словарей он называется ключом, а не индексом. Словарь принимает ключ, переданный при обращении к элементу, и неким образом об- обрабатывает значение этого ключа. В результате обработки получается целочисленное значение, и уже оно используется для определения того, где в "массиве" необходимо со- сохранить или откуда получить требуемый объект. Приведем другие примеры, в которых можно применять словари для хранения объектов: О Требуется хранить сведения о сотрудниках или других людях, индексируемые их номерами социального обеспечения. Номер социального обеспечения — целое число, но массив с номером социального обеспечения в качестве индекса исполь- использовать нельзя, так как в США этот номер может теоретически достигать значе- значения 999999999. Для 32-разрядной системы такой массив невозможно поместить в
270 Глава 7 адресное пространство программы! Кроме того, большая часть массива будет пустой. В случае словаря можно использовать номер социального обеспечения для индек- индексирования работников, но при этом сохранять объем самого словаря небольшим. О Требуется хранить адреса, индексируемые почтовым индексом. В США почтовые индексы представляют собой числа, но в Канаде в них присутствуют и буквы. В Великобритании эквивалентом (почтовым кодом) являются строки, содержащие как числа, так и буквы. О Требуется хранить любые данные об объектах или людях, индексируемые именем объекта или человека. Для клиентского кода словарь выглядит как динамический массив с гибкими возможно- возможностями индексирования, но для обеспечения этого производится большой объем работ. В принципе для индексирования словаря может использоваться объект любого класса, одна- однако в этом классе необходимо сначала реализовать некоторые особенности, прежде чем его можно будет применять в качестве ключа. Среди них метод GetHashCode (), который на- наследуется от System.Object всеми классами и структурами. Мы детально рассмотрим, что такое словарь, как он работает и как во всем этом участвует GetHashCode (). Затем перейдем к примеру MortimerPhonesEmployees, который демонстрирует, как использовать словарь и как создать класс, который может применяться в качестве ключа. Словари в повседневной жизни Словарь назван так потому, что его структура очень похожа на структуру обычного слова- словаря. В настоящем словаре ищется значение слова (или в случае иностранного словаря — све- сведения о том, как перевести слово); эта пара строк, определяющих значение (или перевод) слова, и является теми данными, в которых вы заинтересованы. Тот факт, что большой словарь будет содержать тысячи и десятки тысяч статей, не представляет проблемы, так как слова размещаются в алфавитном порядке. В некотором смысле слово, используемое для поиска, и является тем самым ключом, который служит для получения интересующей нас информации. На самом деле нас интересует не столько слово, сколько связанные с ним данные. Слово предоставляет лишь способ поиска данных в словаре. Отсюда следует, что для создания словаря необходимы три вещи: О Данные, среди которых осуществляется поиск О Ключ О Алгоритм, позволяющий определить, где хранятся данные в словаре Алгоритм — важная часть словаря. Простое знание того, что представляет собой ключ, недостаточно — необходим также способ, с помощью которого можно использо- использовать ключ для поиска положения элемента в структуре данных. В обычных словарях этот алгоритм обеспечивается за счет расположения слов в алфавитном порядке. Словари в .NET В .NET простой словарь представлен классом Hashtable, который работает на основе тех же самых принципов, что и обычные словари, за исключением того, что в его пони- понимании ключ и элемент данных оба имеют тип Object. Это означает, что хэш-таблица мо- может хранить любые структуры данных, в то время как обычные словари используют в качестве ключей строки. Hashtable представляет собой простой словарь, способный хранить все, что угодно. Можно определять и свои собственные, более специализированные классы словарей. Microsoft создала абстрактный базовый класс DictionaryBase, который обеспечивает основные функции словаря и от которого можно наследовать собственные классы. Суще- Существует также готовый базовый класс .NET System.Collections.Specialized.String- Dictionary, который необходимо использовать вместо Hashtable в том случае, если в качестве ключей выступают строки. При создании объекта Hashtable можно указать его начальную емкость так же, как для StringBuildei и Arrayl st: Hashtable Employees = new Hashtable E3) ,- Существует большое число конструкторов, но этот используется чаще всего. Отме- Отметим, что в качестве начального размера указано значение 53. На это есть своя причина. Из-за алгоритмов, используемых в словарях, они наиболее эффективно работают в том случае, если их емкость представляет собой простое число (т.е. число, которое не делит- делится на другое целое число без остатка; например, 3, 5, 7, 11 и 13 являются простыми чис- числами, так же как 53, но 10 — нет, его можно разделить на 5, получив в результате 2).
С# и базовые классы 271 Добавление объекта в Hashtable осуществляется с помощью метода Add (), но Наь',- table. Add() принимает два параметра, причем оба являются ссылками на объекты. Пер- Первый представляет собой ссылку на ключ, второй — на данные. Используя классы EmployeelD и EmployeeData, можно написать: EmployeelD id; EmployeeData data; // Инициализируем id и data для ссылки на некоторого работника // Допустим, что employees является экземпляром Hashtable, // содержащим ссылки EmployeeData employees.Add(id, data); Для того чтобы получить данные элемента, необходимо указать ключ. Нре э ,'р реа- реализует индексатор, с помощью которого можно получить данные,- в результате мы имеем синтаксис массива: EmployeeData иЗаЬа = employees[id]; Можно удалить элементы из словаря, предоставив ключ удаляемого объекта: employees.Remove(id); Количество элементов в хэш-таблице можно выяснить, используя свойство Coi nt: int nEmployees = employees.Count; Отметим, однако, что отсутствует метод Insert (). Мы еще не рассматривали, как ра- работает словарь, но оказывается, что нет никакой разницы между добавлением и встав- вставкой данных. Если массив или ArrayList содержит один большой блок данных в начале структуры и пустой блок в конце, то словарь можно представить таким образом, где не- незакрашенные части словаря являются пустыми областями: СЛОВАРЬ Ключ + Значение Ключ + Значение Ключ + Значение Ключ + Значение При добавлении элемента он может быть размещен где угодно в словаре. Каким об- образом определяется это положение на основе ключа, не треб\ется знать при использова- использовании словаря. Важным моментом является то, что алгоритм, применяемый для получения местоположения элемента, надежен — если вам известен ключ, вы можете пе- передать его в объект Hashtable, который использует его для быстрого определения по- положения элемента и для его получения. Работа этого алгоритма рассматривается ниже. Здесь лишь отметим, что он полагается на метод ключа GetHashCode (). Отметим, что приведенная выше диаграмма является упрощенной. Пары ключ/зна- ключ/значение не хранятся в структуре словаря. Как обычно для типов по ссылке, то, что хранит- хранится, представляет собой ссылку на объект, показывающую, где именно в куче расположен этот объект. Принцип работы словаря Словари (хэш-таблицы) удобное средство, но их недостаток в том, что для определения места размещения каждого объекта Hashtable (и любой другой класс словарей) исполь- использует некоторый алгоритм, и этот алгоритм не предоставляется целиком классом Hash- table. Он состоит из двух этапов, и код для одного из этих этапов должен быть предоставлен классом ключа. Если в качестве ключа применяется класс, написанный Microsoft (например, string), то проблемы нет (так как Microsoft уже написала весь требу- требуемый код). Но если класс для ключа вы пишете сами, то и другую часть алгоритма придется написать самостоятельно.
272 Глава 7 В компьютерной терминологии часть алгоритма, реализованная классом ключа, но- носит название хэш (отсюда и термин хэш-таблица), а в классе Hashtable реализуется хэш-алгоритм: он использует метод GetHashCode () вашего объекта, который наследует- наследуется от System.Object. Если классу словаря требуется определить, где должен располага- располагаться элемент, он вызывает метод GetHashCode () объекта ключа. Именно поэтому при обсуждении System.Object было подчеркнуто, что на перекрытие GetHashCode О на- накладываются жесткие ограничения, поскольку для того, чтобы словарные классы рабо- работали правильно, ваша реализация GetHashCode () должна вести себя определенным образом (очевидно, что если ваш класс не предназначен для использования в качестве ключа словаря, то не требуется перекрывать GetHashCode ()). GetHashCode () возвращает int, и для его генерации он должен как-то использовать значение ключа. Hashtable примет этот int и выполнит его обработку, включающую в себя ряд сложных математических вычислений, после чего вернет индекс того места, где в словаре необходимо сохранить элемент с заданным хэшем. Эта часть алгоритма уже реализована Microsoft, и нам не требуется ничего знать о ней, кроме того, что алгоритм использует простые числа и емкость хэш-таблицы должна задаваться простым числом. Для того чтобы все это работало правильно, к перегруженному GetHashCode () предъявляются жесткие требования. Они звучат довольно абстрактно и устрашающе, но как покажет наш пример MortimerPhonesEmployees, не так сложно создать класс клю- ключа, который удовлетворяет этим требованиям: 3 Он должен быть быстрым (поскольку помещение или получение элементов в словаре должно быть быстрым). 3 Он должен быть непротиворечивым. Если два ключа представляют одно и то же значение, то они обязаны давать одно и то же значение для хэша. О В идеале он должен выдавать значения, равномерно распределенные по всему диапазону чисел типа int. Последнее требование вызвано наличием потенциальной проблемы: что происходит, если в словаре имеются два объекта, чьи хэши дают один и тот же индекс? В этом случае класс словаря начинает искать ближайшее соседнее свободное место для хранения второго элемента, а позже ему потребуется осуществить поиск этого эле- элемента для его извлечения. Очевидно, это снизит производительность, и совершенно ясно, что если большое число ключей будут давать одинаковые индексы, вероятность появления таких проблем возрастет. Часть алгоритма Microsoft работает так, что этот риск минимизируется, если рассчитанные хэш-значения равномерно распределены между int.MinValue и in*. .MaxValue. Риск коллизий между ключами также увеличивается по мере роста словаря, поэтому необходимо убедиться в том. что емкость словаря значительно превышает число содер- содержащихся в нем элементов. По этой причине Hashtable автоматически перераспределя- перераспределяет память для увеличения своей емкости сразу перед окончательным заполнением. Пропорция между заполненной и незаполненной частями таблицы определяется при по- помощи коэффициента загрузки. Используя один из конструкторов, можно вручную указать коэффициент загрузки, по достижении которого таблица должна перераспределить память: // Емкость = 50j максимальная загрузка = 0.5 Hashtable Employees = new HashtableE0, 0,5); Чем меньше максимальная загрузка, тем эффективнее будет работать хэш- таблица, но тем больше памяти она будет занимать. Кстати говоря, когда хэш- таблица перерас- перераспределяет память для увеличения своего объема, она всегда в качестве значения новой емкости выбирает простые числа. Другим важным моментом является то, что хэширующий алгоритм должен быть со- согласованным. Если два объекта содержат одинаковые данные, то они должны давать одно и то же хэш-значение, и здесь появляются некоторые важные ограничения, касаю- касающиеся перекрытия методов Equals () и GetHashCode () из System.Object. Hashtable определяет, что два ключа Айв равны, вызывая A. Equals (В). Это значит, что следующее правило должно выполняться всегда: Если A.Eguala(B) равно true, то A.GetHaahCodeO и В. GetHashCode О должны всегда возвращать один и тот же хэш.
С# и базовые классы 273 Это очень важно. Если в результате перегрузок методов приведенное правило будет выполняться не всегда, то хэш-таблица, использующая экземпляры этого класса в качест- качестве ключей, не сможет корректно работать. Будут происходить странные вещи: напри- например, можно будет поместить объект в хэш-таблицу и больше никогда не получить его обратно или же получать ссылки на другие объекты. По этой причине компилятор С# выдает предупреждение в том случае, если присутствует перегруженный метод Equals О, но отсутствует перегруженный метод GetHashCode (). Для System.Object это условие выполняется, поскольку Equals () сравнивает ссыл- ссылки, a GetHashCode () возвращает хэш. основанный полностью на адресе объекта. Хэш-таблицы, основанные на ключах, не перекрывающих эти методы, будут работать корректно. Однако проблема здесь заключается в том, что ключи будут считаться равны- равными только в том случае, если они представляют собой один и тот же объект. Это означа- означает, что при помещении объекта в словарь необходимо привязываться к ссылке на ключ. Нельзя просто создать еще один экземпляр ключа, имеющий то же значение, так как то же значение является по сути тем же самым объектом. Если версии Equals () и GetHas- GetHashCode О из Object не перекрываются, такой класс неудобно использовать в хэш-табли- хэш-таблице. Более разумно генерировать хэш на основе значения ключа, а не его адреса в памяти. Именно поэтому придется перекрывать Ge .H ishCode () и Equals () для любого класса, который предполагается применять в качестве ключа. Кстати, System.String уже имеет перегруженные методы. Equals!) был перегру- перегружен для выполнения сравнения по значению, a ?<=.tr iCode () — для возвращения хэша на основе значения строки. По этой причине строки удобно использовать в качестве ключей в словаре. Пример MortimerPhonesEmployees Пример MortimerPhonesEmployees представляет собой программу, которая создает словарь сотрудников. Как упоминалось ранее, словарь индексируется при помощи объек- объектов EmployeelD, а каждый элемент в словаре хранится как объект EmployeeData, содер- содержащий сведения о работнике. Программа создает экземпляр словаря, добавляет в него нескольких сотрудников, а затем просит пользователя ввести идентификатор служаще- служащего. Программа пытается использовать идентификатор для индексирования словаря и по- получения сведений о работнике. Процесс повторяется до тех пор, пока не будет введено "X1. Результат выполнения примера выглядит следующим образом: 58бШб 4718661 Fntei' 60 Employee Ent&ir-efi Employee ErttCi-'VM Employe- lS ,' t- Entel- en ■:*• ■■■•.iiT.fJrt--..-.*4"/-* . .-V.. ..Г..-, ; ■ ,A| ^ BOOjsHoi-tiiier . ■;' pldyej■ЛS STf oi^iHt J(S>^/', -HJ234! fli»abel Jonfet ■< " " n -i ■■ Ij. V -• ir " not fouhd:. U> ' lf3?*{*' - >. X tu '*.*■■' ■i ■ ■ exit» ВЙ01 ниа.еаэ.ио (CKit>V«234 !£1O,Q00.00 • Ш 3 1 1 1 ■■вв Этот пример содержит ряд классов. В частности, требуется класс Emp! с. =eID, кото- который является ключом, используемым для идентификации служащих, и класс Employee- Data, который хранит данные о работнике. Сначала мы рассмотрим класс EmployeelD, так как именно в нем осуществляются действия по подготовке для применения в качест- качестве ключа словаря. Определение класса выглядит следующим образом: class EmployeelD {- private readonly char prefix; private readonly int number; . public EmployeelD(string id) prefix = (id.ToUpper())[0]; '■ number = int .Parse (id.Substring(l, ) public- override string ToStringO 3)>;
274 Глава 7 return prefix.ToStringO + string. Format (" {0,3:000} ", number); } public override int GetHashCode() { return ToString() .GetHashCode() ; } public override bool Equals(object dbj) { ' EmployeelD rhs = obj as EmployeelD; if" (rhs -4. null) return false; if (prefix == rhs.prefix 11 number == rhs.number) return true; return false; Первая часть определения класса хранит конкретный идентификатор. Напомним, что идентификатор состоит из одной буквы-префикса, за которой следуют три символа, например В001 или W234. Мы сохраняем его как char для префикса и int для оставшейся части кода: class EmployeelD { private readonly char prefix; private readonly int number; Конструктор берет строку и разбивает ее для формирования этих полей. Отметим, что для упрощения примера не производится проверка ошибок. Мы предполагаем, что в конструктор передается строка в корректном формате. Метод ToString () возвращает идентификатор как строку: return prefix.ToString() +string.Format("{0,3:000}", number); Обратите внимание на спецификатор формата C:000), который гарантирует, что int, содержащее число, дополняется нулями, в результате чего мы получаем В001, а не В1. Для словаря требуются два перекрывающих метода. Во-первых, перекрыт метод Equals (), который теперь сравнивает значения экземпляров EmployeelD: public override bool Equals(object obj) { EmployeelD rhs = obj as EmployeelD; j.f (rhs == null) return false; if (prefix == rhs.prefix I I number == rhs.number) return true; return false; } В первую очередь необходимо проверить то, что переданный в качестве параметра объект на самом деле является экземпляром EmployeelD. Если это не так, то очевидно, что он не равен нашему объекту, и поэтому мы возвращаем false. Проверка типа произ- производится путем приведения объекта к типу EmployeelD с помощью ключевого слова С# as. Если имеется объект EmployeelD, производится сравнение значений полей для про- проверки того, что они содержат те же самые значения, что и объект this. Теперь рассмотрим GetHashCode (). Его реализация короче, хотя на первый взгляд сложно понять, что происходит: public override int GetHashCode() { return ToString () . GetHashCode () ,- } Ранее были представлены строгие требования, которым должен удовлетворять вы- вычисленный хэш-код. Разумеется, можно разработать простые и эффективные хэширую- щие алгоритмы. Вообще говоря, взятие значений полей, умножение их на большие простые числа и сложение полученных результатов — хороший способ, но все дело в том, что Microsoft уже реализовала сложный, но эффективный хэширующий алгоритм для строкового класса, и этим можно воспользоваться. Метод string.GetHashCode () дает хорошо распределенные числа на базе содержимого строки. Он удовлетворяет всем требованиям хэш-кода.
С# и базовые классы 275 Единственный недостаток применения этого метода заключается в том, что преобра- преобразование класса EmployeelD в строку вызывает некоторую потерю производительности. Если это важно и из хэширующих алгоритмов необходимо извлечь последнюю унцию производительности, то придется создать свой собственный хэш. Разработка хеширую- щих алгоритмов — сложная тема, и она не рассматривается в этой книге. Однако мы предложим один простой подход, заключающийся в умножении полей-компонентов класса на различные простые числа и в последующем сложении (по математическим причинам умножение на разные простые числа помогает предотвратить ситуацию, когда различные комбинации значений полей приводят к получению одинакового хэш-кода). Реализация GetHashCode () могла бы выглядеть так: public override int GetHashCode // <_.1.^тернативная реализация { return (int)prefix*13 + (int)number*53; } Этот код будет работать быстрее, чем алгоритм на основе ToString (), используемый в примере, но имеет тот недостаток, что сгенерированные различными EmployeelD хэш-коды вряд ли будут равномерно распределены по всему диапазону int. Кстати, примитивные числовые типы имеют свои методы GetHashCode , но они возвращают значение переменной, поэтому не представляют практической ценности. Отметим, что наши реализации Ge*-.Kas'r '• ае () и Equals () удовлетворяют требова- требованиям равенства: два объекта Е ^ ; 1Z считаются равными, если они имеют одинако- одинаковые значения префикса и числа; в этом случае Tobtring(' также выдаст одинаковые значения для обоих объектов, и в результате будет получен один и тот же хэш-код. Это действительно важное условие, которое должно выполняться. Теперь рассмотрим класс, содержащий данные о сотрудниках. Его определение не требует особых пояснений: class EmployeeData { private string name; private decimal salary; priate EmployeelD id; public EmployeeData(EmployeelD id, string name, decimal salary) { this.id = ^d; this.name = name; this.salary = salary; } public override string ToString() { StringBuilder sb = new StringBuilder(id.ToString(), 100); sb.Append(": ") ; sb.Append(string.Format "{ ,-20}", name)); sb.Append(" ") ; sb.Append(string.Format("{0:C}", salary)); return sb.ToStringf); Отметим, что мы используем объект StringBuilder для создания строкового представ- представления объекта EmployeeData. Наконец, создадим тестовую программу. Она определена в классе _ estHarness: class TestHarness { Hashtable mployees = new HasntablePl1 ; public void Run() { EmployeelD idMortimer = nev SmployeeID("B001") ; EmployeeData mortimer = new EmployeeData(idMortimer, "Mortimer", 100000.00M); EmployeelD idArabel = new EmployeelD("W234"); EmployeeData arabel = new EmployeeData(idArabel, "Arabel Jones", 10000.00M) ; employees.Add(idMort imer, mort imer); employees. Add (idArabel, arabel).; while (true)
276 Глава 7 try { Console.Write("Введите идентификатор служащего (в формате А999, X для выхода) > " ) ; String userlnput - Console.ReadLineО; user Input = userlnput .ToUpperO ; if (userlnput == "X") return; EmployeelD id = new EmployeelD(userlnput); DisplayData(id); ) catch (Exception e) { Console.WriteLine("Произошло исключение. Проверьте формат идентификатора работника!"); Console. WriteLine(e. Message).,• Console.WriteLineО; } Console.WriteLine(); } ' : ' private void DisplayData(EmployeelD id) { object empobj = employees[id]; if (empobj != null) { EmployeeData employee = (EmployeeData)empobj; Console.WriteLine("Сотрудник: " + employee.ToStringt)); } else Console.WriteLine("Сотрудник не найден: ID = " + id) ; } } TestHarness содержит поле, которое представляет собой словарь: Hashtable employees = new Hashtat LeC1) ,• Как обычно для словаря, его начальная емкость задана простым числом, в данном случае — 31. Основная часть тестовой программы содержится в методе Run(). Этот ме- метод устанавливает сведения о двух сотрудниках — mortimer и arabel — и добавляет их в словарь: empl_yee_:. A id _5Xort_mer, mortimen ; employees.Add,IdArabei, arabel); Затем мы входим в цикл while и просим пользователя ввести EmployeelD. Внутри цикла while имеется блок try. Именно здесь обрабатываются проблемы, возникающие при неправильном вводе пользователем значения EmployeelD, что в свою очередь при- приводит к генерации исключения в конструкторе EmployeelD при попытке сконструиро- сконструировать идентификатор из строки: string userlnpj- = Console.ReadLine user_nput = _serlnput.To"pper(); it serlnput == "X") return; EmpioyeelD id = new EmployeelD(userlnput); // <- здесь возможно исключение Если EmployeelD сконструирован корректно, производится отображение соответст- соответствующего сотрудника путем вызова метода DisplayData. Именно в этом методе мы, нако- наконец, получаем доступ к коллекции с использованием синтаксиса массива. Действительно первое, что мы делаем в этом методе,— получаем данные о служащем с указанным иден- идентификатором: private void Displayrai a (Employ _jID Id: object empobj = employees[id]; Если в словаре нет сотрудника с таким идентификатором, employees [id] вернет null. Именно поэтом^' производится соответствующая проверка и выводится сообще- сообщение об ошибке при обнаружении null. В противном случае полученная ссылка empobj приводится к EmployeeData (напомним, что Hashtable представляет собой общий класс словарей). Класс Hashtable хранит объекты object, поэтому при извлечении из
С# и базовые классы 277 него объекта будет получена ссылка на object, и ее необходимо явно привести к типу, который был первоначально помещен в словарь. Получив ссылку EmployeelD, можно отобразить данные о сотруднике, используя метод EmployeeData.ToString(): EmployeeData employee = (EmployeeData)empobj; Console.WriteLine("Сотрудник: " + employee.ToString()); Наконец, заключительная часть кода — метод Main (). Он создает экземпляр объекта TestHarness и запускает его: static void Main(string!] args) { TestHarness harness = new TestHarness0; harness. Пользовательские атрибуты В главе 6 было показано, что в программе можно определять атрибуты для различных элементов. До сих пор мы использовали атрибуты, определенные Microsoft, и компиля- компилятор С# имеет о них представление. В соответствии с этими атрибутами компилятор мо- может настраивать процесс компиляции тем или иным образом (например, располагая структуру в памяти согласно сведениям в StructLayout и в связанных атрибутах). Архитектура атрибутов позволяет также определить свои собственные атрибуты в исходном коде. Очевидно, что эти атрибуты не повлияют на процесс компиляции, так как компилятор не обладает внутренними знаниями о них, но эти атрибуты будут пред- представлены в виде метаданных в полученной сборке. Эти метаданные могут применяться для целей документирования. Однако идея хороша тем, что, используя классы из про- пространства имен System.Reflection, код может читать метаданные во время исполне- исполнения. Это означает, что пользовательские атрибуты способны оказывать прямое действие на работу вашего кода. В этом разделе мы исследуем процесс определения и применения пользовательских атрибутов, а в следующем разделе покажем, как использовать их совместно с отражени- отражением. Будет разработана программа для компании, которая регулярно поставляет обновле- обновления для своего программного продукта и желает, чтобы сведения об этих обновлениях документировались автоматически. В примере будут определены пользовательские ат- атрибуты, показывающие дату создания или последней модификации классов или методов кода, а также какие изменения были сделаны. Затем мы используем отражение для раз- разработки приложения, которое просматривает эти атрибуты в сборке и, следовательно, может автоматически вывести все сведения о том, какие изменения были внесены в про- программный продукт, начиная с указанной даты. Такой тип приложения может быть поле- полезен при сохранении рабочей документации, а также для обеспечения пользователей обновленной информацией при продаже новой версии программного продукта. Другим примером использования атрибутов и отражения является чтение и запись данных в базу данных. С помощью атрибутов можно отметить, какие классы и свойства соответствуют каким таблицам и столбцам базы данных. Читая эти атрибуты из сборки во время исполнения, программа сможет автоматически получать или записывать дан- данные в соответствующую позицию в базе данных без необходимости задания специфиче- специфической логики для каждой таблицы или столбца. Написание пользовательских атрибутов Для того чтобы понять, как создавать пользовательские атрибуты, полезно посмотреть, что компилятор делает, встретив в коде элемент, помеченный с помощью атрибута, не поддерживаемого компилятором. Рассматривая пример с базой данных, допустим, что объявлено свойство С#: [EieldName< "SocialSecuilfcyNumber") ] public string. SocialSecurityNumber LI ..и т.д. Увидев, что метод имеет атрибут FieldName, компилятор прибавит к этому имени строку "Attribute", образовав полное имя FieldNameAttribute, а затем будет искать во всех пространствах имен, указанных в строке поиска (т.е. в тех пространствах имен, которые были объявлены в операторе using), класс с тем же именем. Если отметить
278 Глава 7 элемент атрибутом, чье имя уже заканчивается строкой "Attribute", компилятор оста- оставит строку, представляющую собой имя атрибута, без изменений. Приведенный выше код эквивалентен следующему: [FieldNameAttribute("SocialSecurityNumber")] public string SocialSecurityNumber { // и т.д. Компилятор предположит, что существует класс с таким именем и что этот класс на- наследуется от System. Attribute. Компилятор также ожидает, что класс содержит инфор- информацию об использовании этого атрибута. В частности, класс атрибута должен сообщить, к каким элементам программы он может быть применен (классы, структуры, методы, свойства и т.п.), можно ли его указывать более одного раза для одного и того же элемен- элемента, а также какие обязательные и необязательные параметры этот атрибут принимает. Если компилятор не может найти соответствующий класс атрибута или же находит его, но способ использования атрибута не соответствует тому, что задан в классе атрибу- атрибута (например, класс атрибута показывает, что атрибут можно применять только к по- полям, а в исходном коде он применен к определению структуры), то компилятор сгенерирует ошибку на этапе компиляции. Поэтому следующий шаг — убедиться в том, что объявлен соответствующий пользовательский класс атрибутов. Пользовательские классы атрибутов Предположим, что атрибут Fie] dName объявлен следующим образом: {AttributeUsage(AttributeTargets.Property, AllowMultiple-false-. Inherited=false)] public class FieldNameAttribute : Attribute private string name; public FieldNameAttribute scrlng name) { this.name = name; Здесь содержится достаточно информации для того, чтобы компилятор смог выяснить, как использовать атрибут. Атрибут AttributeUsage Прежде всего отметим, что класс сам по себе отмечен атрибутом AttributeUsage. Компи- Компилятор С# внутренне распознает этот атрибут • на самом деле вы можете возразить, что At- tribute'.'sage вообще не является атрибутом — это скорее метаатрибут, так как он применяется по отношению к другим атрибутам, а не к классу). Главным образом Attri- AttributeUsage служит для указания того, к каким элементам кода может быть применен ваш пользовательский атрибут. Эта информация передается в первом параметре, который представляет собой перечислимый тип At tr-outeTargsts. В примере показано, что атри- атрибут FieldName может применяться только к свойствам, что вполне приемлемо, поскольку именно так он использовался в предыдущем фрагменте кода. Определение Attribute- TargetsEnumeration таково: public enum AttributeTargets { All = 0x0O0C3FFF, Assembly = 0x00000001, Class = 0x00000004, Constructor = 0x00000020, Delegate = 0x00001000, Enum = 0x00000010, Event = 0x00000200, Field = 0x00000100, Interface = 0x00000400, Method = 0x00000040, Module = 0x00000002, Parameter = 0x00000800, Property = 0x00000080, ReturnValue = 0x00002000,
С# и базовые классы 279 Struct = 0x00000008 } Этот список говорит нам все о тех элементах, к которым могут применяться атрибуты. Отметим, что во всех случаях используется обычный синтаксис при определении атрибу- атрибута для элемента программы — атрибут помещается в квадратных скобках непосредственно перед элементом. Однако в приведенном списке присутствует одно значение, которое не соответствует ни одному из элементов программы: Assembly. Атрибут может быть указан для сборки в целом, а не для элемента кода,— в этом случае атрибут помещается в любом месте кода, но при этом он должен быть отмечен с помощью ключевого слова assembly: [assembly: SomeAsserablyAttribute(Parameters)] При указании элементов их можно комбинировать с помощью операции побитового OR. Например, если бы требовалось показать, что атрибут FieldName можно применить как к свойству, так и к полю, то можно было бы записать: [AttributeUsage(AttributeTargets.Property AttributeTargets.Field, AllowMulL Lple=f aj.se, Inheritec .= false) ] public cla_ Field.te .._A^_ Attribute С помощью Attribute^ cirqets -AIj можно указать, что атрибут везде имеет силу. Атри- Атрибут Attribute 'sage содержит также параметры owM"itip] e и Inherited. Для них ис- используется синтаксис <A'_*.r j.buteNdme>=<Attri; ■ вместо простого указания значений атрибутов в определенном порядке. Эти параметры являются необязательными — при желании их можно опустить. Параметр AllowMuiriple показывает, может ли атрибут применяться более одного раза к одному элементу. Тот факт, что в данном случае указано значение false, обязан привести к ошибке компиляции в случае, если в коде встретится что-то вроде: [FieldName("SocialSecurityNumber")] [FieldName ("Natior_a_InsuranceNumber" public string Soc_alSecurityNumb<=.r < // и т.д. Установка параметра Inherit в true показывает, что атрибут, применяемый к классу или интерфейсу, будет также применен ко всем \ наследованным классам и интер- интерфейсам. Если arpimvT применяется к методу или свойству, то он будет автоматически применяться ко всем перекрытым методам или свойствам, и т.д. Указание параметров атрибутов Теперь посмотрим, как можно ука л параметр! . которые принимает наш пользователь- пользовательский атрибут. Когда компилятор встречает опе|: пор: [FieldName, "Social S =curityNur =- public string Soc; a .Security".--:: r { // и т.д. он пытается создать экземпляр объекта типа этого атрибута, следовательно, он должен вызвать конструктор. Принимает атрибут параметры или нет, определяется тем, какие конструкторы доступны. В данном случае указан только один конструктор для FieldNa- meAttribute, и этот конструктор принимает один строковый параметр. Применяя атри- атрибут FieldName к свойству, необходимо указать одну строку в качестве параметра, как и сделано в коде. Если требуется, чтобы атрибут мог принимать различные типы параметров, можно со- создать несколько перегруженных конструкторов, хотя обычной практикой является исполь- использование одного конструктора и свойств для определения необязательных параметров. Необязательные параметры На примере атрибута AttributeUsage был показан альтернативный синтаксис, с помо- помощью которого к атрибуту могут быть добавлены параметры. Этот синтаксис предусмат- предусматривает указание имен необязательных параметров. Он работает с помощью свойств или полей в классе атрибута. Допустим, что определение свойства SocialSecurityNumber было изменено следующим образом:
280 Глава 7 tFieldNameCSocialSecurityNumber", Comment= "Это- поле основного ключа")] public string SocialSecurityNumber { // и т.д. В данном случае компилятор увидит синтаксис <ParameterName>= для второго пара- параметра и не станет передавать этот параметр в конструктор FieldNameAttribute. Вмес- Вместо этого он попытается найти открытое свойство (или поле, но, как было показано, открытые поля не являются хорошим стилем программирования) с таким именем, кото- которое можно использовать для установки значения параметра. Для работы приведенного кода необходимо добавить несколько строк в FieldNameAttribute: [AttributeUsage(AttributeTargets.Property, AllowMultiple=false, Inherited=false)] public class FieldNameAttribute : Attribute { privat.e. string cbnfflKfnfc; Jpubiic string 'Comment { // и т.д. После добавления этого кода и реализации свойства Comment можно передавать не- необязательные атрибуты. Пример WhatsNewAttributes Итак, приступим к разработке примера WhatsNewAttributes, который содержит атри- атрибут, показывающий, когда в последний раз производилось изменение элемента. Этот пример гораздо сложнее всех предыдущих, так как он состоит из трех отдельных сборок. Это: □ Сборка WhatsNewAttributes, которая содержит определения атрибутов. О Сборка VectorStruct, содержащая код, к которому применяются атрибуты. В данном случае выбран пример Vector, который разрабатывался на протяжении нескольких последних глав. О Сборка LookUpWhatsNew, которая содержит проект, отображающий сведения об изменившихся элементах. Только LookUpWhatsNew является консольным приложением. Остальные две сборки — библиотеки. Каждая из них содержит определения классов, но не имеет точки входа про- программы. Для сборки VectorStruct это означает, что был взят пример VectorAsCollection и из него удалены класс тестовой программы и точка входа, а оставлен только класс Vector. Управление тремя связанными проектами посредством компиляции из командной строки непростое дело. Команды для раздельной компиляции исходных файлов будут приведены позже. Если вы загрузите пример с web-сайта Wrox Press, то, вероятно, захоти- захотите отредактировать их как совместное решение Visual Studio.NET способом, представлен- представленным в главе 8. Чтобы вы могли сделать это, файлы доступны в формате Visual Studio.NET. Библиотечная сборка WhatsNewAttributes Начнем с центральной части проекта WhatsNewAttr Lbutes. Ее исходный код содержит- содержится в файле What sNewAt tributes, cs. Прежде мы не компилировали библиотеки. Синтак- Синтаксис для этого действия несложен: в командной строке для компилятора указывается флаг target: library. Для компиляции WuatsNewAttributes необходимо ввести: сес WhatBNewAttributee.ee /target:library /out:WhatsNewAttributes.exe Исходный код для этой сборки содержит два класса атрибутов: LastModif iedAttribute и SupportsWhatsNewAttri^te. LastModifiedAttribute используется для отметки того, когда в последний раз модифицировался элемент. Он принимает два обязательных параметра (параметры, передаваемые в конструктор): дату модификации и строку, со- содержащую описание изменений. Также имеется один необязательный параметр (для него существует свойство для записи). Issues, который может применяться для описания сложных вопросов, связанных с элементом.
С# и базовые классы 281 Не существует разницы между исходным кодом, предназначенным для библиотеки, и кодом, предназначенным для приложения, за исключением того, что в библиотеке отсутствует метод Main(). В повседневной жизни вы, вероятно, пожелали бы, чтобы этот атрибут был приме- применим ко всему. Для упрощения нашего кода ограничим его использование только класса- классами и методами. Однако позволим, чтобы его можно было применять более одного раза к одному и тому же элемент)' (AllowMultiple=true), так как элемент может быть изменен несколько раз, и каждое такое изменение необходимо отмечать отдельным экземпляром атрибута. SupportsWhatsNewAttribute является небольшим классом, который представляет собой атрибут, не принимающий параметров. Идея заключается в том, что это атрибут сборки, используемый для пометки той сборки, для которой поддерживается документа- документация с помощью LastModif iedAttr ibute. Таким образом, программа, которая позже бу- будет исследовать эту сборку, будет знать, что эта сборка является одной из тех, что применяются в процессе автоматического документирования! Приведем полный исходный код для этой части примера: namespace Wrox.ProfessionalCSharp.Chapter?.WhatsNewAttributes { [AttributeUsage! AttributeTargets.Class i Atti t t iri s.Method, AllowMultiple=true, Jnherited=fs_se)] public cLass -JiastiModifie&Attribute : Atti ibct ? • • private OateTirae. dateModified; '■' private string changes: private string issues; public LastModifiedAttribute(string dateModified, string changes) { this.dateMddified = DateTime. Parse (dateModifitd) ,- this,changes = changes; } public DateTime DateModified { set { return dateModified; } } public string Changes { ' get { return changes; } } , public string Issues { - get { return issues; } ' set { issues =s value; J tAttributeUsage(AttributeTargets.Assembly)] pubiic class Suppor.tsWhatsNewAttribute : Attribute Для свойств Changes и DateModified не предусмотрены аксессоры set. В этом нет необходимости, так как мы требуем, чтобы эти параметры устанавливались в конструк- конструкторе как обязательные (аксессоры get нужны для того, чтобы впоследствии мы могли прочитать значения этих атрибутов).
282 Глава 7 Использование атрибутов: сборка VectorClass Теперь воспользуемся этими атрибутами. Для этого обратимся вновь к примеру VectorAs- Collectior.. Отметим, что необходимо явно сослаться на только что созданную библио- библиотеку WhatsNewAttributes. Также нужно указать соответствующее пространство имен с помощью оператора usii g, чтобы компилятор смог распознать атрибуты: using System; using Wrox.Prof<==sionalCSharp. Chapter 7 .'."latsMewActributes; csi з Syb. em. " Hectics; [assembly: SupportsWhatsNew] В этом коде указана строка, помечающая сборку атрибутом Support sWhatsNew. Теперь обратимся к коду класса Vec or. На самом деле мы ничего не меняем в этом классе, лишь добавляем несколько атрибутов LaatDatekodif ied для указания того, ка- какая работа была проделана в последней главе. Однако одно изменение все-таки внесено: Vector определен как класс, а не структура. Это сделано с целью упрощения кода, ото- отображающего атрибуты. В примере VectorAsCollection Vector был структурой, а его перечислитель — классом. В этом случае пример, просматривающий сборку, должен был бы обрабатыва ъ .1 классы, и структуры. Если же оба типа представляют собой классы, не нужно бе покоиться о наличии структур, в результате чего пример становится немного короче: namespace Wrox.Profe£в ionalCSharp.Chaoter7.VectorClass { [LastMciififci: K '16 мая 2001", "Реализован интерфейс IEnumerable,\n" ,+ "в результате чего Vector может теперь рассматриваться как коллекция.")] [LastModifiec,'10 мая 2001", "Реализован интерфейс IFormattable,\n" + "б результате чего Veccor теперь понимает спецификаторы формата N и VE.")] class Vector : IFormattable, IEnumerable { public dc'-l " x, y, z ,- publ-~ Vect J n-ble x -ч Ые у, rouble z) tl.: 3. = ■ this.z } [LastModified lu хая 2001", "Ме'-од добавлен цг.а --тержки сор—тигования. " х.—1 ; s.ring ToStrir_;j :tring ferret, IFormatProvider formatProvider) if : == null, re: _ String (); Мы также отметили содержащийся вн\три класс "ectorEr.umerator как new: [LastModifiecK 6 мая " ']", "Кла^с _эзлан как модуль для поддержки коллекций для Vector. private class VectorEnur ator : iEnimerator { Это все, что мы пока можем сделать с поимером. На данном этапе мы не можем ниче- ничего запустить, поскольку все. что у нас есть.— это всего лишь две библиотеки. Завершаю- Завершающая часть примера, в которой осуществляются просмотр и отображение атрибутов, будет написана после того, как мы изучим работу отражения. Отражение Отражение является общим термином. ;оторый относи гея к различным базовым клас- классам .NET, позволяющим по. чать ш«]х пгацию о типах в своей программе или в других сборках, а также читать mi тл (анпые и м [нифесчж сборок. Большинство этих классов находится в пространстве име J ys : . . Мы рассмотрим лишь малую часть того, что можно девать при помощи io.iilob ory ьгения, но для начала этого будет вполне достаточно. Ниже подроби описывается кчлеч mi i . который позволяет получить информацию об опрс icuni" любого i.nuiqH nioi n тшш данных. Затем коротко рас- рассматривается кл ice . L . , к(, гирын может использоваться для получения информации о данной сборке и пи д. я .чагрглки этой сборки в программу. Затем мы соединим нее кмес с 'i завершим г\у _•■.■■ i i . . i es.
С# и базовые классы 283 Класс System.Type Мы уже использовали класс Туре, но до сих пор это делалось только для получения имени типа: Type t - typeof(double) Хотя мы говорим о Туре как о классе, в действительности он является абстрактным базовым классом. При создании объекта Туре на самом деле создается экземпляр унасле- унаследованного от Туре класса. Туре имеет по одному производному классу, соответствующе- соответствующему каждому конкретному типу данных, хотя в целом производные классы лишь предлагают различные перегруженные методы и свойства Туре, которые возвращают корректные данные для соответствующего типа данных. Они не добавляют новые мето- методы или свойства. В основном существуют три общих способа получения ссылки Туре на заданный тип: О Использование оператора С# typeof (см. выше). Оператор принимает имя типа (без кавычек) в качестве параметра. О Использование метода GetType (*, который наследуется всеми классами от System.Object. double D = 10; Type t = D.GetTypeO; GetType () вызывают для переменной, а не для имени типа. Отметим, однако, что возвращаемый объект Туре по-прежнему связан только с типом данных — он не со- содержит информации, относящейся к экземпляру типа. Этот метод может быть по- полезен в том случае, если имеется ссылка на объект, но при этом не ясно, экземпляром какого класса является этот объект. О Можно также вызвать статический метод класса Type, GetType (): Туре•t = Туре.GetType("System.Double"); В некотором смысле Туре является воротами в технологию отражения. Он реализует большое число методов и свойств — настолько большое, что полный список привести здесь вряд ли удастся. Приводимые ниже методы и свойства должны все же дать вам не- некоторое представление о том, что можно делать с помощью этого класса. Отметим, что все доступные свойства являются свойствами только для чтения. Туре используется для получения информации о типе, его нельзя применять для внесения изменений в тип! Свойства Туре Свойства, реализованные в Туре, могут быть разбиты на три категории: 1. Имеется ряд свойств, которые возвращают строки, содержащие различные имена, связанные с классом: Свойство Возвращает Name Имя типа данных FullName Полное имя типа данных (включая имя пространства имен) Namespace Имя пространства имен, в котором определен тип данных 2. Можно получить ссылки на объекты type, представляющие связанные классы: Свойство Возвращает ссылку Туре, соответствующую BaseType Непосредственному базовому типу для данного типа UnderlyingSystemType Типу, в который этот тип преобразуется в среде исполнения .NET (напомним, что некоторые базовые типы .NET на самом деле преобразуются в особые предопределенные типы, распознаваемые IL).
284 Глава 7 3. Существует большое число свойств boolean, которые показывают, является ли тип, например, массивом, классом, перечислением и т.д. К этим свойствам относятся: IsAbstract, IsArray, IsClass, IsEnum, Islmterface, IsPointer, IsPrimitive (только для предопределенных примитивных типов данных), IsPublic, IsSealed и IsvalueType. Например, используем примитивный тип данных: Type IntType = typeof(int); Console.WriteLine(IntType.IsAbstract); Console.WriteLine(IntType.IsClass); Console.WriteLine(IntType.IsEnum); Console.WriteLine(IntType.IsPrimitive); Console.WriteLine(IntType.IsValueType); Или для класса Vertor: Type IntType = typeof(Vector); Console.WriteLine(IntType.IsAbstract); Console .WriteLine(IntType.IsClass); Console'.WriteLine (IntType. IsEnum) ; Console.WriteLine (IntType. IsPrimitive) ,- Console.WrlreLi.iedncTy ? >. j sValueType) ; // выводит false / / выводит f al se // выводит false // выводит true // выводит true // выводит false / / выводит true // выводит false // выводит false // выводит false Также можно получить ссылку на сборку, в которой определен этот тип. Она возвра- возвращается как ссылка на экземпляр класса System.Reflection.Assembly (см. ниже): Type t = typeof(Vector) ; Assembly ContainingAssembly = new Assembly(t); Методы Большинство методов System.Type используется для получения сведений о членах соот- соответствующего типа данных: конструкторах, свойствах, методах, событиях и т.п. Сущест- Существует большое число методов, но все они следуют одному и тому же шаблону. Например, имеются два метода, которые получают сведения о методах типа данных: GetMethod () и GetMe - лг (). GetMetv -чЗ() возвращает ссылку на объект System.Ref lection.Metho- dlnf о, содержащий сведения о методе. GetMethods () возвращает массив таких ссылок. Разница заключается в том. что GetMe t lods () возвращает сведения обо всех методах, в то время как Ge<-'-"< thod() возвращает сведения только для одного метода с указанным списком параметров. Оба метода имеют перегруженные варианты, принимающие допол- дополнительный параметр Г i ndingr ags — значение перечислимого типа, показывающее, ка- какие члены должны быть возвращены, например, необходимо ли возвращать открытые члены, члены экземпляра, статические члены и т.д. Наиболее простая перегруженная версия 3etMethods () не принимает параметров и возвращает сведения обо всех открытых методах типа данных: Type t = typeof (double) ; Methodlnfо :: Methods = t. GetMethods () ,- foreach (Methodlnfo NexuMethod in Methods) ,"/ и т.л. Этому же шаблону следуют такие члены Туре: Тип возвращаемого объекта О s^ructo^-I L"o Evebtlnfo Fieldlnfo Irb°rfacelnfo Мег erlnfo I Methoaliifo |^PropertyInf з Методы (метод, оканчивающийся на s, возвращает массив) GetConstructor(), GetConstructors() Ge'.Event ( , , GetEvents () GetField(), GetFields() Get Interface!), Getlnterfaces() GetMember(), GetMembers() GetMnthod(), GetMethods() GetProperty(), GetProperties()
С# и базовые классы 285 Методы GetMember () и GetMembers () возвращают сведения о любом или обо всех членах типа данных независимо от того, являются ли эти члены конструкторами, свой- свойствами, методами и т.п. Наконец, отметим, что членов можно вызывать, пользуясь либо методом InvokeMember () Туре, либо методом Invoke () Methodlnfo, Property Info и других классов. Пример TypeView Теперь продемонстрируем некоторые особенности класса Туре, написав короткий при- пример TypeView, который можно использовать для получения списка членов типа данных. Мы применим TypeView к double, но на любой другой тип данных можно переключить- переключиться, изменив одну строку в примере. TypeView отображает больше информации, чем мо- может уместиться в окне консоли, поэтому результат будет выводиться в диалоговом окне. Запуск TypeView для double дает следующие результаты: Type Name: Double FuHName: System-Doubte Namespace: System Base Type.-VakieType UnderfyingSystern Type:Doubte PUBLIC MEMBERS: System.Double Fidd MinValue System.Double Field MaxValue System.Double Field Epsilon System.Double Field Negativelnfinity System.Double Field Positivelnfi*y System.Double Field NaN System.Double Method ToString System.Double Method GetTypeCode Systetn.Double Method ToString System.Double Method CompareTo System.Doubte Method GetHashCode System.Do'jble Method Equals System.Double Method ToString System.Double Method IsNaN System.DouKe Method Islnfinity 5ystem.Double Method IsPostivelnfinity System.Double Method IsNegativelnfinity System.Double Method ToStrr. j System.Double Method Parse System,Doub!e Method Parse System.Double Method Parse System.Double Method Parse System.Object Method GetType OK J В диалоговом окне отображаются имя, полное имя и пространство имен типа дан- данных, а также имя соответствующего системного тина и базового типа. Затем перечисля- перечисляются все открытые члены экземпляра типа данных, причем для каждого члена указываются объявляющий тип, тип члена (метод, поле и т.п. и имя члена. Объявляю- Объявляющий тип — это имя класса, который в действительности объявляет тип (другими слова- словами, System. Double, если член определен или перекрыт в Sys m.Double, или имя типа соответствующего базового класса, если член унаследован от некоторого базового класса). TypeView не выводит сигнатуры методов, так как сведения обо всех открытых членах экземпляра мы получаем с помощью объектов Member*- ю, а информация о параметрах в объекте Memberlnfo недоступна. Для получения этой информации потребуются ссылки на Methodlnfo и на другие более специфичные объекты, а это означает, что для каждого типа члена необходимо получать сведения отдельно. TypeView выводит сведения обо всех открытых членах экземпляра. Но все, что объ- объявлено для double,— это поля и методы. TypeView будет откомпилировано как консоль- консольное приложение, так как не существует помех для вывода диалоговых окон из консольных приложений. Однако тот факт, что используется диалоговое окно, означа- означает, что требуется сослаться на сборку базового класса System.Windows.Forms.dll, со- содержащую классы в пространстве имен System.Windows.Forms, в котором определен необходимый нам класс MessageBox. Код для TypeView выглядит следующим образом. Прежде всего нужно добавить несколько операторов using: using System;
286 Глава 7 using System.Text; using System.Windows.Forms; using System.Reflection; System.Text необходим, поскольку будет использоваться объект StringBuilder для построения текста, выводимого в диалоговое окно, a System.Windows. Forms нужен для са- самого диалогового окна. Весь код содержится в одном классе, MainClass. Он имеет два ста- статических метода и одно статическое поле, экземпляр StringBuilder с именем OutputText, который будет использоваться для создания текста, отображаемого в диалоговом окне. Объявление метода main и класса выглядит следующим образом: class MainClass { static StringBuilder OutputText = new StringBuilderE00) ; static void Main(string[] args; { // эту строку необходимо изменить для того, чтобы получить // сведения о другом типе данных Type t = ~ypi :of (double) ,- Ana LyzeType(t; ; MessageBox.Show(OutputText.ToString(), "Analysis of type " + t.Name); Реализация метода V.z -т. начинается с объявления объекта Туре для представления выбранного типа данных. Затем вызывается метод AnalyzeType (), который извлекает информацию из объекта Туре и использует ее для генерации выводимого текста. Нако- Наконец, полученный результат отображается в диалоговом окне. Класс MessageBox нам ра- раньше не встречался, однако его использование является чрезвычайно простым: мы вызываем статический метод Show О , передавая ему две строки, которые соответственно представляют собой заголовок и текст в окне. Метод Ana lyzeType () выполняет большую часть работы: ^.at.^ rod d AnalyzeType17yj = с { AdcTc Э-i. . '"Type name: " Л'аше); AddToOu-.put 'u]l name: " ^ t .FullName) ; AddToOutput ."!' respace: " - "..Namespace); Type tBase = _.BaseType; if (tBase != nu^l) AddToOutput I"Base Type: " *■ tBase.Name); "Yr'B '-UnderlylngSystem = t . Ur.derlyingSystemType.- j.f (tUnderlyingSystem 1= null AddToOutput("UiaerlyingSysts- Type: " + tUnderlyingSystem.Name); AddToOutput("\nPUBLIC MEMBERS:■ ; Memberlnfo [] Members = t .GecJIembers () ; foreach (Memberlnfo NextMemb ;r in Members) { AddToO itput(NextMember.Dec.aringType + " " + NextMember.MemberType + " " + NextMember.Name); Этот метод мы реализуем, вызывая различные свойства объекта Туре для получения информации об именах, затем вызываем метод GetMerbers () для получения массива объектов Me™i _Ir fo, который можно использовать для отображения сведений о каж- каждом методе. Заметим, что здесь применяется вспомогательный метод AddToOutput (), служащий для построения текста, отображаемого в диалоговом окне: static void AddToOutput(string Text) { OutputText.Append("Yn" ^ Text); Класс Assembly Класс Assembly определен в пространстве имен System.Reflection, который обеспе- обеспечивает доступ к метаданным для указанной сборки. Он также содержит методы,
С# и базовые классы 287 позволяющие исполнить сборку, при условии, что сборка является исполняемой. Подоб- Подобно классу Туре, он содержит большое число методов и свойств. Мы рассмотрим только те из них, которые необходимы для начального знакомства и используются в примере WhatsNewAttributes. Прежде всего необходимо загрузить сборку в выполняющийся процесс. Это можно сделать с помощью статического члена Assembly .Load () или Assembly .LoadFrom(). Разница между ними заключается в том, что Load () принимает имя сборки, которая должна быть указана в сборке, исполняемой в данный момент (другими словами, эта сборка должна быть указана при компиляции проекта), a LoadFrom () принимает путь к любой из сборок, присутствующих в системе: Assembly Assembly1 = Assembly.Load("SomeAssembly"); Assembly Assembly2 = Assembly.LoadFrom (@"C:\My ProjectsXGroovySoftware\SomeOtherAssembly"); Для обоих методов существуют перегруженные варианты, принимающие дополните- дополнительную информацию о правах доступа. После того, как сборка загружена^ для нее можно использовать различные свойства. Например, найдем ее полное имя: string Name = TheAssembly.FullName; Получение сведений о типах, определенных в сборке Одной из приятных особенностей класса Assembly является то, что он позволяет полу- получать сведения обо всех типах, определенных в соответствующей сборке. Для этого необ- необходимо вызвать метод Assembly .GetTypes (), который возвращает массив ссылок System.Type, содержащих сведения обо всех типах. С ссылками Туре можно работать так же, как с объектами Туре, полученными с помощью оператора С# typeof или с помощью Object.GetTypeO: Туре[] Types = TheAssembly.GetTypes(); foreach (Type DefinedType in Types) DoSomethingWith(DefinedType) ; Получение сведений о пользовательских атрибутах Методы, применяемые для выяснения того, какие пользовательские атрибуты определе- определены для сборки или типа, зависят от того, к какому типу объекта прикреплен данный тип. Чтобы узнать, какие пользовательские атрибуты прикреплены к сборке вообще, необхо- необходимо вызвать статический метод GetC .=tomAttributes () класса Attribute, передав в качестве параметра ссылку на сборку: Attribute [] DefinedAttr.butes = Attribute.GetCustomA_~ributes(TheAssembly); Это действительно важно. Возможно, у вас возникал вопрос, почему при определении пользовательских атрибутов необходимо писать для них классы и почему Microsoft не предусмотрела более простой синтаксис. Дело в том, что пользовательские атрибуты существуют как объекты, и как только сборка загружена, можно считывать эти объекты атрибутов, исследовать их свойства и вызывать их методы. GetCustomAttributes (), используемый для получения атрибутов сборки, имеет два перегруженных варианта. Если вызывать его без указания других параметров, кроме ссыл- ссылки на сборку, то он вернет все пользовательские атрибуты, определенные для сборки. Од- Однако можно также вызвать его, указав второй параметр, который является объектом Туре, задающим класс атрибута. В этом случае GetCustomAttributes О вернет массив, состоящий из всех представленных атрибутов класса. В примере WhatsNewAttributes этот перегруженный метод используется для выяснения, присутствует ли в сборке атри- атрибут SupportsWhatsNew. Чтобы сделать это, мы вызываем GetCustomAttributes () и пе- передаем ему имя атрибута. Если атрибут представлен, мы получим массив, в котором содержатся все его экземпляры. Если в сборке не определено ни одного его экземпляра, будет возвращен null: Attribute SupportsAttribute = Attribute.GetCustomAttributes(TheAssembly, typeof (SupportsWhatsNewAttribute)) ,- Отметим, что все получаемые атрибуты представляют собой ссылки на Attribute. Если необходимо вызывать методы или свойства, определенные для пользовательских ат- атрибутов, эти ссылки придется явно привести к соответствующему классу пользовательско- пользовательского атрибута. Сведения о пользовательских атрибутах, прикрепленных к конкретному типу Зак. С9
288 Глава 7 данных, можно получить, вызвав перегруженный метод Assembly .GetCustomAttributes () с передачей ссылки Туре, описывающей тип, для которого нужно получить все прикреп- прикрепленные к нему атрибуты. С другой стороны, если требуется получить атрибуты, привя- привязанные к методам, конструкторам, полям и т.д., то потребуется вызвать метод GetCustomAttributesO, являющийся членом одного из классов Methodlnfo, Const- ructorinfo, Fieldlnfo и т.п. В данной главе это рассматриваться не будет. Завершение примера WhatsNewAttributes Теперь у нас достаточно информации для завершения примера WhatsNewAttributes, т.е. для написания исходного кода сборки LookUpWhatsNew. Эта часть является консольным приложением. Однако она должна ссылаться на другие две сборки. Хотя это — приложение командной строки, мы будем отображать результаты в диалоговом окне, так как будет выводиться большое количество информации, которое не поместится в окне консоли. В исходном коде первое, что мы делаем,— указываем используемые пространства имен. System.Text нужно потому, что нам опять потребуется объект StringBuilder: using System; using System ..Reflection; Using System.Windows.Forms; using System .Text; using Wrox.ProfessionaiCSharpiChapter7.VectorClass; using Wrox.ProfessionalCSharp.Chapter7.WhatsNewAttributes; namespace Wrox.ProfessionaiCSharp.Chapter7.LookUpWhatsNew { Теперь рассмотрим класс WhatsNewChecker. который будет содержать точку входа основной программы и другие методы. Все методы, которые мы определим, будут распо- располагаться в этом классе. Он будет иметь также два статических поля. Первое поле, output- Text, содержит текст, формируемый для вывода в диалоговом окне. Второе, BackDateTo, хранит выбранную дату — будут выводиться все изменения, начиная с этой даты. Обычно для ввода даты отображается диалоговое окно, предлагающее пользователю выбрать значение, но мы не будем рассматривать такой код (кроме того, вы еще не изучили главу Windows Forms, поэтому не знаете, как выводить сложное диалоговое окно!). По этой причине мы инициализируем BackDateTo фиксированным значением 14 мая 2001. Впоследствии эту дату можно легко изменить в коде: class WhatsNewChecker ,i static 'stringBuilder output-Text = new StringBuilderA000); static DateTime BkcfcDateTo '-' new DateTimeB001, 5, 14); static void Main(string!] args) { Assembly TheAssembly = Assembly.Load("VectorClass"); 1 , Attribute SupportsAttribute = Attribute.GetCustomAttribute (TheAs,sembly, typeof(SupportsWhatsNewAttribute)); .string Name = TheAssembly.FullName; AddToMessage("Assembly: " + Name); if (SupportsAttribute == null) { AddToMessage( "This assembly does not support WhatsNew attributes"); ./= return; „ У else AddToMessage ("Defined Types':"); :lype [ ] Types = TheAssembly. GetTypes (); 'foreach (Type DefinedType in Types) DisplayTypelnfo(TheAssembly, DefinedType); MessageBox.Show(outputText.ToString(), "WhatVs New since; + BackDateToiToLongDateString() ); Console.ReadLine(); } Метод Main () сначала загружает сборку VectorClass и проверяет, отмечена ли она ат- атрибутом Support sWhatsNew. Хотя мы только что откомпилировали сборку, в которой этот атрибут присутствует, проверка все равно необходима в том случае, если пользователю будет предложено выбрать сборку для проверки.
С# и базовые классы 289 В предположении, что проблем не возникло, используется метод Assembly .GetTypes () для получения массива всех типов, определенных в сборке. Затем они обрабатываются в цикле. Для каждого из них вызывается метод DisplayTypelnfо (), который добавляет со- соответствующий текст, включая сведения обо всех экземплярах LastModif iedAttribute, к полю outputText. Наконец, выводится диалоговое окно с полным текстом. Метод DisplayTypelnf о () выглядит следующим образом: static void DisplayTypelnfо(Assembly theAssembly, Type cheType) '{ // убедимся в том, что рассматриваются только классы if (!(theType.IsClass)) return; AddToMessage("\nclass " ♦ theType.Name); Attribute [-J Attribs = Attribute.GetCustomAttributes(theType) ; if (Attribs.Length ==0) AddToMessageC'No changes to chis class\n"); else loreach (Attribute'. Attrib in Attribs) ч WriteAttributelnfо(Attrib); Methodlnfo [j Methods = theType. GetMethods <) ,- AddToMessage'C"CHANGES TO METHOD? OF THIS CLASS:"); foreach (Methodlnfo NextKethod in Methods) { ,object [J Attribs2 = NextMethod.GetCustomAttributes(typeof(LastModifiedAttribute), false); if (Actribs != null) { AddToMessage(NextMethod.ReturnType + " " + NextMethod.Name + "()"); foreach (Attribute NextAttrib in Attribs2) WriteAttributelnfо(NextAttrib); Первое, что делается в этом методе,— осуществляется проверка того, что переданная ссылка Туре представляет собой класс. Для упрощения примера было указано, что атри- атрибут LastModif ied может применяться только к классам или методам класса, и мы лишь потеряем время на обработку элемента, не являющегося классом (это может быть в принципе структура, делегат или перечисление). Затем используется метод Attribu - ^.GetCustomAttribjtes О для выяснения того, имеет ли этот класс прикрепленные экземпляры LastModif iedAttribute. Если да, то сведения о них добавляются в выходной текст с применением вспомогательного метода WriteAttributelnf о () (см. ниже). Наконец, для прохода по всем методам класса этого типа данных используется метод Type.GetMethods (', а затем для каждого метода делается то же самое: проверяется, прикреплены ли к нему экземпляры LastModif iedAttribute, и если да, то производится их отображение с помощью WriteAttributelnf о ( . В следующем фрагменте кода показан метод WriteAttributelnfo (), который выясняет, какой текст необходимо отобразить для заданного экземпляра LastModif iedAttribute. Отметим, что в этот метод передается ссылка на Attribute, поэтому сначала он должен привести ее к ссылке на LastModif т. eclAttribute. Для получения его параметров исполь- используются свойства, определенные первоначально для атрибута. Затем проверяется, что дата, указанная в атрибуте, является более поздней, чем заданная, и только после этого производится добавление текста: static void WriteAttributelnfo(Attribute Attrib) : { LastModifiedAttribute LastModifiedAttrib = Attrib as LastModifiedAttribute; if (LastModifiedAttrib == null) return; // проверка того, что дата находится в заданном диапазоне DateTime ModifiedDate «= LastModif iedAttrib.DateModif ied;
290 Глава 7 if (ModifiedDate < BackDateTo) „ return; AddToMessage(" MODIFIED: " + ModifiedDate.ToLongDateString() + " : " ) ; AddToMessage (" ■" + Las.tModif iedAttrib.Changes) ; if (La'stModifiedAttrib. Issues != null) sAddToMessagel" Outstanding issues:" + LastModifiedAttrib.Issues); AddToMessage(""); } Наконец, определим вспомогательный метод AddToMessage (): j. static void AddToMessage (string message) ^ outputText.Append("Лп" + message); } Выполнение кода даст следующий результат: What's New since 141 yktofcisss, <> _,-. _0: 16Мэу2001: ' и,|кЧг.-':'--: Khwmerabte interface Implemented , ""?фл " So Vector can now be treated as a collection '■'■' CHANGES TO METHOOS OF THIS CLASS: System.Collections.IEnumeratorGetEnumerato'O MODIFIED: 16 May 2001: Method added in cvtier to provide collection support Syscem:Strng ToStringO К ' "-*--S ■ 1 I* j!<! Systern.Boplean Equ^lsO "' у System,String ToStr'mgO (. ,:"" System.Oouble get_Rem() ' ?i System.Void set_Item() System.Boolean op_Equatly(> System.Воокал opJnequalityO Vi*ox.ProfessionalCSr»rp.Cha^er7.Vect0rCtass.Vectpr«b Addittonf) •. ,й-' :, •'■ Wrox.ProfesaonaK:Sharp;cbapter7.Vectoraass.Vectof op_MJtiplyO л " Wrox.Profe55ionalCShsrp.Chapter7.VectorClass-Vectcf ор_МиИр1у() System.Doubte opJ'KjItiplyO ,, ' 4 Systefn.DoubleMormO ' *' Syrtem.Type GetTypeO classWrox.ProfessionalCSharp.Chapt^.VectorClasSiVector+Vector^numerator ,r v MODIFIED* 16 May 2001: • -'" •- ■- .,, ! ^ ■ Class created as part of colettion support for vector ' CHANGES TO METHODS Ofi THIS CLASS: ; System. Void ResetO s System.Ob>ectget_CurreM6 System.BooteanMoveNext() . Syitem.Int32GetHashCode0. " Syitem.BooleanEquabO System.String ToStmg() „ Syrtem.Type GetTypeO ■"-■ f1 ■■.' '»- OK Отметим, что при перечислении типов, определенных в сборке VectorClass, на са- самом деле исследовались два класса: Vect ~>r и внутренний класс VectorEnumerator, кото- который был добавлен для преобразования Vector в коллекцию (см. выше). Поскольку BackDateTo была жестко установлена в значение 14 мая, взяты только атрибуты за 16 мая (когда была добавлена поддержка коллекций), но не обработаны атрибуты за 10 мая (когда был добавлен интерфейс IFormattable). Потоки В этом разделе рассматривается поддержка, предоставляемая С# и базовыми классами .NET для разработки многопоточных приложений. Мы познакомимся с классом Thread и разработаем примеры, иллюстрирующие принципы поточности. На этих примерах
С# и базовые классы 291 исследуем некоторые проблемы, возникающие при синхронизации потоков. Это сложная тема, и мы обсудим лишь основные принципы — настоящее многопоточное приложение разрабатываться не будет. Поток представляет собой последовательность исполнения в программе. Для про- программ С#, приведенных выше, существует только одна точка входа: метод Main (). Ис- Исполнение начинается с первой строки этого метода и заканчивается, когда происходит выход из него. Такая структура программы хорошо подходит для приложения, в котором присутст- присутствует одна явная последовательность задач. Но часто программа должна выполнять не- несколько заданий одновременно. Вам наверняка знакома ситуация, когда, запустив Internet Explorer, вы сидите и ждете завершения загрузки страницы. В конце концов, когда вам это надоест, вы нажмете кнопку Back или наберете другой URL. Чтобы это ра- работало, Internet Explorer должен делать как минимум три вещи: захватывать данные для страницы и другие файлы, поступающие из Интернета, отрисовывать страницу и сле- следить за вводом пользователя, который может потребовать от IE выполнения других действий. То же самое происходит, когда программа выполняет какую-то задачу и од- одновременно отображает диалоговое окно, с помощью которого можно в любой момент отменить выполнение этой задачи. Давайте более внимательно рассмотрим пример с Internet Explorer. Упростим проб- проблему, игнорируя задачу сохранения данных по мере их поступления из Интернета, и предположим, что Internet Explorer должен выполнять две задачи: отображать страницу и следить за вводом пользователя. (Допустим, что для вывода страницы требуется много времени. Она может содержать либо JavaScript, либо элемент в отмеченной области, ко- который требует постоянного обновления.) Одним из решений может быть написание ме- метода, который рисует малую часть страницы. Через небольшое время, например через одну двадцатую секунды, метод проверяет, не было ли ввода данных пользователем. Если такое событие произошло, оно начинает обрабатываться (что может означать отмену задачи рисования). В противном случае метод продолжает рисовать страницу в течение следующей одной двадцатой секунды. Такой подход будет работать, но реализация метода окажется очень сложной. На са- самом деле он полностью игнорирует архитектуру событий Windows. Напомним: если по- пользователь вводит данные, система уведомит об этом приложение, создав событие. Нужно изменить метод, чтобы Windows могла использовать события: О Написать обработчик событий, который откликается на ввод пользователя. Отклик может включать в себя установку некоторого флага, указывающего, что отрисовка должна остановиться. О Написать метод, который производит отрисовку. Метод должен быть разработан таким образом, чтобы исполняться в то время, когда не делается ничего другого. Это решение лучше, поскольку оно работает с архитектурой событий Windows, но вряд ли можно позавидовать тем, кому придется создавать метод отрисовки. Посмотри- Посмотрите, что он должен делать. Для начала он обязан внимательно следить за временем своего исполнения. Пока метод исполняется, компьютер не может реагировать на ввод пользо- пользователя. Значит, метод должен отметить, когда он был запущен, затем контролировать время по мере выполнения своей работы и по прошествии определенного времени с мо- момента запуска — максимальное значение для сохранения способности реагировать на действия пользователя составляет одну десятую секунды — должен вернуть управление вызывающей программе. Более того, перед передачей управления методу необходимо будет сохранить свое текущее состояние, чтобы при следующем вызове начать с этой точки. Конечно, можно написать метод, который производил бы все эти действия, и во времена Windows 3.1 именно так и пришлось бы поступить. К счастью, NT3.1, а затем и Windows 95 реализовали многопоточные процессы, предоставив более удобный путь для решения подобных проблем. Многопоточные приложения Приведенный пример проиллюстрировал ситуацию, в которой приложению необходи- необходимо выполнять более одного действия одновременно. Очевидное решение — предоста- предоставить приложению несколько потоков исполнения. Напомним, что поток — это последовательность инструкций, которую исполняет компьютер. Приложение может иметь столько таких последовательностей, сколько необходимо. Все, что требуется, это каждый раз при создании нового потока исполнения указывать метод, в котором должно начаться выполнение. Первый поток приложения всегда запускает метод Main(),
292 Глава 7 поскольку первый поток запускается средой исполнения .NET, а среда исполнения выби- выбирает именно метод Main (). Все остальные потоки запускаются внутри вашего приложе- приложения, следовательно, приложение само выбирает, с какого момента начать исполнение потоков. Принципы работы До сих пор мы довольно свободно говорили о потоках, исполняющихся одновременно. На самом деле один процессор одновременно может выполнять только одно задание. Если у вас многопроцессорная система, теоретически осуществимо одновременное ис- исполнение нескольких инструкций. Но на компьютерах с одним процессором это не- невозможно. В действительности операционная система Windows создает впечатление одно- одновременности различных действий с помощью процедуры, называемой вытесняющей многозадачностью. Вытесняющая многозадачность означает, что Windows выбирает поток для некоторо- некоторого процесса и позволяет этому потоку исполняться в течение короткого промежутка вре- времени. Microsoft не документировала длительность этого интервала времени, поскольку это один из внутренних параметров операционной системы, для которого Microsoft хочет сохранить возможность изменения в будущих версиях Windows с целью обеспечения оп- оптимальной производительности. В любом случае для запуска приложений Windows не тре- требуется знать длительность этого промежутка времени. По человеческим меркам он очень мал — не более нескольких миллисекунд. Этот промежуток известен как квант времени процесса. Когда квант времени выработан, Windows передает управление другому потоку, для которого выделяет свой квант времени. Кванты времени настолько малы, что у нас создается впечатление одновременности многих действий. Даже в том случае, если в приложении используется всего один поток, вытесняющая многозадачность продолжает действовать, поскольку в системе работает множество дру- других процессов, для потоков каждого из которых требуется выделять свои кванты време- времени. Именно по этой причине при отображении на экране большого числа окон, каждое из которых представляет свой процесс, можно щелкнуть мышью в любом из них, и оно тут же откликнется на это действие. Этот отклик не является немедленным — он проис- происходит, когда поток соответствующего процесса, ответственный за пользовательский ввод для этого окна, получает очередной квант времени. Однако, за исключением случа- случаев, когда система занята, задержка перед получением кванта времени настолько мала, что вы даже не замечаете ее. Обработка потоков Работа с потоками осуществляется с помощью класса Thread, который расположен в пространстве имен System.Threading. Экземпляр Thread представляет собой один по- поток — одну последовательность исполнения. Другой поток можно сформировать путем создания экземпляра объекта Thread. Запуск потока Допустим, что вы пишете графический редактор, и пользователь требует изменить глу- глубину цвета для изображения. Этот случай взят для примера потому, что выполнение дан- данной операции для большого изображения может потребовать довольно много времени, т.е. для выполнения обработки вы, скорее всего, создадите новый поток, в результате чего пользовательский интерфейс не перестанет работать на время изменения глубины цвета. Сначала необходимо создать экземпляр объекта Thread: Thread DepthChangeThread = new ThreadО; Здесь переменной присваивается имя Dep ;hChangeThread. Дополнительные потоки, которые создаются внутри приложения для выполнения некоторых задач, часто называют рабочими потоками. Потоку следует давать имя. дружественное по отношению к пользователю. Присвоить потоку имя можно с помощью свойства Name: DepthChangeThread.Name = "Depth Change Thread"; Пока наш поток не сделал ничего полезного. Он просто ожидает, когда его запустят. Для запуска потока необходимо указать, какой метод будет в нем исполняться. Этот метод не должен принимать параметров и обязан возвращать void. Поток запускают вызовом метода Thread.Start О, передавая ему сведения о точке входа. Это означает передачу
С# и базовые классы 293 сведений о методе, так что мы имеем классический случай использования делегатов. На самом деле такой делегат уже объявлен в пространстве имен System.Threading. Он называется ThreadStart (), его сигнатура: public delegate void ThreadStart(); Теперь можно запустить новый поток. Допустим, что имеется метод ChangeColorDepth (), который выполняет необходимую обработку: void ChangeColorDepch() { , JI измерение глубины цвета изображения Это может быть сделано с помощью кода: Thread DepthChangeThread = new Thread(); DepthChangeThread.Name - "Depth Change Thread"; ThroadStart -EntryPoint = new ThreadStart{ChangeColorDepth); DepthChangeThread.Start(EntryPoint); После выполнения последней строки оба потока будут работать одновременно. 3 Bas<ri> i;w.C -Ma Так как точка входа потока (в этом примере — ChangeColorDepth ()) не может при- принимать параметров, придется найти другой способ передачи информации, необходимой методу. Наиболее очевидным способом является использование полей того класса, к ко- которому принадлежит этот метод. Кроме того, метод ничего не может возвращать. (Куда будет передаваться возвращаемая величина? После выхода из метода поток, в котором он выполнялся, завершится, и получать возвращаемое значение будет нечему.) После запуска нового потока его исполнение можно приостановить, продолжить или отменить. Приостановление исполнения потока означает перевод его в спящий ре- режим — некоторое время поток не будет исполняться, т.е. он не будет занимать процес- процессорное время. Позже его исполнение можно будет продолжить с той точки, в которой он был приостановлен. Если исполнение потока отменено, он прекращает свою работу. Windows уничтожит все данные, связанные с этим потоком, поэтому его уже нельзя будет запустить повторно. Продолжая пример с графическим редактором, допустим, что по какой-то причине поток пользовательского интерфейса отображает окно, предоставляющее пользователю возможность временно приостановить процесс преобразования. (Непонятно, зачем это нужно в данном случае, но это всего лишь пример. Более реалистичным примером было бы, если бы пользователь приостанавливал проигрывание звукового или видеофайла.) Отклик на это действие можно было бы осуществить следующим образом (в основном потоке): DepthChangeThread.Suspend(); Если пользователь пожелает продолжить исполнение потока, то: DepthChangeThread. Resu-ne (>; ; Если пользователь решит отменить преобразование (что больше похоже на правду) и нажмет кнопку "Отмена", то: DepthChangeThread:Abort{); Отметим, что методы Suspend () и Abort () необязательно сработают сразу после вызо- вызова. В случае Suspend () среда исполнения .NET может остановить поток через несколько
294 Глава 7 инструкций для того, чтобы достичь точки, в которой поток можно безопасно остано- остановить. Это делается по техническим причинам, для обеспечения корректной работы сборщика мусора; подробности можно найти в документации MSDN. В случае отмены исполнения потока метод Abort () на самом деле генерирует для соответствующего по- потока ThreadAbortException. ThreadAbortException является специальным классом исключения, который никогда не обрабатывается. Это нужно для того, чтобы в случае, если поток исполняет в данный момент код внутри блоков try, для него выполнились бы соответствующие блоки finally перед его уничтожением. Это гарантирует, что бу- будут освобождены все необходимые ресурсы, а также то, что данные, с которыми работа- работает поток (например, поля экземпляра класса, которые остаются после завершения потока), будут находиться в правильном состоянии. На платформах, предшествующих .NET, не рекомендовалось отменять потоки таким образом, за исключением крайних случаев, поскольку соответствующий поток немедленно останавливался, и данные, с которыми он работал, могли быть оставлены в неправильном состоянии, а все ресурсы, используемые потоком, оказывались незакрытыми. Механизм исключений, используемый в данной ситуации .NET, означает, что отмена потоков является более безопасной и приемлемой практикой программирования. Такой механизм исключений делает отмену потоков безопаснее, однако остановка потока может на самом деле занять больше времени, так как теоретически не существует ограничений на размер кода, который может располагаться в блоке finally. В результа- результате после завершения потока вам, возможно, потребуется дождаться того момента, когда он будет остановлен, если последующие действия зависят от того, был ли остановлен поток или нет. Подождать завершения потока можно при помощи метода Join (): DepthChangeThread. Abort(); DepthChangeThread.JoinO ; Join > имеет другие перегруженные варианты, которые позволяют указать предел времени, в течение которого вы готовы ожидать завершения процесса. По достижении этого значения исполнение основной программы будет продолжено в любом случае. Если предел времени не указан, ожидание завершения потока будет производиться столько времени, сколько потребуется. Приведенные фрагменты кода показывают, как один поток выполняет действия для другого потока (или, в случае J^In (), ожидает другой поток). Но что происходит в том случае, если основной поток желает совершить некоторые действия для самого себя? Что- Чтобы сделать это, ему необходима ссылка на объект потока, который представляет свой соб- собственный поток. Ее можно получить, используя статическое свойство CurrentThread класса Threaa: Thread MyOwnThread = Thread.CurrentThread; Класс Thread несколько необычен, поскольку перед созданием новых потоков всегда присутствует один поток - тот, что исполняется в данный момент. Это означает, что классом мождо управлять двумя способами: 3 Можно создать экземпляр объекта Thread, который будет представлять запущенный поток и члены которого будут принадлежать этому потоку. О Можно вызвать любой из статических методов. Эти методы будут в основном применимы к потоку, из которого они вызываются. Один из статических методов, которые могут потребоваться для вызова,— метод Sleep (). Он приостанавливает текущий поток на заданный период времени, после чего его исполнение продолжается. Пример ThreadPlayaround Начнем с простого примера, который мы назовем ThreadPlayaround. Цель этого приме- примера — дать общее представление о том, как работают потоки, поэтому в нем не отражаются реальные ситуации программирования. Мы лишь запустим пару потоков и посмотрим, что происходит. Центральной частью примера ThreadPlayaround является короткий метод Display- Numbers (), который отсчитывает большое число, каждый раз показывая, до какого зна- значения он дошел. DisplayNumbers () также выводит имя и региональные настройки для потока, из которого он запущен:
С# и базовые классы 295 static void DisplayNumbersO { [Thread ThisThread =■ Thread.CurreptThread; string Namei=- ThisThread.Name; Console.WriteLinef "Starting thread: " + Name),- i Console.WriteEine(Name + ": Current culture = " +' ThisThread.CurrentCulture); "for {irit 1=1; I<5= 8 «Interval,- I++) .{ if (I%Interval =* 0) i Console.WriteLine(Name + ": count has reached " -t- I) ; Момент, до которого производится отсчет, определяется полем interval. Его значе- значение вводит пользователь. Код работает таким образом, что при вводе пользователем зна- значения 100 подсчет производится до 800 с отображением результатов 100, 200, 300, 400, 500, 600, 700 и 800. Если пользователь введет 1000, то подсчет будет производиться до 8000 с выводом на экран 1000, 2000, 3000, 4000, 5000, 6000, 7000 и 8000 и т.д. Этот метод в принципе бесполезен, его цель — занять процессор на некоторое время и одновременно показать, какую часть задания он уже выполнил. ThreadPlayaround запускает также второй рабочий поток, который будет выполнять DisplayNumbers (), и сразу после этого сам начинает исполнять тот же самый метод. Это означает, что оба отсчета должны производиться одновременно. Метод Main () и содержащий его класс для ThreadPlayaround выглядят так: class EntryPoint '•*■* Г ' „" ■ - '' Static ifat Interval; static void Main(string!] args) { Console.Write("Interval to display results at?> "); Interval = int.Parse!Console.ReadLineO) ; j , Thread ThisThread = Thread.CurrentThread; ThisThread.Name '= "Main Thread"; ';■ ThreadStart WorkerStart = new ThreadStart(StartMethod); i Thread WorkerThread = new Thread(WorkerStart); WorkerThread.Name = "Worker"; WpirkerThread.Stare (); 7. DisplayNumbers (.) ; Console.Writebine{"Main Thread Finished"); p Console.ReadLineO; . > Здесь показано начало объявления класса, откуда видно, что Interval является ста- статическим полем класса. В методе Main () пользователю предлагается ввести интервал. Затем мы получаем ссылку на потоковый объект, представляющий собой основной по- поток. Это делается для того, чтобы присвоить потоку имя и посмотреть, что происходит на выходе. Затем создается рабочий поток, ему присваивается имя, и он запускается, при этом ему передается делегат, показывающий, что необходимо запустить метод WorkerStart. Наконец, мы вызываем метод DisplayNumbers v) для начала отсчета. Точка входа для рабочего потока: 'static void StartMethod() { ;DisplayNumbers () ; .ConsoleWriteLine("Worker thread finifehed"); } : - ■;:' -'. Отметим, что все три метода: Main (), StartMethod () и DisplayNumbers () — явля- являются статическими методами одного класса, EntryPoint. Два отсчета будут вестись от- отдельно, так как переменная I, используемая в методе DisplayNumbersO для подсчета, является локальной. Локальные переменные располагаются в области видимости того метода, в котором они определены, и видны потоку, исполняющему этот метод. Если тот же самый метод начинает исполняться другим потоком, то этот поток получит свою собственную копию локальных переменных. Начнем с запуска кода с передачей значе- значения 100 в качестве интервала:
296 Глава 7 Interval to display 1№&й1*Ш&ь I Starting thread: Main Т|«*ёа*|- Start insJtlw*? ad Х111а*иёгЖлг'Г~~ж Worker i^builtiia&^eafcliea 100 iStouht' lias reached 200 Если говорить о параллельности исполнения потоков, то здесь она не наблюдается! Видно, что сначала выполняется основной поток, и только после этого — рабочий поток. Проблема здесь состоит в том, что запуск процесса является довольно сложным. После создания нового потока основной поток встречает строку кода: WoricferThread.Start(); Этот вызов Thread. Start () сообщает Windows, что необходимо начать исполне- исполнение нового потока, после чего управление сразу возвращается вызывающей програм- программе. Пока мы считаем до 800, Windows делает приготовления для запуска потока. Это означает, что внутри система выделяет потоку ресурсы и производит проверки прав доступа. К том)' времени, когда будет запущен новый поток, основной поток уже закончит свою работу! Эту проблем)- можно решить, выбрав больший интервал, в результате чего оба потока потратят больше времени на метод DisplayNumbers (). На этот раз попробуем 1000000: InteiHial to-'dispiay wssults. atl 1000000 H Seai-tinft,threadifMain Thread. ВД Ж;-"■■*$* Я Hirt^h^dS-C^ht^l/f^^dr ■ IISirt^TJt^adfJfdOutie^ has reached S80080 i/orker^ abtmt has i-eached зйааааа :..•-, Hairt,iTJ»rdadSlSc'oune has reached 6000009 ft Main.Thread) Finished Теперь видно, что потоки действительно работают параллельно. Основной поток стартует и считает до миллиона. В тот момент, когда основной поток подсчитывает тре- третий миллион, запускается рабочий поток — и теперь оба потока действуют одновременно с одинаковой скоростью до тех пор, пока их работа не завершится. Важно понимать, что, если только работа не производится на многопроцессорной ма- машине, применение двух потоков для задачи, интенсивно использующей процессор, не сэко- сэкономит времени. На однопроцессорной машине время, необходимое для выполнения
С# и базовые классы 297 двух потоков, считающих до 8 миллионов, равно времени, которое требуется одному по- потоку для подсчета 16 миллионов. Вообще говоря, это время будет даже еще большим, по- поскольку операционной системе придется выполнять дополнительные переключения потоков, однако разница незначительна. Использование нескольких потоков дает два преимущества. Во- первых, вы получаете возможность быстрого реагирования на дейст- действия пользователя, так как один поток занимается обработкой ввода пользователя, а дру- другой в это время выполняет текущие действия. Во-вторых, вы экономите время, если один или несколько потоков выполняют операции, не требующие процессорного вре- времени (например, ожидание получения данных из Интернета: пока неактивные потоки заняты ожиданием, другие потоки могут выполнять те или иные действия). Приоритеты потоков Внутри процесса различным потокам можно присвоить разные приоритеты. Вообще гово- говоря, потоку не будут выделены кванты времени в том случае, если существует поток с более высоким приоритетом. Преимущество этого заключается в том, что можно, например, га- гарантировать получение ввода пользователя, присвоив этому процессу более высокий при- приоритет. Большую часть времени такой поток не будет осуществлять никаких действий, а другие потоки смогут выполнять свою работу. Однако если пользователь произведет ка- какие-то действия, поток немедленно увеличит приоритет по сравнению с другими потоками приложения на короткое время для обработки события. Потоки с более высоким приоритетом могут полностью блокировать потоки с низким приоритетом, поэтому следует быть осторожным при изменении приоритетов потоков. Приоритеты потоков определены как значения ThreadPrioi ityEnumeration. Возможные значения: Highest, AboveNorrnal, Normal, BelowNormal, Lowest. Необходимо отметить, что каждый процесс имеет базовый приоритет, а перечислен- перечисленные значения задаются относительно приоритета вашего процесса. Присвоение потоку более высокого приоритета гарантирует, что он получит преимущество по сравнению с другими потоками в данном процессе, однако в системе могут быть запущены процессы, потоки которых имеют еще более высокий приоритет. По очевидным причинам Windows указывает более высокий приоритет для своих собственных потоков. Эффект от изменения приоритета потока можно увидеть, внеся следующие изменения в метод Main () примера ThreadPlayaround: ThreadStart WorkerStart = r_ew ThreadStart (StartMethod) .- Thread WorkerThread = new Tnread(WorkerStart); WorkerThread.Name = "Worker"; WorkerThread.Priority = ThreadPriority.AboveNormal; WorkerThread.Start(); Все, что мы сделали,— это указали, что рабочий поток должен иметь более высокий приоритет по сравнению с основным потоком. Результат впечатляет: Этот пример показывает: если рабочий поток имеет приоритет выше нормального, основной поток выполняется лишь один раз перед началом исполнения рабочего потока.
298 Глава 7 Синхронизация Одним из важных аспектов работы с потоками является синхронизация доступа к пере- переменным со стороны нескольких потоков. Под синхронизацией понимается то, что толь- только один поток должен получить доступ к переменной в данный момент времени. Если это требование не соблюдается, в программе могут появиться трудно обнаруживаемые ошибки. В этом разделе коротко рассматриваются некоторые из основных возникающих проблем. Понятие синхронизации Проблема синхронизации возникает по той причине, что то, что в С# выглядит как один оператор, в окончательной сборке, как правило, транслируется в множество операторов на машинном языке. Для примера рассмотрим оператор: message += ", there"; // message представляет собой строку, которая // содержит "Hello" Этот оператор в С# синтаксически выглядит как один оператор, однако в процессе исполнения кода производится целый рял операций. Для того чтобы сохранить более длинную строку, требуется выделить память, переменной message необходимо присво- присвоить ссылку на новый участок памяти, соответствующий текст нужно скопировать, и т.д. Конечно, строки — один из самых сложных типов данных, но даже при выполнении простейших арифметических операций с примитивными числовыми типами нередко осуществляется гораздо больше действий, чем можно предположить, глядя на код С#. В частности, многие операции не могут быть произведены напрямую для перемен- переменных, хранящихся в памяти, их значения необходимо отдельно копировать в регистры процессора. В любой ситуации, когда один оператор кода С# транслируется в более чем одну команду машинного языка, квант времени потока может закончиться на середине испол- исполнения процесса для этого оператора. В этом случае квант времени может быть предо- предоставлен другому поток) того же процесса, и если доступ к оператору не синхронизирован, другой поток может попытаться прочесть или записать те же самые переменные. Что в нашем примере другой поток намеревался увидеть — новое или старое значение переменной? Проблема может стать еще серьезнее. В сложном операторе некоторые переменные в течение короткого времени могут быть вообще не определены. Если другой поток в этот момент прочитает значение, он получит мусор. Если два потока одновременно попытают- попытаются записать данные в одну переменную, то в результате практически на сто процентов переменная будет содержать неверные данные. Проблема синхронизации не затрагивает пример T'ireadPlaya round, так как оба по- потока используют только локальные переменные. Единственная переменная, к которой имеют доступ оба потока, — это поле Inte.: га1, но оно инициализируется в основном потоке до запуска рабочего потока. Кроме того, оба потока в дальнейшем только читают значение этой переменной. Проблемы синхронизации возникают тогда, когда один поток производит запись в переменную, а другие потоки могут читать или изменять ее значение. К счастью. С# обеспечивает простой способ синхронизации доступа к переменным с помощью ключевого слова ± -^ск. Оно используется следующим образом: 1оск(Х) { DoSomeching() ; } Операторы lock используют объект, известный как взаимная исключающая блоки- блокировка (или мьютекс), для переменной, записанной в круглых скобках. Мьютекс существу- существует до тех пор, пока выполняется составной оператор, указанный для ключевого слова lock. Пока переменная блокирована, другой поток не может получить к ней доступ. Если во время исполнения приведенного кода процесс теряет квант времени, а другой процесс получает его и пытается осуществить доступ к переменной, ему будет отказано в доступе. Windows приостановит другой поток до того момента, как мьютекс будет освобожден. Мьютекс является простейшим из механизмов, применяемых для управления доступом к переменным. К сожалению, мы не в состоянии рассмотреть остальные, однако отметим, что все они используются с помощью базового класса .NET System.Threading.Monitor. На самом деле оператор С# 1с jk является синтаксической оболочкой для вызовов методов этого класса. Мьютексы в действии будут показаны в следующем примере.
С# и бозовые классы 299 В целом переменные необходимо синхронизировать, если существует риск того, что поток может записывать в переменную тогда, когда другие потоки пытаются записать или прочитать ту же самую переменную. Синхронизация потоков довольно обширная тема. Мы обсудим лишь некоторые потенциальные проблемы. Проблемы синхронизации Синхронизация потоков жизненно важна в многопоточных приложениях. Однако она требует особой осторожности, так как здесь возможен целый ряд трудно обнаруживаемых ошибок — в частности, блокировки и состязания. Не злоупотребляйте синхронизацией Синхронизация потоков должна использоваться только тогда, когда это действительно необходимо, поскольку она может серьезно ухудшить производительность. Это происхо- происходит по двум причинам. Во-первых, существуют потери производительности, связанные с блокировкой объекта и ее снятием, хотя они и невелики. Во-вторых, что гораздо важнее, чем больше синхронизации, тем чаще потоки будут ждать освобождения объектов. На- Напомним, что если один поток захватил объект, то все остальные потоки, которым необ- необходим тот же объект, будут приостановлены до его освобождения. Поэтому в блок lock следует помещать небольшой фрагмент код, чтобы он не вызывал ошибок, связанных с синхронизацией потоков. В некотором смысле lock можно рассматривать как операто- операторы, временно отменяющие многопотоковые способности приложения и, следовательно, временно убирающие все выгоды от применения многопоточности. С другой стороны, необходимо сказать, что опасности от слишком частого использо- использования синхронизации (снижение производительности и способности к отклику) менее се- серьезны, нежели опасности, связанные с отсутствием синхронизации, когда она требуется (возникают трудно обнаруживаемые ошибки). Блокировки Блокировка является ошибкой, которая может возникнуть, когда два потока пытаются получить доступ к ресурсам, блокированным друг другом. Допустим, что поток исполня- исполняет следующий код, где Айв являются ссылками на объекты, к которым имеют доступ оба потока: lock (A) { *//■ выполняем действия lock(В) { / / выполняем действия В то же самое время другой поток выполняет код: lock {В) 1 '-'' i /J выполняем действия J-OckJA) IJ. выполняем действия В зависимости от того, в какой момент потоки начнут исполнять различные операто- операторы, вполне возможен такой сценарий: первый поток блокирует А, и примерно в это же время второй поток блокирует в. Спустя некоторое время первый поток встречает опера- оператор lock (В) и приостанавливается, ожидая, когда будет снята блокировка с В. После это- этого второй поток встречает lock (А) и также приостанавливается, ожидая, когда будет снята блокировка с А и Windows продолжит его исполнение. К сожалению, блокировка с А никогда не будет снята, поскольку первый поток, который обладает этой блокировкой,
300 Глава 7 сам приостановлен и не будет снова запущен до тех пор, пока не будет снята блокировка с В, а этого не произойдет, пока второй поток не продолжит свое исполнение. В результате возникает блокировка. Оба потока ничего не делают, ожидая, когда каждый из них сни- снимет свою собственную блокировку. Такая проблема может привести к тому, что зависнет все приложение, и не останется ничего иного, кроме как завершить процесс с помощью Task Manager. В данной ситуации никакой другой поток снять блокировку не может: взаимная исключающая блокировка может быть снята только тем потоком, который ее объявил. Блокировки обычно не возникают, когда потоки захватывают объекты в одной по- последовательности. В приведенном примере, если бы второй поток объявил захват объек- объектов в том же порядке, что и первый поток — сначала А, затем В — то какой бы поток ни захватил А первым, сначала завершил бы свою задачу он, а затем начал бы исполнение другой поток. Таким образом, блокировки бы не произошло. В приведенном коде явно видна возможность возникновения блокировки, и вы, ско- скорее всего, не станете писать такой код. Однако блокировки могут возникать в различ- различных вызовах методов. В этом примере первый поток мог на самом деле исполнять такой код: lock(А) / / выполняем действия Cal ISorneMethod () ; Здесь Cal ISorneMethod . может вызывать другие методы и так далее, и где-то внутри них может встретиться оператор lock (В). В этой ситуации возможность блокировки уже не так очевидна. Состязания Состязание несколько сложнее, чем блокировка. Оно редко останавливает исполнение процесса, однако может привести к повреждению данных. Состязанию трудно дать точ- точное определение, но оно происходит тогда, когда несколько потоков пытаются получить доступ к одним и тем же данным и не принимают во внимание то, что при этом делают другие потоки. Рассмотрим пример. Допустим, существует массив объектов, где каждый элемент массива должен быть подвергнут некоторой обработке, и выполняют эту обработку несколько потоков. Име- Имеются также объект, назовем его ArrayCo-itroller, который содержит массив объектов, и число int, показывающее, сколько объектов было обработано и, следовательно, какой объект должен быть обработан следующим. ArrayController может реализовывать такой метод: int GetObject(int index) // возвращает объект с заданным индексом и свойство только для чтения: int ObjectsProcessed { // показывает, сколько объектов уже было обработано Каждый поток, который участвует в обработке объектов, может исполнять такой код: lock(ArrayController) X int nextlndex = ArrayController.ObjectsProcessed; Console.WriteLineCobject to be processed next is " + index); ++ArrayController.ObjectsProcessed;
С# и базовые классы 301 Object- next => ArrayController.GetObject (); \ ProcessObject(next); Само по себе это должно работать, но допустим, что в попытке избежать захвата ре- ресурсов на срок, больший, чем необходимо, мы решили не захватывать ArrayController на время отображения сообщения пользователю. Поэтому приведенный код перепишем следующим образом: lock(ArrayController) int nextlndex = ArrayController.ObjectsProcessed; Console.WriteLine("object to be processed next is " + index); lock(ArrayController) ++ArrayController.ObjectsProcessed; object next = ArrayController .GetObject (,); ProcessObject(next); Теперь может произойти следующее: один из потоков возьмет, к примеру, 11-ый объект в массиве и приступит к отображению сообщения о том. что он собирается обработать этот объект. В это время второй поток начнет исполнять тот же самый код, вызовет ObjectsProcessed и определит, что следующий объект, который необходимо обрабо- обработать,— это 11-ый объект, поскольку первый поток еще не обновил ArrayControl- ArrayController . Obj ectsProcessed. В то время как второй поток выводит на экран сообщение о том, что он обрабатывает 11-ый объект, первый поток снова захватывает ArrayController и в этот момент инкрементирует ObjectsProcessed. К сожалению, уже слишком поздно. Оба потока теперь участвуют в обработке одного и того же объекта, и это именно та ситуация, которая называется состязанием. Не всегда бывает очевидно, когда могут возникнуть блокировки и состязания в про- программе. Вообще, это область, понимание которой приходит с опытом. При написании многопоточных приложений важно внимательно рассмотреть все участки кода, где тре- требуется синхронизация, чтобы проверить все возможности возникновения блокировок и состязаний, помня о том, что нельзя заранее сказать, когда различные потоки обработают те или иные инструкции. Заключение В этой главе рассмотрены некоторые средства, предлагаемые базовыми классами .NET. Было показано, как можно эффективно конструировать и обрабатывать строки с помощью класса StringBuilder и как использовать механизм регулярных выражений для выполне- выполнения сложного поиска в строках. Затем были описаны доступные объекты коллекций. Про- Пространства имен System.Collections и System.Collections.Specialized содержат большое число классов, которые позволяют хранить различные типы коллекций объектов. В частности, класс Hashtable обеспечивает хранение данных в словаре с эффективным просмотром по любому типу данных. Пользовательские атрибуты, применяемые совместно с отражением, предоставляют мощную возможность проверки кодом другого кода или даже самого себя. Это означает, что исполнение может зависеть от того, какие пользовательские атрибуты были прило- приложены к объектам в коде. Рассмотрен пример WhatsNewAttributes, который автомати- автоматически составляет отчеты о новых особенностях, добавленных в программный продукт. Наконец, были исследованы некоторые проблемы, возникающие при написании много- многопоточных приложений.
г '/ \ Л а в а Программирование в среде. NET Это первая из трех глав, в которых рассматриваются аспекты программирования с исполь- использованием .NET. Мы уже познакомились с самим языком С# и можем перейти к приклад- прикладным разделам книги, в которых говорится о применении С# для создания различных приложений. Однако прежде необходимо изучить инструменты и особенности среды .NET, что позволит добиться наивысшей производительности от программ. В этой главе рассказывается о том, что означает программирование в среде .NET на практике. Дается обзор некоторых инструментов, помогающих в написании и отладке программ, а также приводятся указания по созданию хороших приложений. В частности, обсуждаются: О Visual Studio.NET — основная среда разработки, в которой вы будете писать, ком- компилировать, отлаживать и оптимизировать программы на С#. О WinCV — полезная утилита, позволяющая исследовать базовые классы. О Указания по применению и соглашения по именованию, которых следует придер- придерживаться при написании кода на С#, благодаря чему ваш код будет соответствовать обычной практике .NET и его смогут понять другие люди. Затем в главе 9 будут рассмотрены Windows Forms и то, как писать код пользователь- пользовательского интерфейса, а в главе 10 — сборки и компиляция библиотек. Visual Studio.NET Visual Studio.NET является полностью интегрированной средой разработки. Она созда- создана для упрощения процесса написания кода, его отладки и компиляции в сборку. Visual Studio.NET представляет собой сложное многооконное приложение, в котором можно выполнять практически все, что связано с разработкой кода. Она предлагает: О Текстовый редактор, который позволяет писать код на С# (а также код VB.NET и C++). Это довольно сложный редактор, имеющий представление о синтаксисе С#. В процессе набора текста программы он может автоматически выравнивать код, например, делая отступы строк, совмещая открывающие и закрывающие фи- фигурные скобки, и выделять цветом ключевые слова. Он также будет выполнять не- некоторые синтаксические проверки и подчеркивать код, который, возможно, вызовет ошибки компиляции. Редактор реализует технологию Intellisense, с по- помощью которой можно начать набирать имена классов, полей или методов, а сре- среда автоматически будет отображать небольшое окно со списком возможных вариантов завершения имен или списков параметров, которые доступны из клас- классов, видимых в коде. На рисунке показана эта технология в действии для одного из базовых классов .NET — ListBox:
304 Глава 8 { Line ■ sc.ReadLineO ; listBoxl. ' P Invalidated «hile (Lin;-,»mvoke strro.Close'ES1 invokeRequred > Й? ^Accessible Й? lsOisposed / -sva«Bary> OS1 IsHandleCreated / ' ■ Clean up s tf? ItemHeight /// ....„,„.„.„„,-„■■. Й?ПЯВ1^ИИИИ1Я public ovecrid f KeyDown { P KeyPress tproperty] System.Windows.Forms.UrtBox.ObjectCollection ListBox.ltemsl О Окно разработки редактора кода, которое позволяет визуально размещать эле- элементы управления пользовательского интерфейса и доступа к данным в проекте. При этом Visual Studio.NET автоматически добавит необходимый код на С# в ис- исходные файлы проекта для создания экземпляров элементов управления (это воз- возможно, так как в .NET все элементы управления являются на самом деле экземплярами базовых классов). О Окна поддержки, служащие для просмотра и изменения проекта. Например, до- доступны окна, показывающие классы в исходном коде и определенные свойства классов форм Windows и форм Web. Эти окна можно также использовать для зада- задания опций компиляции, в частности, какие сборки (особенно содержащие базовые классы) должен использовать код. О Компиляцию из самой среды. Вместо того чтобы вручную запускать компиля- компилятор С# из командной строки, можно воспользоваться пунктом меню для компиля- компиляции проекта, и Visual Studio.NET сама вызовет компилятор. Она передаст компилятору соответствующие параметры, указывающие, например, какие сбор- сборки необходимо использовать и как собрать проект (исполняемый файл или биб- библиотека). Если потребуется, то среда запустит откомпилированный исполняемый файл для проверки его работы. Можно также выбирать различные конфигурации сборки — например, окончательная версия или отладочная сборка. О Интегрированный отладчик. Природа программирования такова, что, как пра- правило, код не будет корректно работать при первой попытке запуска. Или при вто- второй. Или при третьей... Visual Studio.NET содержит встроенный отладчик, который позволяет устанавливать точки останова и наблюдения за переменными непосредственно в среде разработки. Если по достижении точки останова вы по- поняли, в чем заключается проблема, то в большинстве случаев сможете отредакти- отредактировать код в текстовом редакторе Visual Studio.NET для исправления ошибки, перекомпилировать код и продолжить его исполнение с той точки, где оно было остановлено. О Интегрированный профайлер. После компиляции и отладки кода Visual Studio может запустить профайлер, который измерит время исполнения каждого мето- метода. Он способен предоставить и другую информацию, например, о том, какие ме- методы исполняются. Эти сведения можно использовать для повышения производительности. Если требуется, чтобы программа работала быстрее, то дан- данные профилирования точно покажут, какие методы требуют большого количест- количества времени для исполнения и, следовательно, являются кандидатами на оптимизацию. Однако возможности профилирования в Visual Studio.NET вклю- включают в себя более еысокий уровень поддержки, чем традиционные профайлеры. Они позволяют осуществить компонентно-ориентированный анализ для всего проекта и анализ производительности для различных компонентов. Это делается с помощью инструмента Visual Studio Analyzer. Профилирование здесь рассмат- рассматриваться не будет, однако подробности о нем можно найти в документации MSDN по Visual Studio Analyzer. О Интегрированную справку MSDN. Visual Studio.NET может обращаться по ва- вашей просьбе к документации MSDN. Например, если, работая в текстовом редак- редакторе, вы хотите уточнить значение ключевого слова, то можете выбрать его, нажать клавишу F1. и Visual Studio.NET вызовет MSDN с соответствующими тема- темами. Аналогично, если вам не понятна та или иная ошибка компиляции, то можно получить документацию по этой ошибке.
Программирование в среде.NET 305 О Доступ к другим программам. Visual Studio.NET способна вызывать целый ряд утилит, которые позволяют просматривать и изменять сведения о вашем компью- компьютере и сети без необходимости выхода из среды разработки. С помощью доступ- доступных инструментов можно проверять запущенные службы, соединения с базами данных. Даже присутствует окно Internet Explorer, что дает вам возможность просматривать страницы Web. Если вам приходилось писать программы на C++ или VB, то вы знакомы с Visual Studio 6 и увидите, что многие из особенностей не являются новыми — возможно, вы уже выпол- выполняли те же самые действия в Visual Studio 6. Однако действительно новым в Visual Stu- dio.NET является то, что она совмещает все возможности, которые ранее были доступны в различных средах разработки VS6. Это означает, что, каким бы языком вы ни пользова- пользовались в VS6, в Visual Studio.NET вы найдете для себя что-то новое. Например, в Visual Basic нельзя было отдельно компилировать отладочную и окончательную версии, а также нель- нельзя было получить доступ к профайлеру. С другой стороны, для разработчика на C++ новы- новыми будут возможность размещения элементов управления для приложения с помощью одного щелчка мыши, которая ранее была присуща только Visual Basic, и поддержка досту- доступа к данным. В среде разработки C++ поддержка всего этого ограничивалась наиболее общими элементами управления пользовательского интерфейса. Каким бы ни было ваше прошлое, вы обнаружите, что среда разработки изменилась для поддержки новых возможностей, стала единой межъязыковой IDE и интегрирована с .NET. Появились новые элементы меню и панелей инструментов, а многие из тех, что существовали в VS6, были переименованы. Поэтому потребуется некоторое время для ознакомления со структурой и доступными командами Visual Studio.NET. Так как это книга для профессионалов, мы не будем подробно рассматривать каждое средство или пункт меню Visual Studio.NET. Предполагается, что вы являетесь достаточно опытным программистом, чтобы самостоятельно разобраться со средой разработки и по- посмотреть, что она может вам предложить. Цель этого обзора Visual Studio.NET — дать вам полное представление о концепциях создания и отладки приложения С#, чтобы вы могли добиться наивысшей отдачи от Visual Studio.NET. Вы должны научиться использовать конкретные особенности среды, которые могли быть недоступны для того языка, кото- которым вы пользовались ранее. На рисунке показано, как может выглядеть Visual Studio.NET в процессе кодирования (поскольку Visual Studio.NET можно настраивать, при запуске среды могут выводиться другие окна в иных позициях, нежели на рисунке): '■sr Е* " 1* i ? ■ * // ■' - «, " 3..,. да» V* fro»! »9П; Гаг ) : w boat ( - if (Self CISC live Socket.. . Jintl э»Г - oner с ing P1K rn I В юл • <>.»■ .. ,n Vrcx а:че: 5P.»«. Key - .tOo.11. Г «,4 • Л - Re t«c Loc*lK«"lii , - oo.' **" " «.iumi-5«««:uii, о ai-H«=«ai it».- —„._,. l->ro«Preso-'; • " • - .tfi \ «9?0РЧ>С ..УЧПИЛ Id 1 ?*^ 1 > X ,. J _| r «- «« ! 1га|1«1исГ 'IS»»»'»» II >.l B1 — ... В последующих разделах рассматривается процесс создания, кодирования, отладки и профилирования проекта. Для каждой из этих стадий будет показано, чем может в данном случае помочь Visual Studio.NET.
306 Глава 8 Создание проекта После установки Visual Studio.NET вы сможете приступить к созданию своего первого про- проекта. В Visual Studio.NET вам не придется начинать с чистого файла и набирать текст кода С# с нуля. Вместо этого вы сообщаете Visual Studio.NET о том, какой тип проекта собирае- собираетесь создать, a Visual Studio.NET автоматически сгенерирует каркас кода на С# для этого типа проекта. Затем вы добавляете код в этот каркас. Например, если вы разрабатываете приложение Windows GUI (или, в терминологии С#, форму Windows), Visual Studio.NET откроет файл, содержащий код для создания простой формы. Эта форма способна взаимо- взаимодействовать с Windows и обрабатывать события. Ее можно развернуть, свернуть или изме- изменить ее размер, но она не содержит в себе никаких элементов управления и никакой функциональности — создать все это вы должны будете сами. Если приложение должно быть консольным, Visual Studio.NET сформирует пространство имен, класс и метод Main, i. Разумеется, можно начать с пустого приложения, если действительно необходимо писать код с чистого листа. Однако Visual Studio.NET не останавливается на создании начального проекта. Когда потребуется откомпилировать код, Visual Studio.NET сама вызовет компилятор. При создании проекта среда устанавливает опции компиляции, которые вы, вероятно, поже- пожелаете передать компилятору С#, независимо от того, будет ли компилироваться консоль- консольное приложение, библиотека или приложение Windows. Также будут автоматически указаны библиотеки базовых классов (приложение Windows GUI будет использовать библиотеки Windows.Forms; консольное приложение, скорее всего, нет). Разумеется, при необходимости все эти настройки можно изменить во время редактирования. При первом запуске Visual Studio.NET выводится так называемая стартовая страни- страница. Это HTML-страница, которая содержит различные ссылки на полезные web-сайты. Она позволяет изменить вид и конфигурацию Visual Studio.NET (ссылка My Profile), открыть существующие проекты или начать новый проект: Microsoft Development Environment [design] • Start Page File But yew Tools to - & Ы О К* Г [ Start Ряде | .*<._■ VectorAsCoJtection Today SelfPlecmgWindow Yesterday FilePt-opertiesAndMovements M/OS/2001 Test Console ie/04/200l Open Proipct ^ \ New Project ; Log a Visual Studic.NET bug report _l debug mode.fabe r- Ha экране показана ситуация, когда Visual Studio.NET использовалась несколько раз, и поэтому выведен список проектов, которые редактировались в последнее время. Мож- Можно открыть один из этих проектов, щелкнув мышью на его названии. Очевидно, что этот список будет пустым при первом запуске Visual Studio.NET. Пункт My Profile позволяет изменить вид Visual Studio.NET для того, чтобы он соответствовал тому, который применялся вами в предыдущих средах разработки - так, что Visual Studio.NET будет выглядеть примерно как старая IDE для \И или C++. Однако отметим, что разрешается менять только положения различных окон на экране. Большинство пунктов меню и панелей инструментов будут новыми.
Программирование в среде.NET 307 Выбор типа проекта Новый проект можно создать, либо щелкнув мышью па соответствующей ссылке на стар- стартовой странице, либо выбрав в меню File | New | Project. Выводимое при этом диалоговое окно даст вам первое представление о том множестве различных проектов, которые можно создать: [New Project Project Types: Templates: ■- ;.. Ё Cli Visual Basic Projects t~S Visual C# Projects LJ Visual C++ Projects £_1 Setup and Deployment Projects i 1 Other Projects t_J Visual Studio Solutions - Web Application Щ Console Application Vi'eb Service |M Windovjs Service Ш—i Web Control Library G3 i a~j Empty Project -d Name: Location: j BasicConsoleApp j F:\C# Projects\Pro C# Code\Ch8_ProgWithDotNet Browse,.. Project will be created at F:\C# Projects\Pro C# Code\Ch8_ProgWithDotNet\6asicConsoleApp. OK Cancel Hdp В этом диалоговом окне вы указываете, какой каркас приложения должна создать Visual Studio.XET и какие опции компиляции необходимы. Здесь же указывается и ком- компилятор, с помощью которого нужно компилировать код: С#, VB.NET или C++. Вы ви- видите в работе языковую интеграцию, которую Microsoft обещала предоставить в .NET! Для нашего примера выбрано консольное приложение С#. К сожалению, мы не в состоянии рассмотреть все возможные варианты проектов. Что касается C++, то остались все прежние типы проектов: приложение MFC, проект ATL и т.д. В VB.NET произошли некоторые изменения - например, можно создать приложение командной строки VB.NET (консольное приложение), что было невозможно в VB6. Можно также создать компонент .ХЕТ (библиотека классов) или элемент управления .NET (библиотеку Windows Control j, однако нмъзя создать элемент управления на основе СОМ (элемент управления .NET де.гает то же самое). Перечислим все опции, доступные в пункте 'Visual C# Projects'. Необходимо отме- отметить, что существует ряд других, более специализированных шаблонных проектов С#, они доступны в пункте 'Other Projects . Если вы выбираете... То генерируются следующий код Глава и опции компиляции.. Windows Application Class Library Windows Control Library Web Application Пустая форма, которая откликается на события. 9 Класс .КЕТ. который может быть вызван 10 другим кодом .NET. Класс .NET, который может быть вызван другим 9 кодом и который имеет пользовательский интерфейс (как элемент управления Active-X). Web-сайт на основе ASP.NET: страницы ASP.NET 16 и классы С#, генерирующие отклик HTML для браузеров с этих страниц.
308 Глава 8 Если вы выбираете., То генерируются следующий код и опции компиляции... Глава Web Service Web Control Library Console Application Windows Service Empty Project Empty Web Project New Project In Existing Folder Класс С#, который действует как полнофункциональная web-служба. Элемент управления, который может быть вызван страницами ASP.NET для генерации HTML-кода, создающего впечатление элемента управления при отображении в окне браузера. Приложение, исполняемое из командной строки или в окне консоли. Служба, работающая в фоновом режиме в Windows NT или Windows 2000. Ничего. Необходимо с нуля написать весь код, но при этом сохраняется возможность использования , всех особенностей Visual Studio.NET в процессе редактирования. То же самое, что и в случае Empty Project, только опции компиляции указывают на необходимость генерации кода для страниц ASP.NET. Новые файлы для пустого проекта. Применяется в случае, если есть некоторый код на С# (например, набранный в текстовом редакторе), и его требуется конвертировать в проект Visual Studio.NET. 17 18 24 В последнем столбце этой таблицы указывается номер главы книги, в которой рас- рассматривается данный тип приложения. Новый проект консольного приложения Если в приведенном выше диалоговом окне выбрать генерацию консольного приложе- приложения С# и нажать OK, Visual Studio.NET создаст несколько файлов, включая файл с исход- исходным кодом Classl.cs. который содержит начальный код каркаса. Мы рассмотрим случай с консольным приложением, однако те же принципы сохраняются для всех осталь- остальных типов проекта. На экране приведен код, сгенерированный Visual Studio.NET: File EdK view Project guild Qetxig loots Window Г j 1 P^ige Ctaulxs | «<Лх*А*рр.аа»1 J If» uaing System; namespace BasicConsoleApp ( /// ^ssumnarv" /// Suttnary description lot class Classi static void KAin(strxnQ[ // TODO: xai code to > 1 > 1 MalnfstrlngU*rQS) Classi. J args) start application hece < > »  1 i ! ! - •- It ♦ 4 i T; i~\ 13 Solution BaacCcnso*eAp( Й- ^ 6a«cConsoleApp -O System .1 —■ pr^tw.: i.:.__: Bu4d Action lorrpde Custom Too) Custom Tool Ныл Advanced -1 ,1 >> ** ^ , pr°|eCt 1 -nl —J*_J!.' j - Ready
Программирование в cpeAe.NET 309 Здесь представлена программа на С#, которая пока еще ничего не делает, но содержит основные программные элементы: пространство имен и класс, включающий в себя метод точки входа программы Main (). Этот код готов к компиляции и запуску, что можно не- немедленно выполнить, нажав клавишу F5 или, что то же самое, выбрав в меню Debug | Start. Однако перед этим добавим всего одну строку кода, чтобы приложение что-нибудь делало: void Main(string[] static args) // // TODO: Добавляем код для запуска приложения IJ Console. WriteLin'e("Привет■ от всех редакторов Wrox Press"); } Если откомпилировать и запустить этот проект, выяснится, что консольное окно ис- исчезает так же быстро, как и появляется, практически не давая времени на прочтение со- сообщения. Дело в том, что Visual Studio.NET, помня об установках, заданных при создании проекта, откомпилировала код в виде консольного приложения и сразу запус- запустила его. Windows поняла, что ей необходимо исполнить консольное приложение, но консольного окна для его запуска нет. Тогда Windows создает это окно и запускает в нем программу. После того, как мы вышли из программы, Windows видит, что консольное окно больше не требуется, и быстро уничтожает его. Все логично, но нет возможности просмотреть результат работы проекта! Этой проблемы можно избежать, вставив следующую строку в метод Main () : static void Mainistring[] args, { // // TODO: Добавляем код для запуска приложения // Console.WriteLine("Привес от всех редакторов Wrox Press"); Console. Eeadbine О; } В этом случае код будет запущен, выведет сообщение, а после этого выполнит опера- оператор Console. ReadLine () и станет ждать нажатия на клавишу Enter для выхода из про- программы. Это означает, что консольное окно будет существовать до тех пор, пока вы не нажмете Enter. Отметим, что эта проблема возникает только для консольных приложений, которые запускаются в целях тестирования из Visual Studio.NET. Если вы пишете приложение Windows, то отображаемое им окно останется на экране до тех пор, пока не будет закры- закрыто явно. Аналогично, если запускать консольное приложение из командной строки, как мы делали до сих пор, проблем с исчезновением окна не будет. Другие созданные файлы Файл с исходным кодом classl. cs не единственный, создаваемый для вас Visual Studio.NET. В папке, которую Visual Studio.NET создала для этого проекта, содержится целая структура каталогов примерно следующего вида: ё* F:\C# Projects\Рго С# Code\Ch8_ProgWitliOotNet\BasicCo»soteApp Г Qle Edit Yaw Fararltes look delp ~~—■ . ■■ ' ' —: < *-* . <4 *■ ^Search *£ X irtJ Ш* Address [ol F;£# Ртаеси\Рто С# Co<te5Cb8J>rogw»№e..Net\B«slcCon5olMK> Folders S-Q смоо ffl-O OluAoVCSh Ф Q Oi7JaseOaaes ЕЕЕЕШЗЗ В'П BasicConsoleApo _( Jbr id Debus Job! ffiO Debug РЫп tlUCIassl.es J Fit =сИк 3 KB C# So^co Ha 4 KB C* Proiett file 2H Vlsui Studio Proiec 1 «ГЕ visual Studio Solution 7 KB Vfeml S(u*; Solutio... IKS 06/05/2001 П:Ог 06/O5/2O0J 17:02 06/05/2001 17:02 06/05/2001 17:02 06/05/2001 17:02 06/05/2001 17:0г 06/05/2001 17'02 06/05K001 17:02 06/05/2001 17:02 {9 ob>sct(s) (disk free space: 286 MB) |lS.9№ SlMyCocrputer
310 Глава 8 Два каталога, bin и obj, предназначены для откомпилированных и промежуточных файлов. Подкаталоги obj содержат различные временные и промежуточные файлы, ко- которые могут быть созданы, а в подкаталогах bin хранятся откомпилированные сборки. Разработчики на VB могут не знать об этом. При запуске кода VB6 и более ранних приложений вы традиционно сначала пишете код, а затем запускаете его. В VB так же, как и в других языках, код должен быть откомпилирован в нечто, содержащее инструкции машинного языка, но при отладке VB скрывает этот процесс. В С# он более явный: для запуска кода его необходимо сначала откомпилировать (собрать), т.е. сначала тем или иным образом создается сборка. Остальные файлы в основном каталоге проекта, BasicConsoleApp, предназначены исключительно для самой среды Visual Studio.NET. Они содержат информацию о проек- проекте (какие файлы в нем содержатся и т.п.), благодаря чему Visual Studio.NET знает, как компилировать проект и как его читать при последующем запуске. Решения и проекты Важно понять разницу между решением и проектом: □ Проект представляет собой набор всех файлов исходного кода и ресурсов, кото- которые будут откомпилированы в одну сборку (или в некоторых случаях в один мо- модуль). Например, проект может быть библиотекой классов или приложением Windows GUI. □ Решение — это набор всех проектов, которые образуют конкретный набор программного обеспечения (приложение). При продаже приложение, как правило, состоит из нескольких сборок. Например, могут присутствовать пользовательский интерфейс, определенные пользовательские элементы управления и другие компоненты, которые поставляются в виде библиотек как части приложения. Может даже предлагаться специальный пользовательский интер- интерфейс для администратора. Каждая из этих частей приложения может располагаться в от- отдельной сборке и, следовательно, расцениваться Visual Studio.NET как отдельный проект. Однако весьма вероятно, что вы будете создавать эти проекты параллельно и во взаимосвязи друг с другом. Поэтому полезно редактировать их в Visual Studio.NET как один большой модуль. Visual Studio.NET дает возможность делать это, считая, что проек- проекты образуют одно решение, и трактуя это решение как модуль, который она затем читает и с которым позволяет работать. До сих пор мы говорили о создании консольного приложения. На самом деле в при- примере, который мы рассматриваем, Visual Studio.NET создала для нас решение, содержа- содержащее всего один проект. Это можно посмотреть в окне Visual Studio.NET, известном как Solution Explorer. Оно показывает древовидную структуру конкретного решения: Д SoJubonBaacConsoteApp'O protect) ai References •О System •CJ System.Data •a System.XML As«mblylnfo,cs Classl.cs !.. ■o» Solution Explorer Г*> Cisss View Наш проект содержит исходный файл Class I. cs и еще один исходный файл, Assem- Assembly Info.cs, в котором записана информация о компиляции проекта (см. главу 10). Кро- Кроме того, в окне перечисляются сборки, на которые ссылается проект, по пространствам имен.
Программирование в среде.NET 311 Если вы не меняли установки Visual Studio.NET по умолчанию, то Solution Explorer появится в верхнем правом углу экрана. Если вы не видите его, выберите в меню View и щелкните на Solution Explorer. Решение описывается в файле с расширением .sin. В нашем примере это файл BasicConsoleApp.sln. Проект описывается различными другими файлами, которые находятся в основном каталоге проекта. По большей части это простые текстовые фай- файлы, и в соответствии с тем принципом, что .NET и инструменты .NET везде, где возмож- возможно, полагаются на открытые стандарты, они будут преимущественно иметь формат XML. Разработчики на C++ могут увидеть сходство между решением Visual Studio.NET и рабочей областью проекта C++ (хранящейся в файле . dsw), а проект Visual Studio соответствует проекту C++ (фаш . dsp). С другой стороны, разработчики на VB найдут, что решение соответствует группе проектов VB (файл . vbg), а проект .NET - проекту VB (файл . vbp). Visual Studio.NET отличается от старой IDE VB тем, что она всегда автоматически создает решение. В Visual Studio 6 разработчики на VB первоначально получают проект, а группу проектов требуется создавать явно. Добавление в решение другого проекта Нам бы хотелось показать, как Visual Studio.NET работает с приложениями Windows. Так что здесь мы попробуем убить сразу двух зайцев: создадим оконный проект и в то же время продемонстрируем решение, содержащее более одного проекта. Мы создадим оконный проект BasicWindow, но не будем использовать для него новое решение, а попросим Visual Studio.NET добавить его к текущему решению BasicConsoleApp. В результате мы получим решение, содержащее приложение Windows и консольное приложение. Это не часто можно встретить - обычно вы будете иметь приложение и несколько библиотек. Однако и такое решение возможно, например, когда необходимо написать утилиту, способную работать как из командной строки, так и в оконном режиме. Это можно сделать несколькими способами. Один из них — щелкнуть правой кноп- кнопкой мыши на имени решения в Solution Explorer. В результате появится контекстное меню, один из пунктов которого служит для добавления элементов в решение. Другой способ — выбрать в меню File пункт New | Project. В любом случае на экране появится то же самое диалоговое окно (см. выше), однако на этот раз в нижней его части выводятся два переключателя, которые позволяют указать, следует ли для этого проекта создавать новое решение или добавить его к \же существующему: New Project Eroject Types: Templates- _J Visual Basic Projects vi Visual C# Projects •_J Visual C++Projects £j Setup and Deployment Projects @ CJ Cither Projects ■ Q Visual Studio Solutions 3 Jb5 Wndows Class Library Windows Application Control Library Web Appfccation Web Service Web Control Library Jd IA project for creating an application with a Windows user interface JBasicForm Location: | F:\C# Projects\Pro C* Code\Ch8.ProgWithDotNeHWit _-J P fiddtoSolution Г OoseSolution л Project we beoeated at F:\...l Browse.. OK Cancel Help При выборе пункта Add to Solution (Добавить к существующему решению) будет со- создан новый проект, а решение будет содержать консольное приложение и приложение Windows.
312 Глава 8 В соответствии с принципами языковой независимости Visual Studio.NET новый проект не обязан являться проектом С#. Разрешается в одно решение помещать проекты СМ, VB.NET и C++. Разумеется, имя BasicConsoleApp больше не подходит этому решению! Его можно из- изменить, щелкнув на нем правой кнопкой мыши и выбрав пункт Rename в контекстном меню. Если переименовать решение в DemoSolution, то Solution Explorer будет выглядеть примерно так: Solution Explorer Solution DemoSoluttotV B protects) щр BosicContoleApp ED- 5*Щ References ■ <J System ! *O System.Data ' -O System .XML ;£] Assemblylnfo.es - pjiJBasicForm ф- ^References •O System •O System.Oata ••_) System .Drawing •<_> System.Windows.Forms •O 5ystem.XML «?) Assemblylnfo.es Ш Formi.cs 5» Solution Explorer | *} Class Vie-w Отсюда также видно, что Visual Studio.NET автоматически внесла в оконный проект ссылки на некоторые дополнительные базовые классы, которые важны для обеспечения функциональности формы Windows. Если посмотреть в Windows Explorer, то можно заметить, что имя решения измени- изменилось на DemoSolution.sin. Вообще говоря, если требуется переименовать тот или иной файл, то лучше делать это в Solution Explorer, так как Visual Studio.NET в этом случае ав- автоматически обновит все ссылки на этот файл из других файлов проекта. Переименова- Переименование файла в Windows Explorer, скорее всего, приведет к порче решения, так как Visual Studio.NET не сможет найти файлы, которые ей требуются. Придется вручную редакти- редактировать файлы проекта и решения для обновления файловых ссылок. Установка начального проекта Необходимо помнить, что если решение состоит из нескольких проектов, только один из них можно запустить в конкретный момент времени. При компиляции решения ком- компилируются все входящие в него проекты. Однако требуется указать, какой из них Visual Studio.NET будет запускать при отладке кода. Если присутствуют исполняемый файл и несколько вызываемых им библиотек, то очевидно, что это будет исполняемый файл. В нашем случае мы имеем два исполняемых файла, и их придется отлаживать по очереди. Можно указать Visual Studio, какой из проектов необходимо запускать, щелкнув пра- правой кнопкой мыши на проекте в Solution Explorer и выбрав пункт Set Startup Project в кон- контекстном меню. Текущий Startup-проект выделяется в Solution Explorer жирным шрифтом, на приведенном выше рисунке это BasicConsoleApp. Код приложения Windows Приложение Windows содержит гораздо больше кода, чем консольное приложение, так как создание окна внутренне является гораздо более сложным процессом. Здесь мы не бу- будем подробно обсуждать код оконного приложения — это тема следующей главы, но для полноты приведем код, который Visual Studio.NET сгенерировала для проекта Basic- Form. Отметим, что создан класс Forml, представляющий собой основное окно: using System; using System.Drawing; using System.Collections; using System.ComponentModel; Using,. System..Windaws...S:t>rms;
Программирование в cpeAe.NET 313 Using System.Data,- namespace BasicForm { /// <summary> /// Общее описание Forml. Ill </surnmary> public class Forml : System.Windows.Forms.Form { '■ /// <summa£y> /// Переменная, необходимая в проекте. /// </summary> private System.ComponentModel,.Container components; public Forjnl () { . II, // 'Требуется для поддержки Windows Form Designer. It InitializeComponentO; // // TODO: Добавить код любого конструктора после вызова // InitializeComponent /// <summary> /// Освободить все используемые ресурсы. /// </summary> public override void Dispose() { base. Dispose (J; ii (components != null) components.Di spose(); #reglon Windows Form Designer generated code /// <summary> ///- М.етод, требуемый для поддержки Designer,— не модифицировать /// содержимое .этого метода в редакторе кода. */// </summary> : * private void InitializeComponent() { this.Components = new System.ComponentModel.Container(); this.Size = new System.Drawing.SizeOOO, 300); this.Text = "Forml"; } .. #endregion /// •<summary> /// Основная точка входа в приложение. /// </summary> [STAThread] static void MainO { Application.Run(new Forml()); Чтение проектов Visual Studio 6 Если вы создаете код в С#, вам не потребуется читать старые проекты Visual Studio 6, так как С# не существует в Visual Studio 6. Однако языковая совместимость является ключе- ключевой частью платформы .NET, поэтому вы можете пожелать, чтобы код на С# работал со- совместно с кодом, написанным на VB.NET или C++. В этой ситуации может возникнуть необходимость в редактировании проектов, созданных в Visual Studio 6. Visual Studio.NET с легкостью прочтет проекты и рабочие места Visual Studio 6, хотя при этом они будут преобразованы в решения Visual Studio.NET. Ситуация отличается для проектов на C++, VB и J++.
314 Глава 8 О В C++ не требуется производить изменений в исходном коде. Весь старый код C++ будет отлично работать с новым компилятором C++. Очевидно, это будет неуправ- неуправляемый код, запускаемый вне среды исполнения .NET. Если же нужно интегриро- интегрировать код в платформ)' .NET, его придется отредактировать. Если прочитать в Visual Studio.NET старый проект C++, то будут созданы новый файл решения и обновлен- обновленные файлы проекта. Старые файлы . dsp и . dsw останутся без изменений, поэтому в случае необходимости проект можно будет редактировать в Visual Studio 6. О Для Visual Basic возникает больше проблем, так как Visual Basic заменен языком VB.NET. И хотя VB.NET построен на основе VB и использует практически тот же синтаксис, это во многом совершенно новый язык. В Visual Basic исходный код состоит в основном из обработчиков событий для элементов управления. Код, со- создающий основное окно программы и элементы управления, не является частью Visual Basic, а скрыт как часть конфигурации проекта. Напротив, VB.NET работа- работает практически так же, как С#, делая открытым код всей программы, в результате чего код, отображающий основное окно и все элементы управления, должен на- находиться в исходном файле. Как и С#, VB.NET требует, чтобы все элементы про- программы были объектно-ориентированными и являлись частью класса, в то время как VB далее не воспринимает концепцию классов в понимании .NET. Если нужно прочитать проект Visual Basic в Visual Studio.NET, то сначала потребуется преоб- преобразовать весь исходный код в код Visual Basic.NET, а это подразумевает внесение большого числа изменений в исходный код VB. Visual Studio.NET может выпол- выполнить эти изменения автоматически и создать новое решение VB.NET. Вы обнару- обнаружите, что полученный исходный код сильно отличается от старого кода VB. Необходимо внимательно просмотреть полученный проект и убедиться в том, что он работает корректно. Visual Studio.NET может даже оставить свои коммен- комментарии по поводу того, что она не смогла точно определить, что должен делать код, и вам придется отредактировать такие участки кода вручную. О По мнению Microsoft, J++ является устаревшим языком и поэтому не поддержи- поддерживается в .NET наьрямую. Однако .NET предлагает ряд инструментов для того, чтобы существующий код на J-1-1- мог работать. Также имеется инструмент, авто- автоматически преобразующий код J-+ в код С#, что аналогично возможности об- обновления с VB6 на VB.NET. Эти инструменты объединены под общим названием JL MP (Java User Migration Path), а сведения о них можно получить на msdn.microsoft com/visualj/jump/default.asp JUMP также преобразует код Java, напи- написанный вне среды разработки VS, в код С#. Просмотр и написание кода для проекта В этом разделе рассматриваются особенности, предлагаемые Visual Studio.NET с целью облегчения написания кода проекта. Сворачивающий редактор Одним из действительно итересных нововведений в Visual Studio.NET является свора- сворачивающий редактор, используемый в качестве редактора кода по умолчанию. На рисунке представлено окно этого редактора: •;S л % ч ?♦. a at«t v >s j uainfstr; n ; _.1«*.кЧ-1СР1.1п^("Не1Д . Irm «11 ttie editJLj ее Ыгох Press");
Программирование в среде-NET 315 В нем показан код консольного приложения, сгенерированный ранее. Обратите вни- внимание на небольшие знаки минуса с левой стороны окна. Они отмечают те места, в кото- которых, по мнению редактора, начинаются новые блоки кода. На этих значках можно щелкнуть мышью, свернув вид соответствующего блока кода, точно так же, как вы за- закрываете ветвь в элементе управления в виде древовидной структуры: - Microsoft щ File Edit View Project &нИ Qebug Iools Analyser Window Help Ctassl.cs I Forml.cs[Design] | Fcrml.cs i ► X using System; В namespace BasicConsoleApp ■*■ /// Surrmary description for С1азз1. class Clessl { acetic void Hain(stiing[; агозц. Done Cdl Chi Это означает, что в процессе редактирования вы можете сосредоточиться только на тех участках кода, которые требуются, а ненужный код можно закрыть. Более того, если вам не нравится способ, которым редактор сворачивает код, вы можете указать другой способ с помощью директив препроцессора С# #region и #endreq»on (см. главу 6). На- Например, мы решили свернуть только код внутри метода Main(). Добавим следующий код: RasicOinsolcAp» - Microsoft Visual (ЖГчЕТ fdesianl E0* ylew Reject Icois Analyzer *' ] Ftxroi.«[0**-y>] using System; etecic void lBin(etnr.y[] *Г(ГЭ) I #regicn Boring aiuff in the Rain routine) Console. Ur it«Lirie ("H ConeDle.PeadLine Г*; In 12 Оэ>53 Ch-H 4 4 Редактор кода автоматически обнаружит блок #region и разместит напротив но- новый минус, позволяя закрыть регион. Заключение кода в регион означает, что можно заставить редактор закрыть блок кода и пометить эту область с помощью комментария, указанного в директиве #region:
316 Глава 8 • BasicCor»oleApp - Microsoft Visual C#.NtT fdesiti... шИ Debug Iools Analyjef yjis-idow .f • ■■ - --£]-'"., , Debug Bte Есй yiew Project \S Clasil.cs* I Fcnnl.cs [Design] Foiml.cs jj+MahtstiingUargs) using System; 3 namespace BasicTonsoleApp j ( Siimnmcy cescrxption Cor Clnsifl. class Classl ( static void Hain(string[] acos) r > 1 1 Q. 1 Cfi) Кстати говоря, сворачивающий редактор не является чем-то новым. Первый раз я встретил его около 10 лет назад при подготовке докторской диссертации. Наше отделение инвестировало большую сумму денег в революционную машину - транспьютер. Идея состояла в том, что в нем работало параллельно множество процессоров, в результате чего программы выполнялись быстрее. {Странно, хотя сама идея наина широкое распространение, название "транспьютер" так и не прижилось.) Но больше всего меня поразила не скорость, а наличие в комплекте поставки сворачивающего редактора программ, который разделял процессы между процессорами. С тех пор сворачивающий редактор находится на вершине моего списка пожеланий относительно того, что должна делать среда разработки. Помимо способности сворачивать код, редактор Visual Studio.NET наследует хорошо из- известные возможности редактора Visual Studio 6. В частности, он использует технологию In- tellisense. Например, если вы набираете имя экземпляра класса, а затем точку, то на экране появляется список, в котором можно выбрать члена класса для вставки в код (этот список можно также вызвать, нажав Ctrl+Space). Как только вы наберете открывающую скобку для вызова метода, будет выведен список его параметров. Это не только сокращает число нажа- нажатий на клавиши, но также гарантирует, что будут переданы верные параметры. Разработчи- Разработчики на О+ отметят, что реализация Intellisense в Visual Studio.NET является более надежной, чем в Visual Studio 6 (где части элементы отсутствовали в списке), и работает быстрее. Редактор кода также выполнит частичную синтаксическую проверку кода и подчерк- подчеркнет большинство синтаксических ошибок волнистой линией еще перед компиляцией кода. Если навести мышь на подчеркнутое слово, появится небольшое окно с описанием ошибки. Этой особенностью разработчики на VB пользовались в течение многих лет, но она будет в новинку для разработчиков C++. Другие окна Помимо рэедактора кода, в Visual Studio.NET существует множество других окон, которые позволяют взглянуть на проект с различных точек зрения. В этом разделе мы рассмотрим некоторые окна. Если какое-то окно не выводится в вашей установке Visual Studio.NET. войдите в меню View и выберите название соответствующего окна. Единственное исключение из этого правила - окно разработки и редактор кода, так как считается, что это две закладки для одного и того же окна. Для их отображения можно щелкнуть правой кнопкой мыши на имени файла в Solution Explorer и выбрать в контекстном меню пункт View Design или View Code либо воспользоваться панелью инструментов в верхней части Solution Explorer.
Программирование в cpeAe.NET 317 Окно разработки Если вы разрабатываете приложение Windows (или приложение ASP.NET), то чаще всего будете пользоваться окном разработки, которое показывает общий вид вашего приложе- приложения. Обычно окно разработки применяется совместно с окном, известным как окно инст- инструментов. Оно содержит большое число компонентов .NET, которые можно перетащить в программу: Data Componeris Wndows Forms It Pointer A wbel A. LintlaM Ы Button g$ ManMenu 17 ChevkBox Groupeox %g) FxturoBox П Panel ij CheckedUstBox Й CorrboEox '-,- TreeV«w JJ TabControl fj OateTmePickft fS MonthCalendar JU HScrotear ?H Timer Clipboard Ring Принцип окна инструментов использовался во всех средах разработки в Visual Studio 6. однако в .NET количество доступных в этом окне компонентов значительно увеличилось. Число категорий компонентов, предлагаемых окном инструментов, зависит от типа редак- редактируемого проекта. Например, вы обнаружите, что этот диапазон гораздо шире для проек- проекта BasicForm в решении DemoSolution, чем в проекте BasicConsoleApp. К наиболее важным группам элементов относятся: О Компоненты доступа к данным. Классы, которые позволяют соединяться с ис- источниками данных. О Компоненты Windows Forms. Классы, которые представляют визуальные элемен- элементы управления, например: текстовые области, списки, древовидные элементы и т.д. О Компоненты Web Forms. Классы, которые в основном делают то же самое, что и элементы управления Windows, но работают в контексте web-браузера и посылают HTML для эмуляции элементов управления в браузере. О Компоненты. Различные классы .NET, которые выполняют разнообразные зада- задания на вашей машине, например, соединение со службой каталогов или ведение журнала событий. Как видно из приведенного рисунка, этот список далеко не полный. В окно инструментов можно добавить свои собственные категории элементов, щел- щелкнув на любой категории правой кнопкой мыши и выбрав в контекстном меню пункт Add Tab. В окне инструментов можно разместить также другие инструменты, выбрав пункт Customize Toolbox в контекстном меню — в частности, вы можете добавить ваши любимые компоненты СОМ и элементы управления ActiveX, которые по умолчанию отсутствуют в окне инструментов. В случае добавления элемента управления СОМ его можно разместить в проекте точно так же, как если бы это был элемент управления .NET. Visual Studio.NET автоматически вставит весь необходимый код для совместимости с СОМ, чтобы ваш проект смог вызвать этот элемент управления.
318 Глава 8 Разработчики на C++ могут рассматривать окно инструментов как новую версию редактора ресурсов Visual Studio.NET (значительно улучшенную). Разработчики на VB могут по началу решить, что в окне инструментов нет ничего нового, поскольку такое окно присутствует в Visual Studio 6. Тем не менее они должны быть осторожны, поскольку это окно инструментов оказывает совершенно иное действие на исходный код, чем окно инструментов VB6. Посмотрим, как работает окно инструментов, разместив в форме нашего проекта BasicForm элемент Textbox. Для этого нужно щелкнуть мышью на Textbox в окне инстру- инструментов, а затем щелкнуть в форме в окне разработки, чтобы разместить в ней Textbox. Окно разработки покажет, как примерно будет выглядеть BasicForm, если откомпили- откомпилировать и запустить этот проект: Интересно то, что при этом среда разработки добавляет код, который создает экземп- экземпляр объекта Тех х в форме. В классе ~ —" появляется новая переменная: .hlic class System.Wi- . - rms.Form r ; vate System.Window.Forms.Те -Ь_х textBox1 ; Кроме того, для ее инициализации добавляется код в метод InitializeComponent (), который вызывается из конструктора For . ' 11 <summary Метод, т.."уемый z.- поддерг юдержимое этого ,да в р immary> pr: j . ^id Ina.tiali2 imponent .signer, — не модифицировать ^ре к or . this.tiextBoxl = nevr System. Wir 5_.. . Forms. TextBox (); this.SuspencUjayout(); // texcBoxl // this.texrBoxl.Loca" n = new this.texrF «1.Name = "textBr- this.textr -< .Size new S- this.textEr .^ab- к = 0; this.textBc-ч ex* "textBcxT .Drawing.Point(8, 16!; ng.Size 224, 20); Редактор кода и окно разработки на самом деле представляют различные виды одно- одного кода. Когда мы с помощью мыши размещаем TextBox в форме, Visual Studio.NET до- добавляет соответствующий код в исходный файл С#. Окно разработки лишь отражает это изменение, поскольку Visual Studio.NET способна анализировать исходный код и опре- определять, какие элементы управления должны присутствовать во время исполнения при- приложения, и отображает эти элементы в окне разработки. Это фундаментальное отличие от концепции \Ъ. где все основывается на визуальной разработке. Теперь код С# управ- управляет приложением, а окно разработки обеспечивает один из способов просмотра исход- исходного кода. Те же самые принципы действуют при написании кода VB.NET с помощью Visual Studio.NET.
Программирование в среде-NET 319 При желании можно подойти к вопросу с другой стороны. Если бы мы вручную доба- добавили тот же самый код, что содержится в исходных файлах С#, то Visual Studio.NET определила бы по коду, что приложение содержит Text Box, и показала бы его в окне раз- разработки на заданной позиции. Однако лучше добавлять эти элементы управления визуаль- визуально и позволить Visual Studio генерировать код — это намного быстрее, чем набирать код вручную! Существует еще одна причина, по которой лучше визуально добавлять элементы управления. Дело в том, что Visual Studio.NET увидит компоненты, если соответствую- соответствующий код будет удовлетворять определенному критерию,— а код, написанный вручную, может не удовлетворять ему. В частности, метод InitializeComponent () , содержащий код для инициализации TextBox, сопровождается комментарием, предостерегающим вас от его изменения. Visual Studio.NET просматривает этот метод при определении того, ка- какие элементы управления присутствуют. Если вы создаете элемент управления в другом месте кода, то Visual Studio.NET не будет знать о нем, и вы не сможете редактировать его в окне разработки или в других окнах. На самом деле код в InitializeComponent () можно менять, но принимая меры пре- предосторожности. Например, нет ничего опасного в изменении значений некоторых свойств, в результате чего элемент управления будет отображать другой текст или иметь другие размеры. На практике среда разработки весьма устойчива. Только помни- помните, что при большом числе изменений в InitializeComponent () появляется риск того, что Visual Studio.NET перестанет распознавать некоторые из ваших элементов управле- управления. Необходимо подчеркнуть, что это не затронет ваше откомпилированное приложе- приложение, но может отключить некоторые возможности редактирования элементов управления в Visual Studio.NET. Поэтому, если необходима дополнительная инициализация, лучше всего выполнять ее в конструкторе Forml или в другом методе. Окно свойств Это еще одно окно, позаимствованное из старой VB IDE. Базовые классы .NET, которые представляют формы и элементы управления, имеют большое число свойств, определя- определяющих их поведение или внешний вид. Это, например, свойства Width, Height, Enabled (пользователь может воздействовать на элемент управления), Text (текст, выводимый элементом управления). Окно свойств отображает и позволяет редактировать значения многих из этих свойств для тех элементов управления, которые Visual Studio.NET обна- обнаружила, просматривая исходный код: "^ и | Г*: J* [raj j, щ L~J Coned ForeCoter FormeordnSOte RjghtTdeft ЮЯН ■B Crhavto» Га1оН>ор I ContextMenu Enabled llmeMoae Bi □ (now) Default McrCKoft Sens Serf, 8.25pt Щ CortcrolTeat 'seabte No False (none) True NoContrd В Data г i | IB Рея Dr i!GridS«e 'lo*«J SfiapTeGnd В rncus True "8,8 fake True Text The text centered r\ the control. Окно свойств может также наказывать события. Для просмотра событий щелкните мышью на значке с изображением молнии в верхней части окна. 12 Зпк. 69
320 Глава 8 В верхней части окна свойств располагается список, где можно выбрать элемент управления для просмотра. Мы выбрали Forml, основной класс формы для проекта BasicForm, и отредактировали текст, введя 'Basic Form — Hello!'. Теперь проверим ис- исходный код: this.AutoScaleBaseSize = new System.Drawing.SizeE, 13); this.ClientSize = new System.Drawing.SizeB92, 269); this.Controls.AddRange(new System.Windows.Forms.Control[] {this.textBoxl}); this.Name = "Forml"; < fchis^Text = "Basic? Form -- Hello!"; Отметим, что все свойства, показанные в окне свойств, явно объявлены в исходном коде. Для необъявленных свойств Visual Studio.NET отобразит значения по умолчанию, установленные при создании и инициализации формы. Очевидно, что если в окне свойств поменять значение одного из свойств, то в исходном коде появится соответствующий оператор, и наоборот. Окно свойств предлагает удобный способ для получения детального представления о виде и свойствах конкретного элемента управления или окна. Окно классов В отличие от окна свойств окно классов берет свое начало в средах разработки C++ (и J++). Очевидно, что оно будет новым для разработчиков на VB, так как VB6 не поддержи- поддерживал концепцию классов, кроме как в смысле компонентов СОМ. Окно классов на самом деле не считается в Visual Studio.NET отдельным окном — скорее, это дополнительная вкладка в окне Solution Explorer. Окно классов показывает иерархию пространств имен и классов для вашего кода. Оно содержит древовидный список, который можно разворачи- разворачивать для просмотра того, какие пространства имен содержат какие классы и какие классы содержат каких членов: *J ь*ил and Interface! • GelTypeO 11 В *jf Forml В -*£ вак$ and Interfaces Приятной особенностью окна классов является то, что если щелкнуть правой кнопкой мыши на имени любого элемента, доступ к которому можно получить в исходном коде, то появится контекстное меню, содержащее пункт Go To Definition, с помощью которого мож- можно сразу перейти к определению этого элемента в редакторе кода. То же самое достигает- достигается двойным щелчком мыши на этом элементе в окне классов. С помощью контекстного меню можно также добавить к классу поле, метод, свойство или индексатор. Вы указываете сведения о соответствующем члене в диалоговом окне, и код автоматически добавляется в ваш проект. Возможно, это не так полезно для полей и методов, для которых не требуется набирать много кода вручную, однако это очень удобно для свойств и индексаторов. Браузер объектов При программировании в .NET вам понадобятся сведения о том, какие методы и т.п. до- доступны в базовых классах и других библиотеках, на которые ссылается сборка. Это можно посмотреть в окне, называемом браузером объектов.
Программирование в cpeAe.NET 321 Браузер объектов похож на окно классов. Он- показывает древовидный список струк- структуры классов приложения, где можно просмотреть членов каждого класса. Интерфейс пользователя слегка отличается: члены классов отображаются в отдельной панели, а не в самом древовидном списке. Основное отличие, однако, состоит в том, что он позволя- позволяет просматривать не только пространства имен и классы вашего проекта, но также про- пространства имен и классы во всех сборках, описанных в проекте. На рисунке показан браузер объектов с открытым классом ArgumentException из базовых классов .NET: -ll ^1 SyMmt»tem»<45ysla«.R»rxme.5e.«lM<«>n.5e.iol;etic [j -■ >♦ Syst«i£x«p(Kn(ltiing,SriC<n.C<apt«l) || • S ф- О 5/Я«я j Ю *5 Art»*» (Ё оЛС Следует помнить о том, что браузер объектов группирует классы сначала по библио- библиотекам, в которых они расположены, а затем по пространствам имен. К сожалению, так как пространства имен для базовых классов часто распространяются на несколько библио- библиотек, у вас могут возникнуть трудности с поиском конкретного класса, если вы не знаете, в какой библиотеке он находится. Браузер объектов предназначен для просмотра объектов .NET. Если по какой-то при- причине необходимо исследовать установленные СОМ-объекты, то для этой цели по-преж- по-прежнему доступен инструмент OLEView, использовавшийся ранее в C++ IDE. Он находится в меню Tools, где расположено еще несколько утилит. Разработчики на VB не должны путать браузер объектов .NET с браузером объектов VB6 IDE - это разные вещи. Браузер объектов .NET предназначен для просмотра классов .NET, в то время как одноименный инструмент в VB6 служит для просмотра компонентов СОМ. Если требуется функциональность старого браузера объектов, необходимо использовать инструмент OLEView. Server Explorer Server Explorer можно использовать для получения сведений о компьютере во время на- написания программы: L I Data Connections ^ <AddConnection...> 1 Щ <AddServer...> В Щ biggybiogy B- OS Event Logs В fail Loaded Modules & О Performance Counters В ЕЗ Processes 6) ^Services ф в SQL Servers Ш % Web Services i I
322 Глава в Как видно на рисунке, с помощью Server Explorer можно исследовать соединения с базами данных, получить информацию о службах, web-службах и запущенных процессах. Server Explorer связан с окном свойств. Если, например, открыть ветвь Services и вы- выбрать конкретную службу, то в окне свойств будут отображены свойства этой службы. Прикрепляющие кнопки Исследуя Visual Studio.NET, вы заметите, что многие из "окон" обладают возможностя- возможностями, более присущими панелям инструментов. В частности, за исключением редактора кода, все они могут быть прикреплены. Другая их интересная особенность в том, что ког- когда они прикреплены, то в правом верхнем углу каждого окна, рядом с кнопкой минимиза- минимизации появляется дополнительный значок с изображением кнопки. Он и действует, как кнопка, — его можно использовать для "прикрепления" окон, чтобы они оставались от- открытыми. Когда окна прикреплены (кнопка нарисована вертикально), они ведут себя как обычные окна. Однако если окна не прикреплены (кнопка нарисована горизонталь- горизонтально), они остаются открытыми только, находясь в фокусе. При смещении фокуса (напри- (например, вы щелкнули где-то еще) они сворачиваются в панель. Например, верхняя часть прикрепленной панели инструментов выглядит следующим образом: m ToolbdiX Data ? Coirppnents •- . Windows Forms ■Щ- ^ tt Pointer ALWCabel^ atj Button 1аЫ TextBox § .MainMenu if X >.T-^^L- f - *» 'I *_ - ■• ■"-'■• Если "открепить" ее, но оставить курсор над панелью, то она будет выглядеть так: Todlb'fe- Data , Components".!. xpic н А g ; '• вь| lows roftns*« "' ■. .-^j. •*•' Pointer UnHabel .-•= TextBox, ■ MainMenu .,..', Если же увести курсор мыши за пределы окна и щелкнуть где-нибудь, то окно исчезнет: '■ЕЕ Hie
Программирование в cpeAe.NET 323 "Прикрепление" и "открепление" окон предоставляет один из способов эффективно- эффективного использования ограниченного пространства на экране. Похожие концепции можно встретить в некоторых программах сторонних производителей, например в PaintShop Pro. Во многих Unix-системах прикрепляемые окна применялись довольно часто. Сборка проекта В этом разделе будут рассмотрены возможности, предлагаемые Visual Studio.NET для сборки проекта. Сборка, компиляция и создание программы Прежде чем приступить к рассмотрению различных вариантов сборки, необходимо уточнить один терминологический момент. Часто встречаются три разных термина, ис- используемых в связи с получением из исходного кода некоторого исполняемого кода: сборка (building), компиляция (compiling) и создание программы (making). Эти терми- термины возникли по той причине, что до недавних пор процесс получения исполняемого кода из исходного включал в себя несколько стадий (и это по-прежнему справедливо для C++). В основном это обуславливалось тем, что программа состояла из большого числа исходных файлов. В C++, например, каждый исходный файл необходимо компилировать отдельно. В результате этого получают так называемые объектные файлы, каждый из ко- которых содержит что-то типа исполняемого кода, но каждый файл соответствует только одному исходному файлу. Для генерации исполняемого кода эти объектные файлы необ- необходимо скомпоновать. Совмещенный процесс обычно называли процессом сборки (по крайней мере, для платформы Windows). Однако компилятор С# является более слож- сложным и способен обработать все исходные файлы за один раз. Поэтому в данном случае отсутствует стадия компоновки, поэтому для С# термины "компилировать" и "собирать" эквивалентны. Термин "создание" (make) означает то же самое, что и сборка, но не используется в контексте С#. Этот термин появился во времена мэйнфреймов, когда для компиляции проекта, состоящего из большого числа исходных файлов, необходимо было создавать отдельный файл, содержащий инструкции компилятору по сборке проекта: какие фай- файлы включать, какие библиотеки компоновать и т.д. Файл был известен как maliejile, и до сих пор это название является стандартным для Unix, Linux и т.п. Такие файлы, как правило, не требуются в Windows, хотя их можно создать самостоятельно (или заставить создать их Visual Studio.NET), если они потребуются. Отладочные и окончательные версии Идея получения различных версий хорошо известна разработчикам на C++, но не совсем понятна разработчикам на VB. Смысл заключается в том, что при отладке от исполняе- исполняемого кода требуется несколько иное поведение, чем от кода, готового к реализации. По- Помимо того, что готовый к реализации код должен работать, необходимо, чтобы размер исполняемого файла был настолько мал, насколько возможно, а скорость работы была максимальной. К сожалению, эти требования вступают в противоречие с требованиями по отладке кода по следующим причинам. Оптимизация Высокая производительность частично достигается за счет того, что компилятор выпол- выполняет оптимизацию кода. Он просматривает исходный код и определяет те места, где мож- можно изменить код таким образом, чтобы это изменение не давало никакого иного эффекта, кроме увеличения производительности. Например, если компилятор встретит код: double InchesToCm(double Ins) .1 return <Xns*2.54; } II далее по коду Y = InchesToCm(X) ; то он может заменить его следующим: Y = X * 2.54,- А код:
324 Глава 8 string Message = "Hi Console.WriteLine(Message); } можно изменить так: Console.WriteLine ("Hi") ,- Таким образом исключается необходимость объявления лишнего объекта в процессе компиляции. Невозможно сказать, какую оптимизацию выполняет компилятор С#, т.е. будут ли произведены такие замены, как описано выше, поскольку это не документировано. По очевидным коммерческим причинам компании, которые создают компиляторы, не очень охотно делятся сведениями о том, какие хитрости используют их компиляторы. Необходимо также подчеркнуть, что оптимизация не действует на исходный код — она затрагивает только содержимое исполняемого кода. Тем не менее приведенные примеры дают хорошее представление о том, что можно ожидать от оптимизации. Проблема заключается в том, что приведенные примеры оптимизации не подходят для отладки. Предположим, что в первом примере необходимо задать точку останова внутри, метода InchesToCm [), чтобы посмотреть, что происходит внутри. Как это сде- сделать, если исполняемый код вообще не имеет этого метода, поскольку компилятор убрал его? Как можно посмотреть содержимое переменной Message, если ее нет в откомпили- откомпилированном коде? Отладочные символы При отладке часто требуется посмотреть значения переменных, в исходном коде они имеют имена. Проблема заключается в том, что исполняемый код не содержит имен — компилятор заменяет их адресами в памяти. .NET несколько изменила ситуацию, в резу- результате чего некоторые элементы в сборках хранятся со своими собственными именами, но это справедливо лишь для небольшого числа элементов, например для открытых классов и методов. Нет смысла просить компилятор показать значение переменной Не- ightlnlnches, так как при просмотре исполняемого кода он видит только адрес, но не видит ссылки на Heightlnlnches. Поэтому для осуществления отладки в исполняемом коде должна содержаться дополнительная отладочная информация. Помимо прочего она будет включать в себя имя переменной и строковую информацию, которая позволит отладчику выяснить, какие инструкции машинного языка в исполняемом файле соответ- соответствуют инструкциям в оригинальном исходном коде. Однако такая информация не нуж- нужна в окончательной версии продукта как по коммерческим причинам (наличие отладоч- отладочной информации значительно облегчает дизассемблирование), так и потому, что она увеличивает размер исполняемого файла. Дополнительные отладочные команды в исходном коде При отладке в код обычно вставляются дополнительные строки с важной отладочной ин- информацией. Очевидно, что эти команды необходимо исключить из окончательной версии программного продукта. Это можно сделать вручную, однако проще было бы пометить их таким образом, чтобы при компиляции окончательной версии компилятор сам игно- игнорировал бы их. В главе 6 было показано, как это можно сделать с помощью соответствую- соответствующего символа препроцессора и атрибута Conditional, т.е. посредством так называемой условной компиляции. Таким образом, при отладке программное обеспечение необходимо компилировать не- несколько иначе, чем при создании окончательной версии. Visual Studio.NET способна учиты- учитывать это, поскольку она хранит все сведения об опциях, передаваемых компилятору при компиляции кода. Все, что требуется от Visual Studio.NET для поддержки этой возможно- возможности,— хранить несколько наборов таких сведений. Различные наборы информации для сборки приложения называются конфигурациями. При создании проекта Visual Studio.NET автоматически создает две конфигурации — Debug и Release: О Конфигурация Debug обычно предписывает, что оптимизация не должна произ- производиться, что в исполняемом файле должна присутствовать дополнительная от- отладочная информация и что компилятор должен считать, что задан символ препроцессора Debug, если только для него явно не указано #undef ined в исход- исходном коде. О Конфигурация Release обычно предписывает, что компилятор должен выпол- выполнить оптимизацию, что в исполняемом файле должна отсутствовать дополнитель- дополнительная отладочная информация и что компилятор не должен считать, что имеется символ препроцессора Debug.
Программирование в среде-NET 325 Кроме того, можно определить свои собственные конфигурации. Это делается, напри- например, в том случае, если требуется собрать версии программного обеспечения Professional и Enterprise, чтобы иметь возможность продавать две версии программного продукта. В прошлом для проектов C++ являлось общепринятой практикой создавать конфигура- конфигурации ASCII и Unicode, так как символы Unicode поддерживались Windows NT, но не поддерживались Windows 95. Выбор конфигурации В связи с тем, что Visual Studio.NET хранит сведения о нескольких конфигурациях, воз- возникает вопрос: как она определяет, какую из них использовать при сборке проекта? Дело в том, что всегда существует активная конфигурация, которая будет применяться в том случае, если Visual Studio.NET требуется откомпилировать проект. (Отметим, что кон- конфигурации создаются для каждого проекта, а не для каждого решения.) При создании проекта активной конфигурацией по умолчанию является отладочная конфигурация. Активную конфигурацию можно изменить, выбрав в меню Debug | Set Active Configuration. Редактирование конфигураций Разрешается просматривать и редактировать активные конфигурации. Для этого необ- необходимо выбрать меню Project (в Solution Explorer должен быть открыт соответствующий проект), а затем пункт Properties. На экране появится диалоговое окно. (То же самое окно можно вывести на экран, щелкнув правой кнопкой мыши на имени проекта в Solution Explorer и выбрав Properties в контекстном меню.) В этом окне выводится дерево просмотра, которое позволяет выбрать пункты для просмотра и редактирования. К сожалению, мы не в состоянии рассмотреть все предла- предлагаемые возможности, но познакомимся с наиболее важными из них. На рисунке дерево просмотра имеет две вершины верхнего уровня: Common Properties (Общие свойства) и Configuration Properties (Свойства конфигурации). Общие свойства — это те свойства, которые являются общими для всех конфигураций, в то время как свойства конфигурации различаются для каждой из них: I BasicConsoleApp Property Pages r. Brfoudfcii JHf* J 2tetW |W £j Common Properties ,JB Application <t General ""' Q^^SCSE""" Designer DefatAs j Output Type References Path ; ■ Default Namespace Ш Configuration Properties 5 Startup Object * Appkation Icon S Project :Proie:tF!* Project Fc*det Output FJe ЦЩ BosicConsoleADD Console Appkation BasicConsoleApp (Not set) BoScCoreoleApp.cspioi F-:,CB Projects\Pro Ot CodoKre.Pr j BasicConsoleApp.exe 'B Wrapper Assembly lor ActiveX/CGM Object* i Wrapper Assembly Key Me __ Wrapper Assembly Key Nam Assembly Name 1 1 The name of the output file that wi hold assembly metadata (mwtfest). OK | Cancel | Aprfy \ X -- Help 1 Здесь показаны общие для всех конфигураций опции компилятора для проекта Basic- BasicConsoleApp, созданного ранее. Необходимо отметить, что можно выбрать имя и тип сборки, которую необходимо создать. Возможные варианты: консольное приложение, приложение Windows и библиотека классов. Разумеется, при желании можно изменить тип сборки. (Хотя, если это действительно требуется, возникает закономерный вопрос, почему необходимый тип сборки не был выбран на этапе создания проекта.)
326 Глава 8 Следующий экран показывает свойства конфигурации для сборки приложения. В верхней части окна присутствует список, в котором можно указать конфигурацию для просмотра. В данном случае для конфигурации Debug определены символы пре- препроцессора DEBUG и TRACE. И, что характерно для отладочной конфигурации, код не оптимизируется и создается дополнительная отладочная информация: BasicConsoleApp Property Pages £onfiguratjon: | Platform: jActlve<.NEr) qpfqunitlon Manager... £j Common Properties £j Conhguratlon Properties •> BuM Oebujgng Advanced Q Code brncrotion CondtionalCcrr«4ation Constants >OC6US;TRACE OpUrrse code jFaba Che* for Antrmwc CnwflowfUndei False Alow unsafe code blocks 7F«4e В Li rent «nd Wormnqs Iwarrtnglnvd wamnglevel' {Treat Warnings As Errors False В Output» Output Path binlDebnA „ xnDociramtatnnFls j Generate Debugging Irformattort 'True Output Path Specifies the location of the output Has For the project's configuration. Caned I (trr>' I H<* На практике не так уж часто требуется менять конфигурацию. Однако вы должны уметь выбирать соответствующую конфигурацию в зависимости от того, как компилируется проект, и знать, в чем состоит разница между конфигурациями. Отладка Принципы процесса отладки — задание точек останова и просмотр значений перемен- переменных — не изменились в Visual Studio.NET по сравнению с различными IDE Visual Studio 6. Поэтому мы коротко рассмотрим особенности, предлагаемые Visual Studio.NET, и оста- остановимся на тех моментах, которые могут оказаться новыми для некоторых разработчи- разработчиков. Более подробно мы обсудим работу с исключениями, так как они могут вызывать проблемы при отладке. В С#, так же как и в других языках, предшествовавших .NET, основная методика от- отладки заключается в установке точек останова и в их использовании для исследования того, что происходит в коде в определенный момент его исполнения. Точки останова В Visual Studio.NET можно установить точки останова на любой строке исполняемого кода. Наиболее простой способ сделать это — щелкнуть на строке в затененной области с левой стороны окна редактора кода (или выбрать строку и нажать клавишу F9). В резуль- результате для этой строки будет задана точка останова. По достижении этой строки исполне- исполнение будет прервано и управление будет передано отладчику. Как и в предыдущих версиях Visual Studio, точка останова обозначается большим кружком с левой стороны редакто- редактора. Visual Studio.NET также подсвечивает эту строку, отображая текст другим цветом. Если щелкнуть на строке еще раз, точка останова будет снята. Если вас не устраивает останов исполнения при каждом достижении заданной строки, то можно сформировать условные точки останова. Для этого необходимо войти в меню Debug и выбрать пункт Breakpoints. На экране появится диалоговое окно, запрашивающее сведения о точке останова, которую вы намереваетесь создать. Среди прочего вы можете: О Указать, что исполнение должно быть прервано тогда, когда точка останова будет пройдена определенное число раз. О Указать, что точка останова должна срабатывать через каждые п проходов, напри- например, через каждые двадцать проходов (полезно при отладке больших циклов). О Задать точку останова для переменной, а не инструкции. В этом случае будет от- отслеживаться значение переменной, и точки останова сработают, когда ее значе- значение изменится. Однако использование этой возможности значительно снижает
Программирование в cpeAe.NET 327 скорость работы кода. Проверка значения переменной после каждой инструкции отнимает большое количество процессорного времени. Окно наблюдения По достижении точки останова обычно требуется просмотреть значения переменных. Наиболее простой способ сделать это — навести мышь на соответствующую переменную в редакторе кода. В результате на экране появится небольшой прямоугольник, содержа- содержащий значение переменной. Однако для исследования содержимого переменных можно также использовать окно наблюдения. Оно появляется только тогда, когда программа за- запущена в отладчике. Выглядит это окно следующим образом: Type „ Nertlne JJextjjne.length _ neytwRead _ "Darrius: I see your ships off ray coast.. 41 J> _" ~Z_ {System.IO.StreamReadarV mt System.IO.StrgaroReader sdWtiteTexM Wrsx. ProfessionsCSrarQ.Chaoteri Ct)Locals j|3Watch На экране показана информация для примера ReadWriteText (см. главу 14). Рядом с переменными, которые являются классами или структурами, выводится значок +. Нажав на него, можно развернуть переменную и просмотреть значения ее полей. Три вкладки этого окна предназначены для просмотра различных переменных: О Autos отслеживает несколько последних переменных, доступ к которым осуществ- осуществлялся в процессе исполнения программы. О Locals отслеживает переменные, которые доступны в методе, исполняемом в данный момент. О Watch отслеживает все переменные, которые были явно указаны по именам в окне наблюдения. Исключения Исключения полезны тогда, когда готовое приложение поставляется клиенту. При правильном применении они гарантируют, что приложение будет работать корректно, а пользователь никогда не увидит технических диалоговых окон. К сожалению, исклю- исключения мешают отладке приложения. Проблема имеет две стороны: D Если возникает исключение, то в процессе отладки чаще всего не требуется, что- чтобы программа автоматически обрабатывала его, особенно если обработка означа- означает завершение программы! Напротив, нужно, чтобы до кода добрался отладчик и можно было посмотреть, почему возникло исключение,— для того чтобы устра- устранить причину его возникновения до продажи приложения. Проблема в том, что если вы пишете надежный и устойчивый код, ваша программа автоматически об- обработает практически все, включая ваши собственные ошибки, которые должны быть обнаружены. □ Если возникает исключение, для которого не был написан обработчик, среда испол- исполнения .NET все равно будет искать его. Как только выяснится, что обработчика иск- исключения не существует, она завершит приложение. После этого уже нельзя будет просмотреть значения переменных, так как все они выйдут из области видимости. Разумеется, можно поставить точки останова внутри блоков catch, но это мало по- помогает, поскольку по достижении блока catch управление, по определению, выйдет из соответствующего блока try. Следовательно, переменные, значения которых необходи- необходимо просмотреть для выявления причины возникновения ошибки, выйдут из области ви- видимости. Нельзя будет даже посмотреть трассировку стека для определения метода, который исполнялся в момент встречи с оператором throw, поскольку управление будет передано за пределы этого метода. Задание точки останова на операторе throw решит проблему. Однако если вы пишете действительно устойчивый код, в нем будет огромное число операторов throw. Как можно определить, какой из них сгенерировал исключение?
328 Глава 8 Ha самом деле Visual Studio дает точные ответы на все эти вопросы. В меню Debug при- присутствует пункт меню под названием Exceptions. Он выводит на экран диалоговое окно, ко- которое позволяет указать, что должно происходить при генерации исключения. Можно выбрать как вариант продолжения исполнения, так и вариант остановки и начала отлад- отладки—в этом случае исполнение прекращается, а отладчик переходит к оператору throw: nceptions Exceptions: S-Q t++ Exception* й О Common Language Runtime Exception! 1 © О System SystefTi.ArgumentExcepbon 5ystem.Argurr<entNua£xcep*»Qn 5уйет.АгКЬте*(сЕхсерЙоп О SyEtem.ArfayTypeMismMchExcepbCpri Syrtem-ContextMiTshfttxception Sy^em.DivKie9y2eroException .- When the exception is thrown: j Г freak fr*o the debugger | <~ Use parent setting - U the exception is not handed: С едок into the debute/ С Con^nue P Ute parent «tbog Действительно мощным этот инструмент делает возможность изменения поведения для каждого класса исключений. Например, на приведенном выше экране мы указали Vi- Visual Studio.NET, что необходимо запускать отладчик каждый раз, когда исключение ге- генерируется базовым классом .NET (обозначается красным крестом), но не вызывать отладчик при возникновении исключения ArgumentOutOfRangeException или Аггау- TypeMismatchException. Visual Studio.NET осведомлена обо всех классах исключений, доступных в базовых классах .NET, а также о некоторых исключениях, которые могут быть сгенерированы вне среды .NET. Visual Studio.NET не знает о ваших собственных классах исключений, однако эти классы можно добавить в список и тем самым указать, при возникновении ка- каких исключений исполнение программы должно прекратиться. Для этого необходимо щелкнуть мышью на кнопке Add (которая доступна в том случае, если в дереве выбран узел верхнего уровня) и набрать имя вашего класса исключений. Другие инструменты .NET Мы тщательно изучили Visual Studio.NET, поскольку этим инструментом вы наверняка будете пользоваться при разработке программ. Однако существует целый ряд других ин- инструментов, облегчающих программирование. Здесь мы поговорим об утилите общего назначения WinCV, которая позволяет просматривать базовые классы. WinCV Созданная Microsoft утилита WinCV может применяться для исследования базовых клас- классов и определения доступных методов. Она похожа на браузер объектов Visual Studio.NET, за исключением того, что это независимое приложение, которое показывает все базовые классы, в то время как браузер объектов показывает только те из них, на которые имеются ссылки из вашего проекта:
Программирование в среде.ЫЕТ 329 L*Svstem.Array rvpeMismatchException - Class Viewer Searching For [exception I, Option] Search RestAs Selected Class Class Name I Nafficsperej £ Excepbonlnfo ► „.Excepbonlnstance J_VaJue£xceptton jAnttguousMatchExcep,. £pp ^ArgimentExcepton ^gArgumentNutExcepticn *SArgumentOutOfRanoe... ^AntrmetkfxceDOon *!j BadlmageformatExce.. ^fCanceWoduleExcepUon System.Ref System.Ref— System.Dat System.Ref System System System System System System System System System.We System 5ystem.Cor System.Cor >/ from tnoau/e 'g:\tin~nnt\itrtcrosorT.net\rrmet*ork\vi.o public class ArrayrypeMi smatchExcepti on : f sys temExcept1on, , Sys tern. Runtime. Seri al i aiati on. XSer t ai 1 zabi e // conscrucccrs public ArrayTypeMi smatchExcepti onQ; public ArrayTypeMismatchExceptionfstring message) public ArrayTypeMismatchException(string message,j s/ Properties public string He 1 pL1nk { virtual get; virtual sett public Exception xnnerException { get: } . public string Message { virtual get; J public string source { virtual get; virtual set; ' public string stackTrace { virtual get; } public MethodBase Targetsite ( get; ) // Methods \ public virtual bool Equal5(object obj); t public virtual Exception GecBaseExceptionO; public virtual int GetHashCodeO; , public virtual void CetObjectOataCSystem.Runtime.! public туре cetTypeO; lie virtual string TostringC); ttr& Ready WinCV удобна в применении. Она запускается либо из командной строки, либо из окна Run — нужно набрать wincv. После ее запуска можно ввести текст в верхней части окна WinCV, и по мере набора утилита будет выполнять поиск среди базовых классов и отображать те из них, чьи имена содержат указанное слово. Эти классы выводятся в ле- левой части окна в списке. Если щелкнуть мышью на конкретном классе, то в правой части окна будут показаны его члены в формате, примерно соответствующем синтаксису С#. Указания по использованию .NET Познакомимся с рекомендациями, которые Microsoft написала для программирования в .NET. В любом языке программирования существуют традиционные стили программирова- программирования. Эти стили являются не частью самого языка, а соглашениями, скажем, по именова- именованию переменных или использованию определенных классов, методов или функций. Если большинство разработчиков будут следовать одинаковым соглашениям, то им бу- будет проще понять код друг друга, что, в свою очередь, облегчает поддержку программы. Так, общим (хотя и не универсальным) соглашением в Visual Basic 6 было то, что строко- строковые переменные должны иметь имена, начинающиеся с s или str, например String sResult или String strMessage. Однако соглашения зависят от языка и среды разра- разработки. Программисты на C++ для платформы Windows традиционно используют пре- префикс psz или lpsz для обозначения строк: char *pszResult; char *lpszMessage;. Но на Unix-машинах такие префиксы не применяются: char *Result; char 'Message;. Из примеров, приведенных в книге, видно, что в соответствии с соглашениями в С# имена переменных не должны иметь префиксов: string Result; string Message;. Кстати, соглашение, согласно которому имена переменных содержат префикс, указывающий тип данных, известно как "венгерский" стиль именования объектов. При чтении такого кода разработчики могут сразу же сказать по имени переменной, какой тип данных она представляет. В то время как для многих языков соглашения по именованию вырабатывались одно- одновременно с развитием языка, для С# и платформы .NET Microsoft написала подробные рекомендации по использованию, которые приведены в документации MSDN для .NET/C#. Следовательно, с самого начала программы .NET будут иметь более высокий уровень совместимости по части понимания кода другими разработчиками. Эти реко- рекомендации были разработаны с учетом опыта, полученного на протяжении более двадца- двадцати лет объектно-ориентированного программирования, и в результате являются тщательно продуманными и хорошо восприняты сообществом разработчиков. Так что имеет смысл следовать этим рекомендациям. Однако необходимо отметить, что рекомендации не то же самое, что спецификации языка. Рекомендаций следует придерживаться по мере возможности. Если у вас имеется ве- веская причина для их несоблюдения, это не будет проблемой. Отклонение от рекомендаций
330 Глава 8 должно быть вызвано реальными причинами, а не простым нежеланием. Во многих при- примерах мы не следовали этим рекомендациям в основном из-за того, что они разработаны для более крупных программ, чем наши. Использование этих соглашений сделало бы наши примеры труднее для понимания. Полные рекомендации для хорошего стиля программирования являются довольно объемными. Мы рассмотрим лишь наиболее важные соглашения. Если вы хотите, чтобы ваш код полностью соответствовал рекомендациям, обратитесь к документации MSDN. Соглашения по именованию Одним из важных моментов является выбор имен для элементов программы: переменных, методов, классов, перечислений и пространств имен. Очевидно, что названия обязаны отражать назначение элемента и не должны конф- конфликтовать с другими именами. Общая философия платформы .NET состоит в том, что имя переменной должно отражать назначение экземпляра переменной, а не тип дан- данных. Например, Height —хорошее название, a integerValue — нет. Однако этот прин- принцип является труднодостижимым идеалом. В частности, при работе с элементами управления в большинстве случаев вам будет удобнее использовать имена переменных, подобные ConfirmationDialog и ChooseEmployeeListBox. Конкретные рекомендации по именованию включают в себя следующие разделы. Регистры имен Практически во всех случаях для имен следует использовать стиль Pascal, при котором первая буква каждого слова в названии является прописной: EmployeeSalary, Confir- ConfirmationDialog, PlainTextEncoJing. Вы заметите, что имена пространств имен, клас- классов и членов в базовых классах соответствуют стилю Pascal. Соединение слов с помощью знака подчеркивания не приветствуется, поэтому не придумывайте такие имена, как employee_salary. В других языках часто используют все прописные буквы в названиях констант. Это не рекомендуется в С#, поскольку такие имена трудно читать, лучше применять паскалевский стиль: "const int Max_mumLength; Еще одна рекомендуемая схема — именование в стиле camel. Именование camel ана- аналогично паскалевскому стилю, за исключением того, что первая буква первого слова не является прописной: employeeSalary, ConfirmationDialog, plainTextEncoding. Существуют две ситуации, в которых лучше применять такое именование. Имена всех параметров, передаваемых в методы, должны записываться в стиле camel: .public void RecordSale(string salesmanName, int quantity )•> Также можно использовать camel-соглашение для того, чтобы отличить два элемен- элемента, которые в противном случае имели бы одинаковые имена. Наиболее общий случай, когда свойство является оболочкой для поля: private string1' employeeName,- ' public string EmployeeName { get - { return employeeName; Приведенный код является совершенно корректным с точки зрения рекомендаций. Отметим, однако, что в этом случае следует применять соглашение camel для закрытых членов и соглашение Pascal для открытых или защищенных членов, чтобы другие клас- классы, использующие ваш код, видели только имена в стиле Pascal (за исключением имен параметров). В большинстве случаев следует применять соглашения Pascal. Тем не менее соглаше- соглашение camel рекомендуется для закрытых переменных, которые не видны вне класса, где две переменные имеют одинаковое назначение. Например, если есть public свойство, которое инкапсулирует pr^vaLe поле с тем же именем, то можно использовать соглаше- соглашение camel для поля и соглашение Pascal для свойства, как в приведенном выше примере Empl oy eeName.
Программирование в среде-NET 331 Также необходимо обращать внимание на чувствительность к регистру. С# чувстви- чувствителен к регистру, поэтому синтаксически в С# допустимо, чтобы имена различались то- только регистром. Однако нужно помнить, что ваши сборки могут быть вызваны из приложений VB.NET, а VB.NET не является чувствительным к регистру. Поэтому испо- использовать имена, отличающиеся только регистром, можно лишь в том случае, если они никогда не будут видны вне сборки (приведенный пример удовлетворяет этому условию, поскольку имя в стиле camel присвоено private переменной). В противном случае код, написанный в VB.NET, не сможет корректно использовать вашу сборку. Стили имен Необходимо по возможности делать так, чтобы стиль всех имен совпадал. Например, если один из методов в классе называется ShowConf irmationDialog (), то другому мето- методу не следует давать имя ShowDialogWarning() или WarningDialogShow(). Он должен называться ShowWarningDialog. Идея ясна? Имена пространств имен Имена пространств имен следует выбирать особенно тщательно для того, чтобы избе- избежать использования такого же имени, которое применяется где-то еще. Необходимо по- помнить, что .NET различает имена объектов в разделяемых сборках только по именам пространств имен. Если использовать для двух пакетов программного обеспечения одно и то же имя пространства имен и установить оба пакета на один компьютер, возникнут проблемы. Рекомендуется создавать пространство имен верхнего уровня с именем вашей компании, а затем вкладывать пространства имен, постепенно сужая их названия до тех- технологии, группы или отдела, где вы работаете, или до названия пакета, для которого предназначены ваши классы. Microsoft рекомендует имена пространств имен, которые начинаются с <НазваниеКомпании>.<НазваниеТехнологии>, например, WeaponsOf- DestructionCorp.RayGunControllers или WeaponsOfDestruetionCorp.Viruses. Примеры в этой книге в основном следуют этой рекомендации. Большинство из них рас- расположены в пространствах имен с названиями типа Wrox. Prof essionalCSharp. Chap- ter<X>.<SampleName>. Эти имена сравнительно безопасны — маловероятно, что кто-то еще создаст пространства имен с такими же названиями. Имена и ключевые слова Важно, чтобы имена не конфликтовали с ключевыми словами. Если попытаться в про- программе назвать элемент по имени одного из ключевых слов С#, это практически всегда вызовет синтаксическую ошибку, так как компилятор предположит, что имя соответству- соответствует оператору. Однако из-за того, что к классам, возможно, будет осуществляться обраще- обращение из кода, написанного в других языках, важно не использовать ключевые слова других языков .NET. Вообще говоря, ключевые слова C++ похожи на слова С#, поэтому пробле- проблемы с C++ маловероятны; кроме того, ключевые слова, характерные для Visual C++, как правило, начинаются с двух знаков подчеркивания. Как и в С#, ключевые слова C++ запи- записываются строчными буквами, и если вы будете следовать именованию объектов в стиле Pascal, то в их имени всегда будет присутствовать как минимум один прописной символ, поэтому не будет риска конфликта с ключевыми словами C++. С другой стороны, возмож- возможны проблемы с VB.NET, в котором значительно больше ключевых слов, чем в С#, а его нечувствительность к регистру означает, что вы не можете положиться на имена в стиле Pascal для классов и методов. В приведенной таблице перечислены ключевые слова и стандартные вызовы функ- функций в VB.NET, которые не следует применять в вашем коде, если он будет вызываться из VB.NET: Abs Add AddHandler AddressOf Alias And Ansi Do Double Each Else Elself Empty End Loc Local Lock LOF Log Long Loop RGB Right RmDir Rnd RTrim SaveSettings Second
332 Глава 8 AppActivate Append As Asc Assembly Atan Auto Beep Binary BitAnd BitNot BitOr Bitxor Boolean ByRef Byte ByVal Call Case Catch CBool CByte CDate CDbl i CDec ChDir ChDrive Choose Chr CInt Class Clear CLng Close Collection Command Compare Const Cos Enum EOF Erase Err Error Event Exit Exp Explicit ExternalSource False FileAttr FileCopy FileDateTime FileLen Filter Finally Fix For Format FreeFile Friend Function FV Get GetAllSettings GetAttr GetException GetObject GetSetting GetType GoTo Handles Hex Hour If Ilf Implements Imports LTrim Me Mid Minute MIRR MkDir Module Month MustInherit MustOverride MyBase MyClass Namespace New Next Not Nothing Notlnheritable NotOverridable Now NPer NPV Null Object Oct Off On Open Option Optional Or Overloads Overridable Overrides ParamArray Pmt PPmt Preserve Print Seek Select SetAttr SetException Shared Shell Short Sign Sin Single SLN Space Spc Split Sqrt Static Step Stop Str StrComp StrConv Strict String Structure Sub Switch SYD SyncLock Tab Tan Text Then Throw TimeOfDay Timer TimeSerial TimeValue To Today
Программирование в среде-NET 333 CreateObject CShort CSng CStr CurDir Date DateAdd DateDiff DatePart DateSerial DateValue Day DDB Decimal Declare Default Delegate DeleteSetting Dim Dir In Inherits Input InStr Int Integer Interface IPmt TRR Is IsArray IsDate IsDbNull IsNumeric Item Kill LCase Left Lib Line Private Property Public Put PV QBColor Raise RaiseEvent Randomize Rate Read Readonly ReDim Remove RemoveHandler Rename Replace Reset Resume Return Trim Try TypeName TypeOf UBound UCase Unicode Unlock Until Val Weekday While Width With WithEvents Write WriteOnly Xor Year Использование свойств и методов Что касается классов, то может возникнуть вопрос: для представления некоторой величи- величины следует использовать свойство или метод? Правила здесь просты, но в целом свойства необходимо применять тогда, когда какой-то объект должен действительно выглядеть как переменная. Это означает, помимо прочего, что: О Клиентский код должен иметь возможность прочитать его значение. Свойства только для записи не рекомендуются, поэтому необходимо использовать метод Set Password i ), а не свойство Password. О Чтение значения не должно занимать слишком много времени. Если оно является свойством, то предполагается, что его можно прочитать относительно быстро. О Чтение значения не должно иметь никаких побочных эффектов. Например, уста- установка значения свойства не дает побочных эффектов, не связанных напрямую с этим свойством. Установка ширины диалогового окна имеет очевидный эффект изменения вида диалогового окна на экране. Это вполне допустимо, поскольку связано с рассматриваемым свойством. О Должна существовать возможность установки свойств в любом порядке. Плохой практикой является генерация исключения в процессе установки свойства из-за того, что другое, связанное с данным свойство не было установлено. Например, если для использования класса, осуществляющего доступ к базе данных, необхо- необходимо задать свойства ConnectionString, UserName и Password, убедитесь при реализации класса, что пользователь может установить их в любом порядке. О Последовательные чтения свойства должны давать один и тот же результат. Если значение свойства будет непредсказуемо меняться, его необходимо реализовывать как метод. В классе, который отслеживает движение автомобиля. Speed не явля- является хорошим кандидатом на роль свойства. В данном случае нужно использовать метод GetSpeedO. В то же время Weight и EngineSize — хорошие кандидаты для свойств, поскольку они не меняются для данного объекта.
334 Глава 8 Если элемент удовлетворяет приведенным критериям, его можно представить как свойство. В противном случае необходимо использовать метод. Использование полей Поля всегда должны быть закрытыми. Лишь в некоторых случаях допустимо, чтобы кон- константные поля или поля только для чтения были открытыми. Дело в том, что если сделать поле открытым, это может помешать расширению или изменению класса в будущем. Приведенные рекомендации должны дать ясное представление о хорошем стиле программирования, и их следует использовать совместно с хорошим стилем объект- объектно-ориентированного программирования. Нужно отметить, что при написании базовых классов .NET Microsoft следовала своим собственным рекомендациям. Если вы хотите получить представление о соглашениях, ко- которые необходимо соблюдать при написании .КЕТ-кода, взгляните на базовые классы — посмотрите, как называются классы, члены, пространства имен и как работает иерархия классов. Если вы будете писать свой код в том же стиле, у вас не должно возникать серь- серьезных ошибок. Заключение Эта глава посвящена инструментам, которые были созданы Microsoft для того, чтобы максимально упростить написание кода на С# (а также на C++ и VB.NET). В частности, мы изучили Visual Studio.NET. Были также рассмотрены некоторые соглашения, кото- которых следует придерживаться при написании кода на С#. .NET предлагает ряд замечатель- замечательных инструментов, которые значительно ускоряют процесс разработки и отладки программного обеспечения, но с другой стороны, на вас лежит ответственность за пра- правильное применение этих инструментов — вы должны писать код, который будет поня- понятен другим программистам и прост в обслуживании, т.е. при создании кода нужно соблюдать приведенные рекомендации.
Приложения Windows При разработке и запуске приложений Windows всегда возникали некоторые трудности. В самом начале для создания приложений Windows программисты на С использовали не- непосредственно Win32 API. Затем появился Visual Basic, который упростил построение приложений Windows, предоставив программисту графическую среду разработки. Про- Программисты на Visual Basic могли создать форму, добавить в нее элементы управления и реализовать обработчики событий. Среды разработки Visual Basic и Visual C++ всегда отличались тем, как они поддержи- поддерживают разработчиков приложений. Программисты на Vusial C++ в значительной степени полагались на Microsoft Foundation Classes (MFC). Программист на Vusial C++ использо- использовал мастера создания приложения, который содержал требуемую функциональность, а затем писал код, применяя MFC, для реализации этой функциональности. Для создания элементов пользовательского интерфейса Visual Basic и Visual C++ предлагают редактор ресурсов, но для каждой среды разработки применяется свой редактор ресурсов. Эти языки имеют различную модель описания пользовательского интерфейса. Visual Studio.XET срчетает лучшие стороны этих сред и предлагает универсальную среду для создания приложений Windows. Разработчики по-прежнему могут воспользова- воспользоваться дизайнером форм. Однако теперь компоненты пользовательского интерфейса пред- представлены общим набором классов. Поэтому независимо от того, используете вы Visual Basic или С#, модель программирования и редактор ресурсов будут одними и теми же. Эта модель программирования носит название Windows Forms (или WinForms). В этой главе обсуждаются темы: О Создание приложения Windows О Использование элементов \ правления в приложении Windows О Создание собственного элемента управления □ Использование в приложении элементов меню Платформа .NET предоставляет классы для построения приложений Windows, a Visual Studio.NET поддерживает интегрированную среду разработки (IDE), используемую для визуального проектирования приложений Windows и для автоматической генерации кода. Visual Studio.NET обеспечивает создание большей части кода для приложений Windows. Если вам знаком редактор ресурсов Visual C++ или Visual Basic, то вам будет по- понятна методика использования IDE в Visual Studio.NET. Элементы управления перетас- перетаскиваются с панели инструментов в форм). С помощью IDE можно установить свойства формы и элементов управления. IDE подробно описывается в главе 8. Основное отличие заключается в том. что IDE представляет собой чистый генератор кода. Вся информация, которая необходима IDE для создания формы, хранится в исход- исходном коде. Кроме того, IDE имеет всегда один и тот же вид независимо от того, какой язык программирования используется. Возможности, поддерживаемые IDE, зависят от осо- особенностей конкретного языка. Примером этого могут быть два раскрывающихся списка в текстовых редакторах (списки объектов и событий), которые доступны при работе в Visual Basic, но не в С#.
336 Глава 9 Для создания приложений Windows не требуется Visual Studio.NET — необходимы толь- только текстовый редактор и компилятор. Однако IDE значительно упрощает процесс создания приложения. Поскольку приложения Windows основаны на применении графического ин- интерфейса пользователя (GUI), визуальная разработка приложений является более простой. В этой главе мы напишем приложение, вычерчивающее кривую Безье. В приложе- приложении вы сможете менять координаты х и у при помощи различных элементов управления пользовательского интерфейса и просматривать изменения кривой. Архитектура Большая часть функциональных средств, необходимых для создания приложений Windows Forms, находится в пространстве имен System.Windows.Forms. Оно содержит все классы для построения приложений, использующих особенности пользовательского интерфейса операционной системы Microsoft Windows. Информация для программи- программистов на C++: типы классов пользовательского интерфейса, ранее входившие в состав Microsoft Foundation Classes, теперь находятся в System.Windows .Forms. Разумеется, вы можете применять в вашем приложении Windows и другие классы .NET. В частности, для выполнения графических операций понадобится System.Drawing. Это пространство имен содержит классы для создания кистей, шрифтов, значков и других гра- графических объектов. Оно также содержит инструмент для рисования объектов и вывода тек- текста на изображаемой поверхности. Наш пример использует это пространство имен для вычерчивания кривой Безье. Если ваше приложение Windows предназначено для про- просмотра и обработки данных, то вы будете использовать пространство имен System.Data для инкапсуляции данных и элементы управления для них из System.Windows.Forms. Итак, приступим к созданию приложения Windows при помощи Windows Forms. Каждое приложение Windows Forms представляет собой класс, который является производным от System.Windows.Forms.Form. Поэтому самое простое приложение Windows Forms будет выглядеть так: using System; using System.Windows.Forms; public class MyForm : Form { -< public MyForm () public static void Main(string!] args) { MyForm aForm = new MyForm(); } } Для компиляции этой программы используйте команду: esc form.cs Каждое приложение Windows Forms обязано иметь точку входа, с которой начинается исполнение программы. Точка входа определяется путем реализации открытого статиче- статического метода Main (). Этот метод может возвращать либо void, либо int. Если необходи- необходимо передать в вызывающее приложение какоето значение, потребуется использовать тип возвращаемого значения int. Main () также служит для передачи аргументов командной строки в приложение. Аргу- Аргументы командной строки могут потребоваться в том случае, если необходимо управлять, например, настройкой конфигурации или загружаемыми файлами. Приведенная программа создает объект MyForm и сразу же завершается. Однако мы хотим, чтобы приложение участвовало в цикле сообщений от операционной системы. Приложения Windows управляются событиями. Они не производят явных вызовов функций, а ожидают, когда система передаст им входные значения в виде сообщений. Цикл сообщений является частью приложения Windows. Он проверяет, не пришли ли сообщения от Windows. Чтобы обработать сообщения для формы, необходимо вызвать метод Run О класса Application: using System,- using System.Windows.Forms;
Приложения Windows 337 public class MyForm : Form public MyForm() public static void Main(string[] args) MyForm aForm = new MyForm(); Application.Run(aForm); Класс Application принадлежит пространству имен System.Windows .Forms. Он ре- реализует статические методы и свойства для фоновой работы, фильтров сообщений, управления приложением и обработки сообщений Windows. Сообщения генерируются как системой, так и другими приложениями. Система формирует сообщение для каждо- каждого события типа нажатия клавиши на клавиатуре и мыши. Приложение может генериро- генерировать сообщения для указания своим собственным окнам на выполнение определенных задач или для связи с окнами других приложений. Метод Application. Run ( обрабатывает сообщения от операционной системы, пред- предназначенные для приложения. Объект Fc .. m отображается посредством передачи его как параметра методу Run (>. Теперь приложение останется на экране и будет ожидать поступ- поступления событий. Как будет вести себя приложение дальше, зависит от разработчика. Позже мы посмотрим, как можно получить свойства приложения из класса Application и отобразить их в диалоговом окне. А сейчас сделаем это в IDE. В Visual Studio создайте новый проект. Выберите шаблон Windows Application: New Project P/O)ect Types: '»?,->»■»'- '< x| Templates: I visual Basic Presets _J Visual C# Projects ffi _| V.su«! C++Prefects ; 1 Setup and Deployment Projects Й-CJ Other Projects Lj Visual Stucko Solutions Windows Class Ubrary Windows Application Control library i Web Application A project for creating an apptcation with a Windows use.- interface Иате; jWinAppDemo Location: | C:\Wrox\Pro C*lki\codl <~ Add to Solution С? 0кг Srtjtten Project will be created at C:\Wrox\Pro &r.NET\code\WinAppOemo. «More. I I OK I Web Service Web Control Library Zl ■*\ ' ■£7i Cancel После нажатия на OK Visual Studio создаст все файлы, составляющие фундамент при- приложения Windows. В частности, будет сгенерирован файл формы. Формы Любое окно, отображаемое приложением, представляет собой форму. Стандартные окна, инструментальные окна, окна без бордюра и плавающие окна — все они могут быть созданы с помощью класса Form. Класс Form можно также использовать для построения модального окна, например диалогового окна. Особый вид формы, форма Multiple Documentation Interface (MDI), может содер- содержать внутри себя другие формы, называемые дочерними MDI-формами. Для создания MDI-формы необходимо установить в true свойство IsMDIContainer. Задание свойства MDIParent в родительской MDI-форме приведет к созданию дочерней формы MDI.
338 Глава 9 Используя свойства, доступные в классе Form, можно определить положение, размер, цвет и особенности управления для создаваемого окна. Свойство Text позволяет изме- изменить заголовок окна. Свойства Size и DesktopLocation задают размер и положение окна при его отображении. Свойство ForeColor служит для указания цвета переднего плана по умолчанию для всех элементов управления, размещенных в форме. Свойства BorderStyle, MinimizeBox и MaximizeBox определяют, можно ли будет свернуть, развернуть или изме- изменить размер формы во время исполнения программы. Свойства формы могут быть установлены с помощью окна свойств в Visual Studio. Например, если изменить свойство Text на WinAppDemo, то окно свойств будет выглядеть следующим образом: Properties jForml System.Windows.Forms.Form zl S Font ForeColor RightToLeft Text El Behavior AllowDrop ContextMenu Microsoft Sans Serif, 8pt Hi ControlText No WinAppDemo False (none) -4 Tent <■- The text contained in the control,. I Properties I Q Dynamic Help Для каждого свойства, изменяемого в окне свойств, создается соответствующий код. Выбрав View, а затем Code в меню Visual Studio, вы увидите код для этой формы. В частно- частности, в нем имеется секция, отмеченная как Windows Form Designer generated code. Можно раскрыть ее и просмотреть сгенерированный код, который будет выглядеть примерно так: #region Windows1 Form Designer generated code // <summary> /// Метод, требуемый для поддержки Designer — не модифицировать /// содержимое этого метода в редакторе кода. /// </sunrnary> private void InitializeComponent() { ' •■ this.AutoScaleBaseSize = new System.Drawing.SizeE, 13); this.Clients^ze = new System.Drawing.SizeB92, 373); this.Text = "WinAppDemo"; } #endregion Для формы установлены свойства Size и Text. Если в окне свойств изменить другие свойства, эти изменения будут также отражены. Помимо свойств для управления формой можно использовать методы класса. Напри- Например, метод ShowDialog ' позволяет отобразить форму как модальное диалоговое окно. Метод Setl esktopLocation служит для позиционирования формы на рабочем столе. События класса Form позволяют откликаться на действия, производимые с формой. Можно использовать событие Activate для выполнения таких действий, как обновление данных, отображаемых в элементах управления формы при ее активизации.
Приложения Windows 339 Элементы управления Очевидно, что форма сама по себе не слишком полезна. Для отображения и управления данными, а также для взаимодействия с интерфейсом пользователя необходимы элементы управления. В Windows Forms существуют два базовых класса для элементов управления: System. Windows. Forms. Control определяет минимальный набор кода и свойств для элемента управления Windows Forms. Элемент управления, унаследованный от Control, содержит основные свойства и реагирует только на общие события, такие как события клавиатуры и мыши. Windows Forms содержит типичные элементы управления, которые обычно пред- представлены в библиотеках классов для графического интерфейса пользователя, включая: □ Метки □ Кнопки □ Переключатели О Меню □ Флажки D Комбинированные списки □ Списки D Текстовые поля □ Вкладки □ Панели инструментов О Деревья просмотра И так далее. К элементам управления относятся: □ DateTimePicker О FontDialog О FileDialog О LinkLabel О MonthCalendar О NumericUpDown О PrintDialog D ProgressBar Отметим также, что форма является производной от класса Control, поэтому она сама является элементом управления. Это означает, что все свойства и события элемента управления присутствуют в форме. Типичный процесс добавления элементов управления выглядит следующим образом: 1. Объявление закрытой переменной типа требуемого элемента управления. 2. Создание элемента управления и связывание его с переменной в конструкторе формы. 3. Установка свойств элемента управления. 4. Добавление элемента управления в коллекцию элементов управления формы. 5. Реализация обработчиков событий для элемента управления. Если для добавления элементов управления в форму вы используете Visual Studio, он выполнит все перечисленные действия автоматически. Чтобы добавить элемент управ- управления в Visual Studio, его необходимо перетащить с панели инструментов в форму при- приложения. Код для добавления элемента управления помещается в код формы автоматически. На приведенном ниже рисунке панель инструментов показана слева:
340 Глава 9 0* 6* IN** £ci*ai ,[^рр,рШ>ЛШД7ЛМ>^ *- - X •(• SptWer '.- -I* If ^^T^^fm^ri'-'^^^'''-- ^0*-^ JW-WW* !•<< Добавим в форму TextBox. Перетащите его в форму с панели инструментов. В окне ре- редактора свойств установите следующие свойства: Name: xTextBox Size: D0. 20) Location: F4. 366) Измените размеры формы на 300, 410. При просмотре кода, сгенерированного Windows Forms Designer, вы увидите изменения, сделанные для TextBox. Код может выглядеть примерно так: private void InitializeComponent() II II хТехсВрх this.XTextBox.Location = new System.Drawing.PointA36, 376); this.XTextBox.Name = 'xTextBox"; this.xTextBox.Size = new System.Drawing.SizeD0, 20); this.XTextBox.Tablndex = 3; this.xTextBox.Text = "; // WinAppDemoForm this.AutoScaleBaseSize = new Syscem.Drawing.SizeE, 13); this.ClientSize - new System.Drawing.SizeB88, 403); this.Controls.ASdRange(new System.Windows.Forms.Control[] {.this.xTextBox} ),- this.Text = "WinAppDemo"; Создается объект TextBox, и для него определяются местоположение, текст, индекс табуляции и размер. Наконец, элемент управления добавляется в коллекцию Controls
Приложения Windows 341 формы. Поскольку это коллекция, дочерние элементы управления другого элемента управления можно просмотреть в любой точке программы. Можно также удалять и до- добавлять их. Отметим, что в форму для текстового поля была добавлена закрытая переменная класса: private System.Windows.Formp .TextBox:,xTextfiex,- Объявляя пространство имен при помощи using, можно использовать типы в этом пространстве имен без указания последнего. Например, вместо того чтобы ссылаться на класс textbox как System.winForms. TextBox, можно применить TextBox. Отметим, что using не дает доступа к пространствам имен, которые могут быть вложены в указанное. Какое из них использовать — оставлено на ваш выбор. Однако помните правило программирования номер один — будьте последовательны. Если вы решили опускать названия пространств имен, делайте это для всех пространств имен, объявленных с помощью using. Теперь добавим элемент управления TrackBar и свяжем его с TextBox. TrackBar пред- представляет собой прокручиваемый элемент управления, аналогичный ScrollBar, но выгля- выглядит по-другому. Пользователь перемещает TrackBar для изменения значения данных. Таким образом, TextBox будет показывать значение, определяемое позицией TrackBar. Следовательно, изменение TrackBar должно быть отражено в TextBox, и наоборот. Перетащите TrackBar с панели инструментов в форму. В окне Properties установите для TrackBar следующие свойства: Name xTrackBar Maximum 200 Size B00, 42) Location F4, 288) TickFrequency 10 TickStyle TopLeft Код будет изменен следующим образом: private void InitializeComponent(, Jthis'.xTextBox. Location = new System. Drawing ..Point A36, this:.acTextBox.Nanie:, = ■xTextBox"*; > > *'^ £'?!|Vj this.x%extBox.Siz^:>= new System.Dtawinp^Sizje^t^ 20); 376), this.xTjextBox.Si~zj£>= new System.Bfawinp^Siae^tov 20); thi*.xTextBox.Tablridex = 3; '-*'tl thisvxTextBox.Text F "; .;'.*■.-.- this.AutoScaleBaseSize = new System.Drawing.SizeE, 13); this.CllientSize = new System.Drawing.SizeB88, 403); tKis.;Controls.AddRange*(new System;windows.Forma.Control [ 1 {this.XTextBox, this.xTextBox}); this.Text = "WinAppDemo"; {(SysteinlCornponentModet. ISupportlni^ialize) ■•:■■ (this.xTrackBar)) .Endlnit[)<;■ ■-■ t Форма должна выглядеть примерно так:
342 Глава 9 Для того чтобы элементы управления знали, что должен делать каждый из них, необ- необходимо использовать обработку событий. События и делегаты Многие языки программирования имеют встроенную поддержку обработки событий. Например, в C++ обработка событий обычно реализуется с помощью классов сообщений, которые используют указатели функций. В С# поддержка обработки событий встроена в язык и представлена событиями и делегатами. Делегат в С# аналогичен указателю функции в С и C++. Программист инкапсулирует ссылку на метод внутри объекта делегата. Метод может быть либо статическим, либо ме- методом экземпляра. Объект делегата передается в код, который сможет вызвать указан- указанный в нем метод, даже если при компиляции не известно, какой именно метод будет вызываться. Разница между делегатами в С# и указателями функций в С и C++ в том, что делегаты являются объектно-ориентированными, безопасными по типу и надежными. Интересное и полезное свойство делегата состоит в том, что он не знает о классе объ- объекта, на который он ссылается. Подойдет любой объект; имеет значение лишь то, совпа- совпадает ли сигнатура метода с сигнатурой делегата. Поэтому делегаты хорошо подходят для "анонимного" вызова методов. Приведем пример объявления, присвоения и вызова делегата: delegate inc MyDelegate(); MyDelegate.' aDelegace = new MyDelegate(myClaes.InstahceMethodj; // Вызываем метод экземпляра: aDelegateiU ; С делегатами связаны события. Событие в С# представляет собой способ, которым класс уведомляет своих клиентов о том. что с объектом что-то произошло. Какую же роль делегаты и события играют в приложениях Windows? Программы, основанные на графическом интерфейсе пользователя, управляются событиями. Поль- Пользователь работает с пользовательским интерфейсом, а программа должна отвечать на происходящие события. Обычно классы, представляющие элементы управления пользо- пользовательского интерфейса, уведомляются при помощи событий. Например, класс, пред- представляющий textbox, получит сообщение о том, что была нажата клавиша. События обеспечивают удобный способ сигнализации об изменении состояния объекта, что может быть полезно для клиентов этого объекта. В С# события объявляют с использованием делегатов. Клиенты класса предоставля- предоставляют ему делегатов на методы, которые должны быть вызваны при возникновении
Приложения Windows 343 события. Когда происходит событие, вызывается делегат(ы), переданный ему клиента- клиентами. Ключевое слово event указывает делегата, который будет вызван при возникновении в коде определенного события. В языке С# события могут применять любой тип делегата, а платформа .NET устанав- устанавливает некоторые ограничения. Если вы намереваетесь использовать свой компонент на платформе .NET, то, вероятно, последуете ее рекомендациям. Рекомендации платформы .NET гласят, что тип делегата, применяемого для событий, должен принимать два параметра: параметр "исходный объект", который указывает на ис- источник события, и параметр "е", который инкапсулирует всю вспомогательную инфор- информацию о событии. Тип параметра "е" должен быть производным от класса EventArgs. Для событий, не использующих дополнительную информацию, платформа .NET уже определила соответствующий тип делегата: EventHand] er. Обработчики событий Обработчики событий, как правило, захватывают пользовательский ввод для элемента управления и информируют программу о том, что что-то произошло. Возможно, вам по- потребуется реализовывать обработчики событий для обновления экрана или проверки пользовательского ввода. В Visual Basic для обработчиков событий применяются такие имена, как Button_Click. Разработчики на Visual C++ обычно используют макросы MFC для создания отображений сообщений, которые связывают события Windows с указателями функций. Windows Forms выполняет функции обработки сообщений с помощью делегатов. По- Помимо методов и свойств все элементы управления содержат коллекции обработчиков со- событий. Если требуется захватить событие, то вы ткнете соответствующий метод и добавляете его в коллекцию для того события, которое требуется обрабатывать. Visual Studio обеспечивает такие же возможности добавления обработчиков событий, что и Visual Basic 6. При двойном щелчке мыши на элементе управления автоматически генерируется код обработчика для события по умолчанию. В нашем примере обработчики событий должны быть реализованы таким образом, чтобы TextBox и TrackBar могли общаться друг с другом. Обработчик события должен иметь те же параметры, что и объявление делегата 7 'ntHandler, которое выглядит так: public delegate void EventKjndll-- object se'der, EventAras e) ; При создании делегата ev •-•_ ■■ =ler указывается метод, который будет откликаться на соответствующее событие. Для того чтобы связать событие с методом обработки со- события, необходимо добавить экземпляр делегата к событию. Метод, обрабатывающий событие, вызывается, когда происходит это событие. Вернемся к связыванию TextBox и ГпикВаг. Если выполнить двойной щелчок мыши на элементах управления, для вас буду)' созданы события по умолчанию — TextChanged. Однако мы будем использовать обработчик Oeave, t.l. ';• обновления формы необходи- необходимо после ввода текста щелкнуть где-нибудь в форме. Со. . не Leave принадлежит к клас- классам Control и возникает в том случае, если элемент управления теряет фокус. Из объявления Leave видно, что это событие: public event EventHandler Leave; Поэтому для добавления метода сообщения в функцию InitializeComponent необ- необходимо вставить следующую строку кода после вызова функции InitializeComponent: xTextBox.Leave += new System.EventKandler(this.xTextBox_Leave); Теперь необходимо реализовать метод, которому будут поступать уведомления. От- Отметим, что имя метода может быть любым. Visual Studio генерирует имя метода в виде objectName_Event: protected void xTextBox_Leave(object sender, System.EventArgs e); Г ' ф. TextBox ' theTextBox; -l- theTextBox = (TextBox)sender ; " // Убедимся, что не был превышен максимум, if (Convert.Tolnt32(theTextBox.Text) >200) xTrackBar.Value = 200; theTextBox.Text = 00";
344 Глава 9 •.xTfackBar.Value' -' Convert.ToInt32(;fcheTextBox.Text) ; Отметим, что параметры метода соответствуют параметрам, объявленным в делега- делегате EventHandler. Код знает о том, кто послал уведомление, поскольку имеется ссылка sender. Нас интересуют события, вызванные TextBox. Для обеспечения доступа к методам TextBox объект sender приводится к типу TextBox. Теперь аналогичный код необходимо реализовать для TrackBar. На этот раз для авто- автоматической генерации кода можно использовать дизайнера формы. После этого останется только добавить код: protected void xTrackBar_Scroll(object sender. System.EventArgs e) {; Trackbar „theTrackBar; ; ,vthe1ftfapkiar = (TrackBar) sender; ^..Value = theTraokBar.Value; xTextBox. Text = xTrackBar.Value.ToStringO ; Метод принимает TrackBar в качестве объекта. Приведя его к классу TrackBar, мы получаем доступ к методам TrackBar. Значение TextBox устанавливается в значение TrackBar после преобразования его в текст. Теперь, если один из компонентов меняет свое значение, автоматически меняется значение другого компонента. Перечислим события, которые доступны для всех элементов управления: Событие ChangeUICues Click ControlAdded ControlRemovec Doubleclick Enter GotFocus HancleCreated HancleDest royed KeyDown KeyPress KeyUp Layout Leave LostFocus MouseDown MouseEnter MouseHover MouseLeave MouseMove MouseUp Описание Возникает после изменения состояния клавиатуры и/или фокуса. Возникает при щелчке мыши на элементе управления. Возникает, когда к текущему элементу управления добавляется новый. Возникает, когда элемент управления удаляется из текущего. Возникает при двойном щелчке мыши на элементе управления. Возникает при вводе в элемент управления. Возникает, когда элемент управления получает фокус. Возникает, когда из элемента управления создается дескриптор. Возникает, когда уничтожается дескриптор элемента управления. Возникает при нажатии клавиши. Возникает при нажатии клавиши. Возникает при отпускании клавиши. Возникает, когда изменяются свойства размещения элемента управления. Возникает, при выходе из элемента управления. Возникает, когда элемент управления теряет фокус. Происходит, когда нажимается кнопка мыши, а указатель мыши находится над элементом управления. Возникает, когда указатель мыши попадает в область элемента управления. Возникает, когда указатель мыши наводится на элемент управления. Возникает, когда указатель мыши покидает область элемента управления. Возникает при перемещении мыши над элементом управления. Возникает при отпускании кнопки мыши в области элемента управления.
Приложения Windows 345 Событие Описание MouseWheel Move PropertyChanged Resize Validated Validating Возникает при прокручивании колесика мыши, когда элемента управления находится в фокусе. Возникает при перемещении элемента управления. Возникает при изменении свойства элемента управления. Возникает при изменении размера элемента управления. Возникает по окончании проверки элемента управления. Возникает при выполнении проверки элемента управления. Некоторые элементы управления имеют дополнительные события, которые они сами генерируют. Например, мы видели, как элемент управления TrackBar генерирует событие Scroll. Альтернативой отслеживанию событий с помощью обработчиков событий является наследование элемента управления и перекрытие в нем метода события. Например, вмес- вместо добавления обработчика для события GotFocus можно перекрыть метод OnGotFocus. Однако в этом случае не забудьте вызвать base.OnGotFocus, чтобы другие зарегистриро- зарегистрированные получатели сообщения также получили его. Ниже будет показано, как перекрыть метод OnPaint. Обычно метод события перекрывают, если необходимо изменить поведение элемен- элемента управления. Например, имеет смысл перекрывать OnPaint, чтобы по-другому рисо- рисовать элемент управления. Обработка событий обычно используется для расширения текущей функциональности, когда требуется сохранить исходное поведение компонен- компонента и добавить к нему что-то новое. На данный момент мы имеем два способа ввода координаты X: TextBox и TrackBar. После того, как мы сделаем то же самое для координаты Y. результат будет выглядеть так: aWinAppDernoFor .: if: - - ....:.,, - y'- -v..-.. : ; io .., ; Группировка элементов управления С помощью элементов управления мы будем манипулировать четырьмя контрольными точками. Для этого мы реализуем четыре переключателя и GroupBox. GroupBox используется в.том случае, если есть набор элементов управления, который должен быть сгруппирован для визуального отделения от остальных элементов формы. GroupBox помещает группу элементов управления в фрейм, который может иметь или не иметь заголовок. GroupBox часто применяется для определения групп элементов управ- управления RadioButton. Использование GroupBox с переключателями позволяет формировать несколько наборов, каждый из которых поддерживает свой собственный набор значений.
346 Глава 9 Перетащите в форму GroupBox и четыре переключателя и разместите их так, как по- показано на рисунке. Используйте окно Properties для задания текста для каждого элемента. Для Point 1 установите свойство Checked в True: Г Point Selection, *?-■-.--. ,у—'- — —- - Теперь необходимо добавить обработчики событий для переключателей. Если дважды щелкнуть мышью на элементе управления, будет реализован обработчик по умолчанию для события CheckedChanged (см. ниже). Следующий шаг в нашем приложении Windows Forms — создать элемент управления, который способен чертить кривую Безье. Но прежде мы должны получить некоторое представление о рисовании с использованием Windows Forms. Классы для рисования Классы для рисования подробно рассматриваются в главе 21. Здесь мы лишь обсудим, как они связаны с приложениями Windows. Если вам необходимо изменить способ рисования элемента управления или если вы создаете свой собственный элемент управления, то скорее всего воспользуетесь классами для рисования. Вам придется делать это в том случае, если стандартные элементы управ- управления Windows не обеспечивают требуемых возможностей или если вы хотите изменить вид элементов управления по сравнению со стандартным. Пространство имен Syster . Drawing предоставляет доступ к базовой графической функциональности GDI+. Более сложные средства содержатся в пространствах имен System.Drawing.Dra.^.g 2D, S,stem.Drawing.Imaging и System.Drawing.Text. Размеры Все типы размеров, такие как Fc nt, Size и Re ^tangle, расположены в пространстве имен Sysee-.Drawing. Для разработчиков важно то, что эти типы умеют отображать себя в виде строк, поэтому код: using using System; System. Сга-л class MainApp { public static void Ma:. ) { Point myPoint = new Point C4, 23) ; ;. Rectangle myRect = new Rectangle B0, Size mi-bize = new SizeA4, 16); Console.Wri _eLi ne(myPoint.ToString()); Console .Wr_ ;eL^ r._ i iy: ect. ToStrir.g () ) ; Console.WriteL-ri3(. iyj.z^ .Tc3tring() ) ; 10, 25, 15);
Приложения Windows 347 даст результат: {Х=34, ¥=23} {Х=20, Y=10, Width=25, Height=15} {Width=14, Height=16} Это значительно облегчает процесс разработки и отладки, поскольку не требуется писать код для отображения значений измерений. Кисти Класс Brush является абстрактным базовым классом, т.е. нельзя создать экземпляр само- самого этого класса, а реализованы могут быть только классы, унаследованные от Hrush. Про- Производные классы служат для определения объектов, используемых для заполнения графических форм, таких как прямоугольники, эллипсы, секторы, многоугольники и т.д. К производным классам относятся SolidBrush, TextureBrush и HectangleGradientBrush. Например, следующий код создает кисть с линейной градиентной заливкой. Кисти с линейной градиентной заливкой начинают ее одним цветом, постепенно перетекающим в другой: Rectangle endPoints = new Rectangle(O, С, 21 , 20C) ; LinearGradientBrush lb = new LinearGradie-itBrushfendPoir.ts, Color.Red, Color.Yellow, LinearGradientMode.BackwardDiagonal); Прямоугольник задает конечные точки линейного градиента. Начальная точка пред- представляет собой верхний левый угол прямоугольника, а конечная точка — нижний правый угол прямоугольника. LinearGradientMode описывает "перетекание" цвета. В данном случае BackwardDiagonal указывает, что градиент должен идти из правого верхнего в левый нижний угол. Линейные градиенты создают приятные эффекты для фонового изображения формы или для отдельных ее областей. Этот класс находится в пространстве имен System. Drawing. Drawing2D. Перья Перо (Pen) является объектом, используемым для черчения линий и кривых заданной толщины и цвета. Перо проводит линию указанной ширины и стиля. Используйте свой- свойство DashStyle для рисования разнообразных пунктирных линий. Линия, рисуемая Реп, может быть заполнена при помощи большого числа стилей заполнения, включая заливку сплошным цветом и текстуру. Стиль заполнения зависит от кисти или текстуры, приме- применяемой в качестве объекта заполнения. Для создания перьев существует целый ряд конструкторов. В следующей строке со- создается простое голубое перо: Pen. bluePeii ==".jiew Pen (Color. B_ le) ; Можно создавать более сложные перья, Указывая для них другие свойства, например кисть, толщину и стиль пунктира: Pen myPen = new Pen mew SoliJRrushO ) ; myPen.Width = 5; myPen.DashScyle = D- shStyle.Da _ 'iDot; Шрифты Шрифты также входят в состав пространства имен System. Drawing. Для создания шриф- шрифта сначала определите семейство шрифтов, с которым вы работаете. Семейство шрифтов представляет собой группу гарнитур, которые имеют общий дизайн, но несколько отлича- отличаются по стилю. На выбор существуют три GenericFontFamilies: Monospace, SansSerif и Serif. Класс Font определяет конкретный формат текста, включая вид, размер и стиль шрифта. В следующем фрагменте создается шрифт Serif 16pt: T&ntFamily .theFamily = new FqntFamily(GenericFontFamilies.Serif); Font' theFont = new Font (theFamily, 16>; >•... С помощью созданного шрифта можно выводить текст.
348 Глава 9 Graphics Наиболее интересным классом является System. Drawing.Graphics. Этот класс инкап- инкапсулирует поверхность для рисования GDI+. R нем присутствуют методы для отображения линий, кривых и строк. Когда вы начнете рисовать на поверхности, первая проблема, с которой вы столкне- столкнетесь,— поиск конструктора для класса Graphics. Хитрость в том, что для него не преду- предусмотрен открытый конструктор. Этот класс использует шаблон Factory для создания объектов Graphics. Поверхность рисования должна быть связана с существующим ре- ресурсом, например с растровым изображением или дескриптором Windows. В классе Graphics имеются статические методы, которые принимают ресурс в качестве парамет- параметра и возвращают объект Graphics. Преимущество данного подхода состоит в том, что операции рисования могут быть абстрагированы от конкретной поверхности рисова- рисования. Например, если вы проводите линию из точки А в точку В, то не важно, что будет окончательным результатом — файл JPEG или окно Windows. Следующий код демонстрирует основные концепции пространства имен System. Drawing: using System; using System.Drawing; using System.Drawing.Text; class MainApp { .' », public static void Main() { Bitmap image = new BitmapB00, 150); Graphics graphic = Graphics.Fromlmagefimage); ! FontFamily theFamily = new FontFamily(GenericFontFamilies.Serif); Font theFont = new Fontfthera: ;ly, 16); SolidBrush theBrush = new SoI-dBrush(Color.AliceBlue); graphic.DrawStr пдГ'Rock'n'roll" , theFont, theBrush, new PointA0, 10)); ; - image.Save("myimage.png", System.Drawing.Imaging.imageFormat- PNG); Сначала создается растровое изображение, и на его основе формируется класс graphic. Инструментами для отображения текста являются кисть и шрифт. Текст выводится на поверхности для рисования. Наконец, растровое изображение записывается в файл в формате PNG. Этот файл можно просмотреть с помощью Internet Explorer. Классы system. Drawing важны для создания приложений Windows по той причине, что пользовательские элементы управления основаны на перекрытии метода OnPaint () и на самостоятельной отрисовке самих себя. Пользовательские элементы управления Windows Forms предлагают большое количество элементов управления для использования в приложениях. Однако в ряде случаев приходится создавать свои собственные элементы управления, например: 3 У вас имеется модель данных, которую не удается представить удовлетворительно с помощью существующих стандартных элементов управления. D Вы хотите реализовать особый метод взаимодействия с пользователем. D Вы хотите, чтобы элемент управления отображался иначе, нежели стандартные элементы. Помните, что в результате создания элемента управления вы получите именно пользо- пользовательский элемент. Никто другой не будет знать, как им пользоваться. Пользовательский элемент управления должен быть легким в изучении и применении. Нам нужен пользовательский элемент управления, который чертит кривую Безье. Для создания пользовательского элемента управления выполним следующие действия: 1. Определим класс, производный от System.Windows.Forms.UserControl. 2. Определим свойства для этого элемента управления. Свойства описывают, как элемент управления будет выглядеть и работать.
Приложения Windows 349 3. Перекроем метод OnPaint для обеспечения логики рисования элемента управления. 4. Снабдим элемент управления атрибутами. Они будут определять, как элемент управления будет выглядеть в Visual Studio. 5. Если необходимо, снабдим элемент управления ресурсами (например, изображениями). Пример: рендерер Безье Для иллюстрации действий по созданию пользовательского элемента управления созда- создадим рендерер Безье. Это будет элемент управления, отображающий кривую Безье. Кри- Кривая будет меняться по мере того, как будут изменяться свойства элемента управления. Для начала познакомимся с самими кривыми Безье. Кривые Безье часто используют- используются в графических приложениях благодаря их способности выполнять плавный переход между двумя фиксированными точками. Четыре точки определяют кубическую кривую Безье. Первая точка задает начало кривой, а последняя — ее окончание. Вторая и третья точка — управляющие. Они указывают, насколько "круто" кривая будет выходить из нача- начальной точки и завершаться в конечной. Управляющие точки не лежат на самой кривой. ШаМ Создайте новый проект Windows Control Library в Visual Studio: New Project Project Types: Templates: (™*~J Visual Basic Projects j—Qi Visual C# Projects Ф CJ Visual C++ Projects \~L3 SetuP anc' Deployment Projects Щ-'SJi Other Projects L-Ql Visual Studio Solutions Windows Application Class Library Web Application Web Service Web Control Library ' A project for creating controls to use in Windows applications Name: j BezierRenderer Location: | C:\Wrox\Pro C#.NET\code3 С Add to Solution (? CJose Solution Project will be created at C:\Wrox\Pro C#.NET\code3№enerRenderer. I | OK ~| Cancel Browse... Help В результате будет получена форма, аналогичная форме, создаваемой для приложений Windows, но на этот раз унаследованная от UserControl: namespage BezierRenderer using System,- using System. Collections,- using System.ComponentModel; .= using System.Drawing; ■I using System.Drawing.Drawing2D; ii using System.Data; .>■ -using System.Windows.Forms; /,// <summary> !" //;/ Общее описание Control 1. . ,/// </summary> . public class BezierControl : System.Windows.Forms--,UserControl
350 Глава 9 Шаг 2 Ha этом шаге объявляются свойства для класса BezierControl. В частности, создаются голубое перо и четыре точки, определяющие кривую Безье: г private Pen curvePen = new Pen(Color.Blue) ; private Point ptl = new Point @, 0) ; private Point pt2 — new PointA0, 30); private Point pt3 = new PointE0, 20); private Point pt4 = new PointA00, 40); /// <summary> /// Переменная, необходимая в проекте. /// < summary> private System.CimponentModEi-Container components; public BezierControlО // Эй з- вызов требуете W I"w: .Fcrms Form Designer. Ir.itializeComponent () ; , / TODO: Добавить инициализацию п ь^зозь InitForm i <sunir.ary /// Освобождение всех /// </summary> public override void Dispose!) j *v. ■-; ресурсов. сол p_-._..r.s < - г =ry> /// *• т. -л —_ збуемый для - .ржки De^-gr.er,- не модифицировать /// содегжи-- этого к ~ редактс, кода. /// </summary> r-'vate void InitializeCon.p cc-_ ; - new System. ..tMode ' . ntainer (i ; Для того чтобы можно было изменить кривую Безье. управляющие точки представлены в виде свойств: public Point Ptl { get { return pt 1 ; } set { ptl = value; Invalica_i ) ; To же самое повторяется для остальных точек: public Point Pt2 get return pt2 ; set
Приложения Windows 351 pt2 = value; invalidated ; public Point Pt3 { get { return pt3; } set { pt3 = value; Invalidate)); } } public Point Pt4 { get { return pt4; } set { pt4 = value; Invalidated ; Этот код подчеркивает важность выполнения действий по установке свойства. При изме- изменении управляющей точки кривую Безье необходимо перерисовать. Вызов Invalidated приводит к передаче сообщений OnPaint О и к перерисовке компонента. ШагЗ Теперь начинается самое интересное. Во-первых, перекрывается метод OnPaintBackg- round () для демонстрации отрисовки пользовательского заднего плана. Это сообщение посылается, когда операционная система генерирует запрос на удаление заднего плана. В данном случае задний план рисуется с помощью LinearGradientBrush (см. выше): ■•■protected override void OnPaintBackground(PaintEventArgs e) ?:, 'Я': { •.„■■;. , - Rectangle r = new Rectangle(O, 0, this.Size.Width, > ■•''.', this.Size,Height); ♦ LdilearGradientBrush lb = new LinearCradientBrush(x, ' >Л ' Color.Red, Color. Yellow, ■jp; ,' ;- LinearGradientMode.BackwardDiagonal) ; ■•^.Graphics.FillRectangle<lb, r) ; ,ь г ■:;.:- В методе OnPaint () класс Graphics используется для вычерчивания кривой Безье и маленьких кружков, представляющих управляющие точки. Для рисования применяется синее перо: protected override void OnPaint(PaintEventArgs e) base.OnPaint(e) ; // Рисуем кривую .Безье e.Graphics.DrawBezier(curvePen, ptl, pt2, pt3, pt4); , f/ Жисуем точки л; eiGraphics.DrawEllipse(curvePen, ptl.Х- - 1, ptl.Y - 1, 3, 3) ; ^ : ©„Graphics.DrawEliipsefcurvePen, pt2.X ,*• 1, pt2,Y - 1, 3, 3); . ;' Г*- ek6raphicsArawEllIpse(curvePen, pt3-X - 1, pt3.Y - 1, 3, 3); e.Graphics.prawEllipse(curvePen, pt4.-X - 1, pt4.Y -' 1, 3, 3);
352 Глава 9 Шаг 4 В С# можно сопровождать членов класса атрибутами. Атрибуты содержат метаданные о члене класса. В случае пользовательских элементов управления указание атрибутов для свойств позволяет Visual Studio интегрировать эти свойства в среду разработки. Например, для первой управляющей точки могут быть заданы категория и описание. Эта информация будет использоваться в среде разработки Visual Studio: Category("Appearance"), Description("Начальная точка кривой.") I ' . public Point Ptl Для этого элемента управления не используются ресурсы. Добавьте необходимые ссылки с помощью окна Solution Explorer и откомпилируйте код в DLL. Пользовательский элемент управления готов к применению в приложении Windows Forms. Использование После компиляции пользовательского элемента управления в DLL он может использовать- использоваться внутри проекта. Его можно объявить в форме точно так же, как любой другой элемент управления: i private :BezierContrbl "bezierControll; -,'* Отметим, что если элемент управления создан в другом пространстве имен, нежели форма, то для класса необходимо указать пространство имен пользовательского элемента управления. Пользовательский элемент управления создается точно так же, как любой другой элемент управления: -^.Afy*. Создание ■йбпьзбватеяьского элемента управления ,■• ■-';*>,'■"■'(- •, ■ • thissbezierControll = *new BezierControl (),; this.bezi'erCoritroll.Boca'tion = new Point<60, 100); this .bezierControlt.Size = new SizeB00, ,200);. Наконец, его необходимо добавить в коллекцию Controls формы: '": «'-'this.Controls.Add(this,bezierControll! ; Любой пользовательский элемент управления, созданный вами, может быть добавлен в панель инструментов Visual Studio. Это позволяет перетаскивать пользовательские компо- компоненты в форму точно так же, как и обычные. Свойства, помеченные в пользовательском элементе атрибутами, могут быть изменены в Visual Studio. Для того чтобы добавить пользовательский элемент управления в панель инстру- инструментов, щелкните правой кнопкой мыши в области панели инструментов и выберите Customize Toolbox. Перейдите на вкладку .NET Framework Components, затем нажмите Browse и укажите только что созданную DLL: I Customize Toolbo» CUM Controls | Modeling Shapes | General Shapes -NET Framework Components iName Namespace Path last. •» ElAdRotator System. Web.UI.WebControls C:i"™NT\assemblytGAa,.. 4/30/ C:\Wrox\Pro C#.№T\code... S/4/2 2/23/ 4/30/ 4/30/ 4/30/ 4/30/ 4/30/ 4/30/ DBtMaskSefector В Button El Button 0 Calendar E Checkbox ElCheckBox Qchecteoxlist EJcheckedUstBox «1 Microsoft.VSDesigner.Data... System.Wlndow5 .Forms 5ystem.Weo,UI.WebCor*rols System. Web.UT.WebControb System. Windows.Forms System. Web.Ul.WebCcntrols System.Web.UI.WebControb System. Wmdows.Forms C:\ProQramHes\Micfosoft... C^WINNT\assembly\GAC\... C:\VWmUassembMGAC\... С :\WINNT\assembly\GAC\... C:\WINNTta5sernbMGAC\... С :\WINNT\assembly\GAC\... C:\W»INT\assembly\GAC\... C:\WINNT\a5sembly\GACV.. pBeaerRenderer.ee2ierControl —■ 1 . >. - j Language: ^Language Neutral | Version: ' (Unavailable) 1 Browse.. OK Cancel I Reset I Help J
Приложения Windows 353 Теперь можно перетаскивать этот элемент управления с панели инструментов точно так же, как любой другой элемент управления: ШшкёШхШш^ v.JT~ Свяжем наш пользовательский элемент управления с интерфейсом пользователя. На- Например, код обработчика событий xTrackBar_Scroll изменится следующим образом. Текущий переключатель определяет, какая из управляющих точек обновляется. При из- изменении свойства управляющей точки пользовательский элемент получит сообщение Invalidated и перерисует себя: private void xTrackBar_S< . ,<b sc n .. r System.EventArgs e) TrackBar nheTrackBar; theTrackBar = (TrackBar)sender; xTrackBar.Value = theTrackBar.Value; xTextBox.Text = xTrackBar.Value.ToString(); ?• if (raddSSButtonPcJintl-Checked == true) bezieifGontroll.Ptl- = new Value, ."else -if <radi'oButtoriPoint2.Checked .== true) 4 ■ new Point(хТгаскДах.Value, beziereonta=oll. Pt2. Y) i rij;bezierControll.Pt2, = .else;;-if <radi6puttonP,oint3.Checked -= bezi€rConpi:oJl.Pt3 ,= new Point(xTrackBar^Value, i-<St- - u- > 4%,'" l^ * • •<» '"■■ , "'■' else, if <radiqButtonPoint4.Checked =^ true) bezierControll.Pt4 = new Point(xTrackBar.Value, bezierControll.Pt4.Y); Text Box и элемент управления Безье получают новые значения. Обновление элемен- элемента Безье приводит к передаче сообщений Invalidate (), в результате чего элемент управления перерисовывает себя: private void yTrackBar_Scroll(object sender. System.EventArgs e) { TrackBar theTrackBar; theTrackBar = (TrackBar)sender;
354 Глава? yTrackBar.Value = theTrackBar.Value; yTextBox.Text = B00 - yTrackBar.Value).ToString(); if (radioButtonPointl. Checked == true) { bezierControll.Ptl = new Point(bezierControll.Pti.X, 200 ^ - yTr ackBar. Value);, } -• else if (radioButtonPoint2.Checked -= true) ( ;' ' ■-*> '" - bezierControll.Pt2 = new Point(bezierControll.Pt2.X, 200 - yTrackBar. Value).; I else if (radioButtonPoint3*.Checked == true) fc bezierQonti-oll.Pt3 = new Point (beziereptit:roll.Pt3.X, 200 - yTrackBarivalue); ■i ~ ** else if fradioButtonPoint4.Checked = truer" " "'\~ ". . i, К ^ ki * , l? I' 1 bezierControll>Pt4 ■*: new Point(bezierCbnerpll..iPt4.X, 200 - yTrackBar.Value); "" * рт-i < void xTextB<^v_T =ave(object sender. System.EventArgs e) TextBox the^ex'-B к; theTextBcx - TextBox)sender; // Убед^-ь -я, что не был п™ ;рышен максимум. if (Convert:." " ■> ^2 (theTextBox. Г'.хг i >200) { xTrackBar.Value - 10; ' heTextBox.Text = xГг . lue = Convert Int32(theTextBox.Text); /< Делаем соответствую1цке изменения, в зависимости от того, У/ какая точка выбрана в данный момент if fradioButtonFcintl. checked == true). { bezierContToll.Ptl = new Point(xTrackBar„Value, bezierControll.Ptl.V); } else if (radioBu-:tonPoint2. Checked» == true) s t ' ' W bezierControll.Pt2 * new Point{xiraekBai4Value, bezierC6ntroll.Pt2;Yb } ■-■-."■ ' ■ else if (radioButtonPoint3.Checked == true) { - • - bezierControll.Pt3 = new Point{XTrackBar.Value, ;. '* bezierControll.Pt3.Y); } else if (radioButtonPoint4. Checked == true) { : - bezierControll.Pt4 = new Point(xTrcfckBar.Value, . bezierControll.Ptl»Y); private void yTex~Box_Leave(obiect sender. System.EventArgs e) { TextBox theTexcBox; theTextBox = '.ex„Box)sender; iE (Convert.Tolnt32(theTextBox.Text) >200) { ■_• rackBar.Value = 200; L> " -xfBox.Text = 00";
Приложения Windows 355 yTrackBar.Value - 200 - (Convert.Tolnt32(theTextBox.Text)); // Делаем соответствующие изменения в зависимости от того, // .какая точка выбрана в данный момент if (ra'dioButtonPointl. Checked == true) С bezierControll.Ptl = new Point(bezierControll.Ptl,X, 200 - - yTrackBar.yalue) ; I ■ else if (radioButtonPoint2.Checked == true> { ' . ' ■bezierControll.Pt2 * new Point (bezierControH;p£2-X, 200' -1 yTrackBar.Value У; .} else if (xadipButtonPoint3.Checked == true) *, { "'И--' bezierCon.fcroll.Pt3- - new Point(bezierControll.Pt3„X, 200'-' " " yTrackBar.Valtie) ; > * .л" "■' '*-■- *;''',.'*'■' ■*».■■ ; else i'S5 <radioButtonPoint4.Checked «-• true) * • { beziefControll.Pt'4 = .new Point(bezierControll.Pt4.X, 200 - yTrackBar.Value); Теперь добавим код обработки событий для переключателей. Выделенные участки кода следует добавить к существующему коду: private void radioButtonPoint.''_C1i i-.-v ^Changed object sender. System.EventArgs e) { хТгйскВаг.Value = bezierControl..Ptl.X; yTrackBar. Value = 200 - bezierControl,l.Ptl.Y; xTextBox.Text = bezierControll.Ptl.X.ToStringO ; yText.3ox.Text = bezierControlT..Ptl;Y.ToString(),; } private void radioButtonPoir.-.2_CheckedChanged(object sender. System.EventArgs e) С ^,* xTjfaCkBar. Value = bezierContrpil.Jjtj.X; ii/4i yTrackBar.value' '= '200 - bezierContioll.Pt2.Y7 ?';}■. xTextBox.Text = bezierControll.Pt2.X.Tostring(); A . yTextBox.Text = bezierCOntroll.Pt2.Y.ToString{); } private void radioButtor?~int3_Chej!<edChanged(object sender. System.EventArgs e) { ...xTrackBar.Value =- bezierControll-Pt3.X; yTrackBar .Value '* 200 - bezierppntroll.Pt3 .Y; xTextBox.Text = bezierCo^troll.Pt3,.X.ToString() ; yTextBox.Text - bezierControll.Pt3.Y.ToString() ,-, private void radioButtonPoir.t4_CheckedChanged(object sender. System.EventArgs e) { xTrackBar/Value = bezierControll.Pt4.X; yTrackBar-.Value = 200 - bezierControll.Pt4,Y; xTextBox.Text = bezierControll. Pt4 .X.ToStringO ; yTextEox.Text = bezierControll.Pt4.Y.ToStringO ; } Для других переключателей пишется аналогичный код — это лишь различные точки управления кривой Безье. Функциональность переключателей генерируется автоматически, но вам придется добавить функциональность для оставшихся текстовых полей и полос прокрутки: / / Требуется для поддержки Windows Form Designer
356 Глава? InitializeComponent(); xTextBox.Leave += new System.EventHandler(this.xTextBox_Leave); - w yT«xtBbx.Le'aVe += new System.EventHaridierithis."yTextBpx_beave> ; " " xTracKBar.Scroll +■= new System.EventHandler<tliis..xTraekBar_Scroil}; yTrackBar.,Scroll += new System.JEventHandler:i!:his..ysTrackBar_Scroll};* Итак, пример закончен. С помощью элементов управления можно менять управляю- управляющие точки, и при этом элемент управления будет обновляться. Однако в приложении отсутствует один из наиболее важных видов элементов управления — меню! Меню Меню являются наиболее известной частыо графического интерфейса пользователя. Платформа .NET предлагает следующие классы для поддержки меню: О System.Windows.Forms.MainMenu О System.Windows.Forms.Menultem О System.Windows.Forms.ContextMenu Класс System. Windows. Forms. MainMenu содержит структуру меню формы. Меню со- составляется из большого числа объектов Menultem, которые представляют отдельные элементы меню в его структуре. Объект Menultem — это либо команда для приложения, либо родительское меню для других элементов меню. Меню создается с помощью дизайнера. Перетащите в форму элемент MainMenu. Затем наберите название меню в прямоугольнике, озаглавленном Туре Неге. Для создания клави- клавиши-акселератора используйте &. Например, E&xit даст в результате Exit. Для получения строки-разделителя введите тире. Для нашего примера потребуются меню &File, содержащее пункт &Save, разделитель (-) и E&xit, а также меню &Не1р, содержащее пункт &About: На следующем шаге необходимо создать код для элемента меню, чтобы он выполнял какие-то действия. Дважды щелкните мышью на пункте About, чтобы создать обработчика события для этого элемента меню, и добавьте код: private vbl^'menultemAboutHelp^Cliek-fobject :•-$,*- * *-%;■ System.EyiE^lArgs e) AboutForm aboutForm = ■new AboutFormf); ,-4*?-" '-^ .., , ■". * aboutPorra.ShowDialpgH; '' ' '. * ' : ' ' -'} ' ' ,*, t •'■л ■•■•■ Этот код создает новую форму, отображаемую как диалоговое окно (см. ниже).
Приложения Windows 357 Диалоговые окна Диалоговое окно характеризуется модальным стилем взаимодействия с пользователем. Модальность означает, что пользователь не может работать с программой в то время, пока на экране присутствует модальное диалоговое окно. Диалоговые окна часто применяются для вывода или получения информации от пользователя. Для создания диалогового окна необходимо добавить форму в проект. Затем для формы устанавливаются свойства: FormBorderStyle: FixedDialog MaximizeBox: false MinimizeBox: false ControlBox: false StartPosition: CenterParent Для создания окна About перетащите элементы управления "метка" и "кнопка". После этого можно изменить текст: The Bezier Curve Generator . Еще одним ключевым моментом является то, что для вызова формы применяется метод ShowDialogO. При вызове 3h~wE;alog() форма отображается как модальное диалоговое окно. Метод ShowDialogO возвращает переменную DialogResult, которую можно испо- использовать для проверки того, на какую кнопку нажал пользователь, если имеется несколь- несколько кнопок. Например, обработчик события для указанной выше кнопки можно было бы изменить, чтобы он возвращал значение ОК. Для создания обработчика события дважды щелкните мышью на кнопке в дизайнере: private void OKbutton_Click(object sender. System.EventArgs e) ((Button) sender) .DialogResult = DialogResult.OK,- this.Close!); Классы WinForms предлагают ряд типовых диалоговых окон. Например, если требуется сохранить данные документа, вы воспользуетесь классом saveFileDialogf). Для нашего примера создадим файл с кривой Безье, который может быть отображен в Internet Explorer. Следующий код является обработчиком события для элемента меню "Save". Создается и инициализируется SaveFileDialogf). Если пользователь указывает имя файла, то возвращается поток, в который могут быть записаны данные, представля- представляющие собой текущий документ. В нашем случае в файл выводится представление на язы- языке Vector Markup Language (VML). Для доступа к этим методам необходимо добавить в класс пространство имен System. 10: private vo.id menuIteroF.iaeS,ave_CIick{<?bject; sender, 'System,;i^ventArgs e) j - ■- si.:. ■ -a,' ■ ■ - - - r- - ■Stream myStream; SaveF.ileDiaibg 'saveFileDialogl = new SayeFileDialogl) ;5 outTexts . "'-•' . , - logjt.Filter = "htm files- i*.htm) I *..htmJAil files - "''. ~ ' (*.*) I*. ** ; ! if {saveFileDialogl.ShowDialogO ,=- DialogResult.OK) 1Л if sUmyStream .=* saveFlleDialogJ.OpenFiliejU) != nu\l) * ^- , // производим Запись в файл StxeamWriter myWriter = new SfcreamWriter (myStr.eajn) ; outText = "<html xmlnsiv=\"urn;schemas^№crosoflt-coni:vrol\1'>"< «utText += "<head><6tyl«>v\\: * {■ behavior.- url,< «default #VMb) -. )
358 Глава 9 </style></head>" ; outText +~= "<body><v:shape style='width: 200; height: 200" stro>:ecolor= 'blue' ,ppordorigin='0 0' • coordsize='200 2OO'>"; outText += "<v:path v='m "; putText- += bezierControll. Ptl.X.ToStringO + ',' + bezierControll.Ptl.Y.ToString(); OutText += " с " + bezierControll.Pt2.X.ToStringf) + ',' + bezierControll. Pt2 .Y.ToStringO + outText += bezierControll.Pt3.X.ToStringO + ',' + bezierControll..Pt3.Y.ToStringO + outText += bezierControll.Pt4.X.ToString() + ',' + bezierControll.Pt4,Y.ToString.(K- ' outText += "" />"; outText += "</v:shape></bodyx/html>11; myWriter.Write (outText) ; inyWriter.ClbseO ; myStrearn.Close(); После того кгис файл будет сохранен, можно дважды щелкнуть на нем мышью, и кривая Безье отобразится в Internet Explorer. Заключительный элемент управления, который необходимо добавить,— это действие при выборе File | Exit в меню: private void menuItemFileExit_Click(object sender. System.EventArgs e) { this.Closet); } Теперь осталось собрать приложение и запустить полученный код. Заключение Несмотря на то, что с появлением динамического HTML приложения для браузера стали сложнее, они по-прежнему остаются лишь приложениями для браузера. Они не использу- используют всей мощи настольного компьютера, которая включает в себя как ресурсы процессора, так и ресурсы видеосистемы. Основное преимущество при написании приложений для Windows в стиле Windows Forms заключается в том, что Windows Forms гомогенизируют модель программирования и исключают многие ошибки, случайности и несоответствия, которыми полно Windows API. Приложения Windows имеют полный доступ к системным ресурсам компьютера, включая локальные файлы и реестр Windows. Приложения Windows Forms также могут использовать классы .NET для применения GDI+. Платформа .NET обеспечивает простую разработку приложений Windows. Про- Пространства имен System.WinForms и System.Drawing содержат большое число классов для создания приложений Windows. Целый ряд элементов управления не был рассмотрен в этой главе. Графические классы также гораздо обширнее, нежели было представлено. Рекомендуем вам самостоятельно исследовать классы System.Windows.Forms и попробовать их в работе.
Сборка В этой главе рассматриваются сборки: что они собой представляют, как могут использо- использоваться и насколько они полезны. В частности, обсуждаются: О Нововведения в сборках по сравнению с предыдущими технологиями О Создание и просмотр сборки О Общая спецификация языка и кросс-языковая поддержка D Создание ресурсных сборок и их использование для локализации О Совместное использование сборок (уникальные имена и проблема версий) Прежде всего выясним, что такое сборка. Понятие сборки До появления платформы .NET приходилось иметь дело с предшественниками сборок: с обычными DLL, экспортирующими глобальные функции, и с COM DLL, экспортирую- экспортирующими классы СОМ. Сама Microsoft придумала термин DLL-Hell (ад DLL) для описания традиционных проблем, связанных с DLL. Часто приложение перестает работать из-за того, что используемая DLL переписыва- переписывается при установке другого приложения. Иногда более поздняя версия DLL переписыва- переписывается более ранней, поскольку программа установки не проверяет версии или версии заданы некорректно. Как правило, старая DLL заменяется новой версией. В теории это не должно вызывать проблем, однако на практике бывает иначе. Хотя более новая DLL должна быть обратно совместима с предыдущей версией, иногда этого не происходит. Подобные проблемы возникают довольно часто. Windows 2000 использует технологию Side-by-Side, которая позволяет устанавливать DLL в каталог приложения. Другая версия уже установленной разделяемой DLL может быть помещена в каталог приложения. Вызов Win32 API LoadLibrary () был переписан таким образом, что сначала осуществляется проверка на наличие файла . local в ката- каталоге приложения. Если он найден, API проверяет, не находятся ли DLL в том же катало- каталоге, что и само приложение, и только после этого использует другие механизмы для поиска совместной DLL. Перезаписывается также путь для COM DLL, который хранит- хранится в реестре. Технология Side-by-Side несколько запоздала и не решает всех проблем. Кроме того, возникают новые проблемы, связанные с COM DLL. Другим инструментом Windows 2000, предназначенным для борьбы с адом DLL, является защита файлов: сис- системные DLL защищены от перезаписи неавторизованными сторонами. Все эти средства Windows 2000 работают с симптомами, а не с причинами. Проблемы с версиями DLL существуют из-за того, что неясно, какая версия конк- конкретной DLL требуется каждому приложению. Эти зависимости не отслеживаются в традиционной архитектуре DLL. COM DLL на первый взгляд решают многочислен- многочисленные проблемы DLL путем лучшего разделения реализации и интерфейса. Интерфейс представляет собой соглашение между клиентом и компонентом, которое, в соответст- соответствии с правилами СОМ, не может меняться и, следовательно, не способно привести к проблемам. Однако даже с СОМ случается, что изменения реализации нарушают работу существующих приложений.
360 Глава 10 Side-by-Side также поддерживает COM DLL. Если вы когда-либо пытались использо- использовать Side-by-Side с COM DLL, то знаете, что это грубое решение проблемы. При приме- применении Side-by-Side COM DLL возникают новые проблемы. Как деинсталлировать DLL, если установлены две версии: будут ли потеряны записи в реестре так, что ни одна дру- другая версия больше не сможет работать? Если не деинсталлировать, то что происходит, когда две версии одного компонента используют различные потоковые конфигурации? Выигрывает последняя установленная версия. Эта проблема возникает из-за того, что конфигурация СОМ-компонента не хранится в самой DLL. Сборки как ответ на ад DLL Ответом .NET на все эти проблемы являются сборки. Сборки — это самоописывающиеся установочные модули, состоящие из одного или нескольких файлов. Сборка может пред- представлять собой одну DLL или ЕХЕ-файл, который включает в себя метаданные, или мо- может состоять из различных файлов, например ресурсных файлов, метаданных, DLL и ЕХЕ. Установка сборки может выполняться как простое копирование всех ее файлов. Другой особенностью сборок является то, что они могут быть частными или общими. Если вы ищете компонент СОМ в реестре или с применением OleView, то вам потребуется просмотреть сотни компонентов. Лишь несколько компонентов предназначается для ис- использования в более чем одном приложении, но каждый из них должен иметь глобальный уникальный идентификатор (GUID). Существует большая разница между частными и общими сборками. Многих разработ- разработчиков вполне устроят частные сборки, для которых не требуется выполнять специаль- специальной обработки, регистрации, контроля версий и т.п. Единственное приложение, которое может иметь проблемы с версиями частных сборок,— это ваше приложение. Ча- Частные компоненты, которые применяются внутри вашего приложения, устанавливают- устанавливаются одновременно с приложением. Локальные каталоги приложения используются для сборок компонентов, поэтому у вас не должно быть проблем с версиями. Никакое другое приложение никогда не перепишет ваши частные сборки. Разумеется, могут возникать проблемы с частными сборками во время разработки. Рассмотрим пример: если компонент, применяемый в приложении, ссылается на версию 1 сборки X, а вы в своем приложении используете версию 2 сборки X, то какая версия сборки будет копироваться в каталог приложения? Это зависит от того, какую из версий вы упомянули первой. Эта проблема должна быть решена во время разработки. Дад уже установленного приложения можно восполь- воспользоваться hotfix, заменив частную сборку ее более новой версией. Проблемы с новой вер- версией могут возникнуть только у того приложения, для которого устанавливается этот hotfix; другие приложения не затрагиваются. Разделяемая сборка используется несколькими приложениями. Она обязана удовлет- удовлетворять целому ряду требований. Разделяемая сборка должна иметь особый номер версии, уникальное имя, и чаще всего она устанавливается в глобальный кэш сборок. Особенности сборок Сборки имеют следующие свойства: О Сборки являются самоописывающимися. Больше не требуется анализировать ключи реестра в поисках места размещения библиотеки типов и т.д. Сборки содер- содержат описывающие их метаданные. Метаданные включают в себя манифест и типы, экспортируемые из сборки. О Зависимости версий записаны внутри манифеста сборки. Версия сборки, на ко- которую производится ссылка, хранится в манифесте сборки. Таким образом, мы точно знаем, какая версия сборки применялась при разработке. Версия сборки, которая будет использоваться, конфигурируется разработчиком и системным ад- администратором. Позже мы посмотрим, какие политики версий доступны и как они работают. О Сборки могут загружаться side-by-side. В Windows 2000 функция side-by-side может применяться для разных версий одной и той же DLL, используемых в одной систе- системе. .NET расширяет функциональность Windows 2000: теперь разные версии од- одной и той же сборки могут применяться внутри одного процесса! Вы спросите, а нужно ли это? Если сборка А использует версию 1 разделяемой сборки Shared, сбор- сборка В — версию 2 той же сборки, а вы задействуете как сборку А, так и сборку В, то попробуйте угадать, какие версии разделяемой сборки Shared потребуются вашему приложению!
Сборки 361 О Изоляция приложений гарантируется использованием областей приложений. С помощью областей приложений несколько приложений могут работать незави- независимо друг от друга внутри одного процесса. Отказы внутри одного приложения не могут повлиять на работу других приложений внутри того же процесса. О Установка может быть очень простой и заключаться в копировании файлов, при- принадлежащих сборке. Достаточно применения команды хсору. Это свойство было названо установкой с нулевым усилием. Необходимость в Microsoft Windows Installer (MSI) Часто спрашивают, почему необходим Microsoft Windows Installer, если для установки приложений .NET достаточно хсору? Ответ заключается в том. что копирование фай- файлов — это еще не все, что требуется от приложений Windows. Как правило, требуется, чтобы можно было получить доступ к приложению из меню Start, установить его в подкаталог Program Files, предоставить пользователю возмож- возможность выбора опций, показать copyright-экран и т.д. Windows Installer поддерживает бо- большое число дополнительных возможностей, которые не доступны при использовании сборок. Приложения могут применять свои собственные установки реестра, политики групп для упрощения администрирования, когда конкретные пользователи получают доступ к определенным средствам, рекламу, когда части приложения устанавливаются позже по запросу пользователя, и средства восстановления поврежденных файлов. Области приложений и сборки До .NET процессы использовались в изоляции, каждый процесс обладал своей собствен- собственной виртуальной памятью; приложение, работающее в процессе, не могло записать в па- память другого приложения и таким образом нарушить его работу. Процесс служит в качестве изолятора и барьера безопасности между приложениями. В архитектуре .NET присутствует новый вид границ для приложений: области приложений. Для управляемо- управляемого IL-кода среда исполнения может гарантировать, что доступ к памяти другого приложе- приложения внутри одного процесса не произойдет. Несколько приложений может быть запущено в одном процессе внутри нескольких областей приложений: Процесс 4711 Область приложений А один два Область приложений В один Процесс 4712 Область приложений С два Сборка загружается в область приложения. На рисунке показан процесс 4711 с двумя областями приложений. В области приложений А созданы объекты 'один' и 'два', пер- первый, возможно, из сборки Один, а второй — из сборки Два. Вторая область приложений в процессе 4711 содержит экземпляр 'один1. Для того чтобы свести к минимуму потреб- потребление памяти, код сборок загружается только один раз в область приложения. Статиче- Статические члены и члены экземпляров не являются общими для разных областей приложений. Невозможно напрямую получить доступ к объектам внутри другой области приложений. Для этого требуется прокси (см. главу 23). Класс AppDomain используется для создания и уничтожения областей приложений, для загрузки и выгрузки сборок и типов, а также для перечисления сборок и потоков в области приложений. Рассмотрим небольшой пример.
362 Глава 10 Сначала создадим консольное приложение С# AssemblyA. В методе Main () мы выпол- выполняем Console .WriteLine (), чтобы посмотреть, когда этот метод будет вызван. Добавлен конструктор с двумя значениями int в качестве аргументов для иллюстрации создания экземпляров с помощью класса AppDomain. Сборка AssemblyA.exe будет загружена из второго приложения: namespace AssemblyA { class Class] { public ClassKint vail, int val2) "' ( Console.WriteLine("Constructor, with the "values {0}, A)" T * "in domain {2} called"; yall, val2f*' I App&main.CurreritDomain.FriendlyName) ; } static void Main(string[] args) L. Console.WriteLine("Main in domain {0} called", AppDomain.CurrentDomain.FriertdlyName); Теперь создадим проект DomainTest, также являющийся консольным приложением С#. Сначала мы выводим имя текущей области. При помощи метода CreateDomain () формиру- формируется новая область приложений с именем "New AppDomain". Затем мы загружаем сборку AssemblyA в новую область и вызываем метод Main () с помощью ExecuteAssembly (): using System; п. ..esj ai 3 лпа: nTest { class Class_ { static void Ks ■ .'Lt-ri. args, I AppDomain currentDomain = AppDomain.CurrentDomain; \. i' " Console.WriteLine(currentDomain.FriendlyName) ; ; AppDomain secondDomain = currentrc.Ti )in.CreateDomain ( *New. AppDomairt"")"; secondDomain.ExecuteAssembly("AssemblyA.exe"); Перед запуском DomainTest. =xe необходимо скопировать сборку AssemblyA.exe в каталог программы DomainTest. ехе, чтобы сборка была найдена. Ссылку на сборку AssemblyA. ехе добавить невозможно, поскольку в Visual Studio.NET могут быть указа- указаны только ссылки на сборки в DLL. Если сборка не будет найдена, будет сгенерировано исключение System.ID.FileNotFoundExc°ption. Выполнение программы даст результат: onainlest.exe ..ain in donain New flppDonain called Press any hey to continue DomainTest. ехе — имя первой области приложения. Вторая строка представляет со- собой результат вывода загруженной в New AppDomain сборки. Даже если вы добавите ме- метод Thread.Sleep ) в метод AssernolyA.Classl.Maint), вы не увидите процесс Assemblyl. ехе в окне процессов. Новый процесс не создается. AssemblyA загружается в процесс DomainTest.exe.
Сборки 363 Вместо того чтобы вызывать для только что загруженной сборки метод MainO, можно создать ее новый экземпляр. В следующем примере мы меняем метод Execute- Assembly () на Createlnstance(). Первый аргумент — это имя сборки, AssemblyA. Второй аргумент определяет тип, экземпляр которого нужно создать: AssemblyA. Classl. Третий аргумент, true, говорит о том, что необходимо игнорировать регистр. Sys- System. Reflection.BindingFlags.Createlnstance является значением перечислимого типа, указывающим на то, что должен быть вызван конструктор: AppDomain secondDomain = currentDomain.CreateDomain»"New AppDomain"); ^■^'Л"^й //secondDbmain.ExecuteAssemblYi"AssemblyA.exe")? ijiJp&V-. : secondDomain.Createlnstjance ("AssemblyA" , ^ . "AssemblyA.Classl", true, '" .'.'' ,',y. ,' System.Reflection.B'indingFlags.Createtnstance, ! .4- ""/<■'- - .. null, new object [j; (.7, 3}, null, null;; null); При успешном запуске получим следующие результаты: V Professional CHJUMMHt Dona inTest.exe Coiflructor with the values Pi*eec any key to continue rJSJ-*J 7, 3 in donain New AppDonain called Итак, мы узнали, как создавать области приложений и обращаться к ним. Во время ис- исполнения области приложений формируются автоматически. ASP.NET создает область приложений для каждого web-приложения, запущенного на сервере. Internet Explorer со- создает области приложений, в которых будут работать управляемые элементы управле- управления. Для приложения полезно создавать области приложений в том случае, если необхо- необходимо выгружать сборку. Выгрузка сборок может осуществляться только посредством уничтожения соответствующей области приложений. Структура сборки Сборка состоит из метаданных сборки, описывающих завершенную сборку, метаданных типов, описывающих экспортируемые типы и мето- методы, кода на MSIL и ресурсов. Все эти части могут находиться внутри од- одного файла или в нескольких разных файлах. В данном примере метаданные сборки, метаданные типов, MSIL, код и ресурсы содержатся в одном файле — Component .dll. Сборка со- состоит из одного файла. Component.dll Метаданные сборки Метаданные типов КодИ. Ресурсы Второй пример демонстрирует сборку, которая распределена по нескольким фай- файлам. Component. dll содержит метаданные сборки, метаданные типов и код MSIL, но не содержит ресурсов. Сборка использует рисунок picture, jpeg, который не включен в Component.dll, но на который производится ссылка из метаданных сборки. Метадан- Метаданные сборки также описывают модуль util.netmodule, который включает в себя мета- метаданные типов и код MSIL для класса. В этом модуле отсутствуют метаданные сборки. Поэтому в модуле нет информации о версии; он также не может быть установлен отдель- отдельно. Все три файла образуют одну сборку. Сборка представляет собой установочный файл. Манифест тоже можно расположить в отдельном файле:
364 Глава 10 Componentdll Util.netmodule Метаданные сборки Метаданные типов КодИ ■Л Метаданные типов КодИ \ Picturejpeg Ресурсы Манифесты сборок Важной частью сборки является манифест, входящий в состав метаданных. Он содержит всю информацию, необходимую для обеспечения возможности ссылки на эту сборку. Манифест включает в себя: О Собственное имя, версию, культуру и открытый ключ. а Список файлов, принадлежащих сборке. Одна сборка должна содержать по крайней мере один файл, но может содержать и несколько файлов. D Список используемых сборок. В манифесте описываются все сборки, включая номер версии и открытый ключ, которые применяются данной сборкой. 3 Набор требований по правам — права, необходимые для запуска сборки (см. главу 25). О Экспортируемые типы не являются частью манифеста, если только они не содер- содержатся в модуле. Модуль — это повторно используемый блок. Описание типов хранит- хранится в виде метаданных внутри сборки. Из метаданных можно получить структуры и классы со свойствами и методами. Это является заменой библиотеки типов, которая применялась в СОМ для описания типов. Для СОМ-клиентов легко сгенерировать библиотеку типов по манифесту. Механизм отражения использует информацию об экспортируемых типах для позднего связывания с классами (см. главу 7). Пространства имен, сборки и компоненты Как пространство имен укладывается в концепцию сборки? Пространство имен совер- совершенно не зависит от сборки. В одной сборке может присутствовать несколько разных пространств имен, и в то же время одно пространство имен может быть распределено по нескольким сборкам. Пространство имен является лишь расширением имени типа — оно принадлежит к имени типа. Приведенная ниже диаграмма должна прояснить используемую концепцию. На ней показаны три сборки, которые мы создадим позже (сборки, созданные с помощью управляемого C++, с помощью VB.NET и с помощью С#). Все эти сборки имеют классы в одном пространстве имен: Wrox. ProCSharp. Сборка Hel loCSharp содержит также класс Math, который находится в пространстве имен Wrox.Utils. сборка HelloMCPP j сборка HelloVB сборка HelloCSharp пространство имен Wrox.ProCSharp класс HelloMCPP класс HelloVB класс HelloCSharp пространство имен Wrox.Utils | класс Math
Сборки 365 Тогда возникают следующие вопросы: что такое компонент в языке .NET? является ли сборка компонентом? Нет. Компонент представляет собой двоичную форму класса. Одна сборка может содержать большое число компонентов. Частные и разделяемые сборки Сборки могут быть разделяемыми или частными. Частная сборка находится либо в том же каталоге, что и приложение, либо в одном из его подкаталогов. В случае частной сборки не нужно беспокоиться о конфликтах имен с другими классами и о проблемах с версиями. Сборки, которые используются во время процесса сборки, копируются в ката- каталог приложения. Частные сборки представляют собой обычный способ создания сборок, особенно если приложения и компоненты создаются внутри одной компании. При совместном использовании сборок необходимо помнить о некоторых правилах. Сборка должна быть уникальной и, следовательно, иметь уникальное имя (строгое имя). Часть строгого имени представляет собой обязательный номер версии. Разделяемые сборки применяются в том случае, когда компонент создается иным производителем, нежели все приложение, или когда большое приложение разбивают на подпроекты. Просмотр сборок Сборки можно просмотреть с помощью утилиты командной строки ILDASM — дизассем- дизассемблера с MSIL. Сборку можно указать в командной строке в качестве аргумента ildasm или выбрать в меню File | Open. На рисунке ниже приведена открытая в ildasm программа HelloCSharp.exe. Утилита ildasm показывает манифест и тип HelloCSharp в пространстве имен Wrox. Prof essionalCSharp. Открыв манифест, можно просмотреть номер версии и атрибуты сборки, а также используемые сборки и их версии. Открывая методы класса, можно просмотреть код MSIL: . ► MANIFEST 9 Щ WioxProCSharp в Щ. HelloCSharp ► class pnvaie auto ansi beforefieldir* ► extends [HeltoVBK'o* P'oCShaip HetoVB В elm. void() В Acid ■ inC2lint32.mi321 В Hello voidf) U Mam. .assembly HetoCS hap Создание сборок Приступим к созданию сборки. На самом деле мы уже создавали сборки (см. главу 3), поскольку исполняемый файл .NET также является сборкой, но теперь мы рассмотрим специальные опции для сборок. Создание модулей и сборок Все типы проектов С# в Visual Studio.NET создают сборку. Не важно, выбираете вы проект DLL или ЕХЕ, в любом случае создается сборка. При помощи компилятора командной строки esc можно также создавать модули. Модуль представляет собой DLL без атрибутов сборки (поэтому модуль не является сборкой, хотя он может быть позже добавлен к сборке). Команда esc /target:module hello.es создаст модуль hello.netmodule. Этот модуль можно просмотреть с помощью ildasm. Модуль также имеет манифест, однако внутри манифеста отсутствует позиция . assembly. У модуля нет атрибутов сборки. Для модулей невозможно конфигуриро- конфигурировать версии или права доступа; это допустимо только для сборок. В манифесте модуля можно обнаружить ссылки на сборки. При помощи опции esc /addmodule можно добавлять модули к существующим сборкам.
366 ГлаваЮ С целью сравнения модулей и сборок создадим простой класс А и откомпилируем его с помощью команды: esc /target :module A. cs Компилятор генерирует файл A.netmodule, который не содержит информации о сборке (в чем можно убедиться, используя ildasm для манифеста). Манифест модуля по- показывает применяемую сборку mscorlib и пункт .module: / MANIFEST .assembly extern «scorlib publickeytoken - (B7 7A SC 56 19 34 EO 89 ) hash - (8B BB SA BD 8D A3 12 7D 08 A2 2S DO 48 17 28 4F 20 57 EA 07 ) ver 1:0:2411:0 .module A.netnodule /' MVID: {F53B087D-18DB-4302-A860-0338B91FDS48} .subsystem 0x00000003 .file alignment S12 corflags 0x00000001 // Image base' ОхОЗОеОООО /V Теперь создадим сборку В, которая включает в себя модуль A.netmodule. Для генера- генерации сборки необязательно иметь исходный файл. Команда для построения сборки: esc /target:library /addmodule:A.netmodule /out:B.dll При просмотре сборки в ildasm можно обнаружить только манифест. В манифесте ука- указана сборка mscorlib. Затем следует секция assembly с хэш-алгоритмом и версией. Частью манифеста является список всех модулей, принадлежащих сборке. Видно, что сборке при- принадлежит .module A.netmodule. Он экспортирует класс Wrox.ProfessionalCSharp.A. Классы, экспортируемые из модулей, являются частью манифеста сборки, классы, экспортируемые из самой сборки,— нет: / MAMFEST .assembly extern mscorlib publickeytoken - CB7 7A SC 56 19 34 EO 89 ) bash - <8B BB SA BD 8D A3 12 7D 08 A2 2S DO 48 17 28 4F // .Z. 20 57 EA 07 ) /V V. . ver 1:0:2411 0 . assembly В " The following custom attribute is added automatically, do not uncc /y custom instance void Cmscorlib]5ystem.Diagnostics.DebuggableAttribut hash algorithm 0x00008004 ver 0 0 0:0 .file A netModule hash - GE 25 E2 1A CC 2E 9E 31 EO 72 8F B3 35 99 20 BE •• ~JS 1 05 58 bC 0D ) •• .XI class extern public Wrox ProfessionalCSharp.A file A.netmodule .class 0x02000002 module B.dll v UVID: t946A91B2-96E3-4555-817D-lS8C8EF6023F> subsystem 0x00000003 file alignment 512 corflags 0x00000001 — /V Image base: ОхОЭОеОООО и , i Каково назначение модулей? Они могут использоваться для более быстрой загрузки сборок, поскольку не все типы находятся внутри одного файла. Модули загружаются то- только тогда, когда в них возникает необходимость. Другая причина применения модулей заключается в том, что вы можете создать сборку, написанную с использованием неско- нескольких языков программирования: один модуль может быть написан на VB.NET, другой — на О, и оба этих модуля можно включить в одну сборку.
Сборки 367 Создание сборок с помощью Visual Studio.NET Как говорилось выше, все типы проектов в Visual Studio.NET создают сборки; в первой версии Visual Studio.NET не поддерживается создание модулей. При создании проекта Visual Studio.NET генерируется исходный файл Assemblylnfo.cs, и мы можем сконфи- сконфигурировать атрибуты сборки с помощью обычного редактора кода. Приведем файл, сге- сгенерированный мастером: using pystem.Reflection; using. 'System.Runtime.CompilerSfervices; //. // Общая информация о сборке управляется при помощи следующего // набора атрибутов. Можно изменять значения этих атрибутов для изменения // информации, связанной со сборкой. V {assembly: AssemblyTitle("")] {assembly; AssemblyDescription("")] [assembly: AssemblyConfigurationt"")] [assembly: AssemblyCpmpany ("")] [assembly: AssemblyProduct("")] (assembly: AssemblyCopyright'"")] .[assembly: AssemblyTrademark ("") 1 [assembly: 'AssemblyCulturel""),] // // Информация о версии сборки состоит из четырех значений: / 11" // //i.«. // ■• Ma] or Minort Version Version Sevislon Build Number // Можно задать как все значения, так и только- Revision и Build Number, // используя '*', как показано ниже: {assembly: ;AssemblyVersion( .0. *") ] > ": /У Для того чтобы подписать сборку, необходимо указать ключ. Для I/ получения дополнительной информация о i цписи сборок If обращайтесь к документации Microsoft по платформе .NET. /А . ^ .... //.„Используйте следующие атрибуты для управления тем, какой //' ключ применяется для подписи. >/ // Примечание: // ■(*) Если кл«зч не указан, сборка не может быть подписана. // (*) KeyNarne соответствует ключу, который был установлен Crypto // Service Provider (CSP) на вашей машине, // (*) Если указаны файл ключа и атрибут имени ключа, то // производится следующая обработкам' // A5 Если KeyName может быть найдено в CSP, используется этот ключ. // B) Если KeyKame не существует, a KeyFile существует, то ключ // . в файле устанавливается в CSP и используется... ff (*) Задержанная подпись является дополнительной возможностью, // "для получения информации обратитесь к документации // Microsoft по платформе .NET. // {assembly. ■AssemblyDelaySign(false)] . {assembly.; AesemblyKeyFi le (." " > J (assembly: .AssemblyKeyName("")] ч Этот файл используется для конфигурации манифеста сборки. Компилятор читает атрибуты сборки и вставляет эту информацию в манифест. Атрибуты [assembly] и [module] являются глобальными. Глобальные атрибуты, в отличие от других атрибутов, не привязаны к конкретному элементу языка. Аргу- Аргументы, которые могут использоваться для атрибутов сборки, представляют собой клас- классы в пространствах имен System.Reflection, System.Runtime.CompilerServices и System.Runtime.InteropServices.
368 Глава 10 Более подробно об атрибутах и их использовании рассказывается в главе 5. Приведем список всех атрибутов сборки из пространства имен System. Reflection: □ AssemblyCompany указывает имя компании. О AssemblyConfiguration содержит информацию о компиляции, например окончательная или отладочная версия. □ AssemblyCopyright и AssemblyTrademark содержат информацию об авторских правах и торговой марке соответственно. □ AssemblyDef aultAlias используется в том случае, если имя сборки плохо читае- читаемо (например, GUID), с помощью этого атрибута может быть назначен псевдоним. □ AssemblyDescription представляет собой описание сборки или продукта. □ AssemblylnformationVersion является версией только для сведения. Этот атри- атрибут не используется для проверки версий, когда сборка применяется в другом мес- месте. □ AssemblyProduct представляет собой имя продукта, к которому принадлежит сборка. □ AssemblyTitle — дружественное имя сборки. Дружественное имя может содер- содержать пробелы. Приведем пример конфигурации атрибутов: [assembly;: AssemblyTitle ("Professional С#")] [assembly;- AssemblyDescription("") ] [assembly: AssemblyConfiguration("Retail version")] ^assembly: AssemblyCompany("Wrox Press")] [assembly: AssemblyProduct("Wrox Professional Series")] [assembly: AssemblyCopyright("Copyright (C) Wrox Press 2001")] [assembly: AssembiyTrademarkC'Wrox is a registered trademark of- Wrox Press ,-! ■ Ltd") ] [assembly: AssemblyCulture("en-US")] Следующие атрибуты содержатся в пространстве имен System.Runtime.Compiler- Services: О AssemblyCjlture сообщает о культуре сборки (см. ниже). □ AssemblyDelaySign, AssemblyKeyFile, AssemblyKeyName используются для создания строгих имен для совместных сборок. □ AssemblyVersiv.". указывает номер версии сборки. Версии играют важную роль в совместных сборках. Дополнительные атрибуты совместимости с СОМ в пространстве имен System. Runtime. interopScrvices могут быть использованы для того, чтобы сделать типы .NET видимыми для СОМ, чтобы указать application-id и т.д. (см. главу 19). Кросс-языковая поддержка Одним из самых больших достоинств СОМ является поддержка различных языков. Клиен- Клиенты СОМ можно создавать в Visual Basic, а использовать в клиенте сценариев типа JavaSc- JavaScript. С другой стороны, можно создать компонент СОМ с помощью C++, которым не сможет воспользоваться программа на Visual Basic. Клиент сценариев имеет другие требо- требования по сравнению с клиентом VB. Клиент C++ способен задействовать большее число возможностей СОМ, чем остальные языки. При написании компонентов СОМ всегда необходимо помнить о клиентах. Сервер должен быть разработан для конкретного языка клиента или для группы языков клиен- клиента. Если компонент СОМ создан для применения в клиенте сценариев, этот компонент можно будет также использовать в C++, но при этом клиент C++ будет испытывать неко- некоторые трудности. При поддержке различных клиентов должно быть учтено множество правил, и компилятор не поможет с СОМ: разработчик СОМ должен знать требования языка клиента и в соответствии с этим создавать интерфейс. Что противопоставляет этому платформа .NET? .NET точно определяет, как должен быть написан компонент, используемый различными языками. Это определение извест- известно как Общая спецификация языка (CLS). Компилятор может проверить код для .NET
Сборки 369 на соответствие спецификации CLS. Однако мы не ограничены применением только тех типов, которые поддерживаются всеми языками. С помощью С# можно создавать компоненты, использующие типы данных, которые не доступны в потребительских язы- языках .NET. Платформа .NET позволяет создавать компоненты, которые не будут доступны во всех потребительских языках .NET. Все типы описаны в общей системе типов (CTS). но не все типы доступны во всех языках. Для создания компонентов, доступных из всех языков .NET, необходимо приме- применять CLS. Общая спецификация языка является подмножеством общей системы типов. Общая система типов определяет правила, которым должны следовать компиляторы языка для определения, использования и хранения типов. Поэтому объекты, написан- написанные на разных языках, могут взаимодействовать друг с другом. В .NET особенности язы- языков не сведены к общему минимуму. С помощью .NET можно создавать компоненты, которые не будут доступны из других языков. Однако поддержка всех языков в .NET явля- является более простой, чем в СОМ. Если вы будете применять только CLS, то гарантируется, что компонент можно будет использовать в любом языке. Платформа .NET была разработана с нуля для поддержки большого числа языков. Во время разработки .NET Microsoft предложила многим производителям компиляторов со- создать свои собственные языки .NET. Microsoft предлагает VB.NET, управляемый C++, С# и Jscript. Кроме того, доступно более двадцати языков от различных производителей, включая Cobol, Smalltalk, Perl и Eiffel. Каждый из этих языков имеет свои преимущества и возможности. Компиляторы всех этих языков были переработаны для поддержки .NET. CLS является минимальной спецификацией требований, которые должен > поддерживать язык. Это означает, что если открытые методы делать только CLS-совместимыми, то ваши классы смогут использовать все языки, поддерживающие .NET! Практически все классы платформы .NET, за редким исключением, являются CLS- совместимыми. Несовместимые классы и методы специально отмечены в документации MSDN. В качестве примера рассмотрим структуру lHnt-32 из пространства имен System. UInt32 представляет собой 32-битовое целое без знака. Не все языки (в частности. Visual Basic.NET) поддерживают типы данных без знака, и такие типы данных не являются CLS-совместимыми. Языковая независимость в действии Посмотрим на CLS в действии. Первая сборка, которую мы создадим, включает в себя ба- базовый класс на управляемом C++. Вторая сборка содержит класс VB.NET, унаследованный от класса C++. Третья сборка представляет собой консольное приложение на С#, содержа- содержащее класс, унаследованный от кода VB.NET, и функцию Main, которая использует класс С#. Реализация должна показать, как эти языки используют классы .NET и как они рабо- работают с числами. Поэтому все эти классы содержат простой метод HelloO, в котором применяется класс System.Console, и метод Add(), который складывает два числа:
370 Глава 10 HelloMCPP +Hello() +Hello2() +Add() HelloVB +Hello() +Add() HelloCSharp +Hello() +Add() +MainO Написание класса с использованием управляемого C++ Первый тип проекта — это Managed C++ Class Library, который создается в Visual Studio под именем HelloMCPP: Г Muw Project Project Types: ; Visual Basic Projects I Veual C* Projects JWSual C++Projects I Setup *nd Deployment Projects fe _l Otr» Projects UJ Vteual Studo Solutions Templates: Weard Managed C+-I AOPkatior, Stored Fro... WbSI v Managed C++ ClaaUbnry Project C3 Managed C++ EiT(Xy Project Ж *5- J A class H*ery that u«s M«Mg*d Extensions for C location: | C:U>rcf «swrwl C*\Assei»t*«\a5Oemo Г Асаю.аАЛоп " go» Solution frowse— Мастер приложения создает класс HelloMCPP, который помечен с помощью атрибу- атрибута управляемого C++ дс для того, чтобы сделать класс управляемого типа. Без этого ат- атрибута класс представлял бы собой обычный неуправляемый класс C++, генерирующий родной машинный код. Сгенерированный заголовочный файл stdafx.h содержит оператор #using <mscorlib.dll>. В C++ другие сборки могут быть указаны при помощи оператора пре- препроцессора #using. Код для класса .NET находится в файле HelloMCPP.h: // ' HelloMCPP.h -** #pragma :onse ,. ' ; using namespace System; public gc class HelloMCPP Г // TODO: Добавьте ваши методы к этому классу,.
Сборки 371 В целях демонстрации добавим три метода. Виртуальный метод Не11о2 () использует функцию времени исполнения С printf (), демонстрируя применение родного машин- машинного кода в управляемом классе. В методе Hello О используется управляемый класс Console из пространства имен System. Оператор C++ using namespace аналогичен оператору С# using. Оператор using namespace System открывает пространство имен System, поэтому не требуется писать System: :Console: :4riteLine(). Метод Hello () отмечен virtual, поэтому его нельзя перекрыть. Мы перекроем Hello () в клас- классах VB и С#. Функции класса C++ не являются по умолчанию виртуальными. К классу до- добавлен третий метод, который возвращает сумму двух аргументов int, так что мы сможем сравнить сгенерированный MSIL с другими языками и посмотреть, как они работают с числами. Все три примера используют одно и то же пространство имен Wrox. ProCSharp. Для создания пространства имен применяется оператор C++ nairuspace. // HelloMCPP.h #pragma once tinclude <stdio.h> using- namespace System; namespace* Wrox { namespace proCSharp f public gc class Hel_oMCPP i public; > virtual void Hello () {• Console: :WrxteLine( "Hello, Maiiaged C-r+"); ') victual void Hello2() I ,, ., ■ :,...■ printf("Hello, calling native code\n"); } int Add (int. vail, int vaI2) I '' , return vail * val2; У Для сравнения программ с запускаемым кодом используем окончательную сборку, а не отладочную конфигурацию. Просматривая сгенерированную DLL при помощи ILDASM, можно увидеть два применяемых статических метода: printf () и DllMainCrtStartup (). Оба метода представляют собой неуправляемые функции на машинном языке, использую- использующие pinvoke. DllKainCrtStari^p ) применяется внутри каждой управляемой C++ програм- программы. Это точка входа DLL, и она вызывается при загрузке DLL. pr^ntr. используется внутри метода Kellof). Закрытые поля $АггауТуре$0хес5а014е и SAi. l -: ype$0xf7078e56 содержат строки "Hello, Managed C++" и "Hello, callira native code\n": mmsss ► MANIFEST ■ Ш Wrox ProCSharp a £ HeDoMCPP У class pdokc euto ansi ■ docvodlj ■ Add.r«32Iin>32.nl32j ■ Hdo vc* ■ Helo2 vad() S JA!i«yType«hrec5«014e i {АпауТурв}(Ы7078е56 * •>?_Ce>_0BDeiHJMFHODF@Hello'W5M«rM9ed75C?ia'ta?tAA@: * r?_C@_0BMi3>POGPABJ@Helo?0'5ce«lng?5n4Uve75eode?6?yiA@- f C _DlMarCRTSi*tup©12: inl321void".un«ignedni32medopl||mjCMlib)S) Q prM: veiag W32 modop:([mscoibbpvsltm Ruolim» CompibiServices Cel I assembly HetoMCPP
372 Глава 10 ' HelloMCPP::Hello2: void!) .Method public nevslot virtual instance void Hello2() cil managed <" Code size 12 (Oxc) . n&xstack 1 Il_0000 ldsflda valuetype SArrayTypeSOxecSeOUe ^„«LO II^OOOS- call varerg int32 Hodopt([Kseorlib]System Runtime.CompilerServi IL_000e pop Xl^OOOb ret > ss end of method HelloMCPP Hello? J if' Метод Hello2 () помещает адрес поля $ArrayType$0xec5a014e, содержащего строку, в стек. В строке IL_0005 вызывается статический метод printf (), где передается указатель на строку "Hello, calling native code". Сама printf () вызывается посредством механизма платформы invoke (см. выше). С помощью платформенного invoke можно вызывать все родные функции, такие как вызовы среды исполнения С и Win32 API (см. главу_19): ' Global Funct)ons::pnntf : vararg Int32 modoptdmMorlibJSystem.Runtime.CompllerSemces.. •ethod public static pinvokeimplC» Ко map */) vaxarg int32 modopt( [mscorlib]System.Runtime CompilerServices.CallConvG printf(int8 modopt([Microsoft VisualCJMicrosoft VisualC lloSignSpecifled: .custom instance void [mscorlib]System Security SuppressUnmanagedCodeSecurity. '/ Embedded native code " Disassembly of native methods is not supported // Managed TaxgetRVA - 0xa22c /v end of method 'Global Functions': printf ifi Метод Hello() представляет собой код MSIL; в нем нет родного кода. Указатель на строку снова передается в стек. Строка IL_0005 создает новый экземпляр строкового объек- объекта; в строке 11__000а мы вызываем метод writeLinet) класса System.Console, передавая ему строковый объект: .method public nevslot virtual instance void HelloO cil managed if, @x10) -v Code size it maxstack 1 11^0000: Idsflda 11^.0005: ne»obj 11^.00 Oai: call IL_OO0f ret > // end of met bed HelloHCPP Hello valuetype SArrayTypeSOxf 7078e56 ??_C*_OBD»HJHFHODF»Hello?0' instance void t»&corlib]System String .ctor(int8«) void [mscorlib)System.Console: :UritcLiae(string) 1 f HelloMCCP::Add methodpuc // Code s aaxstack IL 0000: iiToaoi: IL 0002: ILl0003: } " end Ы : int32iint32.int32l lie instance int32 AddTiatS? ize 2 ldarg ldarg add ret int32 4 @x4) 1 2 method KelloMCPP Add mvp' vail. * v«12) cil managed Для знакомства с тем, как в управляемом C++ используются числа, посмотрим MSIL-код метода Add(). Под именами ldarg. 1 и ldarg.2 переданные аргументы поме- помещаются в стек, затем add складывает значения в стеке и записывает результат в стек, а в строке 11__0003 возвращается результат. В чем преимущество использования управляемого C++ по сравнению с С# и с другими языками платформы .NET? Управляемый C++ упрощает перенос традиционного кода для .NET. Код MSIL и родной машинный код могут легко уживаться вместе. Однако помните, что невозможно создать приложение на управляемом C++ без использования родного кода. В текущей реализации всегда присутствует родной код для запуска.
Сборки 373 Написание класса с использованием VB.NET Теперь создадим класс с помощью VB.NET. Снова воспользуемся мастером Class Library. Проект будет называться HelloVB: New Project Вода! T««!i Cj Visual Or Proiecti Ci visual C++ Projects !_J Setup and Deployment Projects ©Cj Other projects Cj Visual Stud» Solutions Control Lfcrary WndOWS Web webS«rv«e Web Control Appfcation Livery Bam»: ff O»eSoluton Proied«■ b* nested at CAPic>esacni!tc**V«<>r<itite^asDemo\Hetwe. ' (Ж | Cancel ( ,'. ;. Нф { Пространство имен класса необходимо изменить на Wrox. ProCSharp. В проекте VB.NET это может быть сделано путем изменения корневого пространства имен в свойствах проекта: HelloVB Property Pages Г'7 m (Sa , !<Л },"•<»*■* zJ fni)«af»t: CUpurwi» Для того чтобы можно было унаследовать класс от HelloMCPP, необходима ссылка на HelloMCPP.dll. Ссылку можно добавить с помощью Project | Add Reference или окна Solution Explorer. При создании сборки ссылка появится в манифесте: . assembly extern HelloMCPP. Указанная сборка также помещается в выходной каталог для про- проекта VB.NET, поэтому в дальнейшем мы не будем зависеть от изменений, выполненных в первоначальной сборке: /"MANIFEST 0:2411 0 extern UelioHCPP 1 0 476.28327 HelloVB custom instance void [Mscorllb]Systen.CLSCo*pil«ntAttributa ctor( custom instance void [xsoorlib Г custom instance void [»scorlib custom instance void [itscorlib custom instance void {«scorlib custom instance void [mscorlib custom instance void jmscorlib hash algorithm 0x00008004 ver 1 0 476 31917 5ysla».,Reflection A«seKblyTra4e»«r)iA Syste». R«i lection .Asee«blyCopyri9htA Sylto* Reflection lsse»blyProductAtt Syste* Reflection.AsseMblyCoapanyAtt Syite» Reflection Asse»blyDo»criptjo SysleK Redaction Asss«blyTitleAttri module KelloVB dll v HVir {7AF2294B-B2C3-4D8e-9ACC-3D09A}01FCAS> subsystem 0x00000002 file alignment S12 corf lags 0x00300001 ' Image base ОхОЭОеОООО J JJ
374 Глава 10 Класс HelloVB наследуется от HelloMCPP. Для наследования от базового класса в VB.NET служит ключевое слово Inherits. Inherits должно идти на отдельной строке вслед за оператором Class. Метод Hello () базового класса перекрывается. Ключевое слово VB.NET Overrides аналогично ключевому слову override в С#. В реализации ме- метода Hello () метод Hello () базового класса вызывается при помощи ключевого слова VB.NET MyBase. Ключевое слово MyBase имеет тот же смысл, что и ключевое слово base в С#. Также реализован метод Add (), в результате чего мы можем посмотреть, как VB.NET работает с числами. Метод Add () базового класса не является виртуальным, по- поэтому он не может быть перекрыт. В VB.NET присутствует ключевое слово Shadows для сокрытия метода базового класса. Shadows аналогично new в С#: Public Class HelloVB i Inherits HelloMCPP ■ Public, Overrides Sub Hello () MyBase.Hello() System. Console. Writeldne< "Hello. VB.NET"} End Sub ■»'~ Public Shadows Function AddtByVal vail As Integer, ."■', , ' Ч-. ByVal val2 As Iateger) As Integer ./■ . Return vail + val2 „ j, End Function Class , r. . .. Посмотрим, какой MSIL-код создаст компилятор VB.NET. Метод HelloVB. Hello () сначала вызывает метод Hello () базового класса HelloMCPP. В строке IL_0006 строка, хранящаяся в метаданных, помещается в стек с помощью ldstr. По сравнению с VB.NET управляемый C++ использует объект System. String и инициа- инициализирует его, применяя статическую строку. 'HelloVB::Hello : voidM aethod public virtual instance void Hello() cil Managed ИЭЕЗ! 17 @x11) " Code size uxstack 8 IL_0000 Iderg 0 IJ^OOOl call IL_0006 ldstr Ц_000Ь call 11^.0010: ret // end of nethod HelloVB: Hello tL instance void [HelloMCPP]Wrox ProCSharp.Hellc "Hello VB.NET- void [ascorlibJSystea Console Vriteline(stri J Теперь рассмотрим метод Add (). VB.NET использует add. ovf вместо метода add, ко- который применялся в МО+ коде MSIL. Это единственный оператор на MSIL, который отличается для VB.NET и МС++. но оператор add. ovf генерирует больше строк родного кода, поскольку он выполняет проверку на переполнение. Если результат сложения двух аргументов слишком велик для представления в целевом типе, то add.ovf генерирует исключение типа Overf lowException. Напротив, add лишь выполняет сложение двух величин. В том случае, если результат не помещается в выходную переменную, он будет неверным. Таким образом, add является более быстрым, но add. ovf более безопасным: 1 HeMoVB::Add : int32(int32,int32) Btbod public instance int32 " Code size 7 @«7) saxstack 2 locals mit (int32 V_0) Acd(int3Z vail, int32 va!2) cil managec" IL_0000 IL_0001. IL_0002. IL_0003- IL_S00S. IL_0006: ldarg 1 ldarg 2 add.ovf br.s ldloc.O ret 11^.0006 _ end of Method HelloVB. Add
Сборки 375 Написание класса с использованием С# Третий класс пишется на языке С#. Для этого проекта мы создадим консольное приложе- приложение С#. Сборка HelloVB указывается для наследования от класса HelloVB: Cl Visual Base Piojeeu "гЗУКиеГСГ Protects lid Visual C++Project» LJ Set vp «nd Deployment Projects ffl LJ Other Prom» % Console EirptyWeb _ —- ^, Wnoows Empty Project 5erv<e 1 Чел Projert In ; iA pf oject for cro«tr)g ft convMnd-kr* «pikiacion «-&И to Solution Protect и« be сгеЛейЛ C:Wro(«iw>«IC#i4»ert*ei[CLSO«>»\H<*j<:5t«n>, »моге Методы, реализованные в классе С#, аналогичны классам МС++ и VB.NET: Hello () является перекрытым методом базового класса; Add () — новый метод. namespace Wrox.ProfessionalCSharp using System; /// <summary> /// Общее описание HelloCSharp. /// </summary> public class HelloCSharp : He. 3cVB public HelloCSharp( X : Добавьте /ктор public override void Hello() { -; :base.Hello() ; * eonsole.WriteLine("Hell6, C#"); } ■'■ public new int AdcUint vail, inc vai2) . '-{ ." return vail + val2; - } "■' ''■' ■ - ' public static void Main( .{ HelloCSharp. Hello = new HelloCSharp!); Сгенерированный код MSIL для метода Hello () аналогичен коду MSIL компилятора VB.NET:
376 Глава 10 ' HtlluCihdrp-.Htlo: vo««) .method public hidobysig virtual instance void HelloO cil managed 17 @x11) // Code size . «ajtstack 8 11^0000 Idatg 0 H_0001 call 11^0006. Idstr IL_000b: call 11^.0010. ret /V end o£ »ethod HelloCSharp .Hello <J instance voxd tHelloVBJBrox ProCSherp HelloVB: Kellc "Hello. C<- void [itscorlib]Syetex Console Uritetinc(strin9) J Метод Add () аналогичен коду МС++. При выполнении расчетов компилятор С# не использует методы с переполнением, в соответствии с настройками по умолчанию ком- компилятора в проекте Visual Studio.NET. Вместо add. ovf применяется более быстрый ме- метод add; однако эту опцию можно изменить с помощью свойств конфигурации проекта как для С#, так и для VB.NET. Если в проекте С# установлен флаг ' Check for overflow underflow', компилятор С# будет генерировать тот же самый код, что и компилятор VB.NET. В С# можно также задать этот режим с помощью операторов checked и unchecked: /HelloCSharp::Add: int32(int32,int32) ягаи Method public hidebysig instance int32 Add(int32 vail, jj int32 val2) •• Code size 4 @x4) .maxstack 8 II_0000: ldarg.l II_0001: ldarg 2 IL_0002: odd II_0003: ret } s/ end o£ method HelloCSharp Add Так как все языки .NET генерируют код на MSIL и пользуются классами платформы .NET, то часто говорят, что между ними нет разницы по части производительности. Однако небольшая разница все же присутствует. Во-первых, языки поддерживают различные типы данных. Во-вторых, сгенерированный код MSIL также может отличаться. Один из примеров, который мы видели,- работа с числами - осуществляется по-разному. При использовании конфигурации по умолчанию VB.NET занимает более безопасную позицию, а С# выбирает более быструю версию. Кроме того, С# более гибкий. Требования CLS Мы видели CLS в действии при рассмотрении кросс-языкового наследования между МС4-*-, VB.NET и С#. До сих пор мы не уделяли внимания требованиям CLS при сборке проекта. Методы, которые мы определяли в базовых классах, можно было вызывать в производных классах. Если бы метод в качестве одного из своих аргументов принимал System.UInt32, то мы не смогли бы использовать его в VB.NET. Типы данных без знака не совместимы с CLS; язык .NET может не поддерживать такой тип данных. Общая спецификация языка точно определяет требования, делающие компонент CLS совместимым, что позволяет использовать его в других языках .NET. В СОМ при разработке компонента необходимо уделять внимание специфическим требованиям языка: Jscript имел иные требования по сравнению с VB6, требования VJ++ также отли- отличаются. В .NET этого нет. При разработке компонента, который должен использоваться в других языках, его нужно сделать CLS-совместимым. Это гарантирует, что компонент можно будет применять во всех других языках .NET. Если отметить класс как совмести- совместимый с CLS, компилятор укажет на несовместимые с CLS методы этого класса. Все языки .NET обязаны поддерживать CLS. Когда речь идет о языках .NET, следует различать инструменты-потребители и инструменты-расширители .NET. Инстру- Инструмент-потребитель лишь использует классы платформы .NET — он не способен создавать
Сборки 377 классы .NET, которые можно применять в других языках. Инструмент-потребитель мо- может использовать любой совместимый с CLS класс. Инструмент-расширитель .NET име- имеет все требования инструмента-потребителя, но вдобавок может наследовать любой совместимый с CLS класс и определять новые совместимые с CLS классы, которые могут использоваться потребителями. C++, VB.NET и С# являются инструментами-расширите- инструментами-расширителями. С помощью этих языков можно создавать CLS-совместимые классы. Cobol, кото- который доступен в .NET, на данный момент является инструментом-потребителем. В Cobol можно применять CLS-совместимые классы, но не расширять их. Атрибут CLSCompliant При помощи атрибута CLSCompliant можно пометить сборку как совместимую с CLS. Это гарантирует, что классы данной сборки можно будет использовать во всех инстру- инструментах-потребителях .NET. Компилятор выдает предупреждения в случае применения не CLS-совместимых типов данных в открытых и защищенных методах. Типы данных, которые используются в закрытой реализации, не имеют значения — при применении других языков за пределами класса мы не имеем доступа к закрытым методам. Для получения предупреждений компилятора о том, что тип не совместим с CLS для открытых членов, можно установить для сборки атрибут CLSCompliarf. Этот атрибут добавляется в файл Assemblylnfo.cs: [assembly; System.GLSCompliant(true)] Таким образом, все определенные внутри сборки типы и открытые члены являются совместимыми. При использовании несовместимого типа uint в качестве аргумента ком- компилятор выдает ошибку: error CS3001: Argument *-уре uint is r . " Е -— -ant В сборке, помеченной как совместимая, можно определять несовместимые с CLS ме- методы. Это полезно, если требуется перекрыть некоторые методы, чтобы были доступны методы как для совместимых, так и для несовместимых типов данных аргумента. Несовме- Несовместимые методы должны быть отмечены атрибутом ' SComplianc со значением false. Атрибут CLSComp-iant можно указать для типов, методов, свойств, полей и событий: (CbSCompliantKfalse) ]. void Method(uint I) { //. .. Правила CLS Что требуется для совместимости с CLS? D Все типы в прототипе метода должны быть CLS-совместимыми. * О Элементы массива должны иметь совместимые с CLS типы элементов. Индекси- Индексирование массивов также должно начинаться с 0. О Совместимый с CLS класс должен наследоваться от совместимого с CLS класса. Разумеется, System.Object является CLS-совместимым. О Хотя имена методов в CLS-совместимых классах не чувствительны к регистру, ни- никакие два имени метода не могут отличаться только регистром букв в их именах. О Перечисления должны иметь тип Intl6, Int3L или Int64. Перечисления других типов не совместимы с CLS. Все эти требования применимы только к открытым и защищенным членам. Закры- Закрытые реализации не имеют значения — там могут использоваться несовместимые с CLS типы, и от этого сборка не перестанет быть CLS-совместимой. Помимо перечисленных требований необходимо учитывать ряд других правил. Эти правила не являются строгими требованиями по CLS-совместимости, но заметно облег- облегчают жизнь. Классы платформы .NET следуют таким рекомендациям: О Классы должны именоваться с помощью существительных или фраз, состоящих из существительных; классы не должны иметь префикс С, как это обычно делается в VC++, и внутри имени не следует использовать знаки подчеркивания. D Методы необходимо именовать с помощью глаголов или фраз, состоящих из глаголов. D Свойства необходимо именовать с помощью существительных или фраз, состоящих из существительных.
378 Глава 10 О Стиль символов Pascal необходимо использовать для классов, перечислений, со- событий, методов, пространств имен и свойств. В этом стиле (например, BackColor) первая буква слова является прописной. Если имя составлено из нескольких слов, то следующие слова также должны начинаться с прописной буквы. О Стиль символов Camel должен использоваться для параметров. В случае стиля Camel первая буква является строчной. Если имя составное, то последующие слова должны начинаться с прописной буквы. 3 Интерфейсы должны иметь префикс I, классы исключений — суффикс "Exception", а обработчики событий — суффикс "EventHandler". D "Венгерский" стиль именования объектов не должен применяться. Существует спор по этому поводу, поскольку большинство разработчиков на C++ регулярно используют "венгерский" стиль именования объектов. Помните, что CLS предназ- предназначена для того, чтобы сделать компоненты доступными во многих языках. Не все программисты привыкли к "венгерскому" стилю именования объектов. Вы по- получите большее преимущество, если ваши переменные будут иметь хорошие са- самоописательные имена. Имена типов данных зависят от языка. Например, типы С# int, long и float эквивалент- эквивалентны типам VB.NET Integer, Long и Single. Если имя типа данных используется в имени метода, должны применяться универсальные имена, например Int32v Int64, Single: int Readlnt32(); long Readlnt64О; 4-,'.. float ReadSingle(); След\'я спецификациям и рекомендациям CLS, легко создавать компоненты, которые могут использоваться в других языках. При этом необязательно проверять компонент с помощью всех языков-потребителей .NET. Добавление ресурсов к сборкам Сборка может существовать в виде одного или нескольких файлов. Для облегчения заме- замены строк и картинок в прс грамме полезно разместить их в отдельном файле. Тогда пере- переводчики из других стран должны будут отредактировать лишь файлы ресурсов, и им не придется просматривать код С#. Архитектура .NET обеспечивает богатую поддержку ре- ресурсных файлов С помощью Visual Studio.XET легко внедрить в основную сборку такие ресурсы, как картинки: добавьте файл к проекту и установите свойство BuildAction файла в Embedded Resource. Создание ресурсных файлов Картинки, таблицы строк и т.п. могут быть частью ресурсных файлов. Для создания ре- ресурсного файла может использоваться либо обычный текстовый файл, либо файл . resX в формате XML. Начнем с простого текстового файла. Ресурс, который содержит таблицу строк, может быть создан с помощью обычного текстового файла. Текстовый файл назначает строки ключам. Ключ представляет собой имя, которое может использоваться в программе для получения значения. Пробелы раз- разрешены как для ключей, так и для значений. Этот пример показывает простую таблицу строк в файле strings.txt: Title = Professional C# "'*> '** ■ Chapter = Assemblies Author = Christian Nagel Publisher = WROX Press ResGen Утилита -csg< r . "x^ может использоваться для создания ресурсного файла из strings. txt. Набрав: resgen strings. txt вы полл-чите файл о . г ^ urces. Этот ресурсный файл можно добавить в сборку как внешний файл либо внедрить в ЕХЕ или DLL. Утилита resgen поддерживает также создание ресурсных файлов из XML-файлов . r_jX. Наиболее простой способ создания XML-файла заключается в использовании для этой цели самой r^sgen: resgen strings. resources ftxj-u,s.i _.з" создает ресурсный XML-файл t4riios.resX. О работе с ре- ресурсными файлами XML рассказывается ниже.
Сборки 379 Утилита resgen не поддерживает добавление картинок. В примерах SDK платформы .NET вы обнаружите ResXGen. С помощью ResXGen можно вставлять картинки в файл .resX. Добавление картинок к ресурсам может быть осуществлено также с помощью класса ResourceWriter. ResourceWriter ч Можно написать простую программу для создания ресурсных файлов. ResourceWriter представляет собой класс в пространстве имен System. Resources, который поддерживает картинки и другие типы ресурсов. В качестве примера создадим ResourceWriter-объект rw, используя конструктор Demo.resources. После создания экземпляра можно добавить в файл некоторое ко- количество ресурсов общим объемом до 2 Гбайт с помощью метода AddResource () класса ResourceWriter. Первый аргумент AddResource () указывает ключ, а второй — значение. Ресурс в виде картинки может быть добавлен с использованием экземпляра класса Image. Для того чтобы применить класс Image, необходимо указать сборку System.Drawing. Мы также открываем пространство имен System. Drawing с помощью директивы using. Далее мы создаем объект Image, открывая файл logo.gif. Нужно скопировать кар- картинку в каталог исполняемого файла или указать полный путь к картинке в аргументе ме- метода Image.FromFileO. Оператор using указывает, что ресурс изображения должен быть автоматически освобожден в конце блока using. Необходимо уделить внимание тому факту, что объект Image уничтожается до записи pecvpca. Дополнительные про- простые строковые ресурсы добавляются к объекту ResourceWritc^. Метод Closet клас- класса ResourceWriter автоматически вызывает ResourceWriter .Generate О для записи ресурса в Demo.resources: using System; using System:Resources; using System-Drawing; public class rs { public static void MainO ' { ResCureeWriter rw я .new ResourceWriter("Deiro.resources"); r using: (Image image .=' Image. FromFile( "loco .gif")) ''- ( I.' *: , rwiAddResourcet"WroxLogo", image); f , rw,MdResource{ "Title", " Professional C#"); ~: rw.AddResource ( "Chapter", "Assemblies"?; •S " rwiAddResource("Auchor", "Christian Nagel11; rw.AddResource("Publisher", "Wrox Press1); i - rw.cip.se {) ; ! .".,-} '" ' '- :)■ *■ Запуск этой небольшой программы создает ресурсный файл Demo. -resources. Ресурсы будут использоваться в приложении Windows. Использование ресурсных файлов Ресурсные файлы могут быть добавлены в сборки с помощью инструмента генерации сборок А1. ехе с опцией embed или непосредственно в Visual Studio.NET. Для демонст- демонстрации использования ресурсных файлов в Visual Studio.NET создадим приложение Windows C# и назовем его ResourceDemo:
380 Глава 10 New Project ptoject Types: Templates: [usj 1 Visual Basic Projects : di Visual C# Protects . CJ Visual C++ Projects ;■ -2J Setup and Deployment Projects ГЯ £3 Other Projects I ^ Visual Studio Solutions Windows Application ^* Web Application *~—з Class Library Web Service Windows Control library Web Control Library . A protect for creating an application with a Windows user interface j ResourceDemo Location: C:\Professional C*\Assemblies\Resources _»J f~ Add to Soluoon <* floie Solution Project wfl be created at C:\Professional C#'|«ssemble5\Re5ou-res'iResouiceDemo. «More Cancel Erowse... Help С помощью контекстного меню Solution Explorer (Add | Add Existing Item) можно доба- добавить в проект созданный ресурсный файл Demo. resources. В окне свойств этого файла измените значение ?■ ildAction с j\^-~ на Embedded Resource, в результате чего ресурс будет внедрен в выходную сборку: I Properties" | Demo.retources Fie Properties _^J D Advanced None Custom To Custom Tc Compte Content Embedded Resource Ful . Bufld Action .How the file related to the bund end , deployment processes. E§* Properties I O Dynamic Help При компиляции проекта и просмотре сборки с помощью ildasm в манифесте мож- можно увидеть . rrresource. . mresource объявляет имена ресурсов в сборке. Если . mreso- urce объявлен как p~i: lie (как в нашем примере), ресурс экспортируется из сборки и может использоваться в классах в других сборках, .mresource private означает, что ресурс не экспортируется и доступен только внутри сборки. При добавлении ресурса к сборке с помощью Visual Studio.NET он всегда будет от- открытым. Если для создания сборок применяется инструмент генерации сборки, то мож- можно использовать параметры командной строки для указания открытых и закрытых ресурсов. Параметр /embed: demo, resource s,Y команды al добавит ресурс как public, a /embed:demo.resources,N добавит ресурс как private. Если сборка была создана в Visual Studio.NET, то в дальнейшем можно изменить ви- видимость ресурсов. Если открыть сборку в ildasm с помощью меню File | Dump, будет сге- сгенерирован исходный файл MSIL. После этого код MSIL можно изменить в текстовом редакторе, например, в Блокноте. Используя текстовый редактор, можно изменить .mresource public на .mresource private. Затем с помощью ildasm можно восста- восстановить сборку из исходного кода MSIL:
Сборки 381 Г .custom instance void [mscor1ibJSys tern hash algorithm 0x00008004 ver l:0:477:2S338 с lee t ion.Assembly о | «resource public ResourceDemo.For»1.resources •«resource public ResourceDemo Demo resources module ResourceDemo.exe " HVID: {68ES720C-D646-49CB-A2A7-CE9BD2CS6AS3} .subsystem 0x00000002 file alignment S12 .corf lags 0x00000001 Image base 0x030e0000 Добавим в форму несколько текстовых полей и рисунок, перетаскивая элементы Windows Forms с панели инструментов в дизайнер. С их помощью мы будем отображать значения ресурсов. Изменим свойства Text и Name для текстовых полей и меток, как по- показано ниже. Свойство Name для PictureBox устанавливается в значение logo. Оконча- Окончательная форма, открытая в дизайнере форм, будет выглядеть примерно так: 'П : ... ■ ТЩ Resource Oemo'S; 'k: T?le ' •p] Chaple :-Л-\ AUhot a :.".:. . ■ ■■■■■' ■ ■ :-^же .. i |lextBoChaplH : !'MlBo>Authc'.. JtextBoxPuttshei Прямоугольник в левом верхнем углу представляет собой WinForms PictureBox. Для работы с этим внедренным ресурсом можно использовать класс ResourceManager из пространства имен System.Resour res. Сборк\\ в которой хранятся ресурсы, можно пе- передать в конструктор. В данном случае ресурсы внедрены в исполняемую сборку, поэтому мы передаем в качестве второго аргумента результат Ass -"tbly-GetExecutingAssembly ( i. Первый аргумент представляет собой корневое имя ресурсов. Корневое имя состоит из пространства имен и имени ресурсного файла без расширения resources. Утилита ildasm показывает это имя. Необходимо удалить расширение resources. Имя можно также получить программно с помощью метода GetKanifestResourceNames () класса System.Reflect-эп.Assembly. Используя экземпляр rm класса ResourceManager, можно получить все ресурсы, указав ключ: using" Sysiern.Reflection; using Systeiib Resources; System.Resources.ResourceManager rm; , .-■■ public FormlO // Требуется для поддержки Windows Form Designer
382 Глава 10 InitializeComponerx (); Assembly assembly = Assembly.GetExecutingAssembly(); rm = -new ResourceManager ("ResourceDemo.rJemo" logo.Image = (Image)rm.GetObject("WroxLogo"); textBoxTitle.Text = rro.GetStringC'Title") ; teXtBoxChapter.Text = rm.GetString ("Chapter"); textBoxAuthor..Text = rm.GetString ("Author"); textBoxPublisher.Text = rm.GetString ("Publisher"); assembly); В результате запуска кода получим следующее: wrox Tfle Chapter Aulhoi Pubkb |c и гд1С8 I ',sr. tt |Chi«iiao и Картинка и строковые ресурсы загружены. Но в чем же преимущество использова- использования ресурсов по сравнению с хранением этих элементов данных непосредственно в ис- исходном коде? Изменение ресурсных файлов можно поручить непрограммистам, хотя для компиляции файлов потребуется программист или по крайней мере пара коман- командных файлов. При использовании ресурсных файлов не придется искать строки в исход- исходном файле, поскольку все они хранятся в одном месте. Большое преимущество хранения строк в ресурсном файле заключается в том. что их можно чокализовать для различных языков. Для локализации могут применяться сборки-спутники. Сборки-спутники содержат только ресурсы (см ниже). Интернационализация и ресурсы Спутник NASA Climate Orbiter стоимостью S125 млрд. был потерян 23 сентября 1999 г. только из-за того, что одна из инженерных rpvnn использовала метрическую систему, а другая — британскую систему мер для ос\ ществления наиболее важных действий по вправлению аппаратом. При написании приложений, предназначенных для международного распростране- распространения, необходимо помнить о разных культурах и регионах. Культура определяет, кто вы, а регион — где вы. Культура и регион объединяются под общим называнием "место" (Locale). Примером проблем при интернационализации служит десятичный раздели- разделитель: для США десятичный разделитель представляет собой ".", а для Германии это ",". Приведем примеры разных форматов чисел: Американский английский Немецкий Швейцарский немецкий Французский 123,456,789.23 123.456.789,23 Ш'456'789.23 123 456 789,23 Примеры различных форматов дат: Американский английский Английский (Великобритания) Немецкий Французский 6/15/2001 15/06/2001 15.06.2001 15/06/2001 Friday, June 15, 2001 15 June 2001 Freitag, 15Juni 2001 vendredi 15juin 2001
Сборки 383 Культура Культура представляет собой набор сведений, основанных на языке пользователя и куль- культурных соглашениях. Класс Culturelnf о применяется для форматирования дат, времен и чисел, для сортировки строк и определения языка для текста. Пользователь выбирает культуру во время инсталляции системы и может конфигурировать ее, используя Регио- Региональные настройки в Панели управления. Имя культуры строится из двух букв, определяющих язык, и из двух букв кода стра- страны/региона, который следует именам RFC1766. Некоторые примеры имен культур пе- перечислены ниже: Имя культуры En en-GB en-US Fr fr-FR De Культура Английский Английский (Великобритания) Английский (США) Французский Французский (Франция) Немецкий Используя класс Systr .Clobaiizati - i.Culture fo, можно получить имя культу- культуры и поддерживаемые культуры, создать новую культуру с помощью строки названия ку- культуры и т.д. В частности, необходимо отметить некоторые статические свойства CultureTnfo. Различают следующие типы культур: D Cv-rentCulture — культура текущего потока. По умолчанию она устанавливается в соответствии с настройками пользователя. Культуру потока можно изменить. Она применяется для форматирования в зависимости от места, например, чисел и дат. D Currer.t'JIC .-г .re — культура, используемая для поиска ресурсов. Также зависит от текущего потока. Может применяться для получения строк или изображений из ресурсов. П Instal_edUI Culture — системная культура по умолчанию для поиска ресурсов. ■3 Invarian'Culture — нейтральная культура. Не соответствует ни одной реальной культуре, поэтому не является лингвистически корректной (ни одна из существующих культур не подходит). Если вы сталкивались с проблемой, когда дата на Active Server Page отображается по-разному в зависимости от того, кто работает в системе, то причина в этой культуре. Эта независимая от языка культура полезна для служб. Регион Регион отличается от культуры. Он не представляет предпочтений пользователя, но опре- определяет место. Швейцария является многоязычной страной, где существуют культуры de-ch, f r-ch и it-cl. для швейцарцев, использующих свои собственные версии немецко- немецкого, французского и итальянского языков. Имейте в виду, что швейцарский немецкий отли- отличается от немецкого в Германии. В то же время для Швейцарии определен только один регион СН. Культура определяет язык, а регион задает валюту и единицы измерения. Ва- Валюта, используемая в регионе, не зависит от языка. Значимым моментом является то, что швейцарец может переместиться в другую страну, где он по-прежнему будет использовать свою "домашнюю" культуру, но где. конечно, придется поменять регион. Местоположение может быть также установлено с помощью Региональных настроек в Панели управления. Класс System.Globalization.Regionlnfo предлагает методы для получения этих значений. Regionlnfo содержит статическое свойство CurrentRegion, с помощью которого можно получить текущий регион. Объект Regionlnfo можно со- создать, передав двух- или трехсимвольное имя ISO3166 в качестве строки в конструктор. Примеры этой строки: AT или AUT для Австрии, DE или DEU для Германии, FR или FRA для Франции, GB или GBR для Великобритании и US или USA для США. Пространство имен System.Globalization Пространство имен System.Globalization содержит все классы культур и регионов для поддержки различных форматов дат, чисел и даже различных календарей, например GregorianCalenda с, HebrewCalendar, JapaneseCalender и т.д. Используя эти классы, можно получить различные представления в зависимости от места. !як. 69
384 Глава 10 Числа Числовые структуры Intl6, Int32, Int64 и т.п., определенные в пространстве имен System, обладают перегруженным методом ToString (). Этот метод может использовать- использоваться для создания различных представлений числа в зависимости от места. Для структуры Int32 метод ToString () имеет четыре перегруженные версии: public string v'T.oString()/ <v- public string ToString(IFormatProvider);>' ' pubiic string, ToString(sfcring|:; » public, string ToString (siting, I Format Provider); Метод ToString () без аргументов возвращает строку без параметров форматирова- форматирования. Можно также передать строку и класс, который реализует IFormatProvider. Стро- Строка указывает формат представления. Формат может быть стандартным числовым представлением в виде строки или представлением по маске. Для стандартного форма- форматирования строки предопределены: С указывает на запись в виде валюты, D создает деся- десятичное представление, Е — научное, F — представление с фиксированной точкой, G — общее представление, N — числовое представление, а X — шестнадцатеричное представ- представление. С помощью представления по маске можно указать число цифр, разделители и секции групп, процентную запись и т.д. Маска ###,### означает два числовых блока, отделенных друг от друга разделителем групп. С помощью конструктора по умолчанию NumberFormatlnfo создается объект, не за- зависящий от культуры. Используя свойства этого класса, можно изменить все параметры форматирования, такие как положительный знак, символ процента, разделитель групп чисел, символ валюты и многое другое. Независимый от культуры объект только для чтения NumberFormatlnfo, где значения формата основаны на Culturelnfo текущего потока, возвращается из статического свойства Currentlnfo. IFormatProvider является интерфейсом, который реализуется некоторыми служеб- служебными объектами. Этот интерфейс определяет единственный метод GetFormat (), кото- который возвращает объект форматирования для указанного типа. IFormatProvider реализован в NumberFormatlnfo, DateTimeFormatlnfo и Culturelnfo. В последующих примерах используется консольный проект. Первый пример пока- показывает число в формате культуры текущего потока. Второй пример использует метод ToString () с аргументом IFormatProvider. Culturelnfo реализует IFormatProvider, поэтому создается объект Culturelnfo с французской культурой. Третий пример меня- меняет культуру текущего потока. С помощью свойства CurrentCulture экземпляра Thread культура меняется на немецкую: using System; jUSing ^System.Globalization;. using System.Threading; namespace Numbers class Classl static void Main(string;j args) '.?\ int val = 1234567890,- ■//- культура текущего потока Console;..WriteLipe,(val,TjDString(-N")).; ;' Ыу ирпользуем' ^Format Provider new, Culturelnfo (" £r-tr"*i1 ■); fit меняем культуру потока "Thread.CurrentThread.CurrentCulture = . s- v new CulturelnfoC'derde1').; ■Console.Wri teLine(val .TciString ("»"))'.;
Сборки 385 Результаты показаны ниже. Можно сравнить их с перечисленными ранее значениями для американского английского, французского и немецкого: ,234,567,890.00 ■> ;. 234 567 890,00 .234.567.890,00 ress any be у to coritinue— Дата Помимо чисел существует поддержка для дат. Структура DateTime имеет методы для пре- преобразования даты в строк)'. Открытые методы экземпляра ToLongDateString (), ToLon- gTimeStringO, ToShortDateString (), ToShort^ г лд() создают строковые представления с использованием текущей культуры. С помощью метода ToStringO можно присвоить другую культуру: public? string ToString О; public string ToString (IFoxreatProvicier) .- public .strrpmcj- ToString(str T.g) ; public-string ToStr -.gtstri-c, ~* ....aiProvi'er) ; В аргументе string метода ToString () может быть указан либо предопределен- предопределенный символ форматирования, либо пользовательская строка форматирования для преобразования даты в строку. Класс Г~' -' :~eForir>a*. f определяет возможные значе- значения. С помощью аргумента Iе :: " - можно задать культуру. Использование пе- перегруженного метода без аргумента IFormatPrcvi der означает, что будет использоваться культура текущего потока: т DateTime й - = . new Da.teTimeB001, 6, '151; /I текущая культура System.Conso ^.WriteLine(d.~ L>ngDateString()); // с использованием IFor- =- Provider System.Console-WriteLir.e d.ToStriri; T " lew Ciil^u; ._. f iir-fr"))); // с испо ьзованием хучьтуры пот :ка ( Culturelnfo ci = Thread.Currenf'hread.CurrentCulture; < * .CcnsolelWritfcLinetci. ToString () * "-. " + C. ToString ("D") ) ; ,4ci = new Cultutelnfo("de-de"); Thread,CurrentThread.Curr=ntCulture = ci; ,Console.WriteLine(ci.ToScr ,ng() + " : " - d.TcStrmg( "D")) ; В выходных данных нашей программы демонстрируются Тс LongDateString () с теку- текущей культурой потока, французская версия, где экземпляр '"- tturelnfo передается в ме- метод ToString 1, и немецкая версия, где используется свойство CurrentCulture потока, которое меняет значение на de-de: C:\PrefessJaW»! $&\ ..! •**.', Friday, June 15, 2001 - uendredi 15 juin 2001 en-US: Friday» June IS, 2001 de-DE: Freitatfji' IS. Juni 2001 Press an</ key to '.continue^
386 Глава 10 Ресурсы для локализации Помимо того, что в зависимости от места требуются различные системы форматирова- форматирования и измерения, строки должны содержать разный текст. Кроме того, в зависимости от места могут меняться некоторые изображения. Рассмотрим применение сборок-спутников. Сборки-спутники Сборки-спутники используются в приложениях для поддержки строк, зависящих от язы- языка. Сборки-спутники содержат только ресурсы, но не код: таблицы строк, изображения, видео и т.д. Разумеется, должен быть установлен атрибут AssemblyCulture. Сборка-спут- Сборка-спутник для культуры de должна размещаться в подкаталоге de программы. Сборка-спутник, поддерживающая австрийскую версию немецкого, помещается в подкаталог de-at. de и de- at имеют лишь небольшие отличия. В архитектуре сборок-спутников .NET не требуется хранить все ресурсы в сборке de-at, достаточно хранить только отличия. Необходимо создать ресурсный файл с отличающимися строками. Если у пользователя в системе выбран регион de-at и не удается найти ресурсы, используется родительская сборка. Родительской сборкой для de-at является de. Если ресурс и там не найден, при- применяются ресурсы нейтральной сборки. Нейтральная сборка представляет собой сбор- сборку, для которой культура не задана. Именно по этой причине не следует устанавливать культуру для основной сборки. Пространство имен System.Resources Пространство имен System.Р= ->ит ■ _.= предлагает следующие классы для работы с ре- о-рсами: D ResourceManager может использоваться для получения ресурсов текущей культуры из сборок-спутников. С помощью ResourceKanager можно получить ResourceSet для конкретной культуры. О ResourceSet представляет ресурсы для конкретной культуры. При создании экзем- экземпляра ResourceSet он перечисляет все ресурсы с помощью класса, реализующего I rceReader, и сохраняет результаты в Hashtable. О Интерфейс IResourceReader используется в ResourceSet для перечисления ресурсов. Этот интерфейс реализует класс ResourceReader. а Класс Resr jeWritei" применяется для создания ресурсного файла. ResourceWriter реализует интерфейс IResourceKr] ter. О Дополнительные классы: "'esXResour-eSet, ResXResourceReader и ResXResource Mriter. Эти файлы аналогичны ResourceSet, ResourceReader и ResourceWriter, но создают ресурсный XML-файл . resX вместо двоичного файла. Для того чтобы создать ссылку на ресурс, но не включать его в XML-файл, можно использовать ResXFileRef. Пример локализации с использованием Visual Studio.NET Для демонстрации локализации с использованием Visual Studio.NET создадим простое приложение WinForms. Оно не использует сложные Windows Forms и не выполняет ни- никаких функций, поскольку служит лишь для локализации. В исходном коде укажем про- пространство имен Wrox. DroCSharp и имя класса BookOfTheDayForm. Пространство имен устанавливается не только для исходного файла BookOfTheDayForm. cs, но и для проек- проекта, в результате чего все сгенерированные ресурсные файлы будут также располагаться в этом пространстве имен. Это делается в Common Properties меню Project | Properties. Для того чтобы показать особенности локализации, в программе присутствуют изоб- изображение, текст, дата и число. Изображение тоже локализуется, так что французская его версия будет иной. Форма создается с помощью Windows Forms Designer:
Сборки 387 wrox t!( » ol Ihr Dap joate Fuooo com - ■ Значения свойств Name и числены в таблице: .-ь. используемых для элементов Windows Forms, пере- пере! Name Text JRookO_ '■ ~ _abell" x textB xl. _€ text = "> iitle I textE 3xItemsSo~_ Book of t''p rlay Books sold Dite Professional C# 10000 Помимо этой формы необходимо выводит ь дпало овое окно с приветствием, вид ко- которого зависит от текущего времени. Это продемонстрирует нам. что локализация для динамически создаваемых окон осуществляется по-другому. В методе WelcomeMessa- ge() мы отображаем диалоговое окно, использ\я MessageBox. Show (). Метод Welnrae- Message () вызывается в конструкторе класса формы BookOfTheDayForn до вызова ~] LtializeComp * .: i ). Код метода " leMessa" :: public void WelcomeMessage( 'JateTime f-me = DateTi^e.Xow; string Tsssage; if (time.Hour <= 12) message = "Good "orning"; else _: (time.Hour --= 19) message = "Good Afternoor. ; .se I message = "Good 3vening ; MessageBcx.Show(message + nThis is a localization sample."): Число и дата в форме должны быть установлены с использованием параметров фор- формат ировапия. Это нельзя сделать при помощи свойств, поэтому для установки значений добавим новый метот SetDateAr.aKumber (). В реальном приложении эти значения мо- могут быть получены от Web Service или из базы данных, но в данном примере мы это не par натриваем — нас интересует только локализация. Этот метод мы вызываем до вызо- вызова cidi :eC . Дата форматируется с помощью параметра D (отображение длш пого имени даты). Число выводится по маске ###,###,###, где '#' представляет цифру, а ',' разделитель группы: uLlic vo1 Л SetDateAndNumber() "•■ teTime date = DateTime.Today; .p-^tBoxDate.Text = dace.ToStringC'D") ; иг irensSold = 327444; :ел Qox: t pjhsSo] J.Text ; itemsSold.ToString( "###, ###,###" ) ;
388 Глава 10 Самое интересное начинается, когда в дизайнере Windows Forms мы устанавливаем свойство Localizable формы в true. В результате для диалогового окна создается ре- ресурсный файл XML, который хранит все ресурсные строки, свойства (включая положе- положение элементов WinForms), внедренные изображения и т.д. Кроме того, меняется реализация метода initializeComponent (): создается экземпляр класса System.Reso- System.Resources. ResourceManager. Для получения значений и позиций текстовых полей и изоб- изображений применяется метод GetObjectO вместо прямого указания этих значений в коде. Метод GetObject () использует свойство CurentUICulture текущего потока для поиска корректных ресурсов. До установки в true свойства Localizable часть InitializeComponent (), в которой задаются все свойства textBoxTitle, имеет вид: "private voird InitializeComponeHtO { •;„-. ■'■ ; ■ *..* ■ ■'.. *?*" th'isГ/textBoxDate = new System.Windows.Forms.TextBoxO ; this.textBoxTitle.Location = Iv'. . ifS, , new .System,Drawing.Poxnt-C2, 136); !'*r ~j-i- ' this,, texj;B9xT.i:tie..Name = "tjextBoxTitle"; * this.textBojfTitle.Size = new System.Drawing.3iae{432, 20)>; this. textBoxTitle. Tablndex = 3; -. - * ■„ this.textBoxTitle.Text = "Professional Cff"; 'V Когда свойство Localizable устанавливается в true, код для InitializeComponent () автоматически изменяется и принимает вид: private ,void- InitializeComponent (j { >" System.Resources.ResourceManager resources = \- . • new Systen..Resources.ResourceManager ( ' t'^i-''■ typeof{BookOfTheDayForm)); , r 1 <!'" *b this.tex'tBdxDate =< Hew System. Windows. Forms. TextBoxU,- ; .'-it-, •tliiS'.itextBoxTitle.AccessibleDescription = ((string) <res^urces.GetObject:("textBoxTitle.AccessibleDescription") )) ;• this:textBdxTitle.AccessibleKame = ((string). (resources,GetObject{"textBoxTitle.AccessibleName"))); this. textBoxJitle. Anchor' = (tSystem.Windows.Forms.AnchorStyles) • 1 -\',\ ■-. tresourqeSiGetObject ("textBoxTitle.Anchor"))); •' , -'^.' ■' this .textBoxTitle. AutoSize = ((bopl) «■; ' "^'"(resources,GetObject("textBoxTitle.AutoSize"M) ; this .'textBoxTitle.Backgroundlmage = ((System.Drawing:. Image) (respurces.GetObject("textBoxTitle.Backgroundlmage"))); this.textBoxTitle.Cursor = ((System.Windows.Forms.Cursor) (resources.GetObj ect("textBoxTitle.Cursor"))); this.textBoxTitle.Dock = ((System.Windows.Forms.DockStyle) (resources.GetObject("textBoxTitle.Dock"))); this.textBoxTitle.Enabled = {(bool) {.■resources.Getobject("textBoxTitie.Enabled"))); „; *-t this.textBoxTitle,Pont = ((System.Drawing.Font) ч~ . ' .. .(resources. GetObject ("textBOxTitle^.Font"))); ,' ... thie.textBoxTi.tle.jmel^cde •= ((System.Windows.Forms.IjneModeh . (reecurces.GetObJecfcX"textBoxTitle,imeMode"))); -* this.textBoxTitlerliocaition = ((System,Drawing.Point) * (resources. Getobject( "textBoxTi'tle.tiocation").)-I; ■ this/textBoxTitle.Maxfifength = ((irit) . " i Resources. GetObj ect (" textBoxTitle. MascLength" J)) -r • -i this.textBoxTitle.Name ,= "textBoxTitle",- f this,6'extBqxTitle.PasswordChar ,=- { (cliar) ;*■:. \J (resources. GetObj ect ("textBoxTitle. PasswordChal?") },}■;. * ... \ .. thi.s.textBoxT.i'tle.RightToLeft" = ('(System,Windows.Forms,RightfoLeft) .- ...«■►»'■' (resources. GetObject4"te,xfcBpxTi tie .RightToLeft"),,)); % ■ ^
Сборки 389 ,; _," this.textBoxTitle.ScrollBars •= ((System-Windows.Forms;ScrollBars> ,..'[■ (resources;.GetObjec t (" textBoxTit.le.ScrpHBars")} )'; t'.,,". w. this.textBoxTi tie.Size = f (System-Drawing. Size)" ■ ■ ' -' (resources.GetObjectСtextBoxTitle, Size")},);; .; ••; this. textBoxTi'tle-.Tablndex = <(int) ' ' ' ^ '• (resources.GetObject ("textBoxTitle.Tablndex"),).);; - .' ?P t his. textBoxTitle '..Text, = resources. Getobj ect (" textBoxTitle. Text",) t. J, '"£**-' this.textBoxTitlfe.TextSlign = * ' ■- , *■' л ((System «Windows.Forms.НогizbntalAlignment) (resources.GetObjectCtextBoxfitle.TextAlign"))); : .this.textBoxTitle.Visible = ((bool) У:- ; i; (resources.Get0b3ect'("textBoxTiele.Visible") )); :■-.-'■■ this.textBoxgjitie:Wordwrap = ((booi) «, -.^( , * •^•4i.?r it , (resovsrces.GetObjectJ"textBoxTitle.Wordwrap*1 ).)); Как это работает? Где менеджер ресурсов берет данные? Одновременно с тем, как свойст- свойство Localizable устанавливается в true, генерируется ресурсный файл BookOfTheDay. resX. В этом файле сначала представлена схема XML-pecypca, а затем идут все элементы формы: тип, текст, положение, индекс табуляции и т.д. Следующий пример показывает, что Name для textBoxTitle имеет значение text- textBoxTitle, Туре является System.WinForms.TextBox, Parent представляет собой this, что означает, что это сама форма, Location имеет тип System.Drawing. Point со значе- значением 32, 136 и т.д. Почему местоположения и размеры также хранятся в XML-файле? Переведенные строки будут иметь совершенно другие размеры и могут не умещаться в первоначальных позициях. Когда позиции и размеры хранятся вместе в ресурсном файле, то все, что не- необходимо для локализации, содержится в этих файлах, отдельно от кода С#: ^ _, <data name=-"'textBoxTitlei-Lbeation"" type—"System.pawing.Point, J^P'' :;•'■' ■ •■ ;■ ;Systeni«brawing"> "'f1 ? <value>32, 136</valuei- ; , «. T , </data> _ _• ,'??;' -A\ , t ■. '<■ <data ;n$me="textBoxTi,tle.:MaxLength" type="System.Int32;,^ft?iscorlib'l> *• , <valde>32767</vaiue> ,., - ?*v ■'■-.?'■}' </аа^а> ^•^;l, - ■ f <data iaame="textBoxTitle.RightToLeft" -'=;»..*.•,,■.„' - type= «^System. Windows. Forms. RightTol4f.t, System.Windows.Forms"> <va3.ue^Inherit<yyalue> .. ., ■ 4 JiSme="textBoxTitievScrollBars" '■'.'';" "; •:,-., type="System.Windowe,i'orms.ScroilBars, ^y stem*. Windows. Forms "> \'-Ч-" <valu'e>None</values- " . \ t -^j£$S!ffi?<. J.;U</data>. ■!,'.- _ ., ."■<"«>4'-■■" :- | 'kdata name-"textBoxTitie-iSize" type="System/Drawing.Size# , -» ■ System;'0rawing"> - " . 20</va'lue>- ч . . ■ - ; -: ' " '■' " type="System.Int32, mscorlib"> %value>3</varue> ■ aata ' % ' * <data ■;name="textBoxTi£le.Text"> ; ';<yalue>Professoinal C#</value> h У i .. • . - .jiame="texcBQxT£tle.Visible" type='System.Booiean; тасогИЬ"> - :<valwe>True</valiie> i <Vdata> :". - . <data name="textBox1liule.wordwrap"- type'^S^stem^Bobteani iniscorlib"> : ','. "<vatuei-True</value> ^ • <data hame="&gt,«£gt; textBoxTitle.Name"> ; ;*5yalui|>textBoxTitle</value> ,■'•... - *,>.■■■ </da£a>> _; ".i .•■.',' .. ' -1" ' hi <aata,: name="&gt;&gt,-textBoxTitle.Typ6"> '':. ' r " .' u*,-, <vaJufe>Systenl.Windows»Forms.TextBox, . SyetemiWuidowSiF,9rws<7vaIue> ata name="&gtr&gt;textSoxT;itle,Parent"> >•'. :O'/-' : ic/(Blta> name="&gt;&gt:;textBpxTitle.ZOrder."> ч *'■>"., !-.-i--i! <Vji.lue:>5</valu.e> . ii "•• • v .",■»*'■! v'"i;'VvV"*' '•! '
390 Глава 10 Для изменения отдельных значений ресурсов не требуется напрямую работать с XML-кодом. Ресурсы можно изменить в дизайнере Visual Studio. Если меняются свойст- свойство Language формы и свойства некоторых элементов формы, то для указанного языка создается новый ресурсный файл. Немецкая версия формы формируется установкой свойства Language в German, а французская версия — установкой Language в French. Для каждого языка создается ресурсный файл с измененными свойствами BookOfTheDay- Form.de.resx и BookOfTheDayForm. fr.resx. Приведем изменения, необходимые для немецкой версии формы: Немецкое имя Значение $this.Text (заголовок формы) labelltemsSold.Text labelBookOfTheDay.Text Buch des Tages Bucher verkauft: Buch des Tages Для французской версии помимо прочего меняется изображение, поскольку у Wrox Press есть офис во Франции с локализованным логотипом: Французское имя Значение $ this. Text (заголовок формы) labelltemsSold.Text labelBookOfTheDay.Text Le livre du jour Des livres vendus Le livre du jour В процессе компиляции для каждого языка создается сборка-спутник. В подкаталоге debug (или release, в зависимости от активной конфигурации) создаются языковые подкаталоги, например de и fr. В этом подкаталоге сохраняется файл BookOfThe- Day.resources.dll. Эти файлы представляют собой сборки-спутники, включающие в себя только локализованные ресурсы. Открыв сборку с помощью ildasm, мы увидим ма- манифест с внедренными ресурсами и определенным местом. Сборка содержит в атрибу- атрибутах место de, поэтому она располагается в подкаталоге de. В .mresource можно посмотреть имя ресурса: оно имеет префикс Wrox. ProfessionalCSharp, за которым следуют имя класса BookOfTheDayForm и код языка de: .hash algorithm 0x00008004 ver 0:00 0 locale - F4 00 65 00 00 00 ) // d.e. manifestres public Brox.ProfessionalCSharp BookOfTheDayForm de resources > .module LocalizationDemo.resources mcl /v HVID: {D42B7O2F-3E84-4D84-BS0D-D73E176901F3} LlI Программное изменение культуры После перевода ресурсов и создания сборок-спутников мы получим корректный перевод для сконфигурированной культуры пользователя. Приветственное сообщение пока еще не переведено. Это будет сделано по-другому. Помимо значений системной конфигурации, должна обеспечиваться возможность пе- передачи приложению кода языка в виде параметра командной строки в целях тестирования. Метод Main () и конструктор BookOfTheDayForm изменяются для поддержки аргументов командной строки. В методе Main мы передаем строку культуры в конструктор BookOfThe- BookOfTheDayForm. В конструкторе создается экземпляр Culturelnfo для передачи его в свойства CurrentCulture и CurrentUlCulture текущего потока. Напомним, что CurrentCulture используется для форматирования, a CurrentUlCulture — для загрузки ресурсов.
Сборки 391 Для того чтобы использовать класс Culturelnfo без указания пространства имен, необходимо открыть пространство имен system.Globalization. Для класса Thread нужно указать пространство имен System.Threading: [STAThread] public static void Main(string[J args) .; ' - String culture = '"".; ' "*'V~ '''I*. '~"'kc ' ,,i£ "{args,.Length ==* 1) vV*.* ' -"*i-'"" "'-^'"^- culture s args [01;. .; "Wfe • ".^'' Application.Run(nev? BookOfTheD^'F6rm{cultur.eJ); -*:A., public BookOfTheDayForm(string culture) 'if (culture, i- "")- ■'-"■' b4;.i,.t ^ Ctilturelnfo .cultujjelnfo = new Cuiturelnf о (culture) ; II установка культуры для форматирования Thread.CurrentThfead.CiirrentCulture = 'culturelnfo; i // установка культуры для . ресурсов <г Thread.CurrentThread.CurrehtUICulture = Culturelnfo; ,: WelcomeMessage(); // Требуется для поддержки Windows Form Designer InitializeComponent(); SetDateAndNumber ); Теперь можно запустить приложение с использованием параметров командной стро- строки. Применяются параметры форматирования и ресурсы, созданные в Windows Forms Designer. На приведенных рисунках показано приложение, запущенное для культур f r и de: wroxfrance II Ы> вины [Piof«M>ulCa Otttxnvenlui |И7*М wroDC.com Использование двоичных ресурсных файлов Однако еще не решена проблема с приветственным окном сообшения. Эти строки нахо- находятся внутри программы. Поскольку они не являются свойствами или элементами внут- внутри формы, дизайнер форм не извлекает ресурсы XML так, как он делает это для свойств внутри метода InitializeComponent О при изменении свойства Localizable формы. Нам придется самим создавать ресурсы. Английский текст и его переводы на немецкий и французский представлены в таблице: Английский Немецкий Французский Good morning Good Afternoon Guten Morgen Guten Tag Bonjour Bonjour
392 Глава 10 Английский Good Evening This is localization sample. Немецкий Guten Abend Das ist ein Beispiel mit Lokalisierung. Французский Bonsoir C'est iin exemple avec la localisation. Создадим простой текстовый файл (welcome. txt), содержащий версию по умолчанию, а также немецкую и французскую версии: Welcome.txt: » 'GoodMorning^Good Morning -„. GoodAftemoon=Good Afternoon -. GoodEvening=Good Evening Descripti,on=This is localization sample. „ ?,- Welcome.de.txt: .G6,odMorning=Guten Morften ~-- '-■ GoodAfternoo^Guten Тад 1 Gq$d&vening=Guten Abend Deseription=Das ist ein Beispiel mit Lokalisierung. * ■ -. Welcome.fr.txt: ;; * GcodMorning=Bonjour , ^ 4 *' G6odAfternoon=Bonjour "■' GooaEveniiig=Bonsoir Descrfiptiofi=C'est un exenvple avec la localisation. ,. ., При помощи resgen можно создать двоичные ресурсные файлы Welcome. resources, Welcome.de.resources и Welcome.fr.resources. Команда resgen welcome.de.txt создаст Welcome.de.resources. Эти файлы могут быть добавлены к решению с помо- помощью меню Add I Add Existing Item в Solution Explorer. Для всех этих ресурсных файлов свойство BuildActior. необходимо изменить с None на Embedded Resource; в против- противном случае сборки-спутники не будут созданы. Имя ресурсов может быть, как обычно, найдено с помощью ildasm. Ресурсы из файла Welcome.de.resources называются Wrox.ProfessionalCSharp.Welcome.de — имя пространства имен, за которым следует имя файла. Вместо использования двоичных ресурсных файлов в проекты Visual Studio.NET можно добавлять ресурсные XML-файлы (см. ниже). Использование ресурсных XML-файлов С помощью команды resgen создадим из текстовых файлов XML-ресурсы: О resgen welcome.txt welcome.resx D resgen welcome.de.txt welcome.de.resx О resgen welcome.fr.txt welcome.fr.resx Сгенерированные ресурсные XML-файлы загружаются в проект с помощью Add | Add Existing Item в Solution Explorer. Для файлов .resx не требуется менять BuildAction, так как по умолчанию она уже имеет значение Embedded Resource. При компиляции проекта ресурсы помещаются в сборки-спутники. Теперь в сборке-спутнике присутствуют два пункта .mresource: ресурс Wrox.Pro- fessionalCSharp.BookOfTheDayForm.de является ресурсом, который был первонача- первоначально создан в Windows Forms Designer, a Wrox. ProfessionalCSharp.Welcome.de — это ресурс, полученный из нового ресурсного файла Welcome.de.resx: 'MANIFEST |.assembly ГоскОсIheDay.resources bash algorithm 0x00008004 ver 10 483:31652 locale - F4 00 65 00 00 00 ) .«resource public BookOfTheDay.velcome.de.resources .module BookOfTheDay.resources.dll " HVID: {C73F1B4D-515C-47AF-9A6D-9ABD46A01384} subsystem 0x00000003 .file alignment 512 corflags 0x00000001| '/ Image base ОхОЗОеОООО
Сборки 393 Для использования ресурсов необходимо также изменить исходный код Weicome- Message (). Для того чтобы получить из текущей сборки ресурс по имени Wrox. Pro- fessionalCSharp.Welcome, создается экземпляр ResourceManager. Этот менеджер ресурсов позволяет нам получить ресурсы, созданные ранее в ресурсных файлах с по- помощью методов GetString (). Для класса ResourceManager необходимо указать про- пространство имен System.Resources, класс Assembly находится в пространстве имен System.Reflection: public void WelcomeMessage() { ResourceManager resource = ' . new ResourceManager("Wjrox.Ero£^ssionalCSharp>Welcome", <■' w.'• ,.- Assembly.GetExecutingAssembly i.H; DateTime time = DateTime.now; string message; if (time.Hour <= 12) { * '.- message = resource GetStringJ^GoodMorning").; } else if (time.Hour <= 19) { message = resource.GetString( "GoodAfternoon11) ; } else { message = resource.GetString( "G'oodEveriing"); I. -HessageBox. Show (message "\n\n" + resource.GetString("Description")); При запуске программы для английского, немецкого и французского языка получаем следующие окна сообщений: Good Afternoon This is a localization sample. Guten Tag Das ist en Behspiel rnit lokalisierung. ! dTTT| Bonjour Cest in example avec la localisation. Изменение ресурсов для подъязыков Для французской и немецкой версий мы включили все ресурсы в сборки-спутники. Не требуется, чтобы при этом менялись все значения. В сборке-спутнике можно хранить то- только изменяемые значения, в то время как остальные значения могут находиться в роди- родительской сборке. Например, для de-at (Австрия) мы можем изменить значение ресурса GoodAf ternoon на Gru? Gott, но другие значения должны остаться прежними. Во время исполнения при поиске ресурса GoodMorning в случае его отсутствия в сборке de-at бу- будет осуществлен его поиск в родительской сборке. Для de-at родительской сборкой яв- является de. В случае если сборка de также не содержит этого ресурса, поиск будет производиться в родительской сборке для de. Это нейтральная сборка, не имеющая кода культуры. Помните: код культуры основной сборки должен быть пустым! Глобальный кэш сборок Глобальный кэш сборок представляет собой кэш для сборок. Большинство разделяемых сборок устанавливаются в этот кэш, но здесь могут содержаться и некоторые частные сборки. Если частная сборка компилируется в родной код с помощью генератора родно- родного кода, то откомпилированный код также помещается в этот кэш. Сборки хранятся в главном кэше и в том случае, когда компоненты .NET внутри HTML-страниц загружаются клиентом.
394 Глава 10 Генератор родного кода При помощи генератора родного кода Nger. exe можно откомпилировать код IL в род- родной код во время установки. Таким образом, программа может работать быстрее, поско- поскольку не будет требоваться компиляция во время исполнения. Утилита Kgou устанавливает родной код в кэш родного кода, который является частью глобального кэша сборок. Используя ngen -.vassembly, можно откомпилировать код MSIL в родной код и уста- установить его в кэш родного кода. Если требуется разместить сборку в кэше родного кода, это действие необходимо выполнить в программе установки. После компиляции сборки в родной код нельзя удалить оригинальную сборку на MSIL, поскольку по-прежнему требуются содержащиеся в ней метаданные. Кроме того, в случае изменений в политике безопасности системы родной код будет перекомпилирован. Команда r.jjen с опцией /show отобразит все сборки, находящиеся в кэше родного кода. Если к опции /show добавить имя сборки, то мы получим информацию обо всех установленных версиях этой сборки: :!4WINNT4Hic»>osoft.NEiVFrameuok-Vwl .Й.27В8>п9ей •show System-Windows.Forns n - CLR Native Inage Generator -■ Uersion 1.0.2728.0 Copyright <C> Microsoft Corp. 2001. Й11 fights reserved. ■•yoten- Windows. Forns. Uersion=1.0.2411.Й. Culture^neutral, PublicKe«»Toker»=b??ai г61934В89 ' [O:\WINNT\Microsoft. NEINFraneuorkVul-0.2728> Средство просмотра глобального кэша сборок Глобальный кэш сборок можно просмотреть с помощью файла shtusion.а] ., который является расширением ' 'олочки W-ndows, предназначенным для просмотра и управле- управления содержимым кэша. Расширение с'олочки Windows представляет собой COM DLL, которая интегрируется в Пр водник Windows. Необходимо запустить Проводник и войти в каталог < л-i..r _ Если расширение оболочки не работает на вашей машине, то COM DLL можно зарегистрировать вручную, используя г^ 2 shf '.on.d? . Если вам требуется созда/ ь польза тсл\ Кие расширение оболочки, то Wrox предлагает замечательную книгу п этом вопросу Visual С- * Windows Shell Programming а тюра Dir.o Esposito. На приведенном ниже риотже показан средство просмотра кэша сборок: и Ш СЛидачПмчянЫу ,■ £te 6* №« Favorites Тех* Help f - -* . ij ^search -jj Folders '.£ Adaess |_1| C:\WINNTassembly Global Assembly Name | r-e ■JblEHoit sUBymWrappe J&№oosoft.J3cript sSbl4crosoft.3Sccipt ;ЙМ<сто5оп .visuaieasic 5S&MKr(»oft.Vi5ualB«sK J^M«roioft.VisjiieasK.Comp4t.b«y ^ui^'Cr osorc - vi&uaKasK. coff^at iDiKy. l?<k4 iUMi«050ft.VisjalC iftMttiosoft.ViiualC i^Mitro«jft.Visual5tudio Fre:* iUMiaosoft.VpsualStudio.CSPrc set :<&MKrosoft.Visual3tiKl:o.vC iijMcroioft.ViiualStudio.VCCodeModel JiilMicrojoft.VisuaBtudio.vCPtoiect ^JMicrosoft.VBualStudio.VCPtoiectEriginc 4i if > Version 1.0.MI1.0 1.0-0.0 l.O.Z-lll.l '.0 9188 7.0 9174 T 09185 170 .?1Ь8 0.9Ic^.u 7.0.9189.O 7.0.9171.8 1.0.2411.0 1.0.0.0 AC. 'И88.1 и l.U.i. l.ljCu 7 П О1АЙ П z. Cutute f PublicKe-j I_ _n bO3l'5f~ ,lc50a3a ЬС'btflleE iba ЬО S'" к ;а3а bj3f' ^7fll ,. 43a Ь„3'5.'Л11с50аЗа l>j3f5f;.'..cM0a3a b.'r-"'"' . d50a3a 1 ""T5f~ ' ldbOaia ЬО: '$f7md5Oa о , J3fSf7fUdS0« e J3fSf7flldS0i3-i bO3teffIl'4O43a ЫХ.^7(ЦA5йаЗ« DUjrEPfllc^uaSa л if5t7fll<K0a3a bOii «■'ИЛ зЗи 1 d | J [ Zi
Сборки 395 Assembly Viewer показывает глобальное имя сборки, тип, версию, культуру и маркер от- открытого ключа. Поле Туре позволяет выяснить, была ли установлена сборка с помощью генератора родного кода. Используя контекстное меню, можно удалить сборку или про- просмотреть ее свойства: System.Drawing-Design Properties QP j Щдщ |Ve.KXt J Name Reference! UHModfed »■« г оз/ялоот 18461Б Моле 1 02111 0 H.MCAWIMoxANEl/FnmM Canal [ Файлы и каталоги кэша сборок можно посмотреть с помощью командной строки. В каталоге c:\winnt\assembly находятся подкаталоги GAC, с wnloaded и Nativelma- ges_vl. 0.2728. Подкаталог downloaded содержит сборки, которые получены с web-сер- web-сервера или из общего каталога. -; С — это каталог для совместных сборок, а Nativelmages_vl. 0.2728 содержит сборки, откомпилированные в родной код. Углу- Углубившись в структуру каталогов, вы увидите имена каталогов, аналогичные именам сбо- сборок, а далее — каталоги с версией и сами сборки. Таким образом, могут быть установлены различные версии одной и той же сборки. Утилита глобального кэша сборок (gacutil.exe) Средство просмотра сборок может быть запущено из Windows Explorer, однако его не- невозможно использовать из кода сценария, да cut- . х- представляет собой утилиту для установки, деинсталляции сборок и вывода списка сборок с помощью командной строки. Ее можно применять в коде сценария в целях администрирования. Приведем некоторые из опций gacutil: □ gacutil '1 выводит список всех сборок в кэше сборок. □ gacutil i mydll устанавливает совместную сборку mydll в кэш сборок. О gacutil /u mydll деинсталлирует сборку mydl . О gacutil /upre mydll удаляет сборку из кэша родного кода. Создание совместных сборок Сборки могут быть изолированы для применения одним приложением — это опция по умолчанию. При использовании частных сборок не нужно уделять внимания требованиям, которые важны для совместных сборок. Совместная сборка должна иметь строгое имя, уникально идентифицирующее сборку. Строгое имя решает проблему, связанную с тем, что совместная сборка обязана иметь универсальное уникальное имя Строгое имя состоит из следующих элементов: о Само имя сборки. О Номер версии. При помощи номера версии можно использовать различные вер- версии одной сборки одновременно. Различные версии способны работать бок о бок: они могут быть_ одновременно загружены в один процесс. О Открытый ключ. Гарантирует, что строгое имя является уникальным и что ука- указанная сборка не может быть замещена из другого источника.
396 Глава 10 О Культура. Культуры полезны также для частных сборок (см. выше). Рассмотрим эти элементы более подробно. Имена совместных сборок Имя разделяемой сборки должно быть уникальным. Кроме того, необходимо обеспечить защиту имени. Никто другой не должен иметь возможности создать сборку с таким же именем. Технология СОМ решила только первую проблему, воспользовавшись глобальным уникальным идентификатором (GUID). Однако по-прежнему остается вторая проблема, поскольку злоумышленник может украсть GUID и создать другой объект с тем же самым идентификатором. Обе проблемы решаются с помощью строгих имен сборок .NET. Строгое (разделяемое) имя представляет собой простое текстовое имя, с которым связа- связаны открытый ключ и цифровая подпись. Не требуется создавать новый открытый ключ для каждой новой сборки, достаточно иметь один такой ключ на всю компанию, кото- который будет уникально идентифицировать сборки вашей компании. Этот ключ нельзя использовать в качестве доверительного ключа. Сборки могут содер- содержать подписи Authenticode в качестве надежных ключей. Ключ для подписи Authenticode может отличаться от ключа, используемого для строгого имени (см. главу 25). При разработке может применяться другой открытый ключ, который позже можно будет заменить реальным ключом. Чтобы уникально идентифицировать сборки вашей компании, для именования классов должна применяться полезная иерархия пространств имен. Приведем простой пример организации пространств имен. Wrox Press может использовать основное про- пространство имен Wrox для своих классов и пространств имен. В иерархии ниже про- пространства имен должны быть организованы таким образом, чтобы все классы были уникальными. Классы для книги "С# для профессионалов" могут размещаться в про- пространстве имен Wrox. ProCSha , поэтому классы, созданные для другой книги, будут отличаться, даже если они будут иметь одинаковые имена; вспомогательные классы, используемые в нескольких книгах, размещаются в пространстве имен Wrox. Utilities. Имя компании не обязано быть уникальным, поэтому для создания строгого имени должно использоваться что-то еще. Для этого применяется открытый ключ. В основе строгих имен лежит принцип открытого закрытого ключа. Поэтому, не имея доступа к вашему закрытому ключу, никто не сможет создать сборку с деструктивными целями, которая может быть случайно вызвана клиентом. Криптография с открытым ключом Если вы уже знакомы с криптографией с открытым ключом, можете пропустить этот раз- раздел. Безопасность .ХЕТ обсуждается также в главе 25. Для остальных мы коротко расскажем о ключах. Что касается шифрования, то следует различать симметричное шифрование и шифрование с открытым/закрытым ключом В случае симметричного ключа один и тот же ключ может использоваться как для шифрования, так и для дешифровки, но в случае открытого/закрытого ключа дело об- обстоит иначе. Если зашифровать какую-либо информацию с помощью открытого ключа, то она может быть расшифрована только с помощью закрытого ключа, и наоборот — если зашифровать данные закрытым ключом, то расшифровать их можно только с помощью открытого ключа. Открытые и закрытые ключи всегда создаются парами. Открытый ключ может быть доступен всем, и его можно даже разместить на web-сайте, но закрытый ключ необходимо хранить в тайне. Рассмотрим примеры, в которых используются открытые и закрытые ключи. Если Сара посылает почтовое сообщение Юлиану и хочет, чтобы никто, кроме Юли- Юлиана, не смог прочитать ее письмо, то она использует открытый ключ Юлиана для шиф- шифровки сообщения. Юлиан получает почтовое сообщение и расшифровывает его с помощью своего секретного ключа. Это гарантирует, что никто, кроме Юлиана, не сможет прочесть сообщение Сары. Остается еще одна проблема: Юлиан не может быть уверен в том, что почтовое сооб- сообщение пришло именно от Сары. Кто угодно может использовать открытый ключ Юлиа- Юлиана для шифровки предназначенного для него сообщения. Мы можем расширить этот подход. Вернемся к отправлению почты Сарой Юлиану. Она добавляет к сообщению свою подпись и шифрует ее с помощью своего собственного закрытого ключа. Затем она шифрует сообщение с использованием открытого ключа Юлиана. В результате,
Сборки 397 во-первых, гарантируется, что никто, кроме Юлиана, не сможет прочитать файл. После того как Юлиан расшифрует сообщение, он обнаружит зашифрованную подпись. Под- Подпись может быть расшифрована с помощью открытого ключа Сары. Юлиан может полу- получить доступ к открытому ключу Сары, поскольку он является открытым. После расшифровки подписи Юлиан будет уверен в том, что письмо пришло от Сары. Теперь рассмотрим, как принцип открытого/закрытого ключа используется для сборок. Целостность при использовании строгих имен При создании совместного компонента должна применяться пара открытый/закрытый ключ. Компилятор записывает открытый ключ в манифест, создает хэш всех файлов, принадлежащих сборке, и подписывает этот хэш закрытым ключом. Закрытый ключ не хранится внутри сборки. Таким образом гарантируется, что никто не сможет изменить вашу сборку. Подпись можно проверить с помощью открытого ключа. Во время разработки клиентская сборка должна ссылаться на совместную сборку. Компилятор записывает открытый ключ используемой сборки в манифест клиентской сборки. Для того чтобы уменьшить размер, в манифест клиентской сборки записывается не открытый ключ, а маркер открытого ключа. Маркер открытого ключа представляет собой последние восемь байтов хэша открытого ключа и является уникальным. Во время исполнения при загрузке совместной сборки (или во время установки, если клиент инсталлируется с применением генератора родного кода) хэш совместно исполь- используемой сборки может быть проверен с помощью открытого ключа, хранящегося внутри клиентской сборки. Никто, кроме владельца закрытого ключа, не может изменить со- совместно используемую сборку. Не существует способа, при помощи которого компонент Math, созданный производителем А и используемый в клиенте, может быть заменен ком- компонентом хакера. Только владелец закрытого ключа может заменить совместный компо- компонент более новой версией. Целостность гарантируется до тех пор, пока совместная сборка будет принадлежать ожидаемому поставщику: Клиентская сборка Манифест Ссылка РК:ЗВ ВА 32 .. . N. - -■ - - v Совместный компонент Манифест РК:ЗВ ВА 32 Подпись Замена ключа Закрытый ключ компании должен храниться в надежном месте. Компании не предоставля- предоставляют доступ к закрытому ключу всем разработчикам; к нему имеют доступ лишь несколько че- человек с соответствующими правами. Именно поэтому ключ внутри сборки может быть заменен позже. Если глобальный атрибут сборки AssemblyDelaySign установлен в true, то в сборке не содержится ключ, но резервируется достаточно свободного места для обеспече- обеспечения возможности добавления этого ключа позже. Однако без ключа мы не можем проверить сборку и установить ее в глобальный кэш сборок. Для тестовых целей можно использовать временный ключ, а позже заменить этот ключ настоящим ключом компании. Создание совместной сборки Для примера создадим совместную сборку и клиента, использующего эту сборку. Начнем с простого проекта Visual C# Class Library. Пространство имен необходимо изменить на Wrox. ProCSharp, а имя класса на SimpleShared. Этот класс читает в StringCollection все строки файла, переданного в качестве параметра конструктору, а в методе QuoteOf - TheDay () возвращает случайную строку из этой коллекции: using System; using System. Col lections. Specialized,- using System. 10; namespace Wrox.ProCSharp
398 Глава 10 public class SimpleShared { private StringCollection quotes; private Random random; public SimpleShared(string filename) { quotes = new StringCollection(); Stream stream = File.OpenRead(filename); StreamReader streamReader = new StreamReader(stream); string quote; while ((quote = streamReader.ReadLine()) != r.ull) { quotes.Add(quote); } streamReader.Close(); stream.CloseO ; random = new Random(); } public string GetQuoteOfTheDay( { int index = random.Next (I, quotes.Count ,'; recurn quotes;index] ; Создание строгого имени Для того чтобы можно было совместно использовать компонент, сначала необходимо создать строгое имя. Для этого можно применить утилиту строгих имен (sn): sn -k mykey. snk Утилита строгих имен генерирует пару открытый/закрытый ключ. Теперь можно установить атрибут AssemblyKeyFi" _ в созданном мастером файле Assemblylnf~.cs. Атрибут должен содержать либо абсолютный путь к файлу ключа, либо этот файл дол- должен быть указан относительно каталога %Projer.^Jirector- I ,obj\<configuration>, например, . . / . . mykey. s .< ссылается на ключ в каталоге проекта. При сборке проекта ключ устанавливается в Crypto Service Provider (CSP). Если ключ уже находится в CSP, то вместо этого можно использовать атрибут Asser - yKeyNan Внесем изменения в Assemblylnfc. cs. Атрибут Asserrv. yKeyFi le ссылается на файл mykey. sr.k: [assembly:'' AssemblyDel^ySign(false) ] [assembly: AsseinblyKeyFile ("../.. /mykey. snk" ) J- [assembly': ^ssgHJblyKeyName ("") I Если после сборки проекта просмотреть сборку с помощью л Ida i, то внутри мани- манифеста можно обнаружить открытый ключ. assembly SimpleShared .custom instance void [»scorljb]Syste» Reflection AssemblyKeytlameAttribute custom instance void [mscorlibjSystem Ref lection. AssemblyKeyFileAttnbute custom instance void [mscorlib]System Reflection AssemblyDelaySignAttribu custom instance void [mscorlibjSystem Reflection.AssemblyTradenorkAttribu custom instance void [mscorlibjSystem Ref lection. AssemblyCopyrightAttribu custom instance void [mscorlibjSystem Ref lection. AssemblyProductAttribute custom instance void [mscorlibjSystem Reflection AssemblyCompanyAttribute custom instance void (mscorlibjSystem. Ref lection. AssemblyConfigurationAtt custom instance void [mscorlibjSystem Reflection AssemblyDescriptionAttri custom instance void [mscorlibjSystem.Reflection AssemblyTitleAttribute " The following custom attribute is added automatically, do not unco» ss custom instance void f.Mscorlib}System Diagnostics.DebuggableAttribute 00 00 00 06 02 00 00 •• S 04 00 00 01 00 01 00 /s S RSA FB IB 67 71 64 6C ID •' - 22 7A 7A 40 8A 82 B4 •• о 62 9B 1С B8 29 99 E6 " С Т 79 67 26 DE IE 5A 14 " 1 Л.\ 02 75 58 E2 BB FC 70 // ' Y 9D 72 2F 56 80 F7 06 // Л 2_ CC F2 EA 89 7C BS 8E ss . - DE 16 31 6A 68 9A CO ) /V p . ■ .publickey - @0 00 01 D6 F2 37 99 3B 87 8E 24 24 3D 6F DB 8A FD 74 El D5 00 00 DS 09 EF 9F 60 1A 19 94 00 00 DE D2 43 74 F9 E4 2D 70 hash algorithm ОжС000800< .ver 1 1:484-3135$ } <r- 04 S2 Al 80 14 D8 59 F6 CD E9 80 53 CD OE 54 5D A0 9E 80 BA 00 41 91 60 C4 FA 82 32 2E 9E 1 00 31 8A 3F 37 9A t>6 D2 9B 65 94 00 Fl 20 C3 35 D6 C2 05 8D
Сборки 399 Установка совместной сборки Теперь, имея открытый ключ в сборке, можно установить ее в глобальное хранилище сборок, используя инструмент gacutil с параметром /i: gacutil /i SimpleShared.au Убедиться в том, что сборка была успешно установлена, и проверить версию совместной сборки можно с помощью средства просмотра глобального кэша сборок. Использование совместной сборки Для использования совместной сборки создадим консольное приложение С# Client. В дан- данном случае необходимо создать новое решение, чтобы совместная сборка не перекомпили- перекомпилировалась при перекомпиляции клиента. Пространству имен дадим имя Wrox. ProCSharp, а классу — имя Client. На сборку SimpleShared.dll мы ссылаемся точно так же, как на частные сборки: ис- используя меню Project | Add Reference или контекстное меню в Solution Explorer. После этого необходимо нажать кнопку Browse и найти сборку' simpleShared.dll. Так как сборка SimpleShared является совместной, то свойство CopyLocal для этой ссылки ав- автоматически устанавливается в False, поэтомI сборка не копируется в каталог клиента; будет использоваться сборка, установленная в глобальный кэш сборок. Приведем код клиентского приложения: using System,- namespace Wrox.Samples.ProfessionalCSharp.Assemblies.Client { class Client { public static void Ma:-; stringf] args) { SimpleShared quotes = new SimpleShared(@"C:\Wrox\Quotes.txt"); for (int 1=0,- I < 3; I++) { Console.WriteLine(quotes.GecQuoteOfTheDay() ; Сопз"^е.WriteLine(); При просмотре манифеста клиентской сборки с помощью ildasm можно увидеть ссылку на совместную сборку SimpleShared: . assembly extern SimpleShared. Часть этой информации представляет собой номер версии и маркер открытого ключа. / М«ШЕ51 assenbly extern kscorlib pubiickeytoken ver 10 2411:0 (B7 7Д 5C 56 19 34 E0 89 ) ibly extern SimpleShared { publickeytoken - FC 49 58 71 97 F6 F8 C2 ) .ver 1.0 479 36320 .assembly Client .custom instance void [nscorlibJSyste*.Reflection.AsseinblyKeyH- custom instance void («iscorllbjSyste» Ref lection. Assembly Key F .custom instance void [mscorlibjSystem Reflection.AssemblyDela »J «I I ■ Маркер открытого ключа можно также просмотреть внутри совместной сборки с помо- помощью утилиты строгих имен: sn -T показывает маркер открытого ключа в сборке, sn -Тр показывает маркер и открытый ключ. Обратите внимание на использование прописной буквы т! Результат работы программы может быть следующим:
400 Глава 10 "Conputer in the future nay weigh no nore than 1.5 tones.". Popular Mechanics, f orecasting the relentless narch of science, 1949 "I think there is л world market for maybe five computers". I. ihtson, Chairman of IBH. 1943 "We Dill think about software nore as л service than we have in the past-". Bill Gate», Microsoft Chief Software Architect, 2009 Press any key to continue Конфигурация Для конфигурирования компонентов СОМ применялся реестр. Конфигурирование при- приложений .NET осуществляется с помощью файлов конфигурации. При использовании реестра невозможно копировать приложения командой хсору. Конфигурационные фай- файлы применяют XML-синтаксис для указания параметров запуска и рабочих параметров приложения. Параметры конфигурации можно сгруппировать в следующие категории: □ С помощью параметров запуска указывается версия требуемой среды исполнения. В одной системе может быть установлено несколько сред исполнения. Версия среды задается с помощью элемента <startup>. □ Рабочие параметры указывают, как должна осуществляться сборка мусора и как работает связь со сборками. С помощью этих настроек можно также задать поли- политик)- версий и кодовую базу (см. ниже). D Настройки удаленной работы применяются для конфигурирования приложений, использующих .NET Remoting (см. главу 23). □ Настройки безопасности рассматриваются в главе 25, где описываются конфи- конфигурация Crypto и права доступа Эти параметры могут содержаться в двух типах конфигурационных файлов: D В конфигурационных файлах приложения хранятся настройки, характерные для приложения, например: информация о связях сборок, конфигурация удален- удаленных объектов и т.д. Конфигурационный файл помещается в тот же каталог, что и исполняемый файл; он имеет то же самое имя, что и исполняемый файл, только добавляется расширение . con fig. Конфигурационные файлы ASP.NET называются web.conf ig. Конфигурирование приложений ASP.NET описывается в главе 16. 3 Конфигурационные файлы машины используются для конфигурации системы. Здесь также можно указать настройки связей сборок и удаленного исполнения. В про- процессе связывания сначала анализируется конфигурационный файл машины, а затем конфигурационный файл приложения. Конфигурация приложения может перекрыть настройки машинной конфигурации. Настройки конкретного приложения следует хранить в файле конфигурации приложения, чтобы машинный конфигурационный файл имел меньший размер и был более управляемым. Машинный конфигураци- конфигурационный файл расположен в %runtime_install_path%\config\Machine.config. Каким образом используются файлы конфигурации? Как клиент находит сборку (процесс связывания), зависит от того, является ли она частной или совместной. Част- Частные сборки должны находиться в каталоге или подкаталоге приложения. Для поиска такой сборки применяется процесс зондирования. Версии для частных сборок не име- имеют значения, однако культура является важным аспектом, как было показано в примере с локализацией. Совместные сборки могут инсталлироваться в глобальный кэш сборок, размещенный в каталоге, на сетевом диске или web-сайте. Для этого используется механизм CodeBase. Открытый ключ, версия и культура имеют большое значение при связывании с совмест- совместной сборкой. Ссылка на требуемую сборку записывается в манифест клиентской сборки, включая имя, версию и маркер открытого ключа. Для применения корректной политики версий просматриваются все файлы конфигурации. Проверяются указанные в файлах конфигурации глобальный кэш сборок и кодовые базы, затем проверяются каталоги приложения и применяются правила зондирования.
Сборки 401 Версии Для частных сборок версии не важны, поскольку применяемые сборки копируются вместе с клиентом. Клиент, который использует сборку, содержит ее в своих собственных каталогах. Однако для совместных сборок дело обстоит иначе. Рассмотрим традиционные проб- проблемы совместного использования. Совместные компоненты применяются несколькими клиентскими приложениями. Новая версия совместного компонента может нарушить ра- работу существующих клиентов. Нельзя прекратить поставку новых версий, поскольку по- постоянно требуются новые возможности, которые реализуются в новых версиях существу- существующих компонентов. Можно пытаться программировать аккуратно и добиваться обратной совместимости, но, как нам всем хорошо известно, это не всегда удается. Решением может стать архитектура, которая позволяет устанавливать различные версии совместных компонентов, а клиенты будут использовать ту версию, на которую они ссылались в процессе создания. Это решает многие, но не все проблемы. Что проис- происходит, когда мы обнаруживаем ошибку в компоненте, на который ссылается клиент? Не- Необходимо обновить компонент и убедиться в том, что клиент использует новую версию, а не ту, которая была указана в процессе создания. Поэтому в зависимости от обстоятельств иногда требуется использовать более новую версию, а иногда — более старую. Все это возможно в архитектуре .NET. В .NET по умолчанию применяется первоначально указанная сборка. С помощью файлов конфигураций можно указать для использования другую версию. Версии играют ключевую роль в архитектуре связывания — клиент получает именно ту сборку, в которой находятся его компоненты. Номер версии Сборки имеют номер версии, состоящий из четырех частей, например 1.0.479.36320. Это означает: <Major>.<Minor>.<Build>.<Rev.Sion> Как используются эти числа — зависит от конфигурации приложения. Хорошей практикой является изменение старшего (Major; или младшего (Minor) разряда в случае значительных изменений по сравнению с предыдущими версиями. Номер версии сборки указывается в атрибуте сборки AssemblyVersion. В проектах Visual Studio.NET этот атрибут хранится в Assemblylnfo.cs: fassembly: AssemblyVersioni.О.*">] Первые два числа указывают старший и младший разряды версии, * означает, что Build и Revision генерируются автоматически. Номер Build представляет собой число дней с 1 января 2000 г., a Revision — число секунд, прошедшее с полуночи по местном)' времени. Разумеется, можно указать все четыре значения, но не забудьте изменить их при повторной компиляции сборки. Версия хранится в секции .assembly манифеста. При указании сборки в клиентском приложении версия сборки помещается в манифест клиентского приложения. Программное получение версии Для обеспечения возможности проверки версии сборки, которая применяется в клиент- клиентском приложении, добавим в класс SimpleShared метод GetAssemblyFullName ( , ко- который будет возвращать строгое имя сборки. Для упрощения использования класса Assembly следует указать пространство имен System.Reflection: public gtring GetAssemblyFullName() {' Assembly assembly = Assembly.GetExecutingAsserably(); return assembly.FullName; 1- FullName является свойством класса Assembly. Оно содержит имя класса, версию, место и маркер открытого ключа. Все это можно увидеть на экране в результате вызова GetAssemblyFullName () из клиентского приложения. В клиентском приложении добавим вызов GetAssemblyFullName () в метод Main О после создания разделяемого компонента: static void Main(string[] args) { SimpleShared quotes - new SimpleShared(@"c:\wrox\quctes.txt"); -, Console-.WriteLine(quotes.GetAssemblyFullName());
402 Глава 10 Не забудьте зарегистрировать новую версию совместной сборки SimpleShared в гло- глобальном кэше сборок с помощью qa^ut- i". Если указанная версия не найдена, генериру- генерируется исключение Syst m.IO.F- -Loao^xception, так как не удается выполнить связывание с корректной сборкой. При успешном запуске будет выведено полное имя используемой сборки: VV '■<•%*«. IrifleSliared. Uersion=l.0.484.29127» Culture*neutral, PiiblicKeyloken=6ca95«7197f tf8c2 i'ress any hey to continue Используя ипл- клиентскую программ), мы можем испытать различные конфигурации совместного компонента. Конфигурацией, /е файлы приложения При помощи файла конфигурации можно указать, что связывание должно производиться с другой версией совместной библиотеки. Допустим, мы создали новую версию совместной сборки . pleShared со старшим и младшим разрядами версии 1.1. Мы не желаем заново собирать клиента — нужно лишь использовать для существующего клиента новую версию сборки. Такой сценарий полезен в случаях, когда в совместной сборке исправлена ошибка или необходимо избавиться от старой версии, поскольку новая версия совместима с ней. Средство просмотра глобального кэша сборок покажет, что для сборки SimpleShared установлены иерсии 1.0.479.36320, 1.0.484.29127 и 1.1.484.31355: Is _T > •"> lc" '. \ .e 'iis_! lev Tdrn I 0.2411.0 Pf-X I 0 G7.23005 bO3fS7flldSO*3* 0.2411.0 10 2411.0 1 0.2411.0 J Л CblKtil Манифест клиет кого при..ожения говорит о том, что оно использует версию 1.0.484.29127 сборки SimpleSha ■: publlckeytoken - ,'tC A9 58 71 97 Ft> FS C2 ) ver 1 0 484 J91Z7 Ы lier.t cu tc il t ance void [mscorj j.b]SysteM Reflection As custofc instance void [isiscorlibjSyste» Reflection As t-uston instance void [tascori-t-JSystea Reflection As Теперь нам нужен файл конфигурации приложения. Нет необходимости напрямую работать с XML, конфигурационные файлы приложения и машины может создавать ин- инструмент .NET Admin. Инструмент .NET Admin является интегрированным в ММС при- приложением, которое может быть запущено командой: тше mscorc Eg.msu
Сборки 403 .NET Framework Administration for CNAGEL Browse Shared Ass*rnbbei for аЛ appbca&oos oa 1Ы machine CTqnfifturc A«embhtf for afl application* uttnqthu vertion ofthe НЕТ Framework Set iiccuntv Pobcv for the НЕТ Framework Выбрав в левой панели Applications и воспользовавшись меню Action | Add..., можно со- создать файл конфигурации приложения для нашего клиентского приложения. После до- добавления клиентского приложения к инструменту .NET Admin, можно просмотреть взаимосвязи сборок: jg My Computer % Shared Assemblies jjffif Configured Assembles J§2 RemoUrvg Services Ш vi Runtime Security Pokey В ■§ Applications Jg Configured «sserrUe _J2 Recnoung Sergei 1 H mscorJb Sanple5hared System neutral 1.0.2411.0 1.0.2411,0 neutral neutral neutral Ь77а5с5619Э4еОВ9 " 6са9587Т9716гвс2 1 >l Выбрав Configured Assemblies и используя меню Action Add..., можно сконфигурировать зависимости для сборки Simplest = ed в списке зависимостей: SimpteShared Properties Geneial fimdins Р»--У | Coctebaies | ТЫ is where you Cdn &el binding pcftcy for youf asiemDv The format of a veruon numbei п Major Mrar.Revision.6u)cl A minimum of Mapf Мкют is requved Example» 1 2 1.2-1 4 R eque^ed Version 10 1.214 1.2- J _ - Ne^Veition 1 1 OK | Cancel |
404 Главою При помощи Requested Version задается версия, которая указывается в манифесте кли- клиентской сборки. New Version определяет новую версию совместной сборки. Major .Minor представляет собой минимальное требование к указанию версии, хотя можно добавить и Bui Id. Revision. Можно также установить диапазон версий: 1.2 — 1.4 означает, что младшая версия может быть 2, 3 или 4. Теперь в том же каталоге, в котором находится файл Client. exe, можно найти кон- конфигурационный файл приложения Client .exe. con fig, содержащий следующий код XML: <configuration> <runtime> ;j; ossemblyBinding xmlns= "urn: setemas-microsoft-com: asm. vl "> /■ <deperidenfcAssejnbly xmlns=='"> " * ,. <assemblyIdentity name=11SimpieShared" 5 '■ " pubi.icKeyToken=ca9Se7197f6f8c2" /> *».. ■ <bindingRedirect oldVersion="a.O" newVersior^-l^.l" /> '•u </dependentAssenibly> ..r '• * <t/asSemblyBindittg> ' <7rvmtime> </configuration> ■"■ При помощи элемента <runtime> могут быть сконфигурированы настройки среды исполнения. Подэлементом <runtime> является <assemblyBinding>, который в свою очередь имеет подэлемент <dependentAssembly>. У элемента <dependentAssembly> есть обязательный подэлемент <assemblyldentity>. С помощью <assemblyldentity> указывается имя требуемой сборки. Атрибут name единственный обязательный для <assemblyldentity>. Необязательные элементы — publicKeyToken и culture. Другим подэлементом <dependentAssembly>, необходимым для переопределения версий, являет- является <bindingRedirect>. С его помощью указываются старая и новая версии используемой сборки. Запустив клиента с этим конфигурационным файлом, получим новую версию 1.1. Версия среды исполнения В конфигурационном файле приложения можно не только менять версии используемых сборок; в нем можно также определить треб\ем\"ю версию среды исполнения. Различные версии среды исполнения .ХЕТ можно установить на одну машину. Версия, необходимая для приложения, может быть указана в конфигурационном файле приложения: <configuratibn> <startup> <regujredRuntime version="vl.0,2728" safeMode="true" /> </startup> , c/CQQfiguration> Атрибут version элемента <reguiredRuntime> указывает номер версии среды испол- исполнения. Номер версии должен совпадать с названием каталога среды исполнения. В нашем случае используется среда из каталога с: \wirj.t\Microsof t .NET\Framework\vl. 0.2728, поэтому мы указали версию vl. 0 .2728. Конфигурирование каталогов Помимо указания версий используемых сборок, можно конфигурировать целый ряд дру- других параметров! Например, необязательно устанавливать совместную сборку в глобаль- глобальный кэш сборок; совместные сборки могут быть также найдены с помощью специальных настроек каталогов в конфигурационных файлах. Эта особенность может использовать- использоваться в том случае, если нужно сделать доступными совместные компоненты на сервере. Другой возможный сценарий: вы желаете использовать совместную сборку в своих прило- приложениях, но при этом не хотите помещать ее в глобальный кэш и делать общедоступной — поэтому вы помещаете ее в совместный каталог. Существуют два способа определения каталога для сборки: с помощью элемента со- deBase в конфигурационном файле XML и с помощью зондирования. Конфигурация CodeBase доступна только для совместных сборок, а зондирование возможно как для частных, так и для совместных сборок.
Сборки 405 <codeBase> Элемент <codeBase> можно сконфигурировать с помощью .NET Admin. В дереве Applica- Application следует выбрать Configured Assemblies и в окне свойств приложения SimpleShared можно задать кодовые базы. Аналогично политике связывания, списки версий можно сконфигурировать на вкладке Codebases. На приведенном ниже рисунке показано, что версия 1.0 должна загружаться с web-сервера http://CNagel/WroxUtils j SimpleShared Properties Genoiel) BindrePofcai С«йЬм«,|'. **-. ] .'< The is tvhee you can *etcodabo!»W«rn«lion (я >«£«■;. „ .M«j«.M!noi.Rev«iori.8uld A irnmum of НфМкт a-, - ,-*, i a, reaiied LIBIs musl conlain (be стйосЫ Wwroalion. ' \v; htlpZ/wtmnidouRcom ' ■Й ! 'I t.Z-1.4 'Hk///cV HwjjejtedVeSkm, i./' 1.0 №K/AX«Ml/Wro>4JUs j OK (l='l Инструмент .NET Admin создает для этого приложения конфигурационный файл: "^■Ч* ' ■•'>■- .'.- v^.-- ' , "'A--'j5^.-%^»it.'.>^,- idiftrf ■■*3tmlnsr= t'ui^: schemas'bmicr-osof t'-fdom: asm. vl • > :^.i»_ffitt,_. 1_li*li.rl'_i'i1*47-f*".J' "'* в="SimpleShared" Элемент <dependentAssembly> тот же самый, что применялся выше для замены вер- версий. Элемент <codeBase> имеет атрибуты version и href. В version должна быть ука- указана версия оригинальной используемой сборки; в href определяется каталог, из которого должна загружаться сборка. В нашем примере задан путь, использующий про- протокол HTTP. Каталог для локальной системы или общего диска указывается с помощью href="file:C:/WroxUtils". При загрузке этой сборки по сети возникает исключение System.Security.Permissions. Для сборок, загружаемых по сети, необходимо конфигурировать права. В главе 23 будет показано, как настраивать параметры безопасности для сборок. <problng> Если <codebase> не сконфигурирован и сборка не хранится в глобальном кэше сборок, то среда исполнения пытается найти ее с помощью зондирования. Среда исполнения .NET ищет сборку с расширением . ЕХЕ или . DLL в каталоге приложения. Если сборка не найдена в этом каталоге, поиск продолжается. Каталоги для поиска можно задать в эле- элементе <probing> секции <runtime> конфигурационных файлов приложения. Для со- создания конфигурации XML можно также воспользоваться окном свойств приложения в .NET Admin. Каталоги, в которых должен осуществляться поиск, указываются в строке поиска Search Path:
406 Глава 10 Получаемый в результате XML-файл выглядит следующим образом: <?xml version=.0"?> <configuration> <r a*. Tie> <c__ ncurrent enabled="enabled" <asse- .yBinding xmlns="urn: scheraas-microsoft-com:asm.vl"> <probing privatePath="bin;utils;'*'"krains**" /> ' * ' £* asseniD-i Ь:' > »ng> Элемент <probing> имеет только один обязательный атрибут: privatePath. Файл конфигурации приложения говорит среде исполнения о том, что сборки необходимо искать в базовом каталоге приложения, а затем в каталогах bin и util. Оба каталога яв- являются подкаталогами основного каталога приложения. Невозможно указать частную сборку за пределами основного каталога приложения. Сборка за пределами основного каталога приложения должна быть совместной и может быть задана с помощью элемента <codeBase> (см. выше!. Распространение То, как сборки должны упаковываться и распространяться, зависит от типа приложения. Приложения WinForms необходимо упаковывать в Windows Installer Package и распро- распространять с помощью Windows Installer. Элементы управления внутри web-страниц нужно помещать в cab-файлы или в DLL. Распространение может происходить вместе с загруз- загрузкой кода. Элементы управления рассматриваются в главе 18. Приложения ASP.NET должны распространяться с использованием хсору или ftp. В главе 16 приводится более подробная информация о запуске приложений ASP.NET. Здесь мы поговорим о распространении простых DLL. Распространение DLL Упаковка DLL применяется в разных ситуациях: DLL могут быть упакованы "как есть". Если сборка существует в виде единственной DLL, этой DLL будет достаточно для упа- упаковки. Также можно упаковать DLL и все зависящие от нее файлы в cab-файл. При помо- помощи файла . cab можно разместить больше файлов DLL и файлов конфигурации внутри одного упакованного файла. Таким образом, загрузка будет осуществляться быстрее и проще, но необходимо следить за тем, чтобы файл .cab не стал слишком велик из-за боль- большого числа сборок. Другой полезный формат для упаковки — Windows Installer Package. Вероятно, DLL будет устанавливаться вместе с теми приложениями, которым она необходима. В
Сборки 407 большинстве случаев выгодно применять Merge Module, служащий для создания повтор- повторно используемых установочных модулей. Файл Merge Module (. msm) представляет собой единый пакет, который содержит DLL, записи в реестре, ресурсные файлы и алгоритм установки для инсталляции компонента. Когда создается Windows Installer Package для приложения Windows, Merge Module можно включить в установочный пакет. Создание Merge Module Merge Module может быть легко создан при помощи Visual Studio.NET. Разумеется, Instal- IShield и Wise для Windows обладают более богатыми возможностями, чем Merge Module в Visual Studio. Однако проект Merge Module содержится внутри Visual Studio.NET и по- позволяет решать вопросы установки, когда "старшие братья" не требуются. При выборе Build | Deploy Solution для открытого проекта библиотеки классов появля- появляется окно с сообщением о том, что необходимо создать проект запуска. Следует выбрать Merge Module Project В Visual Studio.NET будет открыт File System View. Левая панель пока- покажет каталоги, где можно сконфигурировать файлы для установки. Три папки создаются автоматически: О Папка Common Files содержит общие файлы, которые совместно используются приложениями. По умолчанию в качестве этой папки применяется C:\Program FilesVComition Files. В этой папке могут храниться совместные сборки, которые не установлены в глобальный кэш сборок О Сборки, помещенные в папку Global Assembly Cache » процессе инсталляции будут установлены в глобальный кэш сборок. D Папка Module Retargetable в большинстве случаев применяется для модулей Merge Module. Merge Module будет использова, ься в пакете MSI. Пакет MSI сможет определить, в какие каталоги должны быть помещены файлы из папки Retarge- Retargetable. Также можно добавить специальные папки, например: папку Program Files, папку рабочего стола пользователя и т.д. При выборе Module Retargetable Folder в контекстном меню Add | Project Output откроется диалоговое окно: , Ooojrnentatior- F - Debug Symbols Content Files Source Files Jj Configuration: |(Artive) z\ zi OK Cancel { Help Здесь необходимо выбрать Primary output, который включает в себя файлы DLL и ЕХЕ, и (если есть) Localized resources, в которых содержатся сборки- спутники. Все указан- указанные сборки будут автоматически включены в Merge Module. Теперь этот модуль можно использовать внутри пакетов Windows Installer для других приложений.
408 Глава 10 Заключение Сборка представляет собой новый установочный модуль для платформы .NET. Micro- Microsoft учла недостатки предыдущих архитектур и полностью переработала модель для иск- исключения старых проблем. Сборки могут быть самоописывающимися файлами DLL или ЕХЕ, для которых не требуется дополнительное конфигурирование реестра или библио- библиотеки типов. Существует разница между частными и совместными сборками. Создание и использование частных сборок не вызывает трудностей, для них не требу- требуется задавать версию или открытый ключ. Применяя совместные сборки, мы избавляемся от проблем, связанных с совместным использованием DLL, поскольку версия теперь явля- является обязательным атрибутом, который записывается в манифест клиентской сборки. По- Политика по умолчанию всегда использует ту же совместную сборку, которая применялась во время разработки. Если требуется определить иную политику, можно использовать конфигурационные файлы приложения или машины для перекрытия этих настроек. Не стоит забывать и о сборках-спутниках, применяемых для локализации. Простая локализация встраивается в архитектуру сборок.
п f/ % * a в </ ^ a Доступ к данным с помощью MET В этой главе мы обсудим, как получить доступ к данным .NET с помощью ADO.NET. Рас- Рассмотрим следующие вопросы: О Соединения О Исполнение команд О Вызов хранимых процедур О Наборы данных О Использование XML и схем XML Будут также представлены соглашения по именованию в ADO.NET. Но для начала дадим краткий обзор ADO.NET и посмотрим, что он нам предлагает. Обзор ADO.NET Как и многие компоненты платформы .NET, ADO.NET представляет собой нечто боль- большее, нежели тонкая оболочка вокруг некоторого существующего API. Сходство с ADO за- заключается только в имени — классы и методы доступа к данным являются совершенно другими. Единственное, что можно использовать из "старого" мира,— драйверы OleDB. Познакомимся с основными классами ADO.NET. Совместные классы О DataSet — этот объект может хранить набор таблиц DataTable и связи между этими таблицами. Разработан для применения в пассивном состоянии. О DataTable — контейнер данных. Состоит из одного или нескольких столбцов DataColumn и при заполнении будет иметь одну или несколько строк DataRow, содержащих данные. О DataRow — некоторое число значений, похожее на строку таблицы базы данных или строку электронной таблицы. О DataColumn — содержит определение столбца, включающее в себя имя и тип данных. О DataRelation — связь между двумя DataTable внутри DataSet. Используется для отношений внешнего ключа и отношений родительский/ дочерний (master/detail). О Const ra int — определяет правило для DataColumn (или для набора столбцов данных), например, уникальные значения. О DataColumnMapping — отображает имя столбца в базе данных на имя столбца в DataTable.
410 Глава 11 О DataTableMapping — отображает имя таблицы в базе данных на DataTable внутри DataSet. Классы, относящиеся к базам данных О SqlCommand, OleDbCommand — оболочка для операторов SQL и вызовов хранимых процедур. 3 SqlCommandBuilder, OleDbCommandBuilder — класс, используемый для генера- генерации команд SQL (таких, как операторы insert, update и delete) из предложения selec*". О SqlConr.ection, OleDbConnection — соединение с базой данных. Аналогично ADO Connection. 3 SqlDataAdapter, OieDbDataAdapter — класс, используемый для хранения команд select, insert, update и delete, которые затем применяются для запол- заполнения DataSet и обновления DataBase. О SqlDataReader, OleDbDataReader — активный считыватель данных (только чтение). О Scl Parameter, OleDbParameter — определяет параметр для хранимой процедуры. О SqlTransaction, OleDbTransaction — транзакция базы данных, оболочкой ко- которой является объект. Наиболее важное отличие классов ADO.NET состоит в том, что они предназначены для работы в пассивном состоянии. ADO 2Л ввело понятие бессвязного набора записей (не имеющего ссылок на источник данных', но его сложно было использовать, поскольку такое поведение не было предусмотрено с самого начала. Все классы ADO.NET, кроме од- одного (Sql/OleDb DataReader), разработаны для применения в отсутствие базы данных. Пространства имен Во всех примерах этой главы тем или иным образом осуществляется доступ к данным. Следующие пространства имен содержат классы и интерфейсы, используемые для доступа к данным в .NET О System. Da;a — все основные классы достша к данным О System. Data. Corr-..- — классы, совместно используемые (или перекрытые) отдель- отдельными провайдерами данных О S_. _ —п.Data.OleDb — классы провайдера OleDb О Sysre- Г» '.SqlClier.- — классы провайдера Scl Server О System.Z&z:-.. BqlTypes — типы данных SqlSer^'er Классы и интерфейсы из этих пространств имен буд\т рассмотрены по ходу главы. Мы в основном сконцентрируемся на классах Sql, поскольку примеры из Framework SDK устанавливают базу данных MSDE (или SqlServer). Классы OleDb в большинстве случаев точно копируют код Sql. Соединения Для осуществления доступа к базе данных необходимо указать такие параметры соедине- соединения, как машина, на которой запущена база данных, и сведения для входа в систему. Любо- Любому, кто работал с ADO, понятно назначение классов соединений .NET OleDbConnection и SqlConnection. System.Data.lDbConnection о System.Data.lDbConnection о System.Data.lSqIConnection о System.Data.OleDb.OleDbConnection System.Data.SqICIient. SqlConnection При установке соединения требуется указать его параметры в той или иной форме. В примере ниже показано, как создать и установить соединение с базой данных Northwind. Эта база данных будет использоваться в последующих примерах, она устанавливается вместе с примерами .NET Framework SDK:
Доступ к данным с помощью .NET 411 string source = "server=(local) WNetSDK; " + "uid=QSUser;pwd=QSPassword;" + "database=Northwind"; SglConnection conn = new SqlConnection (source1 ; conn.Open() ; // Делаем что-нибудь полезно= conn.Close(); Если вы когда-либо работали с ADO или OLEDB, то заметите, что строка соедине- соединения похожа на ту, что вы использовали ранее. Yj ш вы применяли провайдера О LeDb, скопируйте код из старых проектов. Как только соединение установлено, можно подавать команды источнику данных, а после их выполнения соединение можно закрыть. Транзакции Когда требуется выполнить в базе данных сразу несколько обнов шний, их необходимо осуществлять в виде транзакции. Транзакция в ADO.NET начинае гея с помощью вызова одного из методов ■> jainYranf- n \) объекта соединения с базой данных. Эти мето- методы возвращают объект, который реализует ir-r рфепг r hmransactit .. определенный в System.! ata. Следующий код инициир\ет транзакцию для соединения с Sql Server: string source = SqlCcr.nection _ conn.Open(); SqlTransaction tx "server=i_c~a_;\\NetSDK;" + "uid=QSUser;pwd=QSPassword;" "database=Northw.ir ^ , = new Sqls = с -.Irensaction () ; --анзакцию // Выполняем какие-либо команды, а затрм _х _ tx.Commit О г - inn.Close () ; Начиная транзакцию, можно указап \ровень изоляции для команд, исполняемых внут- внутри нее. Этот уровень определяет, насколько изолиропна транзакгия т др\тих транзакций, происходящих на сервере базы данных. Сервеоь ' j акнь.ч способны поддерживать те или иные из представленных ниже уровней изоляции. Уровень изоляции Описание ReadCor- it - ReadUncoirjr - -. .= i RepeatabieRead Значегие по умолчанию для SQL Server. Гарантирует, что данные, записанные одной транзакцией, будут доступны второй транзакции .олько пс ie фиксации первой. Разрешает транз~1кции читать данные в базе данных, даже если они не бы. и зафиксированы другой транзакцией. Например, если два пользователя осуществляют доступ к одной и той же базе данных и первый пользователь вставляет некоторые данные, не завершая транзакцию с помощью j ь или Rcl ack), то второй пользователь с уровнем изоляции " eadUru. di i- ted может прочесть эти данные. Расширяет уровень Re _3 _nun "' ed. Гарантирует, что если внутри транзакции выполняется один и тот же оператор, то независимо от других потенциальных обновлений базы данных всегда будут возвращаться те же данные. Этот уровень требует, чтобы на данные были наложены дополнительные ограничения, что может ощутимо отразиться на производительности. Гарантирует, что для каждой строки в первоначальном за- запросе не может быть произведено никаких изменений дан- данных. Однако допускает появление "фантомов" — новых строк, которые мог>т быть созданы другой транзакцией во время работы вашей.
412 Глава 11 Уровень изоляции Описание Serializable Наиболее ограничивающий уровень транзакций. Организует последовательный доступ к данным в базе данных. На этом уровне изоляции строки-фантомы не могут появиться, поэтому оператор SQL внутри упорядоченной транзакции всегда будет выдавать одни и те же данные. Не следует недооценивать действие упорядоченных транзакций на производительность. Уровень изоляции SQL Server по умолчанию, ReadCommitted, является хорошим комп- компромиссом между когерентностью и доступностью данных, поскольку в этом режиме на дан- данные накладывается меньше ограничений, чем в случае RepeatableRead или Serializable. Однако существуют ситуации, в которых уровень изоляции должен быть повышен, поэ- поэтому в .NET можно начать транзакцию с отличным от первоначального уровнем. Не су- существует сложных и быстрых правил для выбора конкретного уровня — эти знания приходят с опытом. Команды В своей простейшей форме команда является строкой текста SQL, которую необходимо передать в базу данных. Команда может также быть хранимой процедурой или именем таблицы, которая вернет все строки и столбцы этой таблицы (т.е. предложение в стиле SELECT -). Команда может быть сконструирована путем передачи конструктору предложения SQL в качестве параметра: string source = "server = (local) WNetSDK;" * "uid=QSUser;pwd=QSPassword;" + "database=Northwind"; s-.r.r.c г 1= * - "SELECT ContactName, Cc-panyName FROM Customers"; .=--;-"'"£"-■ ■ = new SqlCcinection(source) ; ~~nr~.. „cer. SqlCoEinand cmd = new SqlCommand(select, conn)> Классы SqlCommand и ieDbCommar i обладают свойством CommandType, которое ис- используется для определения того, является ли команда предложением SQL, вызовом хра- хранимой процедуры или оператором для всей таблицы. Эти типы приведены в следующей таблице Тип команды Пример Text Earing select = -SELECT ContactName FROM Customers"; (по таолчанию) Sc]Command cmd = new SqlCommand (select, conn) ; StorecFrocedure SqlCommand cmd = new SglCommandCCustOrderHist", conn); emu.CommandType = CommandType.StoredProcedure,■ cmd.Parameters.Add("@CustomerID", "QUICK"); TableDirect OleDbCommand cmd = new 01eDbCommand("Categories", conn); cmd.CommandType = CommandType.TableDirect; При выполнении хранимой процедуры может потребоваться передать ей парамет- параметры. В приведенном выше примере параметр @CustomerID устанавливается напрямую, хотя существуют и другие способы установки значения параметра (см. главу 12). Примечание. Тип команды TableDirect существует только для провайдера OleDb. При попытке использовать этот тип команды с SqlProvider будет сгенерировано исключение.
Доступ к данным с помощью .NET 413 Исполнение команд После определения команды ее необходимо исполнить. Оператор может быть исполнен самыми разными способами в зависимости от того, что необходимо получить (если вооб- вообще что-то нужно). Классы SqlCommand и OleDbCornmand предоставляют следующие методы для исполнения: О ExecuteNonQuery исполняет команду, но не возвращает никаких результатов. □ ExecuteReader исполняет командуй возвращает типизированный IdataReader. О ExecuteScalar исполняет команду и возвращает единственное значение. Класс SqlCommand предоставляет также следующие два метода: □ ExecuteResultset зарезервировано для последующего использования. О ExecuteXmlReader исполняет команду и возвращает XmlReader. Код примеров этого раздела можно найти в подкаталоге 01_ExecutingCornmands. ExecuteNonQuery Этот метод широко используется для операторов UPDATE, INSERT и DELETE, когда един- единственное возвращаемое значение — число строк, над которыми были выполнены опера- операции. Тем не менее этот метод способен возвращать результаты, если производится вызов хранимой процедуры, имеющей выходные параметры: string source - "serve" _o;al) WNetSDK; " + "uid=QS'Js Br;pwd=QSPassword;" + "database=Northwind"; string select = "UPDATE CUSTOMER " + "SET NAME = 'Bob1 " + "WHERE NAME = 'Bill1"; SglConnection conn = new Sq~C_nnection(source ; conn. Open () ,- SqlCommand cmd = new SqlCommand (select, со: -. ; crnd. ExecuteNonQuery () ; conn.CloseO ,- ExecuteNonQ_2ry возвращает число строк, над которыми были произведены действия, в виде int. ExecuteReader Этот метод исполняет команду и возвращает объект sglDataReader или OleDbDataReader в зависимости от применяемого провайдера. Возвращаемый объект можно использовать для итерации по возвращаемой записи(ям), как показано в следующем коде: string source = "server=(local \NetSDK-" + ■-id=QSUser;pwd=QSPassw^rd;" + spring selpct = "SELECT Contac-..ате, Co.i_ianyName FROM Customers"; SqlConnect-эл conn = rew SqlCor.r.ection i source) ; conn.Open(, ; SqlCommand .- = new :qlCqmmand select, conn); SqlDataReader reader = cmd,ExecuteReader(); while (reader.Read!)i) { ;, Console. WriteLinef" {0,-30} {1}", reader[0], reader[U); } Объект DataReader рассматривается ниже. ExecuteScalar Во многих случаях оператор SQL должен возвращать один единственный результат — на- например, число записей в таблице или текущую дату/время сервера. В этих случаях можно использовать метод ExecuteScalar: string source = "server= (local) WNetSDK;" + "uid=QSUser;pwd=OSPassword;" + "database=Northwind"; string select = "SELECT COUNT(*) FROM Customers"; SqlConnection conn = new SqlConnection(source); conn.OpenO ;
414 Глава 11 SqlCommand cmd = new SqlCommand(select, conn); Object o= = cmd.ExecuteScalart) ; Этот метод возвращает object, который в случае необходимости можно привести к требуемому типу. ExecuteResultSet (только для провайдера Sql) Этот метод зарезервирован для использования в будущем, и в случае его вызова будет сгенерировано исключение System.NotSupportedException. ExecuteXmlReader (только для провайдера Sql) Этот метод исполняет команду и возвращает объект XmlReader. SQL Server позволяет включать в предложение SQL предложение FOR XML. Это предложение может принимать одну из трех форм: 3 FOR XML AUTO О FOR XML RAW О FOR XML EXPLICIT Полное описание этих параметров содержится в книгах по SQL Server. В этом примере используем AUTO: string select = "SELECT ContactName, Company-Name " T "FROM Customers FOR XML AUTO"; SqlConnection conn - new f q.'О "■"■ ч 1 ^r i sc jrce) ,- conn.Open(); SqlCommand cmd = new SglCommand select, ., I ; XmlReader xr = cmd". ExecuteXmlReader (); while (xr.ReadO) I V ■. Console.WriteLine(xr-ReadOuterXml()); i "-'*?: ■'■ ■■ ■•....■ conn.Closed ; Здесь в оператор SQL включается строка г "R XML A_ то, а затем вызывается метод Ехесх'еХг-'.Г аЗэ -. Результат работы кода показан ниже: П C:\WINNT\Sy«tem3Z\cmd.exe i*»* SqlProuidei1 »»» |Use ExecuteXnlRcadei- with a FOR XML ftUTO SQL clause <Custoners ContactNane-"Haria finders" ConpaniiMane="ftlfreds Futterkiste'V> <Custor*er* ContactNane*~*^ntonio Moreno" ConpanvNane~"ftntonio Moreno Taqucrxrt"/> <Ciistoncr2 ContactHane~"C)<ristina Bcrglund" Conp.inyNanc="Ber3lunda snabbkSp'V> <Custoncrs ContactHanc-'Trederique Citeaux" ConpanyNaoe="Blondcsddsl pcre et fils'V> <Custoncrs ContactNane="Lanrence Lebilmn" ConpanyNane="Bon арр&ароз;"/> <Custoners ContactNane»"Uintoria flsluioj-th" ConpaniyHane="B8rapos;s Beuei-ages'V> <Custoners ContactNane-"Francisco Chana" ConpanyNane'"Centro concrcjal Mocte2uncVV> <Custoners ContactNane-"Pedro flfonso" CompanyName="Conercio Mineiro'V> <Custooers ContactNane»"Suen Ottlieb" ConpanyNane*"D»-ecl>cnblut Dclikatessen'V> <Cnstoners ContactNane="ftnn Devon" ConpanyHane="Eastern Connection'V> <Custoners ContactNane«-"ftria Cruz" ConpanjFNane="Fanilia ni4iuibaldo'V> <Custoners ContactNane="Martine Ranee" Co»pans(Nane="Folies gonrnanden'V> <Custoners ContactNane-"Peter Franken" Conpans>Nanei>"Fi-Ankenuersand"/> <Custoners ContactNane°"Paolo flccorti" ConpanyNane-'TiMncbi S.p.fl."/> <Custor>ers ContactNane°"Eduardo Snawedra" ConpanuNane~"Galeria del 9astr6»ono'V> ^.„» ^^..^ "ftod>-e F " " *\.-i--../- — ^ .---. »-^ 'I Вызов хранимых процедур Вызов хранимой процедуры представляет собой определение имени хранимой процедуры, добавление определений параметров для каждого параметра процедуры и вызов одной из функций Execute, рассмотренных выше. Для того чтобы сделать примеры этого раздела более полезными, определим набор хранимых процедур, которые могут использоваться для вставки, обновления и удаления записей из таблицы Region базы данных Northwind. Эта таблица выбрана по той причи- причине, что она является небольшой, но при этом позволяет привести пример для каждого из типов хранимых процедур.
Доступ к данным с помощью .NET 415 Вызов хранимой процедуры, не возвращающей значение Наиболее простым примером вызова хранимой процедуры является случай, когда не производится возврат результата. Ниже определены две такие процедуры, одна для об- обновления существующей записи Region, а другая — для удаления записи. Обновление записи Обновление записи Region является чрезвычайно простым, поскольку (при условии, что основные ключи изменить нельзя) существует только один столбец, который можно модифицировать. Эти примеры можно набрать в анализаторе запросов SQL, либо мож- можно запустить файл StoredProcs. sql из подкаталога 1 2_S^oredProcs, в результате чего будут установлены все хранимые процедуры, рассматриваемые в этом разделе: CREATE PROCEDURE RegionUpdate(SRegionID INTEGER, @KegionDescription NCKARE 0))AS , SET NOCOUNT OFF; UPDATE Region SET RegiontJescriptiorl = @RegionDescription WHERE RegionID = @RegionID; GO В случае, более приближенном к реальности, команда обновления, вероятно, заново выберет запись и вернет обновленную запись Чтобы исполнить эту хранимую процедуру из кода .NET, необходимо опред&шть команду SQL и выполнить ее: SqlCommand -aCommand = new SqlCommand("RegionUpdate", conn); aCommand.CommandType = CommandType.StoredProcedure; aCommand. Parameters. Add (new SqlParameter ("SRpnir г тг)", С, "г egionID")) ; aCommand.Parameters.AddCnew SqlParameter .■SRegionDescription", SqlDbType.NChar, 4.5°. "RegionDescription")); aCommand.UpdatedRowSource = UpdateRowSource.None; Этот код создает новый объект JqlCommand по имени Regionr =" ete и определяет его как хранимую процедуру. Затем перечисляются параметры, и наконец устанавлива- устанавливается тип ожидаемого результата как одно из значений перечисления "pca^eRcwSource (см. ниже). Хранимая процедура принимает два параметра: уникальный основной ключ записи region, которую необходимо обновить, и новое описание для этой записи. После создания команды ее можно исполнить, например, так: aCommand.Parameters[0].Value = 999; aCommand.Parameterst1],Value = "South Western England"; aCommand.ExecuteNonQuery(}; Здесь производится установка значений параметров, а затем осуществляется вызов хра- хранимой процедуры. Так как процедура ничего не возвращает, достаточно будет Execute- ExecuteNonQuery (). Параметры команды могут быть установлены либо по индексу (как показано выше), либо по имени. Удаление записи Следующая хранимая процедура выполняет удаление записи region из базы данных. CREATE PROCEDURE RegionDelete (ORegionlD INTEGER) AS SET NOCOUNT OFF; DELETE FRO*} Region WHERE RegionID = ORegionlD; GO Этой процедуре необходимо только значение основного ключа записи. Команда SqlCommand для вызова процедуры выглядит так: SqlCommand aCommand = new SqlCommand("RegionDelete", conn); aCommand.CommandType = CommandType.StoredProcedure; aCommand.Parameters.Add(new SqlParameter ("ORegionlD", SqlDbType.Int, 0, "RegionID")); (,9
416 Глава 11 aCommand'.flpdatedRowSource = UpdateRowSource.None; Следующая команда принимает единственный параметр, после чего производится исполнение хранимой процедуры RegionDelete: aCpmmand.Parameters["@RegionID"].Value = 999; aCoromand.ExeputeNonQuery{)j Вызов хранимой процедуры, которая возвращает выходные параметры Если хранимая процедура возвращает параметры, то они должны быть определены в клиенте .NET, чтобы их можно было заполнить при выходе из процедуры. Следующий пример показывает, как вставить запись в базу данных и вернуть основной ключ этой записи. Вставка записи Таблица region состоит только из основного ключа (RegionID) и поля описания (Regi- onDescription). Для вставки записи необходимо сгенерировать числовой основной ключ и затем вставить в базу данных новую строку. В этом примере мы упростим генера- генерацию ключа, создав его внутри хранимой процедуры. Используемый при этом метод со- совершенно "сырой" — этого пока достаточно, а генерация ключей будет рассмотрена в отдельном разделе. CREATE PROCEDURE Regionlnsert (@Reg'ioriDescr.iption NCHARI50), @RegionID -INTEGER. OUTPUT) AS SET, NOebUNT' OFF; ' " ' ' ' SELECT: «RegionID = MAX(RegionID)+1 FROM Region; .INSERT- INTO Region{RegionID. RegionDescriptiori) Ц VA№pS( ©RegionID, SRegionDescription) ; GO :'?. * .; Пропедура вставки создает новую запись Region. Так как значение основного ключа генерируется самой базой данных, оно возвращается в качестве выходного параметра процедуры. Этого достаточно для данного примера, но для более сложных таблиц (осо- (особенно со значениями по умолчанию) обычно выбирают вставленную строку целиком и возвращают ее вызывающему. Классы .NET могут работать и так, и так. SqlCommand aCocnand = new SglCprraiand( "Regionlnsert", conn);' aComriiandi'CqramandType = ComraahdType.StoredProcedure; aCoromand.''Psr^meters.Add(new SqlParameter ("dR'egionDescription", ■'• ;'-' - SqlDbType.NChar, 50,- "< > llRegionDescrlption")); aCommand.Parameters.Addjrtew SqlParameter ("@RegioniP", '*■■ ■ ■ SqlDbType.Int, '*'■** °< ■■ . . ... -,'j. , ' T-;■■,*":'- ParaneterDirection.Output, ;' ■''■■' '"VllT..; -false, ■■'•'■• o, .0. "RegiorilD", DataRowVersion.Default, j, nullj )■; aConanand-UpdatedRowSource "= UpdateRowSotirce.OutputParameters; Здесь определение параметров более сложное. Параметр ORegioniD в данном приме- примере является Output. Параметр UpdateRowSource задает, что указанная процедура дол- должна возвращать данные в OutputParameters. Этот флаг в основном используется при выполнении вызовов процедуры из DateTable (см. ниже). Вызов этой хранимой процедуры аналогичен предыдущим примерам, за исключением того, что в данном примере необходимо прочитать выходной параметр после исполнения процедуры: aCoromandiParametersX-eRegianDescription"]-.Value - "South West"; aConvfflahd..rExecjiteNonQuery (); int newRegionlt) = <int). aCoromand.Parameters["SRegionlD11] .Value; После исполнения этой команды мы читаем значение параметра eRegionlD и приводим его к целому типу.
Доступ к данным с помощью .NET 417 Может возникнуть вопрос: что делать, если вызываемая хранимая процедура возвра- возвращает выходные параметры и набор строк? В этом случае необходимо определить парамет- параметры соответствующим образом, но вместо Execu, eNonQuery v, вызвать какой-то другой метод (например, Ехе< .. ^Reai -), который позволит пройтись по всем возвращаемым записям. Считыватели данных DataReader является самым простым методом для выбора некоторых данных из источни- источника данных. Его способности весьма ограничены. Экземпляр Datar ader нельзя создать на- напрямую — он возвращается из объекта п CommanJ или . _эС.тг .3 в результате вызова метода ExecuteReader Следующий код демонстрирует, как выбирать данные из таблицы stomer базы дан- данных Northwir.d. В примере выполняются соединение с базой данных, выбор нескольких записей, проход в цикле по этим записям и вывод результатов на консоль. Мы используем провайдера OLE DB. В большинстве случаев эти классы представля- представляют собой то же самое, что и их брат» я SqlCLie. Ь. Например, объект OleDbConnection аналогичен объект) SglCoi ^менявшемуся в предыTvmnx примерах. Для исполнения команд нчл исоч] i '>vi данных OLE DH i .no' .лусгся класс OleDB- Command. В следующем примере елполняет' прсс ни '• и рат >р SQL и считываются записи путем возврата объекта С ел-эНеабсг. Код этого примера находится в каталоге _~а -aReader: using System; Этот оператор u. .g импортирует классы tl^db в пространство имен. Все провайде- провайдеры данных, доеппные в текущий мом"ь г тлвляюгея внутри одной DLL, поэтому для импортирования классов, исио ьз\емых в данном разделе, достаточно сослаться на сборку „yster . nta.D]l: using System.Data.OleDb: public class О ^aReader { public stat-i ■ void v Kstriu. args) { string source = "Provider-^vLOLEDB;" J- 'server=(local)\\NetSDK;" + 'uld=QSUser;pwd=QSPassword;" * "c, ;abase=r rthwir.d" ; I string selt-L =: ' SET ЕСТ О .?.ctNa;. _, ~CTipanyName FROM Customers"; OleDbConnec^ on conr = new leDbCoi.r ion (source) ; conn.Open(); OleDbCortunand cmd = - :w Ole "ommandtse ' , conn) ; OleDbDataReader aRea<? зг =, с. 5 . Executekea :r () ; whi 1 e (aReader.Read() Console.Wr LteLine(" {0} ' om {1}", aPeader .GetString(O)", = Reader С ■»■ String A) ) ; aReader.Close(); borin.Closel); Этот код демонстрирует многие концепции С#, рассмотренные в др\тих главах. Для компиляции примера используйте команду: esc /t:exe /debug* DataReader.es /r:System.Data.dll Следующий код создает новое соединение с базой данных OleDb.NET, основываясь на исходной строке соединения: ■.OleDbConnection con = new OleDbConnection(source); cbnVOpen0; >; OleDbCommand cmd = new OleDbCommand(select, con); Третья строка создает новый объект OleDbCommand, основываясь на конкретном опе- операторе select и соединении с базой данных, которое необходимо использовать при вы- выполнении команды. Команда возвращает инициализированный OleDbDataReader: -.. OleDbDataReader aReader s cmd ExecutePeadet '); -
418 Глава 11 CleDbDataReader представляет собой однонаправленный "присоединенный" кур- курсор, т.е. можно просматривать записи в одном направлении, а используемое соединение с базой данных держится открытым до тех пор, пока DataReader не будет закрыт. OleDbDataReader поддерживает соединение с базой данных открытым до тех пор, пока оно не будет закрыто явно. Нельзя явно создать экземпляр класса OleCLOataRead^r - он всегда создается в резуль- результате вызова метода ExecuteReader класса oieDi Command. Существуют различные способы доступа к данным, содержащимся внутри открытого DataReader. Когда объект OleDbDr aReader закрывают явно (при помощи явного вызова Close () или в результате действий сборщика мусора), может быть закрыто и лежащее в его основе соединение, в зависимости от того, какой из методов ExecuteReadei () был вызван. Если вызвать Execu = =ader() и передать CommandBehav ; or .CloseConi ction, то можно закрыть соединение при закрытии считывателя (см. ниже). Класс Оп 3 jDataReade' содержит индексатор, который позволяет осуществлять доступ (хотя и небезопасный по типу') к любому полю с использованием знакомого синтаксиса массива: otn а - aReader[0]; ob ejc о =" aReader{"CategoryID"Э; Допуская, что полр Cat ^g -у. является первым в операторе sei ect, применяемом для заполнения считывателя, эти две строки (^нкционально эквивалентны. Мы гово- говорим функционально, поскольку существ\ет олно отличие — последний оператор будет более метченным, чем первый. Автор написал простое тестовое приложение, которое осуществляет миллион операций доступа к одному и тому же столбцу в открытом считывателе данных, для определения скорости работы. Да. вряд ли вы станете читать один и тот же столбец миллион раз, но ||рсь важна каждая секунда, и нсобхо чимо писать настолько оптимальный код, насколько ВО.эМО>1\ Ю. Для нения, индексирование числом потребовало около .67 с для миллиона обра- обращений л инлс i чние текстом — 3 0 с. Причина этого отличия заключается в том, что текс Bi-.li 1етод i.o-нчает номер столбца из схемы, а затем осуществляет доступ к столбцу, исполь^я его номер. Очевидно, что если эта информация известна заранее, можно лучше организовать доступ к данным. Так что? Следует исполы вать индексирование числом? Может быть, однако существует лучший способ. Помимо индексаторов, приведенных выше, ^DBRead jr содержит набор безопасных по тип\ методов, которые могут использоваться для чтения столбцов. Их имена интуитив- интуитивно понятны, и все начинаются с Get. Сутдсствиот методы для чтения большинства типов данных, например: Ge'_:_..: _, CetFloat, j_d и т.д. Миллион итераций с использованием ~с Int32 занял 0.57 с. К накладным расходам для чистового индексатора относятся получение типа данных, вызов того же кода, что и в GetIг. "^, а затем упаковка (и последующая распаковка) целочисленного значения. Поэтому, если вы заранее знаете схему, готовы применять числа вместо названий столб- столбцов и способны использовать безопасную по типу функцию для каждого доступа к столб- столбцу, то вы получите примерно шестикратное увеличение скорости по сравнению с применением текстовых названий столбцов (при выборе миллиона копий одного и того же столбца). Ясно, что здесь приходится выбирать между удобством поддержки и скоростью. Если вам необходимо использовать числовые индексаторы, определите константы в области видимости класса для каждого из столбцов, доступ к которым вы будете осуществлять. Приведенный выше код может применяться для получения данных из любой базы данных OLE DB. однако существуют классы, специфичные для SQL Server, которые можно использовать для улучшения производительности (взамен переносимости). Следующий пример аналогичен предыдущему, за исключением того, что в нем заме- заменены OLE DB провайдер и все ссылки на классы OLE DB соответствующими SQL-копия- SQL-копиями. Изменения в коде по сравнению с предыдущим примером выделены. Пример находится в каталоге C4_CataReaderSql: using System; using System.Data.SqlCl^ent;
Доступ к данным с помощью .NET 419 public class DataReaderSql public static void Main(string[] args) },-. stringi source' = ■ server^i local) \YNetSDK;" + "| -s "uid=QSUser;pwd=QSPassword; " + "database=Northwind" ,- string select = "SELECT ContactName, CompanyName SqlConnection conn = new SqlConnection (source) ,- conn.Open(); ■SftlCommand cmd =. mew SqlCommand-jselect, conn); = cmd.ExecuteReader ();, ■{0}' from {1}", aReader.GetString(O), aReader.GetStringA)); FROM Customers" .'aReader while (aReader.ReadO ) Console.WriteLine(" aReader.Closed ; conn.Close() ; return 0 ; Заметили разницу? Необходимо заменить cieDb на Sql, изменить строку для источ- источника данных и перекомпилировать программу. Те же самые тесты на производительность были выполнены для провайдера Sql, и на этот раз числовые индексаторы показали примерно 0.23 с для миллиона доступов, а строковый индексатор — 3.12 с. Можно было предположить, что родной провайдер Sql работает быстрее, чем OleDb,— так оно и есть. Если вы желаете провести тестирование на своем собственном компьютере, то озна- ознакомьтесь с примерами 05_IndexerTt=stingOleDb и 06_IndexerTestingSqI. И последнее. При вызове ExecuceReader () без параметров для провайдера SqlC- L- ent переданное соединение автоматически закрывается при закрытии считывателя, в то время как поведение провайдера OleDb прямо противоположно. Вероятно, это осо- особенность реализации — код написан с использованием Build 9188 (апрель 2001). По всей видимости, один из этих методов будет изменен — скорее всего, для SglClient. Чтобы обойти эту проблему, вызывайте Ехе - =- =ader (Command3eL'.avior.Key Info), и соеди- соединение не будет закрываться. Наборы данных Класс DataSet был разработан как оффлайн-контейнер данных. Он не предполагает на- наличия соединения с базой данных. Содержащиеся внутри DataSet данные необязатель- необязательно должны быть получены из базы данных — это могут быть записи из файла CSV или точки, переданные измерительным устройством. Набор данных состоит из набора таблиц, каждая из которых имеет набор столбцов и строк данных. Помимо данных можно также определить связи между таблицами внутри DataSet. Один из широко распространенных сценариев — определение отношений роди- родитель/потомок (широко известных как master/detail). Одна из записей в таблице (к приме- примеру. Order) ссылается на несколько записей в другой таблице (например, Order_Details). Это взаимоотношение может быть задано внутри DataSet. DataSet Data Table—-___^ \ Ж№* DataColumn DataRow Data Table —-~_____^^ \ DataColumn
420 Глава 11 В следующих разделах описываются классы, используемые в DataSet. Таблицы данных Таблица данных похожа на физическую таблицу базы данных — она состоит из набора столбцов с конкретными свойствами и может содержать нуль или более строк данных. Таблица данных может также определять основной ключ, состоящий из одного или не- нескольких столбцов, а также содержать ограничения для столбцов. Всю эту информацию в совокупности называют схемой. Существует несколько способов определения схемы для конкретной таблицы данных (и набора данных в целом) (см. ниже). На следующем рисунке показаны некоторые из объектов, доступных в таблице данных. DataTable Columns Rows DataCoIumn j DataRow I Constraints ExtendedProperties Constraint Object Объект DataTable (а также DataCo. in) может иметь произвольное число связан- связанных с ним дополнительных свойств. Эта коллекция может быть заполнена любой поль- пользовательской информацией, касающейся объекта. Например, некий столбец может хранить маску ввода для проверки содержимого этого столбца; канонический пример — американский номер социального обеспечения. Дополнительные свойства особенно по- полезны тогда, когда данные конструируются в среднем звене и передаются клиенту для по- последующей обработки. Например, можно сохранить критерий допустимости (минимум, максимум) для числовых столбцов. После заполнения таблицы посредством чтения информации из базы данных, файла или вручную с помощью кода коллекция Rows будет содержать полученные данные. Коллекция —s содержит экземпляры DataCoIumn, которые были добавлены к таблице. Они определяют схему данных, например: тип данных, способность прини- принимать значение null, значения по умолчанию и т.д. Коллекция Constraints может быть заполнена правилами для основного или уникального ключа. Один из примеров, в котором задействуется информация о схеме для таблицы дан- данных.— отображение данных в DataGrid (см. главу 12). Элемент управления DataGrid при определении того, какой элемент управления использовать для данного столбца, приме- применяет такие свойства, как тип данных столбца. Битовое поле в базе данных будет отобража- отображаться в виде флажка DataGrid. Если для столбца определено свойство NOT NULL, эта информация будет храниться в DataColurir. для ее проверки при. попытке пользователя выйти за пределы строки. Столбцы данных Объект DataCoIumn определяет свойства столбца таблицы данных, например: тип дан- данных этого столбца, предназначен ли столбец только для чтения и т.д. Столбец может быть создан при помощи кода или автоматически сгенерирован средой исполнения. При создании столбца полезно дать ему имя, иначе среда исполнения создаст имя ав- автоматически в форме Columnn, где п будет представлять собой возрастающее число. Не слишком наглядно. Тип данных столбца может быть задан либо путем его передачи в конструктор, либо при помощи установки свойства DataType. После загрузки данных в таблицу тип столб- столбца изменить нельзя — будет сгенерировано исключение ArgumentException. Столбцы данных могут содержать следующие типы данЯых: Boolean Byte Char DateTime Decimal Double Intl6 Inc32 Int64 Sbyte Single String TimeSpan Ulntl6 UInt32 UInt64
Доступ к данным с помощью .NET 421 Следующее, что необходимо сделать после создания столбца,— указать для него дру- другие свойства, например способность принимать значение null или значение по умолча- умолчанию. В приводимом ниже фрагменте кода показана установка нескольких параметров для DataColumn. DataColumn customerlD = new DataColumnt"CustomerID", typeof (int)); customerlD.AilowdBNull = false; customerID.Readonly = false; customerlD. Auto&icrement = true; customer ID ;AutoincrementSee<3 = 1000;. s .- DataColumn Tiame = new DataColumn{ "Name", typeof (string)); name.AllowDBNuil = false; name.Unique = true; Для DataColuirr. могут быть установлены следующие свойства: Свойство AllowDbNull Autolncrerent AutoIncrementSeed AutoIncrementStep Captio""- ColumnKapping ColurrJName Datatype DefaultValue Expression Описание Если true, позволяет устанавливать значение столбца В DBNulI. Определяет, что значение этого столбца генерируется автоматически с возрастанием. Начальное значение для свойства Autolncrement столбца. Определяет шаг между автоматически генерируемыми значениями столбца, по умолчанию установлено значение 1. Может использоваться для отображения имени столбца на экране. Определяет, как столбец отображается в XML при сохранении Г=~аЗе~ вызовом DataSet.WriteXML. Имя столбца. Автоматически генерируется средой исполнения, если отсутствует в конструкторе. Значение столбца System.Type. Может определять значение столбца по умолчанию. Позволяет определить столбец как вычисляемый. Строки данных Этот класс представляет собой др\т\то часть класса DataTable. Столбцы внутри табли- таблицы определяются с помощью класса DataColumn. Доступ к собственно данным в табли- таблице осуществляется с помощью объекта DataRow. Следующий пример показывает, как осуществить доступ к строкам в таблице. Код этого примера находится в каталоге C7_simpleDatasetSo_. string source « "server^(local;\\NetSDK;" * "uid=QSUser;pwci=O.SPassword;' + fc "database=northwind"; :" string select = "SELECT ContactName, CompanyName FROM Customers"; SqlConnfection conn = new SglConnection(source); Следующий код представляет собой класс SqlDataAdapter, который используется для выбора данных в dataset. SqlDataAdapter сгенерирует предложение SQL и запол- заполнит таблицу Customers в наборе данных результатом, полученным по этому запросу. Класс DataAdapter будет рассмотрен позже. SqlDataAdapter da = new SqlDataAdapter (select, conn) ,- DataSet ds, = new DataSet(); da.Fill(ds,. "Customers"); Обратите внимание на использование индексатора [ ] в DataRow для доступа к значени- значениям в строке. Значение столбца может быть получено с помощью одного из перегруженных индексаторов: по номеру столбца, по имени или DataColumn. foreach (DataRow rowfin ds. Tables t "Customers" \ .RowsI Console.writeLineC" {0}' from Ш", * v ..•' rowfOl, . • „. rOW.[l]); ■-/ -T-r •- -.-: чг
422 Глава 11 Одним из наиболее привлекательных аспектов DataRow является поддержка разных версий. Можно получать для одного столбца разные значения. Версии описаны в следую- следующей таблице. DataRowVersion enuro Описание Current Default Original Proposed Значение, в данный момент присутствующее в столбце. Если не производилось редактирование, это будет то же самое, что и оригинальное значение. При выполнении редактирования это будет последнее введенное допустимое значение. Значение столбца по умолчанию. Значение столбца, первоначально полученное из базы данных. При вызове метода AcceptChanges объекта DataRow это значение обновляется текущим. При изменении столбца можно получить измененное (т.е. предложенное) значение. Если для строки вызвать BeginEdit ( и затем произвести изменения, то каждый столбец будет иметь предложенное значение до тех пор, пока не будет вызван EndEc ;) или CancelEdit (). Версия данного столбца может использоваться по-разному. Например, при обновлении строк в базе данных обычно создается такой оператор SQL: UPDATE Products SET Name = Column.Current WHERE Product ID = xxx ANE TIame = Column.0rigir_3_ ; Очевидно, что такой код никогда не будет откомпилирован, однако он показывает один из вариантов использования первоначального и текущего значений столбца. Для получения из aRow значения треб\емой версии используйте один из методов -. ~?хег[], принимающий значение 1 -__' .-.' :sion в качестве параметра. Информацию о состоянии содержит не то-.ько столбец. Вся строка имеет флаг состо- состояния R^aSt ate, который можно использовать для определения того, какие действия необходимо произвести над строкой при ее передаче обратно в базу данных. Флаг RowSta- te отслеживает все изменения в DataTa^ такие как добавление новых строк, удаление существующих строк и изменение столбцов вн\три таблицы. Когда данные согласовывают- согласовываются с базой данных, флаг состояния строю! используется для выяснения того, какие опера- операции SQL необходимо выполнить. Эти флаги определены в перечислении DataRowState. DataRowState enum Added Deleted Описание Detached Modified Unchanged Строка была добавлена в коллекцию Rows DataTable. Все строки, созданные на стороне клиента, имеют это значение. При согласовании с базой данных это приводит к генерации SQL-оператора Insert. Строка была помечена как удаленная из DataTable с помощью метода DataRow.Delete (). Строка по-прежнему существует внутри DataTable, однако не выводится на экран (если только не был явно установлен DataView). DataView рассматривается в следующей главе. Строки, помеченные как удаленные, будут удалены из базы данных при согласовании. Строка находится в этом состоянии сразу после создания, кроме того, она может быть переведена в это состояние вызовом DataRow. Remove (). Изолированная строка не рассматривается как часть DataTable, поэтому для строки в таком состоянии не будет генерироваться SQL. Строка считается измененной, если изменено значение в любом столбце. Строка не была изменена с момента последнего вызова AcceptChanges().
Доступ к данным с помощью .NET 423 Состояние строки зависит также от того, какие методы были для нее вызваны. Ме- Метод AcceptChanges () вызывается после успешного обновления источника данных (т.е. после внесения изменений в базу данных). Наиболее типичный способ изменения данных в DataRow — использование индек- индексатора. Однако если требуется сделать ряд изменений, необходимо также рассмотреть методы BeginEdit () и EndEdi'- .). Когда внутри DataRow осуществляется изменение столбца, то для DataTable, к которой принадлежит строка, генерируется событие .-lumnChar.g^ng. Это позволяет перекрыть ProposedValue и изменить его нужным образом. Это один из способов проверки данных для столбцов. Если вызвать Begir.FiJ t () до начала изменений, то событие Columnrhan- ging не будет сгенерировано. Это позволяет произвести сразу несколько изменений, а за- затем вызвать EndEaii ) для их сохранения. Если требуется вернуться к первоначальным значениям, вызовите ^ancelEdit (). Строка DataRow может быть связана с другими строками данных. Это допускает со- создание ссылок между строками, что часто используется в сценариях master, detail. Data- Row содержит метод ^etChildRows (). Он возвращает массив связанных строк из другой таблицы в том же DataSet, в котором находится текущая строка (см. ниже). Генерация схемы Существуют три способа создания схемы Х1я DataTable. Это: О Позволить среде исполнения самой выполнить эти действия О Написать код для создания таблицы (таблиц) О Использовать генератор схем XML Генерация схемы во время исполнения Пример DataRow, приведенный выше, содержит такой код для получения данных из базы данных и заполнения DataSet: SglDataAJapter da = new SqlDataA . lect, conn); DataSet -\s = ne _aSet () ; da.Fill -it,, "Cu: ls") ; Он прост в применении, но имеет j ..д недостатков. Например, что делать с именами столбцов? В некоторых случаях необх. димо переименовать физический столбец в базе данных (скажем, ?К") и дать ему б< iee дружественное имя. Можно переименовать столбцы внутри предложения SQL. например ^Н1£ ГТ PID AS PersonID FROM Per nTable, но SQL не терпит имен с пробелами, которые так нравят- нравятся пользователям. Не рекомендуется переименовывать поля в SQL. так как единственное место, где требуется хорошее" имя столбца.— это экран. Другой потенциальной проблемой при автоматической генерации DataTable/Data- Column является то, что вы не имеете контроля над типами столбцов, которые среда ис- исполнения выбирает для данных. Она выполняет хорошую работу по определению корректного типа данных, однако в ряде случаев вам требуется больше прав. Например, вы могли определить перечислимый тип для столбца с целью упрощения пользователь- пользовательского кода для вашего класса. Если принять типы столбцов, которые по умолчанию гене- генерирует среда исполнения, то скорее всего столбец будет иметь тип целого 32-разрядного числа, а не enum с пятью значениями. Последнее и, вероятно, самое неприятное то, что при использовании автоматической генерации таблицы у вас отсутствует безопасный по типO доступ к данным внутри Data- Table — вы находитесь во власти индексаторов, возвращающих экземпляры object, а не производные типы данных. Если вы согласны насыщать свой код выражениями приведения типов, пропустите следующие разделы. Схема, создаваемая вручную Генерация кода для создания DataTable, заполняемой связанными DataColumn, не пред- представляет проблем. Примеры в этом разделе осуществляют доступ к таблице Product из базы данных Northwi nd. Код находится в примере 08_Manuf acturedDataSet.
424 Глава 11 Products Column Name ProducHD ProductName SupplierlD Category-ID QuantityPerUoit UnitPnce UnitsInStock UnitsOnOrder Reorderleve! Discontinued | Data Type int nvarchar int int nvarchar money smaftnt smaJint smaSmt bit | Length | Allow Nulls 4 •10 •1 •1 20 8 2 2 г I у/ s/ V s/ y/ 1 zi Следующий код создает DataTable, которая соответствует приведенной выше схеме, public static tfoid ManufactureProductDataTable(DatSSet ds) "Products"); ProductID", typeof(int))j; ProductName", typeof (string) ) ) ; SupplierlD", typeof(int))); CategoryID", typeof(int))); QuantityPerUnit"v cypeof<string))); UnitPrice", typeof (decimal) )) OnitsInStock" , typeof(short))) ; UnitsOnOrder", typeof(short))) ; Reorder Level", typeof(short))j; Discontinued", cypeof,(bool)) 1; PataTable products f new DataTable( products.Columns.Add(new DataColumn(" -.: products.Columns.Add(new DataColumn( " i' products.Columns.Add(new DataColumn(" products.Columns.Add(new DacaColumn(" products.Columns.Add(new DataColumn(" products.Columns.Add(new DataColumn'" -' .„products. Columns. Add (new DataColumn' " Products.-Columns. Add (new ''DataColuxn products.Columns. Add (new DataCcl—T-. " products.Columns.Add new DataCol-Ли". " . !;A., > -6sfables. Add (products); Для того чтобы использовать новое определение таблицы, необходимо следующим образом изменить код в примере DataRcv;: string source = "server=l_calhost;" - "integrated secure;y=sspi;" ~ "database=Northwinc■, string select = "SELECT * FROM ^Products"; SglConnection cor. = new SglConnectici(source , SqlDataAdapner cmc = new SglDataAdap-er(select, con) ; DataSet ds = new rataSet ' ; ManufactureProQuctDacaTable(ds); cmd.Fill.(ds, "Products"); foreach (DataRow row in ds.TablesfProducts"] .Rows) Console.WriteLinel"■@)■ from {1}", row[0], row[l]); Метод Manuf actureProductDataTable создает новую DataTable, добавляет по очере- очереди каждый столбец и присоединяет полученную таблицу к списку таблиц DataSet. Data- Set содержит индексатор, который принимает имя таблицы и возвращает DataTable. Приведенный пример, однако, не является безопасным по типу, поскольку для полу- получения данных используются индексаторы столбцов. Лучше, если бы класс (или набор классов), производный от DataSet, DataTable и DataRow, определил бы безопасные по типу аксессоры для таблиц, строк и столбцов. Вы можете создать этот код и в результате получите действительно безопасные по типу классы для доступа к данным. Если вы не желаете генерировать безопасные по типу классы самостоятельно, по- помощь рядом. Платформа .NET поддерживает использование схем XML для определе- определения DataSet, DataTable и других классов, рассмотренных выше. За более подробной информацией обратитесь к разделу, посвященному схемам XML.
Доступ к данным с помощью .NET 425 Отношения между данными При написании приложения часто требуется получить и поместить в кэш различные таб- таблицы с информацией. DataSet представляет собой контейнер для этой информации. В обычном OleDb необходимо было реализовывать странный диалект SQL для получения иерархического взаимоотношения между данными, да и самому провайдеру приходи- приходилось применять нестандартные подходы. С другой стороны, класс DataSet с самого начала разрабатывался для обеспечения уве- уверенных связей между таблицами. Для примера вручную сгенерируем и заполним данными две таблицы. Даже если у вас нет под рукой SQL Server или базы данных Northwind, вы все равно сможете запустить этот пример. Код доступен в каталоге ??_DataRelationships: DataSet ds = new DataSet("Relationships"); dsj.Tables.Add(CreateBuildingTable()) ; 4s.Tables.Add(CreateRoomTable()); Cs. Relat ions. Add ( " Rooms " , ds.Tables["Building"].Columns [ "BuildinglD1'], ds.Tables["Room"].Columns["BuildinglD"]); Таблицы содержат основной ключ и поле имени, а таблица rooms имеет BuildinglD в качестве внешнего ключа. Building JtjJ BuildinglD iJName Ю———I Л ."'щмТХЗ Room l_£JRoo<nID rjName Pj BuildinglD Затем в каждую таблицу добавим некоторые данные по умолчанию. После этого можно производить итерацию по Buildings и Rooms с помошью приведенного ниже кода. foreach (DataRow theBuilding in ds.Tables!"Building"!.Rows) { DataRowU children = theBuilding.GetChildRows I'Rooms") ; int roojnCount = children,Length; Console.WriteLine (^Building {0 contains {1} rQom{2.}", ' theBuilding["Name"], roomCount, roomCount > 1 "s" : " .„ s'7/ проходим в цикле по комнатам foreach (DataRow theRoom in children) Console.WriteLine("Room: {0)", theRooml'Kame"]) ; ) Существенное отличие между -d'-aSet и более старым иерархическим recordset заключается в организации связи. В иерархическом recordset связь представлена в виде псевдостолбца в строке. Этот столбец и является recordset, по которому можно производить итерацию. DataRowf] Children = theBuilding.GetChildRows("Rooms"); Однако в случае ADO.NET связь осуществляется путем вызова метода GetChi Id- Rows (). Этот метод имеет несколько перекрытых версий. В примере, приведенном выше, применяется имя связи для перехода между родительскими и дочерними строками. Он возвращает массив строк, который можно обновить, используя индексаторы (см. выше). Более интересным в отношениях между данными является то, что можно осуществ- осуществлять переход в обе стороны. Разрешается перемещаться от родительских строк к дочер- дочерним, а также можно найти родительскую строку (или строки) по дочерней строке, используя свойство ParentRelations таблицы: foreach (DataRow in ds,Tables["Room"].Rows) i DataRowt] parents = theRoom.GetParentRows("Rooms"); foreach (DataRow theBuilding in parents) Cdhsole.WriteLineC'Room {0} is contained in building A)", , .■ ■ ,. .- ■ theRoom ["Neune"] ,
426 Глава 11 thefiuilding["Name"]); ) Существуют два метода с различными перекрытыми версиями для получения родитель- родительских строк: GetParentRows (возвращает массив из нуля или более строк) и GetParentRow (получает единственную родительскую строку для данного отношения). Ограничения для данных DataTable полезна не только для изменения типа данных столбцов, созданных на сторо- стороне клиента. ADO.NET позволяет создать набор ограничений для столбца (или столбцов), которые затем будут использоваться с целью приведения в жизнь правил для данных. На данный момент среда исполнения поддерживает следующие типы ограничений. Ограничение Описание FcreignKeyConstraint Заставляет использовать связь между двумя DataTable в DataSet. UniqueConstraint Гарантирует, что данный столбец является уникальным внутри DataTable. Как свойственно таблице в реляционной базе данных, вы можете указать основной ключ, который может состоять из одного или нескольких столбцов DataTable. Приведенный ниже код создает основной ключ для таблицы Product. Код примера доступен в каталоге 08_ManufaccurerataSet: public static void ManufacturePrimaryKey(DataTable dt) i '...." ' DataCo,lumn{] pk = new DataColumnll]; pk[O] = dt\polumns["ProductID"] ;. dt.PrimaryKfey" s=, pk; } Так как основной ключ может содержать несколько столбцов, его тип — массив из OataCt>iuru-. Основной ключ таблицы может быть представлен в виде этих столбцов путем простого присвоения массива столбцов данному свойству. Основной ключ таблицы — это лишь одна форма ограничения. При добавлении основного ключа к DataTatj.e среда исполнения также генерирует уникальное ограни- ограничение для столбца(ов) ключа. Это происходит потому, что на самом деле нет типа огра- ограничения FrimaryKey — основной ключ является уникальным ограничением для одного или нескольких столбцов. Для просмотра ограничений таблицы можно произвести итерацию по Constraints- Collection. Именем автоматически сгенерированного ограничения, созданного приве- приведенным кодом, является Cor.s'raintl. Это совершенно бесполезное название, поэтому лучше сначала создать в коде ограничение, а затем определить, какие столбцы составляют основной ключ. Именованные ограничения более понятны, но большинство баз данных формируют загадочные имена для ограничений. Следующий код перед созданием основного ключа именует ограничение: DataColumr. pk = new DataColunm[l] ; pk[O] = dt.Columns["ProductID"]; dt>Constxaints,Add.(new UniqueConstraint ( "PK_Products", pk[O])) ; dt.PrimaryKey = pk; Уникальные ограничения можно применять к любому числу столбцов. Помимо уникальных ограничений, DataTable может содержать ограничения Foreign Key. Они в основном используются для обеспечения связей master/detail, но могут также применяться для репликации столбцов между таблицами в том случае, если ограничение установлено правильно. Отношения master/detail — это такие отношения, когда существу- существует одна родительская запись (скажем, Order) и много дочерних записей (Order Lines), связанных между собой основным ключом родительской записи. Ограничение внешнего ключа может работать только для таблиц внутри одного Data- Set. Следующий пример использует таблицу Categories базы данных Northwind и создает ограничение между ней и таблицей Products.
Доступ к данным с помощью .NET 427 Products J. ProductID J ProductName j Supplier ID CategorylD QuantityPerUnit UnuPrice Un»$InStock UnKsOnOrd» Reorderlevel Discontinued JJJ Categories Column Name CategorylD CategoryName Description Picture 4 Data Type |ur.gth |Allow Nulls in! nvarchar nUxt 1 image I» F Сначала необходимо сгенерировать новую таблицу для таблицы категорий. Пример 08_ManufactureDataSet содержит такой код: DataTable categories = new DataTable("Categories"); Categories. Columns. Add (new DataColumnl "CategorylD", tyj, (int))); categories.Columns. Add (new DataColumn("CategoryName", tyr f (string) ) ) ,- categories.Columns.Add(new DataColumn("Description", type^ =tring))); categories,Constraints.Add(new UniqueConstraint("PK_Categori categories.Columns["Cacegoryir ); categories.PrimaryKey = new DataC^-.I т .[1] Е ./IjiosI "CategorylD" ] }; Последняя строка кода создает основной ключ для таблицы Categories. В данном случае основной ключ представляет собой один столбец, однако при помощи приведен- приведенного синтаксиса массива можно создать ключ из нескольких столб нов. Теперь необходимо создать ограничение между двумя таблицами- DataColumn parent = ds.Tab.es."Ca^egoi-c= DataColumn child = ds .Tables ["Produces ■_ .<"■ _■ ForeignKeyConstraint fk = ?. new ForeignKeyConstraint("FK_Producr_Cace=cr^ ID", pars fk.UpdateRuie = Rule.Cascade; fkiDeleteRuile = S jle.SetNull,- ds .^Tables [ "Products" ] .Constraints.Add(fk) ; nns["CategorylD"]; j--egoryID"] ; child); , но лучше Это ограничение применяется для связи между Categories. ~- --тогуТГ и Pi tegorylD. Существуют четыре различных конструктора для ~ использовать те, которые позволяют указать имя ограничения. Помимо задания определенного типа ограничений для родительской и дочерней таб- таблиц, можно указать, что должно происходить при обновлении столбца этого ограничения. Приведенный пример устанавливает правила для обновления и удаления. Эти прави- правила используются, когда выполняется действие над столбцом (или строкой) внутри роди- родительской таблицы — правило определяет, что должно произойти со строками внутри дочерней таблицы. Наиболее очевидный пример: что происходит с дочерними записями при удалении родительской записи. Могут использоваться четыре разных правила: □ Cascade. Если родительский ключ обновлен — скопировать новое значение клю- ключа во все дочерние записи. Если родительская запись удалена — удалить также все дочерние записи. Это режим по умолчанию. □ None. Никакого действия. Этот режим приведет к появлению потерянных строк в дочерней таблице. О SetDefault. Для каждой дочерней записи столбцы внешнего ключа принимают значение по умолчанию, если таковое было указано. О SetNull. Ключевые поля всех дочерних записей принимают значение DBNull. Следуя соглашениям по именованию, которые использует Microsoft, это правило должно называться SetDBNull, однако кто будет проверять. Ограничения осуществляются внутри DataSet только в том случае, если свойство Enf orceConstraints установлено в true. Мы рассмотрели основные классы DataSet и показали, как вручную создать каждый из них. Существует другой способ определения таблиц DataTable, DataRow, DataCo- DataColumn, DataRelation и Constraints — использование файлов схемы XML и инструмента
428 Глава 11 XSD, который присутствует в комплекте поставки .NET. В следующем разделе рассказы- рассказывает о том, как определить простую схему и сгенерировать безопасные по типу классы для доступа к данным. Схема XML XML четко закреплен в ADO.NET — в самом деле, форматом для удаленной передачи дан- данных между объектами теперь является XML. В среде исполнения .NET можно описать Га;аТаЫе с помощью файла определения схемы XML (XSD). Более того, можно опреде- определить весь Da -q^ эс, содержащий несколько Da эТаЫе, набор отношений между этими таблицами и включить другие сведения для полного описания данных. Среда исполнения предлагает новый инструмент, который преобразует созданную схему из файла XSD в соответствующие классы для доступа к данным, такие как безопас- безопасный по типу класс TataTable Prrduct, показанный выше. Начнем с простого файла XSD, который содержит ту же информацию, что и пример products, а затем включим в него дополнительную функциональность: <?xml versior.= Ч . О" encoding="utf-8"?> <:xsd: schema id="Products" targetNamespace="http://tempuri-org/product.xsd" xmlns="http://tempuri.org/XMLSchemal.xsd" xmlns:xsd="http://www.w3.org/20Cl/XMLSchema" > <xsd:element name="Product"> <xsd:cc PicxTyp > <xsd:sequence> <xsd:element <xsd j element <xsd: cement <xsd:e* lent <xsd:eT iit <xsd:ele: . H • xsd:eler xsd:eleme _ -i 3:elemer._ <>. 1: element </xsd: quence> ; </xsd:con;p . ?xt"ype> </xsd:element> i;/xsd:schema> name= ,name= name= name= name= name= najne= name= -.ame= ' ie= "ProductID" "PrOductName "SupplierID" "Category JV>" "(« 'е-. "" . "Г "■ :" " Ur - "Un_ _ . _cr. type="xsd:int" /> >" type= "xsd: string" type="xsd:int" /> type="xsd:int" /> /> ij-t" type=" xsd: string1 cype="xsd:ru*nber" / к" type="xsd:shore" r" type="xsd:short" "Reor e _evel" type="xsd:short" "Disco ;. „ue > /> /> /> d" type="xsd:boolean" /> В этом файле мы не учитываем ьсех возможных вариантов, поскольку это тема отдель- отдельной книги. Файл описывает схем% с атрибутом id, установленным в Products. Определя- Определяется сложный тип с именем Pr ct, который состоит из некоторого числа элементов, по одному для каждого из полей в таблице продуктов. Эти элементы отображаются на классы данных следующим образом. Схема Products отображается на класс, производный от Da .adet; сложный тип product — на класс, про- производный от ^ataTabj. _; каждый подэлемент — на класс, производный от DataColumn; коллекция всех столбцов — на класс, производный от DataRow. Платформа .NET содержит инструмент, который создаст код для этих классов по за- заданному файлл' XSD. Назначение этого инструмента — обработка файлов XSD, поэтому сам инструмент называется XSD.EXE. Генерация кода при помощи XSD Предположим, что вы сохранили представленный выше файл как Product.xsd. Этот файл можно преобразовать в код с помощью команды, введенной в командной строке: E:\ProfeSSxonal CftNCiMpter 9\Code\XSD DataSet>xsd product.xsd /d Microsoft <B> Xol Schenas'DataTypes support utility microsoft -NET Framework Version 1.0.2728.2] Copyright <C> Microsoft- Corp 2080.. flll rights reserved, Writing file '.product.cs' . ' ' E:\Professional CttvClwpte»- '9\<iode^SD DataSet>
Доступ к данным с помощью .NET 429 Можно указывать в команде различные ключи для изменения генерируемого результата. Некоторые из наиболее часто используемых ключей приведены в таблице: Ключ Описание /dataset (/d) Создает классы, производные от DataSet, DataTable и DataRow. /language :<language> Позволяет выбрать язык, на котором будет записан выходной файл. По умолчанию это С#, но можно указать VB для Visual Basic или JS для JavaScript. /namespace: <namespace> Определяет пространство имен, в котором должен располагаться генерируемый код. По умолчанию пространство имен не задано. Сокращенная версия результата работы XSD для схемы products показана ниже. Здесь приводятся лишь наиболее важные аспекты, а также изменено форматирование, чтобы код поместился на двух страницах. Если вы хотите получить полный результат, запустите XSD для схемы products (или своей собственной) и просмотрите созданный файл . сз. Пример, находящийся в каталоге 1"_Х. Z_DataSet, включает в себя полный исходный код и файл ~Рг^ с :^ s .'ASZ. И ,- ~~ // <autogenerated> ■„ ._ // This code was generated by a tool. // Runtime Version: 1.0.2728.2 // // Changes to this file may z .se „.c^rr. cehavior and will be lost if IJ che code is regenerated. II </autogenera~ea> // using System; using System.Data; using System, Run-ime.Serializat_on; public 'class Products : SysL^ri.Data.DataSet { private ProduotDataTable z.ibleProduct; public Produces { thi s.lnitClassO ; } public ProductlDataTable Product { get { return this.tablerroduct; } } private void InitClasst) { this.DataSetName = "Products"; this.Namespace = "http://tempuri.org/product.xsd"; this.tableProduct = new ProductDataTable() ,- this.Tables.Add(this.tableProduct); } /* public class ProductDataTable : DataTable, System.Collections.IEnumerable public class ProductRow : DataRow •/ Код разбит на три части. Выделенные жирным и закомментированные определения ProductDataTable и ProductRow показывают местоположение двух вложенных классов, которые будут реализованы позже. Конструктор Products вызывает закрытый метод Initciass, который конструирует эк- экземпляр производного от DataTable класса ProductDataTable, и добавляет таблицу в кол- коллекцию Tables набора DataSet. Доступ к таблице product можно осуществить с помощью следующего кода: DataSet ds = new Products!); DataTable produces = ds.Tables["Produces"];
430 Глава Или используя свойство Product, доступное в производном объекте DataSet: DataTable products = ds.Product; Так как свойство Product строго типизировано, следует использовать ProductData- ProductDataTable, а не ссылку DataTable, показанную выше. Приведем код класса ProductDataTable: : public delegate void ProductRowChangeEventHandler(object sender, ProductRowChangeEvent e) ; public class ProductDataTable : DataTable, System.Collections.IEnumerable { private DataColumn columnProductID; II Другие private DataColumn убраны для упрощения private. DataColumn columnProductName; private DataColumn columnSupplierlD; private DataColumn columnCategorylD; private DataColumn columnQuantityPerUnit; private DataColumn coiumnUnitPrice; private DataColumn columnUnitsInStock,- private DataColumn columnUnitsOnOrder; private DataColumn coluriReorderLevel; private DataColumn col\iTriOiscontinued; internal ProductDataTable : base("Product") { this.InitCiassO; }. public int Count ' { get { return this.Rows.Count; } } internal DataC 1.in ProductlDColumn { get { return this.columnProductlZ; // Остальные аксессорк строк удалены для упрощения // Остальные внутренние аксессоры полей улалены для упрощения internal DataColumn ProductNameColumn { get { return this.columnProductName; } } internal DataColumn SupplierIDColumn { get { return this.columnSupplierlD; } *} internal DataColumn CategorylDColumn { get { return this.columnCategorylD; internal DataColumn QuantityPerUnitColumn f get.
Доступ к данным с помощью .NET 431 return this.columnQuantityPerUnit; } } internal DataColumn UnitPriceCqlumn { get { return this.columnUnitPrice,- } I ' Internal DataColumn UnitsInStockColumn i get { return thj.s.colvimnUnitsInStock; } } internal DataColumn UnitsOnOrderColumn { get { return this.columnUnitsOnOrder; internal DataColumn ReprderLevelColumn с " ■ ' get i return this.columnReorderLevel; . 3 } internal DataColumn DiscontinuedColtimn f ; get < return this..-columnDisco"tinued; public ProductRow thisfint index] f get { return ((ProduetRow)(thi s.Rows[index])); } } public event ProductRowChangeEver.;:Handler Pr_cuctRowChanged; public event ProductRowChangeEver.cHandler FroductRowChanging; public event ProductRoKChangeEver.cHandler FroductRowDeleted; public event ProductRowChangeEven-Handler ProductRowDeleting; •public void AddProductRow(ProduccRow row) ( this.Rows.Add(row) ; } public ProductRow AddProductRow(int ProductID, string PrductName, int SupplierID, int CategoryID, string QuantityPerUnit, System.Decimal UnitPrice, short UnitsInStock, short UnitsOnOrder, short ReorderLevel, bool Discontinued) ProductRow rowProductRow = ( (ProductRow) (this.NewRowf) )),- rowProductRow.ItemArray = new Object!] { :ProductID, ProductName, SupplierlD, CategorylD, OuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued); this.Rows.Add(rowProductRow); return rowProductRow;
432 Глава 11 public System.Collections.IEnuraerator GetEnumerator() С return this,Hows.GetEnumerator() ; } private void InitClas's() { this-columnProductlD = new DataCplumnCProductlD*,-. typeqf{Int) , " " , System .'Da,ta. MappingType. Element) ,- this.columnProductip.AllowDBNull = false; this.Columns.Add(this.columnProductID); // Другие столбцы убраны для упрощения this.columnProductName = new DataColumnCProductName", typeof(string),"", System.Data.MappingType.Element); this.columnProductName.AllowDBNull = false; this.Columns-Add(this.columnProductName); this.coluimSupplierlD = new DataColumn("SupplierlD", typeof (int) , " " , System.Zata.MappingType.Element); this.columnSupplierlD.All,. wDBNull = false; this .Columns. Add (this. colur"iSupplierID) ; this.columnCategorylD = new DataColumn("CategoryID", typeof(int), "" , System.Data.McppingType.Element); ch-3 ._. 1_т_ Car = joryrD.AIlowZBNull = false; this.Cclu^i.s.Aidf-.hi^ .cr" inCategorylD) ; this.coIumnQuantityPerl'n^r = new DataColumnf "QuantityPerUnit", typeof(string),"", System.Data.MappingType.Element); this.columnQuantityPerUnit.AllowDBNull = false; this.Columns.Add(this.columnQuantityPerUnit); this.coiumnUnitPrice = new DataColamn.t "UnitPrice", typeof(System.Decimal),"", System.Data.MappingType.Element); his.aolumrUnitPrice.AllowDBN^il = false; chis.Columns.Add(this.columnUnicPrice); tr^s.cu'ZumnUr tsInStock - new DataColumn("UnitSlnStock", typeof ishort) ,"", Si'sreni. Data.MappingType. Element) ; th_ = .columnUn__3lnStock.AllcKrB"i.Il = false; this. "olumns .Add, this. coluFT.Ur.itslnStpck) ; this.columnOnits; "Order = r.ew 3ataColumn( "UnitsOnOrder", :ypeofIshor- ,"", Syscer-.Cata.MappingType,Element); .this.col-imnUnitsCr. ■"rder.AlIowDBKull = false; this.ColL—ns.Add(thi3.columnTJr.ics0n0rder) ; •this.colunmReorderI.evel = new DataColumri( "ReorderLevel", typeoffshort "", 'Sys;: 5П1.Data.Mapp.ngType.Element) ; this-collar-Reorder L . ■•el.AllowDBNull = false; this.Columns.Add(thi=.columnReorderLevel ; this..coIumnDiscontir .ed = new DataColumn( "Discontinued", typeof (bool) , ' Systerr.Data.MappingType.Element) ; this.colu.-nnDiscontir.i--;d.Allov;DBNull = false; this.Columns.Add(this.^olumnDiscontinued.; ) public ProducCRow NewProductRowl) { return (ProductRow)(this.NewRowf))); } protected override DataRow NewRowFromBuilder(DataRowBuilder buider) { return new ProductRow(builder); } protected override System.Type GetRowTypel) { return tygeof(ProductRow); protected ovefride void OnRowChangedtDataRowChangeEventArgs e) { base.OnRowChanged(e); if ((this.ProductRowChanged != null)) { thi s.ProductRowChanged(this, new ProductRowChangeEvent(((ProductRow)(e.Row)), e.Action));
Доступ к данным с помощью .NET 433 protected override void .OnRowChangingfDataRowChangeEventArgs e) I { base.OnRowChanging(e); ' if ((this.ProductRowChanging != null)) ; { thi s.ProductRowChanging(thi s, new ProductRowChangeEvent(((ProductRow)(e.Row)), e.Ac . on) protected override void OnRowDeleted{t>ataRowChangebventArgs e) I base.OnRowDeleted(e); if ((this.ProduetRowDeleted != null)) { this.ProductRowDeleted(this, new ProductRowChangeEvent(((ProductRow)(e.Rcw)), e.Acсion)) protected override void OriRowDeletingtDataRowChangeEv „.ltArgs e) < base.OnRowDeleting(e) ; if ( (this.ProductRowZeleti.ig != nul_ { this.ProductRowDeleting(this, new ProductRowChangeEvent (( (ProductRow) (e.R' л . e.Actior. public void RemoveProductRowlProductRow row) { thi s.Rows.Remove'row\ ; Класс ProductDataTable, производный от DataTab и включающий в себя интер- интерфейс 1ЕпшпегаЫз, определяет закрытый экземпляр г aColuirb. для каждого из столб- столбцов внутри таблицы. Они инициализируются из конструктора плтем вызова закрытого метода InitClass. Для каждого столбца указан внутренний аксессор, который использует класс DataRow (см. ниже). Добавление строк в таблицу выполняется п\тем перегрузки методов AddProductRow(). Один из них принимает уже сконструированныи LataRow и возвращает void. Следующий принимает набор значений, по одному для каждого столбца в DataTable, конструирует новую строку, устанавливает для нее значения, добавляет строку в DataTable и возвращает ее вызывающему. Такие совершенно разные функции, по моему мнению, не должны иметь одинаковых имен. Точно так же, как член InitC ас э в классе, производном от DataS^ , добавляет табли- таблицу в DataSet, член InitClass для t roductl acaTable добавляет столбцы в DataTable. Свойства для каждого столбца устанавливаются соответствующим образом, после чего столбец помещается в коллекцию столбцов. Параметры DataCc umr рассматриваются ниже. Метод NewRowFromBuider вызывается внутри класса из метода DataTable NewRow (). Здесь он создает новую строго типизированную строк)-. Экземпляр DataRowBuider созда- создается с помощью DataTable, а его члены доступны только внутри сборки System.Data. Последний класс, который необходимо рассмотреть, это класс ProductRow, производный от DataRow: public class PrqQuctRow : DataRow ; { private ProductDataTable tableProduct; Internal ProductRow(DataRowBuilder rb) : base(rb) { .,.; this.tableProduct = ((ProductDataTable)(this.Table)); . } public int Productlp { get '.". J..,•■„■ •■•:' .return ((int) {this[this. tableProduct. productlDColumn]));
434 Глава 11 set { this[this.tableProduct.ProductlDColumn] = value; } } // Остальные свойства столбцов убрана для упрощения public string ProductName { get { return ((string)(this[this.tableProduct.ProductNameColumn])); } set { this[this.tableProduct.ProductNameColumn] = value; } ) public int SupplierlD { get { re :tr (f-'-t) ^Ык [ihis. tableProduct. SuDDlierlDCoIumn])); } set { . , this[this.tableProduct.SupplierlDCo-umn] = value; } } public int CategorylD { get f return )(this [thi= .rab-el-roduct. Category IDColumn])); } set { this[this.tableProduct.Ca-.egorylDColumn] = value; } } public string QuantityPerUnit { get I return '(string).this[this.-ableProduct.QuantityPerUnitColumn])); } set { this[this.tableProduct.QuantityPerUnitColumn] = value; } . } public System.Decimal VnitPrice { get { return (.(System.Decimal) (this[this.tableProduct.UnitPriceColumn])); } set { this[this.tableProduct.UnitPriceColumn] = value; } } public short UnitsInStock {' get { return ((short)(this[this.tableProduct.UnitsInStockColumn])); ■ } ...
Доступ к данным с помощью .NET 435 set ( this [this. tableProfluct .UnitsItiStockCol'uinn.] = value; } } public short UnitsOnOrder { get { return ( (short) (this [this. tableProduct. onitsOnOrderColunn]))'; > .* set f this[this.tableProduct.UnitbOnOrderColumn] = value; ) } public short- Reorder Level. { ?et 4 return ( (short) (this [this. tablePr ><~.uc-_ .ReorderLevelColuran]) ) ; } set { this[this.tableProduct.ReorderLeveJColumn] = value: } ) public bor.. Discontinued { "et return ( (boo*, thisithis.tableProduct.DiscontinuedColumn.})); set i chis[this.tablePrbduct.DiscpntinuedColuma] = value; public clas - ProductRowChar.geEvent : S^entArgs { s private FroductRQw ever.cRow; '■+ private SyLiem.Data.DacaRowActior. eventAction; public Proc_^tRowCha-;geSyent(ProductRow row, OataRowAction action) •; { this.eventRow = row; this.ever-.Actipn = action; } public ProductRow Row { get { return this.eventRow; } •} public DataRowAction Action { gee { retirn this .event Act ion; T } ' } Итак, мы написали код для классов доступа к данным. Используем эти классы для по- получения данных из таблицы products и их отображения на консоли: using System; using System.Data; using System.Data.SqlClient;
436 Глава 11 public class XSD_DataSet public static void MainO string source = "server=(local)' \NetSDK;" + "^id=QSUscr;pwd=QSPassword;" + "database~Northwi ld"; string select = "SELECT * FROM Products"; SqlConnection conn = new SqlConn^ctionlsourre); SqlDataAiapter da = new riqlDataAdapter(select, conn); Products ds = new Products!); da.Fill (ds, "Product"); foreach (Products,ProductRow row in ds.Product) Ccisoie.WriteLineC ■ {0} ■ from {1)", row;. PrpductID, row-. ProductName); Интересующие нас строки выделены. Результат, полученный из XSD-файла, содер- содержит производный от DataSet (Products I класс, который создается и заполняется при помощи адаптера данных. Выражение foreach использует строго типизированный ProductRow и свойство Product, которое возвращает таблицу product. Для компиляции примера исполните команды: xsd product.xsd /d esc /t:exe /debug+ /r:Systera.dll /r:System,Data.dll /recurse:*.CS Первая строка генерирует файл .cs из XSD, а команда esc использует параметр /recurse: * . дчя просмотра всех файлов .cs и для добавления их в окончательную сборку. Заполнение набора данных После того как вы полностью определили схему вашего набора данных, содержащую Г ataTable, Г ataColuir-.. onstraints и все остальное, необходимо заполнить DataSet какой-либо информацией. Существуют два основных способа чтения данных из внешнего источника и помещения их в JataSet: з Использование адаптера данных О Чтение XML Заполнение DataSet с помощью адаптера данных В разделе, посвященном строкам данных, мы уже использовали класс SqlDataAdapter. Приведем код: str^vj select = "SE17 "Г ContaccName, CoinpanyName FROM Customers"; SqLCc ction conn = _iw SqlCcr..:iectior. source) ; SqlDataAdapter da '"= new SqlDataAdapter (select', conn); DataSe" ^ = new CacaS^t(); da.Filllds, "Customers"); Две выделенные строки показывают применение SqlDataAdapter. OleDbDataAdapter практически идентичен по функциональности своему Sql-эквиваленту. SqlDataAdapner и OleDbDataAdapter являются производными от общего базового класса, а не от набора интерфейсов, так же как и большинство других специфичных для SqlClient и Oleob классов. Иерархия наследования имеет вид: System.Data.Common.DataAdapter System.Data.Common.DBDataAdapter System.Data.OleDb.OleDbDataAdapter System.Data.SqlClient.SqlDataAdapter Для того чтобы поместить данные в DataSet, необходима некоторая форма коман- команды, которая выбирает эти данные. Такая команда может представлять собой оператор SQL SELECT, вызов хранимой процедуры или, для провайдера OleDb, команду TableDi- rect. В примере используется один из доступных конструкторов для SqlDataAdapter, который преобразует передаваемое предложение SQL SELECT в SqlCommand и исполняет его при вызове метода Fill для адаптера. Возвращаясь к примерам хранимых процедур, напомним, что были определены команды для вставки, обновления и удаления. Однако не была представлена процедура
Доступ к данным с помощью .NET 437 select. В этом разделе мы ликвидируем пробел, а также покажем, как вызвать хранимую процедуру из SqlDataAdapter для заполнения DataSet данными. Использование хранимой процедуры в адаптере данных Сначала необходимо определить хранимую процедуру и установить ее в базу данных. Код этого примера доступен в каталоге ll_DataAdapter. Хранимая процедура select выгля- выглядит следующим образом: CREATE PROCEDURE RegionSelect AS ; SET NOCODNT OFF i / SELECT-,;'*' FROM Region; , . ;_ ., GO ' .' - 'Ч|-' т.'' ■- ■'■ Пример является тривиальным, в данном случае хранимая процедура не дает ника- никаких преимуществ по сравнению с использованием непосредственно оператора SQL. Эту SQL-процедуру можно набрать в анализаторе запросов SQL, либо можно запустить файл StoredProc.sql. Теперь необходимо определить SqlCommand, которая исполнит эту хранимую про- процедуру. Код очень простой, и большая его часть уже приводилась выше: private static- SqlCommand GenerateSelectConunand(SqlConnection conn) с ; SqlCommand aCommand = new SqlCoitmand( "RegionSelect", conn); > aCommand. CommandType = CommandType.StoredProcedure; j aCommand .UpdatedRowSource =■ OpdateRowSource.None; * return" aCommand; i Этот метод генерирует SqiCrr!-.:. которая при исполнении вызовет процедуру RegionSelect. Остается лишь подключить эту команду к .r.DataAdapter и вызвать метод Fill: DataSet ds, ■= new DataSet(); // создаем адаптер данных для заполнения DataSet, SqlDataAdapter ca = riew SqlDataAdap,ter(); /■/ Устанавливаем для- адаптера данных команду select da.SelectCommand = GenerateSelectCdmmandteonn). ; da.Fill(d$r "Region"h Здесь создается новый SqlDa-.aAdapter, параметру Select Command адаптера данных присваивается сгенерированная SqlCommand, а затем вызывается Fij.1, в результате чего будет выполнена хранимая процедура и все возвращенные строки будут вставлены в DataTable Region (которая в данном случае создается средой исполнения). Адаптер данных способен делать больше, нежели выбирать данные посредством ис- исполнения каких-либо команд. В части, посвященной сохранению изменений в наборе данных, мы рассмотрим другие возможности адаптера данных. Заполнение набора аонных из XML Помимо генерации схемы для данного DataSet, связанных с ним таблиц и т.п., DataSet может читать и записывать данные в родном XML, например, в файле на диске, в потоке или считывателе текста. Для того чтобы загрузить XML в DataSet, вызовите один из методов ReadXML (). На- Например, показанный ниже метод читает данные из дискового файла: DataSet ds => new DataSet (); ds. ReadXmi,{". WMyData. xml") ; Метод ReadXml пытается загрузить любую встроенную информацию о схеме из вход- входного XML, и в случае обнаружения такой информации он использует эту схему при про- проверке данных, загружаемых из файла. Если внутренняя схема не применяется, DataSet будет расширять свою внутреннюю структуру по мере загрузки данных. Это аналогично поведению Fill в предыдущем примере, который получает данные и конструирует Data- Table на основе выбранных данных.
438 Глава 11 Сохранение изменений, произведенных в наборе данных После редактирования данных в DataSet, как правило, требуется сохранить внесенные изменения. Наиболее типичным примером является получение данных из базы данных, вывод их на экран и сохранение сделанных изменений в базе данных. В менее "подключенном" приложении изменения могли бы быть сохранены в XML-файле, затем переданы на сервер приложения среднего звена и там обработаны для обновления нескольких источников данных. DataSet можно использовать в любом из этих случаев, и, что самое важное, этЗэ дейст- действительно просто. Обновление с помощью адаптеров лонных Помимо команды SelectComnand, которую обычно включает в себя SqlDataAdapter, можно определить команды InsertCommdnd, UpdateCommand и DeleteCommand. Как по- подразумевают их названия, эти объекты являются экземплярами SqlConrand или OleDb- Command для OleDbDataAdapter, поэтому любая из этих команд может быть либо оператором SQL, либо хранимой процедурой. С таким уровнем гибкости можно настроить приложение на применение хранимых процедур для часто использ\'емых команд (например, select и insert) и SQL для редко используемых команд (delete). В этом разделе мы восстановим код хранимых процедур для вставки, обновления и уда- удаления записей Region, используем процедуру Regior.Select, написанную ранее, и приве- приведем пример, который применяет каждую из этих команд для получения и обновления данных в DataSet. Основной код показан ниже, полный исходный код доступен в каталоге 12_DataAdapter2. Вставка новой строки Существуют два способа добавления новой строки в DataTable. Первый метод заключает- заключается в вызове функции NewRow ), возвращающей пустую строку, которую можно заполнить и добавить в коллекцию Rows следующим образом: DataRow r = cs. Tables [ "Region* ]-NewRowO ; г["RegionID"]=999; rI"RegionDescription";="North West"; ds.Tables!"Region'].Rows.Add(r); Второй метод заключается в передаче массива данных в метод Rows. Add (): DataRow r = ds. Tables ["Region"] .Rows..-..Id (new object [] {999, ■ -V rth West"}); Для каждой новой строки, добавленной в DataTab] с, RowState будет установлено в - Tied. Пример выводит список всех записей перед каждым изменением базы данных, ni .этому после добавления строки (любым способом) в DataTable строки будут выгля- выглядеть следующим образом. !W 1 1 2 3 4 999 ending ir. = e- -■ - .. = = - — :.- г-- = Sou^ir- North лезс nto databf=B Unchanged Unchanged Unchanged Unchanged Added Для обновления базы данных с помощью DataAdapter вызовите один из методов Update, как показано ниже: da.Update.(ds, "Region"); Для новой строки будет исполнена хранимая процедура (в данном случае — Regionln- sert), и мы снова приводим записи в DataTable: Xew row updated and new RegionID assigned by database Eastern Unchanged z Western Unchanged 3 Northern Unchanged 4 £outhern Unchanged 5 North West Unchanged Обратите внимание на последнюю строку в DataTable. Мы установили код региона в 999, но после исполнения хранимой процедуры Regionlnsert значение изменилось
Доступ к данным с помощью .NET 439 на 5. Это имеет определенный смысл: часто база данных сама будет создавать для вас основные ключи, и обновленные данные в DataTable присутствуют потому, что определение Command в нашем исходном коде имеет параметр UpdatedRowSource, установленный в UpdateRowSource.OutputParaneters: SqlCommand aCommand = new SqICommandf "Regior.L.sert", j ..-.r : aCommand.CommandType = CommandType.StoredProced_re; aCommand. Parameters. Add (new SqlParamet°r ("@Reg^ rDescription SqlDbType. N'Char, 50, "RegionDescription")); aCommand.Parameters.Addfr.ew SqlParame-.er {"SRegior.i: , SqlDbTyp э.Int, 0, ParameterD^rection.Outpuc false 0, 0, "RegionID", // Определяет столбец SOURCE aCommand.UpdatedRowSource - UpdateRowSource.OutputParameters; Это означает, что, если "ataAdapter исполняет эту команду, выходные параметры должны быть установлены по исходному материалу строки, которым в данном случае яв- является строка в DataTable. Флаг показывает, что данные необходимо обновить,— храни- хранимая процедура имеет выходной параметр, который отображается в : ataRow. Он воздействует на столбец Regie T. . так как это указано в определении команды. Значения для VpdateRo ' .. .а таковы: UpdateRowSource enum Bot Firs-Г =turne None OutputParameters Описание Все выходные параметры плюс первая возвращаемая запись применяются к исходной строке. Это означает, что команда возвращает запись и что содержимое этой записи необходимо совместить с оригинальным ■ itaRow. Это полезно, когда таблица имеет несколько l "олбцов по умолчанию (или несколько вычисляемых столба в1» поскольку после исполнения оператора insert их нео х димо синхронизировать с Г aRow на стороне клиента Все данные, возвращаемые командой, отбрасываются. Все выходные параметры команды отображаются на соответствующие столбцы в DataP .. Обновление существующего столбца Обновление столбца, который уже существует в DataTable, представляет собой случай использования индексатора DataRow либо с именем столбца, либо с его номером, как по- показано в следующем коде: г["RegionDescription"]="North West England"; r[l] .= "North East England"; Эти операторы (в данном примере) являются эквивалентными: Changed RegionID 5 description 1 Eastern Unchanged 2 Western Unchanged 3 Northern Unchanged 4 Southern Unchanged 5 North West England Modified Перед обновлением базы данных для измененной строки выставляется состояние Modified. Удаление строки Удаление строки осуществляется при помощи метода Delete!):
440 Глава 11 г.Delete(); Состояние удаленной строки устанавливается в Deleted. Вы не можете прочитать столбцы из удаленного DataRow, поскольку' они больше не являются допустимыми. Когда будет вызван метод адаптера Update (), для всех удаленных строк будет выполнена команда DeleteCoitimand, которая в данном случае исполняет хранимую процедуру RegionDelete. Запись выходных данных XML В DataSet встроена хорошая поддержка для определения собственной схемы в XML. Можно читать данные из XML-документа и записывать их в XML-документ. Метод DataSet. WriteXml и его перегруженные версии позволяют вам выводить раз- различные фрагменты данных, хранящихся внутри DataSet. Вы можете на выбор вывести только данные или данные и схему. Следующий код демонстрирует оба варианта для приведенного выше примера Region: ds.WriteXMLt"... \\WitboutSchema.. xrrd "); nl!'., XmlWriteModfe.WriteSchemab Файл WithoutSchema.xml показан ниже: <?xml version=.0" standalone="yes"?> <NewDataSet> <F_jion> <RegionID>l</RegionID> <Pegi"iDescriptlon>Eastern < Reg^onDescription>'. <Region> <RegionID>2</RegionID> <RegionDescription>Western </RegionDescription> < ?egion> <Pe '!ion> <-4egionID>3< 'RegionID> <S =jionDescr_ption>Northerr. </Regj_oriDescription> </Reci .;> <Regio:_ <Regi_ _ID>4</Rec_ nID> <Regio. 2script^:r Southern < Дзд: ;escription> </NewDataSet> Закрывающий тег для RegionDescnption находится в правой части страницы, поскольку столбец базы данных определен как NCHAR 50), что означает строку длиной 50 символов, заполненную пробелами. Результат, сформированный как файл v,"ithSchema.xml, содержит схему XML для Г ^aSet и сами данные: <~хг- 'ersion="l. С " = tandaZ. -r.e="yes"?> <XewZ._*_ et> <xsu. hema id= ^^wDataSer" targetKamespace="" xmlns="" x.~l..., :xsd="htr; : /www.v:3 .org/200- XMLSchema" xmlns:nisdatia="iini:schemas-raierosoft^oora:xml-rasda'ta° > <xsc:ele-nent name="NewLataSet" -jdata: IsDacaSet="true" ~ =5ara:Locale="en-GB"> <xsu:jomplexType> <xsd:choice maxOcc^rs="unbo ... 5ed"> <xsd:element name="Region" msdaLa:Locale="en-GB"> <xsd: complex'IVpe> ^xsd:sequence> <xsd:element name="RegionID" .sdata: Aucolncrement=" true" rasdaia:AutoIncrementSeed=" type="xsd:int" rasdata:Ordinal=" /> <xsd:element name="RegionDescription" type="xsd:string" msda':a:Ordinal="l" /> </xsd:seguence> </xsd: complex'IVpe> </xsd:element> </xsd:choice> </xsd:complexType> </xsd:element>
Доступ к данным с помощью .NET 441 </xsd:schema> <Region> <RegionID>l</RegionID> <RegionDepcription>Eastern </RegionDescription> </Region> <Region> <RegionID>2</RegionID> <RegionDescr ipt ion>Wes t ern <.' f<egi onDescr ipt ion> </Region> <Region> <RegionID>3</RegionID> <RegionDescription>Northern </Regic lescription> </Region> <Region> <RegionID>4</RegionID> <RegionDescription>Southern </RegionDescription> </Region> </NewDataSet> Обратите внимание на использование внутри этого файла схемы msdata, которая опре- определяет дополнительные атрибуты для столбцов DataSet, такие как Ant d [ncrement и Auto- Increment Seed. Эти атрибуты соответствуют свойствам, определяемым для DataColumn. Работа с ADO.NET В этом разделе рассматриваются некоторые общие сценарии, возникающие при разработке приложений доступа к данным с помощью ADO.NET. Многоуровневая разработка Создание приложения, предназначенного для работы с данными, обычно осуществляется путем разбиения приложения на уровни. Как правило, используется трехуровневая модель: уровень приложения (т.е. клиентская часть), уровень служб данных и сама база данных. Одна из сложностей, связанных с этой моделью, заключается в определении того, ка- какие данные необходимо перемещать между уровнями, и в выборе формата, в котором они должны передаваться. В ADO.NET эти проблемы исключены, поскольку поддержка такой архитектуры встраивалась в него изначально. Копирование и слияние донных Пытались ли вы когда-нибудь копировать весь набор записей OleDb? В .NET это легко: DataSet source = {some dataset}; DataSet dest = source.Copyt); Будет создана точная копия исходного OataSet — все : ataTable, DataColumn, Data- Row и Relation будут скопированы, и все данные будут находиться точно в таком состоя- состоянии, в каком они находились в источнике. Если необходимо скопировать схему DataSet, можно попробовать следующее: DataSet source = {some dataset}; DataSet dest = source.Cloned ; Будут скопированы все таблицы, отношения и т.п., однако на этот раз все скопированные DataTable будут пустыми. Что может быть проще? Общее требование при написании многоуровневой системы на основе Win32 или Web — перемещать между уровнями как можно меньше данных. Вообще говоря, следует передавать по сети наименьший объем данных, поскольку это снижает потребление ресурсов. Для удовлетворения этому требованию DataSet имеет метод GetChanges (). Этот простой метод выполняет большой объем работы и возвращает только измененные строки набора по сравнению с исходным DataSet. Это идеальный способ передачи данных между уровнями, поскольку по проводам необходимо переслать минимальный набор данных. Следующий пример показывает, как получить DataSet с изменениями: jQataSet source = {some dataset}; „. .' DataSet destr= source.GetChanges.(); t **s * .■
442 Глава 11 Все тривиально. Внутри же немного интереснее. Существуют два метода GetChanges, последний принимает значение из перечисления 2„ RowSt ate и возвращает только стро- строки, которые ее ответствуют этому состоянию (или состояниям). GetChanges () вызывает GetChanges .tted , ".-. iried b.dJL_. ,. Этот метод сначала проверяет, были ли вы- выполнены изменения, вымывая На: С" nges. Если изменений нет, сразу же возвращается null. Следующая операл.ш — клонирование гегмцего " Set. После выполнения этого действия для нового ^aSet устанавливается " Force' „rain.. = false (что приво- приводит к игнорирован ю нарушений ограничения и в новый DataS - копируется каждая измененная строка хш каждой таблицы. Полученный " a^aSet, содержащий только изменения, можно передать на другой уровень для бработки. После записи в "а данных набор данных с изменениями может быть возвращен вызывающем)- (например, если требуется передать некоторые выход- выходные параметры, значения которых обновляются хранимыми процедурами). Эти измене- изменения могут быть затем объединены с оригинальным DataSet с помощью метода Merge (). Последовательность действий показан i на рисунке: Клиентские 1н-'3е1 ь Уровень служб flaf Г Изменения DataSet !г - -Обновления новые данные— База данных Клиент готови шные, вызывая ..^.iges. Они переда отся на уровень служб дан- данных, KOTopu'ii обнов. ■ базу данных. Ь .измененные столбцы возвращаются клиенту, гея с cyщecтв^ , ш набором данных. где измененля объеди. Генерация ключа для SQL Server Хранимая проце.» pa Regio: " L (см ue) была дним из примеров генерации зна- значения основного к. ча при вс~ - ке в баз1 .днных. Эт _ метод генерации ключа доволь- мым, П( ому для pea. ьного приложения необходимо ерации :_ ючей. о опре лить стол(\ ц Identity и возвращать значе- ~уры. С е ^ющая хра.1имая процедлра демонстрирует, к!цы С Dries базы данных K^rthwind. Наберите осов SQL или выполните файл StoredProcs. sql из но груб и не являем разработать другук Первое, что хоче 'ие @@IDEKTITY ИЗ ..г 1 тго можно реали текст роцедуры в ан каталог CREATE -> часштабир ратегию ~. сделать МОЙ Пр ь для 1 .. .аторе з ys: "URE С"". rylr..-- @Cate■- : ryNaroe NVARCHAR (i 5 'Desс lotion NTEXT, SCate--1] у ID INTEGER O'JTPUTJAS SET NOCO1; OFF; INSERT I- . ■ C- sg j e: VALUES ar ;■_ \arr SELECT SCe. . i г- z; .j "/War oti- . sc ot GO В результате будет до "явлена i о >ая сч рока l .аблнцу Ся ?до -у и сгенерированный основной ключ будет nu3i пег bl_ i тдем\'. Fipoi сдуру можно протестировать, на- набрав в анализаторе ;«1,;оа , SQL залр v- ЕХс УС ^c -у I 'at ID OUTPUT; При ис ei ш гюго ..абора кима.ш п таблицу 'г: п.. будет вставлена новая строка, возвр^и ел - текущей записи, koi _.i- ir "нем будет выведена на экран.
Доступ к данным с помощью .NET 443 Несколько месяцев спустя кто-то (а может быть, вы сами!) решает добавить простой журнал аудита, в котором будут фиксироваться все изменения и модификации, касающи- касающиеся имени категории. Вы определяете таблицу, которая будет содержать новое и старое значение Category: ' Categories 7|CateQ0t)'ID Categr- * эгпе | Category Audit * Column Nb -e Date Тур- AuditIB Category 1Г OkWame irtf «it. Сценарий для создания этой таблицы включен в файл StoredPr ~ _^ . sql. Столбец AuditID определен как столбец TDENTI-V. После этого вы создаете пару триггеров, ко- которые будут записывать изменения поля goryNarr":: CREATE TRIGGER CategorylnsertTrigger ON Categories AFTER UPDATE AS INSERT INTO CategoryAudit'CategcrylD, OldNarae, NewName) SELECT olc.CategorylD. old.С ^egoryName, n~w.CaLegoryName FROM Deleted AS o^d. Categories *-._ new WHERE old.Category ZZ = new.С :■ - ZZ>; GO Для тех. кто пользовался хранимыми процедурами Oracle, поясним, что SQL Server не имеет концепции ОГ " и NEW строк. Вместо этого для т и. 7е insert формируется в па- памяти таблица по имени ". serted, а для удалений и обновлении старь е строки доступны в таблице г eleted. Этот триггер получает Cat ед\ т модифи1шрованнои записи и сохраняет ее вместе со старым и новым значениями с "ча С it' -yName. Теперь при вызове своей первонач.ь ьиой хранимой npv чедуры для вставки новой Category" j вы получаете значе! ие + . Однако это \же не значение _ i;~tity для строки, вставленнеш в таблицу _^ . .з, а новое значение, сгенерированное для строки в таблице Св -yAud .\. Чтобы понять возникшую проблемл. откройте копию менеджера SQL Server Enterprise и просмотрите содержимое таблицы > ;egories. * Category ID 1 г 3 4 5 б 7 8 20 1 Cateaorvf.ar'ie Beverages Condiments Confections Dairy Products Grains/Cereals Meat/Poultry Produce Seafood Pasties Description Sofr drinks, coffees, teas, beers, and ales Sweet and savory sauces, relishes, spreacs. and seasonings Desserts, candies, and sweet breads Cheeses Breads, crackers, pasta, and cereal Prepared meats Dried f rut and bean curd Seaweed and fish heaven Sent Grub Следующим значением Identity для таблицы Categories должно быть 21. Добавим новую строку с помощью кода, приведенного выше, и посмотрим, какой ID вернется при выполнении: DECLARE SCatlD int; EXECUTE CategoryInsert ■Pasties•, PRINT SCaLID; 1 Heaven Sent Food' SCatlD OUTPUT;
444 Глава 11 Полученное в данном случае значение равно 17. Если заглянуть в таблицу CategoryAudit, то выяснится, что это identity вновь созданной записи аудита, а не значение для созданной за- записи категории. АшйГО TcategoryltP lOldName |NewMame 17 30 <MJU> vegetables Проблема заключается в работе ©©IDENTITY. Она возвращает последнее значение identity, созданное в вашей сессии, поэтому не является надежной на 100%. Вместо @@IDENTITY можно использовать две другие функции, однако ни одна из них не лишена недостатков. Первая, SCOPE_IDENTITY [), вернет последнее значение Identi- Identity, созданное в пределах текущей "области видимости". SQL Server определяет область видимости как хранимую процедуру, триггер или функцию. Это будет работать в боль- большинстве случаев, однако если кто-либо еще добавит в хранимую процедуру оператор INSERT, вы получите это значение, а не то, которое ожидается. Вторая функция, IDENT_^TJRRENT( , вернет последнее значение Identity, сгенери- сгенерированное для данной таблицы в любой области видимости. Например, если два пользо- пользователя обращались к SQL Server одновременно, то существует вероятность получения значения Tdentity, сгенерированного другим пользователем. Как вы можете представить, отслеживание причины Этой проблемы является нелег- нелегким делом. Мораль такова, что следует остерегаться использования столбцов Identity в SQL Server. Соглашения по именованию Работая с приложениями баз данных, я определил для себя несколько правил по име- именованию элементов. Они не совсем связаны с .NET, однако являются полезными, осо- особенно при именовании ограничений. Если вы уже имеете свои собственные взгляды на эту тему, м жете смело пропустить »т«т раздел. Таблицы баз донных 3 Всегда используйте имена в единственном числе: Product, а не Products. Это де- делается в основном лля удобства объяснения потребителям схемы базы данных - - грамматически лучше говорить Таблица product содержит продукты", нежели "Таблица products содержит продукты". База данных Northwind является приме- примером того, как не стоит телать. 3 Соблюдайте единообразие при именовании полей, содержащихся в таблице. Наши поля имеют имена <Тас. ~ >_ID для основного ключа таблицы (в предположении, что основной ключ занимает один столбец), Name для имени, которое представляет собой дружественное по отношению к пользователю имя записи, и Description для хранения текстовой информации о самой записи. Если используются хоро- хорошие соглашения по именованию, то. взглянув на таблицу в базе данных, вы сразу же поймете, для чего предназначены ее поля. Столбцы базы данных а Используйте имена в единственном числе. О Все столбцы, связанные с другой таблицей, должны иметь то же самое имя, что и основной ключ этой таблицы. Поэтому ссылка на таблицу Product должна быть ProducL_JJ, а на таблицу sample — Saraple_ID. Это не всегда возможно, особен- особенно если одна таблица содержит множественные ссылки на другую. В таком случае поступайте по собственному усмотрению. □ Поля данных должны иметь суффикс _Оп, например: Modif ied_On, Created_On. Это помогает читать некоторые результаты SQL и делать вывод о том, что озна- означает столбец, по его имени. О Поля, которые содержат сведения о пользователе, должны иметь суффикс _Ву, например: Modif ied_By, Created_By. Это способствует получению более четких и разборчивых имен.
Доступ к данным с помощью .NET 445 Ограничения а По возможности включайте в имя ограничения имя таблицы и столбца, т.е. испо- используйте форму СК_<Та6лица>_<Чоле>. Например, для ограничения проверки столбца sex таблицы person это будет CK_PERSON_SEX. Пример внешнего ключа для связи между продуктом и поставщиком: FK_Producl_Supplier_ID. О Показывайте тип ограничения с помощью префикса, например: Z" для ограничения проверки и FK для ограничения внешнего ключа. Применяйте более подробное имя, скажем, ;K_PERSG>;_AGE_GTC для ограничения столбца age, показывающее, что возраст должен быть больше нуля. а Если необходимо уменьшить длину имени ограничения, [елайте это в отношении имени таблицы, а не столбца. В случае нарушения огранччения обычно легко определить, какая таблица содержит ошибку, но тяже то выяснить, какой из столбцов вызвал проблему. Oracle ограничивает длину имени '> символами. Помимо всего прочего, будьте последовательны при именовании юъектов базы данных или кода. Производительность Текущий набор управляемых провайчеров. доступных в .NET. в некотором роде ограни- ограничен — вы можете использовать iliii . nL. OleDb допускает соединение с лю- любым источником данных, содержащим драйвер OLEDB (т е. Oracle), a .: :" Provider работает с SQLServer. Провайдер SqlC был написан целиком с использованием управляемого кода и для соединения с базой данных применяет настолько мало \ровней, насколько возмож- возможно. Этот провайдер записывает пакеты TDS (Tabular Data Stream) непосредственно в SQL Server, что должно быть быстрее, чем провайдер 1 эБЬ, которому приходится преодолевать несколько уровней, чтобы добраться зп Сазы данных. Чтобы протестировать это предположение, д ш одной и той же базы данных на од- одной и той же машине был запущен следующий фрагмент кода, разница заключалась лишь в использовании управляемого провайдера JqlClient поверх провайдера ADO: SqlCoimand cmd = :.=■-. S^lCommand . _ :z, con); long timelnitial, "inerlapsed; timelnitial = System. Diagnostics _■ ,nter.Value; for (int 1 = 0; I < iteratic-=,- ""I cmd.ExecuteNOnQuery■ ; timeElapsed = System.Diagnost _" .". .-ir.Value - ■__~eInitial; Соответственно версия OleDi использчет OleDbComc а вместо SqlCommar.d. Была создана простая таблица базы даннь х с дв\~мя столбцами, в которую вручную добавлена одна строка: TempData Используемое предложение SQL представляет i обой простую команду UPDATE: UPDATE TempDatL. :* ,-. AVa^ue ■- 1 V? Мы используем довольно простой запрос SQL с целью выявления различий между провайдерами. При различных комбинациях итераций были получены следующие резуль- результаты (в секундах): Провайдер OleDb Sql 100 0.2586 0.1185 1000 1.726 0.787 10000 16.586 8.384 50000 95.925 39.954 Если вы предполагаете работать только с SQL Server, то очевидным выбором будет провайдер Sql. Если же вам требуется что-то помимо SQL Server, следует использовать провайдера OleDb. Но всегда ли?
446 Глава 11 Microsoft проделала большую работу по обеспечению общего доступа к базам данных с помощью классов System. Data.Common, поэтому лучше написать код для этих классов и использовать во время исполнения соответствующего управляемого провайдера. Сей- Сейчас несложно переключиться с OleDb на Sql, и если другие производители баз данных напишут управляемых провайдеров для своих продуктов, то вы сможете переключиться с помощью ADO на родного провайдера с незначительными изменениями кода. Заключение В этой главе говорилось о том, как осуществлять доступ и управлять данными с помощью ADO.XET. Было рассмотрено несколько важных концепций, таких как заполнение набо- наборов данных, использование схем XML совместно с XSD.EXE для предоставления альтер- альтернативных решений по доступу к данным, а также применение в коде хранимых процедур. Обсуждались вопросы производительности и соглашений по именованию, используе- используемых при работе с ADO.NET. В следующей главе мы рассмотрим применение Vusial Studio и элементов управления данными .NET Windows Forms.
г Л // \\ a // \, в a Просмотр данных MET Предыдущая глава была посвящена различным способам выбора и изменения данных. Продолжим эту тему и покажем, как отображать данные хтя пользователя при помощи различных элементов отравления Windows. Возможности .NET по привязке данных аналогичны элементам управления ADO и Visual Basic. Все языки .NET способны использовать те же самые элементы травления и мето- методы. Наиболее революционным аспектом новой модели доступа к данным .NET является Da'iaGrid, который будет рассматриваться в этой главе. Мы также обсудим применение редактора схем XSD в Visual Studio.NET на примере сгенерированного им кода. В заключение будет приведен пример использования проверки на попадание и отра- отражения строк в DataGric. Этот пример объединяет темы, обсуждаемые в других главах книги, такие как отклик на события, отражение, пользовательские атрибуты и, конечно же, доступ к данным. Элемент управления DataGrid DataGr i - представляет собой совершенно новый элемент управления, созданный специ- специально для .NET. Он позволяет отображать данные различными способами. В наиболее простом случае можно выводить данные (как в DataSet) путем вызова метода SetData- Binding . В элемент управления встроены и более сложные возможности, которые будут рассматриваться по ходу главы. Отображение табличных данных В главе 11 было рассмотрено множество способов выбора данных и помещения их в таб- таблицу, однако сами данные отображались самым простым образом — с помошью метода Console. Wr iteLine (). В первом примере будет продемонстрировано, как получить дан- данные и отобразить их в элементе управления DataGrid. Ниже показан вид приложения, которое мы хотим создать. Исходный код для этого приложения доступен в подкаталоге 01_DisplayTabularData: ^ак. 69
448 Глава 12 | CustomerlD JALFKI rANATR jANTON ! ABOUT BERGS BLAUS bLonp BDLID BONAP CornpanyNa ContactName ConlactTrtle Something Or Maria Anders Sales Repres .Ana Trujillo E Ana Trujillo Owner lAntonio More _Antonio More JDwner 'Around the H Thomas Hard Sales Repres •Berglundssn Christina Вer Order Admini BlauerSeeD Hanna Moos Sales Repres BJondesddsl Fredenque Ci Marketing Ma B6lido Comid Martfn Somm Owner IBonaop' Laurence Leb Owner. Obere Scr. 57_ Avda. de la С Mataderos 2 120 Hanover Berguvsvage Forsterstr. 57 24,jplaceJ<le C/Araquil. 67 12, rue des В ]Мё Lor I Lull Ma StH Ma Ma. Приложение (настолько простое, насколько возможно) выбирает каждую запись из таблицы Customer базы данных Northwind и отображает их пользователю в DataGrid. Соответствующий код представлен ниже: using System; using System.Windows.Forms; using System.Data; using System,Data.SqIClient; public class DisplayTabularDaca : System.Windows.Forms.Form { private System.Windows.Forms.E xton retrieveButton; private System.Windowj.Forms.DataGrid dataGrid,- public DispIavTabularDataO .{ this.AutoScaleBaseSize = new System.Drawing.Size to. 13); this.ClientSize = new Syster.Drawing.SizeD64, 253 ; tbis.Text = "O^DisplayTabularData"; Теперь создадим табличный элемент ^правления и установим его свойства. Первая строка, dataGr-з .Begirlr.it О, отключает генерацию событий для DataGrid, что по- полезно при совершении многочисленных изменений элемента управления. Если не сде- сделать этого, каждое изменение сетки будет приводить к перерисовке DataGrid. После этого указываются положение и размер элемента управления, определяется индекс табу- табуляции и осуществляется привязка элемента управления к левому верхнему и правому нижнему углам окна, в результате чего он будет автоматически менять свой размер: this;DkfiaGrid = new System-Wincows.'Forms.DataGridO ; 6ataGridiBeginIr.it () ; dataGrid;Location = new System.Drawing.Point(8, 8) ; ■" dataGrid.Size = new System.Drawing.SizeD48, 208); dataGrid.Tablndex = 0; dataGrid.Anchor = AnchorStyles.Bottom i AnchorStyles.Top I AnchorStyles.Left I AnchorStyles.Right,• this.Controls.Add(this.dataGrid); dataGrid.Endlni t(); Создадим кнопку. При инициализации кнопки выполняются те же самые действия: this.retrieveButton = new System.Windows.Forms.Button(); retrieveButton.Location = new System.Drawing.PointC84, 224); retrieveButton.Size = new System.Drawing.SizeG5, 23); retrieveButton.Tablndex = 1; retrieveButton.Anchor = AnchorStyles.Bottom I AnchorStyles.Right; ret?"ieveButton.Text = "Retrieve"; retrieveButton.Click +=■ new System.EventHandler (this.retrieveButton_Click); this-Controls.Add(this.retrieveButton); ■I Событие Click вызывает обработчика события retrieveButton_Click:
Просмотр данных .NET 449 protected void ,retrieveButtbn_Click (object sender. System.EventArgs e> <{•"■'•" ' '■ retrieveSutton.Enabled = false,- , string source = "server= (local) VWetSDK,'" + "uid=QSUser;pwd-QSt;assword; " + "databaseknorthwinU".; После выбора данных из таблицы Customers и заполнения набора данных вызывается SetDataBinding, который связывает DataSel. и DataGrid. Этот метод принимает набор данных и имя таблицы из этого набора, которую необходимо отобразить. DataGrid спо- способен отображать данные одновременно только из одной DataTable, даже если DataSet содержит несколько таблиц. Ниже будет показано, как выводить данные из DataSet с несколькими DataTable. Разумеется, данные могут поступать из многих таблиц базы данных: "^String select = "SELECT * FROM Customers";- <■< SqlConnection conn- = new SqlConnection (source) ; SqlDataAdapter da s new SqlDataAdapter [select1, conn) ; *" DataSet ds = new DataSet(); ;" da,~Fill (ds, "Customers"); ' dataGricl.SetDataBinding(ds, '.'Customers") ; '' } * static void Main!) ,„■ l' - _ ~ [Application. Run (new DisplayTabularDataO) ; Для компиляции этого примера наберите в командной строке следующее: свс /t:winexe /debug+ /r:System.dll /r:System.Data.dll /r:System.Windows.Forms.dll /recurse:*.cs Параметр /recurse: * . cs компилирует все файлы . cs в текущем каталоге и во всех его подкаталогах. Поскольку используется сокращение, не требуется запоминать имена файлов приложения и в то же время гарантируется, что компилируются только необхо- необходимые файлы. Источники данных DataGrid предлагает гибкий способ отображения данных. Помимо вызова SetDataBinding с набором данных и именем таблицы для отображения, этот метод можно также вызывать для следующих источников данных: □ Массив (DataJ id может быть связан с любым одномерным массивом) □ DataTable О DataView О DataSet или Da:aViewKanager О IListSource О IList Соответствующие примеры приводятся ниже. Массив На первый взгляд все просто. Создаем массив, заполняем его данными, а затем вызываем SetDataSource (array, null) для DataGrid. Например: string[] stuff = new stringH {"One", "Two", "Three"); dataGridl.SetDataBinding(stuff, null); SetDataBinding принимает два параметра. Первый параметр представляет собой источник данных, который в данном случае является массивом. Второй параметр дол- должен иметь значение null, если только первым параметром не является DataSet или DataViewManager,— в этом случае он должен содержать имя таблицы для отображения. В примере обработчика события retrieveButton_Click можно заменить код при- приведенным выше кодом для массива. Однако при этом возникает следующая проблема:
450 Глава 12 Вместо строк, определенных в массиве. ^ataGrid отображает длину этих строк. Причина в том, что в случае использования массива в качестве источника данных для LataGrid, он ищет первое открытое свойство объекта в массиве и отображает его значе- значение, а не строку, как можно было бы ожидать. Первое (и единственное) открытое свойство строки — ее длина, поэтому она и выводится. Один из способов решения этой проблемы — написать для строк класс-оболочку, например: protected С public class Xtem Item(string text) < M m_text = text; } public stiing Text { get{return m_text;} } private tring m_text; При добавлении массива этого класса Item (который вполне может быть и struct) вы получите ожидаемый результат. Исходный код примера доступен в каталоге 02_Data- S ;^r ;eArray: DataTable Существуют два способа отображения DataTable в DataGrid: О Если ^talab^e является самостоятельной, вызывайте SetDataBinding (Data?at e, null). О Если DataTable содержится внутри DataSet, вызывайте SetDataBinding (DataTable, "Table"). На рисунке (пример 03_DatasourceDataTable) видны некоторые из столбцов данных: ib 31.23 IP 439 м 456 Ю 12373 jta 258Э 15 49 26 0 10 0 0 0 0 0 0 30 0 0 15
Просмотр данных .NET 451 Обратите внимание на последний столбец — он отображает флажок, а не элемент ре- редактирования. В отсутствие другой информации DataGrid читает схему из источника дан- данных (который в данном случае представляет собой таблицу Products) и по типу столбцов определяет, какой элемент управления необходимо отобразить. К сожалению, на данный момент поддерживаются всего два типа — элементы редак- редактирования и флажки; все остальное необходимо реализовывать вручную. Позже мы вернемся к теме изменения типов столбцов. DataView DataView обеспечивает возможность фильтрации и сортировки данных внутри Data- Table. После получения данных из базы данных можно разрешить пользователю отсор- отсортировать их щелчком мыши на заголовках столбцов и т.д. Помимо этого вы, возможно, пожелаете отфильтровать данные для вывода лишь некоторых строк, например тех, что были изменены пользователем. DataView позволяет ограничивать строки, показываемые пользователю, однако он не ограничивает столбцы в DataTable. DataView не разрешает указывать столбцы для отображения и работает только со строками. Пример ограничения вывода столбцов приводится ниже. Создадим DataView на основе существующей DataVable. Пример доступен в каталоге О 4_DataSourceDataView: DataView dv*!5=l.riew DataView (clataTable),- - " После создания bataview можно менять его свойства, что будет влиять на данные и действия, допустимые для этих данных, при отображении DataView в DataGrid. Вот несколько примеров: О Установка AllowEdit в false запрещает для строк все действия по редактированию столбцов. О Установка AllowNew в false запрещает работу с новыми строками. О Установка AllowDe^ete в fan = 5 запрещает возможность удаления строк. □ R-^wScat eUlter служит для отображения только строк с данным состоянием. О RowFilter служит для фильтрации строк. □ Возможна сортировка по определенным столбцам. Фильтрация строк по данным После создания DaraView можно изменить отображаемые им данные с помощью свойства RowFilter. Это свойство вводится как строка и используется как способ фильтрации на основе некоторого критерия. Критерий фильтра задается значением строки. Она аналогична предложению WHERE в обычном SQL, только применяется к данным, уже выбранным из базы данных. Некоторые примеры предложений для фильтрации показаны в следующей таблице: Предложение Описание UnitsInStock > 50 Показать только те строки, в которых столбец UnitsInStock содержит значение, большее 50. Client = 'Smith' Показать только записи данного клиента. County LIKE 'С*' Показать все записи, в которых поле County начинается на С. Например, это могут быть строки Cornwall, Cumbria, Cheshire и Cambridgeshire. В качестве маски для одного символа можно использовать %. Среда исполнения сделает все возможное, чтобы привести типы данных, используе- используемых в предложении, к соответствующим типам данных исходных столбцов. Например, можно написать "UnicsInStock > ' 50 '", хотя столбец имеет тип integer.
452 Глава 12 Фильтрация строк по состоянию Каждая строка в DataView имеет определенное состояние, которое может принимать одно из следующих значений: DataViewRowState Added CurrentRows Deleted ModifiedCurrent ModifiedOriginal OriginalRows Unchanged Описание Все только что созданные строки. Все строки, кроме удаленных. Все строки, которые были выбраны, а затем удалены. Не показывает только что созданные строки, которые были удалены. Список текущих значений для всех строк, которые были изменены. Список начальных значений для всех строк, которые были изменены. Все строки, которые были первоначально выбраны из базы данных. Не включает новые строки. Показывает первоначальные значения столбцов (т.е. Не текущие значения в случае внесения изменений). Все строки, которые не менялись. Посмотрим на практике, какое действие оказывают эти состояния на DataGrid. Со- Создадим приложение, которое выводит два DataGrid: один отображает данные, которые выбраны из базы данных и с которыми можно работать, а другой показывает строки, имеющие одно из перечисленных состояний: * 69 74 ч> ;■ 77 * ■- _±^J - Products 1 4 Outback l*j№ LonglfeTchj Ho«?ny*otf 1? r ч i; 23 12 В 15 i 4 i 7 I r г Э 4 г*- 12тЬс ICei*g, 2^-O.Slbot 500 fld utoexe :0 SQuopA с H X IS ID tl 7.7 ' 19 13 _ 3 RS 21.S Помимо вывода строк, фильтр отражает состояние столбцов внутри этих строк. Это ясно видно на примере Modif iedOriginal и Modif iedCurrent. Эти состояния основы- основываются на перечислении DataRowVersion (см. главу 11). Если, например, вы каким-то образом обновили строку, она будет показана как в случае Modif iedOriginal, так и в случае Modif iedCurrent, однако значением будет либо Original, полученное из базы данных (если вы выбрали Modif iedOriginal), либо текущее значение DataColumn (если вы выбрали Modif iedCurrent). Сортировка строк Помимо фильтрации, часто требуется отсортировать данные внутри DataView. В эле- элементе управления DataGrid можно щелкнуть мышью на заголовке столбца, в результате чего будет произведена сортировка по этому столбцу в возрастающем или убывающем порядке. Единственная проблема в том, что этот элемент управления может сортировать только по одному столбцу, в то время как DataView способен производить сортировку по нескольким столбцам:
Просмотр данных .NET 453 1 PDataSourceDataView .>: •' ■ ШШШ ProduclID Г 50 63 " ""'95 97 ► 64 '94 ""98 147 «1 ШШШ PtoductNa с UncieBobYO ШШШ SuppfedD .'3 Valkoinensuk!23 Vegie-sptead Wibble V/ibble Wirmers gul Woodjfiis" " WoodyRis Zaanse koeke | i> ,5 16 J12 '1 ji i22 ШШШ Category©  3 2 2 3 5 4 5 3 " шшшш ; Guar%PeiU " '^I2-1 ibpksiT 7Ji2-id0gbai J15- 625 g jars ~ fiOOMoie MOO ■ 20 bags* 4 pi 1334 " ,233 10-4o гЬохе 2nj_xj H Гзо lib }*?■ jo d ,0 _J Если столбец отсортирован, либо посредством щелчка мыши на заголовке, либо с по- помощью кода, DataGrid отображает стрелку, показывающую, какой столбец отсортирован. Для программной сортировки столбцов используйте свойство Sort: clataView. Sort dataView.Sort = "PrpductName"; = "PrtoductName ASC, ProductID DESC" DataView поддерживает сортировку как по возрастанию, так и по убыванию. По умолчанию используется сортировка по возрастанию. Если внутри DataView сортируется более одного столбца, то DataGrid не будет показывать стрелки сортировки. Если вам приходилось использовать при программировании элементы управления Win32 Listview, то вы оцените работу, которую провела команда .NET для реализации сортировки внутри DataGrid. Так как каждый столбец в DataGrid может быть строго ти- типизирован, порядок сортировки не основывается на строковом представлении столбца — вместо этого применяются реальные данные. Связанная с этим проблема заключается в том, что если в DataGrid есть столбец даты, то при попытке его сортировки DataGrid будет осуществлять сравнения по дате, а не сравнения строк. DataSet DataGrid способен отображать одновременно только одну DataTable. Однако, как будет видно из этого примера, на экране можно перемещаться по связям внутри DataSet. Для генерации такого DataSet, основанного на таблицах Customers и Orders из базы дан- данных Northwmd, может использоваться следующий код. Он расположен в каталоге 05_DataSourceDataSet. Пример добавляет две DataTable, а затем создает связь между этими таблицами с именем CustonerOrders: string' source = "server= (local) \VNetSDK; "' +, .;, "uid=QS0ser;pwd^QSPassw6rd;" + "database^northwind"; string Orders = "SELECT * FROM Orders"» string customers = "SELECT * FROM Customers"; ^ SqlConnectlon con = new SqlConnection(source); SqlDataAdapjter da = new' SqlDataAdapter(orders, con); DataSet ds =• new DataSetO;- "" "*"„. da.FilKds, "Orders") ;' . -,f -n • s%f '. da - new Sqlbataftdapter(customers, con); Щ- ч '"-*'■ ~, da.FilKds, "Customers"); ■ " v ds.Relations.Add("CustoroerOrders", ' dsvTablest'Customefs^X.edluimsfCustbfnerlD*.}, f. ds.Tables["Orders"] .Columns("CustomerID"]}; -. После создания можно связать DataSet и DataGrid с помощью вызова SetDataBinding: dataGridlvSetDataBinding(ds, "Customers'J; _ . ♦ T0?*f.i-:'i. f'-J*. " В результате мы получим примерно следующее:
454 Глава 12 -fofxl i CuslomerlD J GhANATR AN'ON CompanyNa ConlaciName ConiactTHIe Address Something Oi Maria Andeis Sates Repres ObeieSli. 57 Ber AnaTfuiilloE AnaTfuiillo Owner Avd3.de la С Me Antonio More Anton» Mote Owner Maladeros 2 Me Cu<tOfnerQrders J Ш,АЯОиТ Around trie H Thomas Hard Sales Repies 120 Hanover Lor O-6ERGS EeigfurJ: n Christina Бе OlderAdimni Beiguvsvage Li* E'BLAUS В iuei Sei 1+vwaMoot S^tesRepte» Forstersli 57 Ma BBLONP Bljndesdds Fr^deriqueC. MaketmgMa 24 place Kle Stu ,E РП1 (П йпЬНпГптпН M^rtmSnmm П npr Г/АглгвЛ Р7 _L Обратите внимание гл знак '+' стева от каждой записи. Его появление отражает тот факт, что был создан j1" aF« t с \щ -я. »той связью между покупателями и заказами. В коде можно определить несколько таклх в;.-..! «оотноше гий. Если щелкнуть мышью на знаке '+ . то будет и к ijjh список связей (щи скрыт, если он был видим). Щелч к и имени связи приведет к ноьн 'ению в ^caGrici связалных записей, в данном случае б- лт перечислены все заказы, размещенные выбранным покупателем: 4 Cuttomei»: Zusto :i С 1 г [*" <\ OidetlO 103E5 10507 JO loss: 108ГЗ CjS: ЭГ1Г ANTC.N ANTON ANTON ANTON TON 1 DN CornpanyName Antonio MorerioTaquer EmployeelD OideiOale ■ - 21 25 1,1996 4'1997 ■j .397 * :J97 ,,- RequiedDate 2? '2/1996 '" 13, j5.'1997 10/^6/1997 17/07 "997 20/10-1 Э?" 23/10/19J~ 'a ► Sh^. 02/ 22/ 21/ 20/ 26/ 01/_ 25Л2/1998 10/ Кроме того, в правом верхнем углу пояр :яется пара новых кнопок. Стрелка позво- яет вернуться к родительской строке. 3ai ювочная строка, показывающая сведения о р ^дительской записи может быть выведена или убрана нажатием на другую кнопку. Отображение данных в Dat.aVi^wManai выполняется точно так же, как и в случае 2='.= Однако при создании DataViewK ager для Cfe.aSet отдельный Dataview со- создается ххя каждой Da1- ~ =, что позволяет выводить строки на основе фильтра или со- состояния строки, как показаю в примере С aview. Даже если не требуется фильтрация данных, следует при отображении оборачивать Da":aSe : в DataViewManager, поскольку это дает больше возможностей при пересмотре кода. Следующий фрагмент кода создает DataV ewMar.ager на основе DataSet из преды- предыдущего примера, а затем изменяет DataV- =w для таблицы Customers, чтобы показать покупателей из UK: DataViewManager -dvm = new DataViewManager (ds) ,- dvffl.DataViewSettingst"Customers"].RowFilter = "Country^1UK1»; dataGrid.SetOataBindingfdvm, "Customers"); Этот код создает новый объект DataViewManager, а затем указывает, что для табли- таблицы Customers должны быть перечислены только покупатели из UK. Результат работы этого кода показан ниже:
Просмотр данных .NET 455 m i ! s E 1 B В к ■ш CustomeilO AROUT BSBEV COMSH EA-'TC SLAT NORTS SEVES ■■■ CompanyNa Around the H B's Beverage Consolidated Eoitetn Conn and Trodin N -nth/Scuih t- en Se-as i ConlactName, ContactTitle r iiaiHaid j Sales Repres f1 ..idAshw {Sales Repres EI«Eei:iBfO 'SalesRepies Arr ?' лп SolesAgenr iHte Ее—-' iMaiketingMa 'SimcT' о ' Sales Associ 'Наг * ?alesManag 1 ■1 Address 'i20 Hanover : Berkeley Gar ;35KingGeo( GaidenHous SoUh House ; v/adhurst ■■ Londoi Londoi Londoi Londoi Cov.e: i Londoi 1 Londoi ► ! 1 ■'" 1 IListSource и I List DataGrid поддерживает любой объект, содержащий один из этих интерфейсов. В IList- IListSource имеется только один член, GeLLis . который возвращает интерфейс IList. IList более интересный, он реализован боньшим числом классов среды исполнения. Среди этих классов: Агга . - и lection. При использовании II .■.; нужно \"читывать тот же момент, что и в случае реализа- реализации Array (см. выше). Если вы применяете Str_r:J:. _ _ п в качестве источника данных для Da1- Id, то в таблице будет отображаться длина строк, а не текст. Иерархия классов DataGrid Иерархия классов для осн< вкь.х с став .ян mix ^ имеет вид: Object Магь. -, -■ .С :<: :i С" =nt Control DataGrid DataGridCc1 —S*>* DataG' ,5oolColumn DataG- JTextBoxColumn DataGridTableStyle ValueType DataGridCe DataGrid состоит из нуля или большего числа Da_aGridTableStyles. Эти стили со- содержат нуль или больше т э 3C&numrc les. Определенная ячейка в DataGrid мо- может быть выбрана при помощи структуры _ a aGridCell. Мы коротко коснулись Da _ -; 7П= п =.~ .-1 s и DataGridColumnStyies, однако клас- классы DataGridTable и DataGric ~_ содержат больше возможностей, чем при простом создании их средой исполнения. В следующих разделах будут описаны эти и другие основ- основные классы, приведенные на рисунке, а также будет показано, как с их помощью можно изменить параметры отображения на экране. DataGridTableStyle и DataGridColumnStyle DataGridTableStyY содержит визуальное представление DataTable. DataGrid вклю- включает в себя коллекцию стилей, доступ к которой осуществляется с помощью свойства TableStyles. При отображении D, ^а а - - выполняется проверка всех объектов Da- DataGridTableStyle в поисках того, чье MappingKaire равно TableName для DataTable. Если совпадение найдено, этот стиль будет использоваться для отображения таблицы.
456 Глава 12 DataGridTableStyle позволяет определять визуальные параметры DataGrid, такие как цвет переднего и заднего плана, шрифт, используемый в заголовках столбцов, и дру- другие свойства. DataGridColumnStyle позволяет уточнить параметры отображения для каж- каждого столбца, например, установить выравнивание данных в столбце, текст, отображаемый вместо значения NULL, а также ширину столбца на экране. Когда DataGrid отображает DataTable с определенным DataGridTableStyle, можно указать, какие столбцы данных следует выводить, добавив (или не добавляя) DataGridColumr.Styles. Отображаться будут только те столбцы, которые имеют опре- определенный стиль, и это может быть полезно при сокрытии таких столбцов, как основной ключ. Можно также задать стиль столбца как Readonly. Следующий код показывает пример создания DataGridTableStyle. Код создает объ- объект DataGridTableStyle, добавляет к нему два объекта DataColumnStyle, а затем ото- отображает все данные в таблице Customers. Приведем этот код целиком, поскольку он является основой для примеров этого раздела. Первая часть кода уже знакома вам: using System; using System.Windows.Forms; using System.Data; using System.Data.SqlClient; public class CustomDataGridTableStyle System.Windows.Forms.Form { private System.Windows.Forms.Button retr_eveButton; private System. Windows. Forrrj. DataGrid dataGrid; public CustomDataGridTablejtiпе () { this.AutoScaleBaseSize = :.=..■; System.Drawing.SizeE, 13); this.ClientSize = new Syscer.Drawing.SizeD64, 253); '■- this. Text = 7_CustomDataGrid1iableStyle"; this.DataGrid = new System.Windows.Forms.3ataGrid(),- dataGrid.Beginlnit(); iataGrid.Location = new System.Drawing.Point(8, 8); на. iGrid.Size = new System.Drawing.SizeD48, 208); datc.3rid.Tablndex = 0; ca--aGr-. .Anchor = AnchorStyle з. Bottom I AnchorStyles .Top. I AnchorStylsr.Left I AnchorStyles.Right; this.Controls.Adc ^his.dataGri dataGric. Sndlnit () ,- this.recr_3veButtor. = new System.Windows.Forms.Buttoni) ; retrieveb-t: on.Locat-^. = new c.-stem. Drawing. Point C84 , 224); retrieveBuc,->n.Size = new Sys~t~.drawing.Size G5, 23); retrieves^" ;n.TabIncex = 1,- retrieveButt zz..Anchor = AnchorStyles.Bottom I AnchorStyles.Right; retrieveButt?-. .Text = "Retrieve"; retrieveBut" Dr..Click т= new System.Evt-r^tHandler this.retrieveButton_Click) ; this.Controls.Aid(this.retrieveButton); prctejted void re-.r ieveB^t i_Clic:< (object sender. System. EventArgs e) ( retrieveButton. Enabled = talse,- Эти строки создают набор данных и DataGridTableStyle для использования в примере, затем происходит связывание DataGrid с DataSet. Функция CreateDataSet не содержит ничего нового — мы выбираем все строки из таблицы Customers: ;| DataSet ds = CreateDataSet(); -' CreateStyles(dataGrid); ;' * d9t%Gr,id.SetDataBindxng(ds* "Customers"); '-. У Функция CreateStyles является более интересной. Первые несколько строк созда- создают новый объект DataGridTableStyle и определяют его MappingName. Это свойство используется, когда DataGrid отображает заданную DataTable. DataGrid может выво- выводить строки разными цветами. Приводимый код определяет цвет для каждой второй строки, а результат показан на рисунке: private void CreateStyles (DatiaGrid dg) ■''■'.' '^ DataGriaTableStyle style = new DataGridTableStyle О ; ■ **;Л style.MappingName. - "Customers"; : - ,, •,, ,.„,, „■. .,
Просмотр данных .NET 457 style.AlternatingBackColor = System.craying.Coior.Bisque,- DataGridTextBoxColumn customerlD = new "DataGridTextBoxCdlumnO,- customerlp.HeaderText = "Customer ID"; * ■ customerID,MappingName - "CustomerlD"; custpmerlD.Width = 200; DataGridTextBoxColumn name ■ = new OataGridTextBoxCol'umn () ; name. He^derText = "Name",- name.MappingName = "CompanyName"; name. Width = 300;. После определения столбцов они добавляются в коллекцию GridColumnStyles объекта DataGridTableStyle, который сам затем добавляется к свойству TableStyles элемента управления DataGrid: style.GridColumnStyles,. AddRange Л 1 ( new DataGridColumnStylef] {customerlD, name});; s dg.TableStyles.Add(style); *•;."} [ ' ;; private DataSet CreateDataSet {) J { ""'.''"'''' ' string source -» ^server= (local) WNetSDK;" t : "uid=QSUser;pwd=QSPassword;" +(',-^ "database=northwind"; " string- customers = "SELECT * FROM Customers"; ., SqlConnection con = new SqlConnection(source); ^» *• SqlDataAdapter da = new SqlDataAdapter(elastomers, con)r' DataSet ds = new JJataSetl) ; da.Fili(ds,, "Customers"); return ds; static void Main() Application.Run new ;ustomData3ridTab.3Scyle() ) ; После создания объекта DataGridTableStyle мы создаем два объекта, производных от DataGridColumnStyie, в данном случае это текстовые поля. Для каждого столбца определяется ряд свойств. Все эти свойства перечислены в таблице: Свойство Alignment FontHeight HeaderText MappingName NullText PropertyDescriptor Readonly Width Описание Одно из перечислимых значений HorizontalAlignment: Left, "enter или Right. Показывает, как выравниваются данные в столбце. Размер шрифта в пикселях. По умолчанию будет равен значению в DataGrid. Текст, отображаемый в заголовке столбца. Столбец из DataTable, который отображается в данном столбце на экране. Текст, отображаемый в случае, если значение данных равно DBNull. См. ниже. Флаг, показывающий, что столбец доступен только для чтения. Ширина столбца в пикселях. Результатом работы этого кода является следующее:
458 Глава 12 1 Ho7 CustomDataGridrableStyle Н№ЭЯ| шшшшшт Customer 10 * [ALFKI -ANATR "'ANTON ARDUT "_ BERGS BLAUS BLONP ,BDUD BONAP 1РПТТЫ [Name -»| Somelhing Oi Dthei —1 Ana Tiujillo Empjredadosy helados Anlonio Moieno Taquaia Around the Horn Berglynds snabbkdp 6laue> See Oellkatessen Blor.desddsl pere et № Botdo Comidas pieparadas Bon app' . Привязка данных Data :__ _ является единственным элементом управления в среде исполнения .NET, кото- который может использоваться для отображения данных. Процесс связывания элемента управления с источником данных носит название привязки данных. Если вы имеете опыт программирования приложений Windows на MFC, то, вероятно, вы пользовались Dvnamic Data Exchange (DDX) для прикрепления переменных одного из классов к элементу управления Win32. .ХЕТ упрощает привязку данных к элементам управления. Данные можно привязать не только к элементам управления Windows, но и к web-страницам с помощью ASP.NET. Простая привязка Элемент управления, поддерживающий простую привязку, обычно отображает только одно значение, например текстовое поле или переключатель. Следующий пример пока- показывает, как привязать столбец из Da*" "able к TextBox: DataSet ds = v.'reafceDataS':c () ; textbox. DataB^r.c: ngs .Add i" Text" ds, "Product s.ProductName"); После получения некоторых данных из таблицы Products и сохранения их в Data- Set вторая строка привязывает свойство Text элемента управления (textBoxl) к столб- столбцу Products.ProJ~ -Name. Если бы вы написали этот код и получили данные из базы данных Northwi-.. . то увидели бы на экране примерно следующее: Chd Это текстовое поле показывает нечто, полученное из базы данных. Если вы хотите убедиться в том, что используются нужный столбец и значение, запустите анализатор за- запросов SQL Server для проверки содержимого таблицы Products: SELECT ProductI»,ProductMame FROM Products ORDER BY ProductID (ProductID|ProductName Chai : Chang Aniseed Syrup Г*1 Grids [Ш Messages
Просмотр данных .NET 459 Наличие на экране простого текстового поля без возможности перемещения между записями и обновления базы данных вряд ли полезно, поэтому в следующем разделе мы рассмотрим более реалистичный пример и познакомимся с другими объектами, необхо- необходимыми для привязки данных. Начнем с обзора некоторых используемых классов. Объекты привязки данных - Binding BindingContext BindingManagerBase ; CurrencyManager PropertyManager Object MarstiallByRefObject BaseCo ection BindingsCollection ControlsBindingsCqllection Component Control U В предыдущем примере мы использовали свойство . . .. *. niings элемента управления TextBox для привязки столбца из набора данных к свойству т элемента управления. Свойство DataBindings является экземпляром ~_ntrolBind_r_g- _ л I e _-_ textbox.DataBindings.Add("Text", ds, "Products.JToductName "); Эта строка добавит объект Binder ~ в : -ntrolBindingsCollection. Контекст привязки Каждая форма Windows имеет свойство BindingC- ■.-.*■ cxt. Form является производной от Control, в котором определено это свойство, поэтому большинство элементов управле- управления имеют данное свойство. Этот класс поддерживает коллекцию экземпляров isinding- ManagerBase. Экземпляры создаются и добавляются к объекту управления привязкой при привязке элемента управления к данным: BindingContext CurrencyManager DataSource Текущая позиция CurrencyManager DataSource BindingContext может содержать несколько источников данных, оболочкой кото- которых является CurrencyManager или PropertyManager. Решение о том, какой класс ис- использовать, основывается на самом источнике данных. Если источник данных содержит список элементов, например DataTable, DataView или любой объект, реализующий IList, то будет использоваться CurrencyManager,
460 Глава 12 поскольку он может содержать текущую позицию внутри источника данных. Если источ- источник данных представляет собой единичное значение, то внутри BindingContext будет сохранен PropertyManager. CurrencyManager или PropertyManager создается только один раз для источника данных. Если вы привязываете два текстовых поля к строке из DataTable, то в контексте привязки будет создан только один CurrencyManager. Каждый элемент управления, добавленный в форму, связывается с менеджером при- привязки формы, поэтому все элементы управления используют один и тот же экземпляр. При первоначальном создании элемента управления его свойство BindingContext име- имеет значение null. Когда элемент управления добавляют в коллекцию Controls формы, то Bindir.gContext устанавливается в BindingContext этой формы. Для того чтобы привязать элемент управления к столбцу, необходимо добавить запись к его свойству DataBindings, которое является экземпляром ControlBindingsCollec- ControlBindingsCollection. Следующий код создает новую привязку: textBQxl .DataBindings .Add ("Text", ds, " Products. ProductName") ,- Внутренне метод Add коллекции ContrcuBindingsCollection создает новый экземпляр объекта 3ir.cirg из параметров, переданных этому методу, и добавляет его в коллекцию привязок: Элемент управления DataBindingsCollect'on Bind.ng Property Data Source DataMember Binding Property - DataSource DataMember Контекст привязки CurrencyManager DataSource \ Этот рисунок показывает в общих чертах, что происходит при добавлении Binding к Control. Привязка соединяет элемент управления с источником данных, который под- поддерживается в BindingContext формы Form (или самого элемента управления). Измене- Изменения внутри источника данных отражаются в элементе управления, так же как и изменения в элементе управления. Привязка Этот класс связывает свойство элемента управления с членом источника данных. При из- изменении члена обновляется свойство элемента управления. Обратное также верно — если обновляется текст в textbox, это изменение отражается в источнике данных. Привязки могут быть установлены между любым столбцом и любым свойством эле- элемента управления, поэтому, например, можно привязать столбец к textbox, а другой столбец — к цвету (или длине) textbox.
Просмотр данных .NET 461 Свойства элемента управления можно привязывать к совершенно разным источникам данных — возможно, цвет ячейки определен в таблице цветов, а сами данные содержатся в другой таблице. CurrencyManager и PropertyManager При создании объекта Binding создается также соответствующий объект CurrencyMana- CurrencyManager или PropertyManager, если данные из этого источника данных привязываются первый раз. Цель этого класса — определить позицию текущей записи в источнике данных и координировать все ListBindings при изменении текущей записи. Следующий пример отображает два поля из таблицы продуктов и позволяет переме- перемещаться между записями с помощью элемента управления trackbar. JKonbu J2 kg box Код этого примера приводится полностью. Он доступен в каталоге 09_Scrolling- Databinding: using System; using System.W_ndows.Forms; using System.Da~a; using System.Data.SqlClient; public class ScrollingDataBinding : Sj s zer..Windows.Forms.Form { private Button retrieveButt" private TextBox textName; private TextBox uexcQuan,- private TrackBar trackBar; private DataSet ds; Приложение создает окно и все элементы управления для этого окна в конструкторе ScrollingDataBir J i у. public ScrollinaDataBindir.g .) { this.AutoScaleEaseSize = new System.Drawing.SizeE, 13); this.ClientSize = new System.Drawing.SizeD64, 253); this.Text = "C9_ScrollingDataBinding"; this.retrieveButton = new ButtonО; retrieveButton.Location = new System.Drawing.PointD, 4) ; retrieveButton.Size = new System,Drawing.SizeG5, 23); retrieveButton. TaMndex = 1; retrieveButton.Anchor = AnchorStyles.Top I AnchorStylesLLeft; retrieveButton,Text = "Retrieve"; retrieveButtonlCli'ck -+=; new System.EventHaadler (this.retrieveButton_Click); this.Controls.Add(this.retrieveButton); this.textName = new TextBoxO; textName.Location = new System.Drawing.PointD, 31); textName.Text = "Please click retrieve..."; textName.Tablndex = 2; textName.Anchor = AnchorStyles.Top I AnchorStyles.Left I AnchorStyles.Right; textName.Size = new System.Drawing.SizeD56, 20); textName, Enabled = false.; this.Controls.Add{this.textName); this.textQuan s= new TextBoxO; textQuan.Location = new System.Drawing.PointD, 55); textQuan.Text = textQuan.Tablndex = 3;
462 Глава 12 teXtQuan.Anchor = AnchorStyles.TQP I AnchorStyles.Left I AnchorStyles.Right; textQuan.Size = new System.Drawing.SizeD56, 20); textQuan.Enabled - false; this.Controls.Add(this.textQuan); this.trackBar =* new TrackBart); trackBar. Beginlnit (); trackBar.Dock * DockStyle.Bottom,- trackBar.Location = new System-.Drawing.Point@, 275); trackBar.Tablndex = 4; trackBar.Size = new System.Drawing.SizeE04, 42); trackBar.Scroll += new System.EventHandler(this.trackBar_J>croll); trackBar.Enabled = false; i this.Controls.Add(this.trackBar); * } Когда нажимают на кнопку Retrieve, обработчик события выбирает все записи из таб- таблицы Products и сохраняет эти данные внутри закрытого набора данных ds: protected void retrieveEutton_Click object sender. System.SverAArgs e) { : retrieve3utton.Enabled = false; ds = CreateDataSet(); Далее привязываются два текстовых элемента управления: textName.DataBindings.Acd "Text", ds, ■Procucts.ProcuctName"); textQuan. DataBmdings. Add ("Text", ds, "Products.QuantityPerUnit"); trackBar.Minimum = 0; trackBar.Maximum =t this.BindingContext[ds, "Procucts"] .Count - 1; IJ textName. Enabled - true; textQuan.Enabled = true; trackBar. Enabled ч true; } Теперь представим простой механизм прокрутки записей, который откликается на перемещения движка Tz з :Чзаг: "V protected void trackBar_Scroll(object sender. System.EventArgs e) { i this.BindingContextfds, "Products"].Position = trackBar.Value; } private DataSet CreateDataSet О * i String source = "server= (local) WNetSDK;" -r ^ lluid=QSUser;pwd=QSPassword; " + "database=northwind"; * string customers = "SELECT * FROM Products"; SqlConnection con = new SqlConnection(source); SqlDataAdapter da - new SqlDataAdapter(customers, con) ; DataSet ds = new DataSet(); da.Fill(ds, "Products"); return ds; } static void Main() { Application.Run(new ScrollingDataBindingO ) ; При первом получении данных максимальная позиция TrackBar устанавливается в значение, равное числу записей. Затем в приведенном методе прокрутки мы устанавли- устанавливаем позицию BindingContext для DataTable Products в позицию движка полосы прокрутки. Это изменяет текущую запись в DataTable, поэтому обновляются все элементы управления, привязанные к текущей строке (в данном примере — два текстовых поля).
Просмотр данных .NET 463 Visual Studio и доступ к данным В новой версии Visual Studio появились новые возможности по встраиванию доступа к данным в приложение. В этом разделе обсуждаются некоторые из способов, посредством которых Visual Studio.NET позволяет интегрировать данные в GUI. в результате чего вы сможете работать с данными, как с обычными элементами управления Доступные инструменты позволяют создать соединение с базой данных с помощью классов OleDbConnection или SqlConnecti r . Выбор класса зависит от того, какую базу данных вы используете. После определения соединения вы можете создать "ataSet и заполнить его в GUI Visual Studio. В результате для DataS= будет создан файл XSD, точно так же, как мы делали это вручную в предыдущей главе, и автоматически сгенери- сгенерирован код . cs. Таким образом, формируется безопасный по тит набор данных. В этом разделе будет показано, как создать соединение, выбрать данные, сгенери- сгенерировать dataset и, наконец, использовать все сгенерированные объекты для создания простого приложения. Создание соединения Прежде всего создайте приложение Window, вы получите чистую форму. Теперь необ- необходимо создать соединение с базой данных. Вызовите Server Explorer, нажав Ctrl + Alt + S или выбрав Server Explorer в меню View. В результате будет выведено окно, аналогичное следующему: Server Explorer £3 Нк Data Connections <Add Connection. В б^ skinnerm\netsdk.MatrixTest.dbo E 'ix skJnnerm\NETSDK.Northwind.QSUser ^ Servers В этом окне можно управлять различными аспектами доступа к данным. Нам необхо- необходимо создать соединение с базой данных Nort-'.v.ind. Двойной щелчок мыши на Add Con- Connection... запустит мастера, в котором можно выбрать используемого провайдера OLE DB. Если вы хотите соединиться с базой данных Northwir.d. устанавливаемой как часть примеров Framework SDK, выберите Microsoft OLE DB Provider for SQL Server. Вторая стра- страница диалогового окна Data link имеет вид: , Data Link Properties Provide! Connection | Advanced |AI | Specify the Wowing to connect tg SOL Serve dauc 1. Select of entet a3£rver name |(Local|\NETSDK ' 2. Entei Womation tg leg on lo the server I" Use Windows NT Integrated «ecmy & Ца а specific wet name and pwnvord: Lisa name- [QSUrei Password | " !~ Blank fuuvwd Г 3 <? Select Ihe database on the server JNoilhwrKl <~ AUactja database He as a databa» name J ;rt*e;jConnection l OK Cancel Help
464 Глава 12 В качестве имени сервера введите (Local)\\NETSDK, затем выберите Use a specific user name and password. Именем пользователя будет QSUser, паролем — QSPassword. Можно также использовать аутентификационную информацию Windows NT, если вы соответст- соответствующим образом настроили SQL Server. В списке баз данных выберите базу данных Northwind и для проверки своих действий нажмите на кнопку Test Connection. В результате этого будет произведена попытка соеди- соединения с базой данных, и по завершении будет выведено окно сообщения. Разумеется, если вам необходимо установить сервер согласно конфигурации вашей машины, то имя пользователя, пароль и имя сервера могут быть другими. Для создания объекта соединения перетащите только что добавленный сервер в основ- основное окно приложения. В результате будет создана переменная типа System.Data. SqlCli- ent .Sql Connect ion или System.Data.OleDb.OIeDbConnection, в зависимости от выбранного провайдера, и добавлен следующий код в метод InitializeComponent основной формы: this,SqlConnectionl = new System. Data . SqlClient .SqlConnection(),- :// sqlCormectionl this.SqlConnectiohl.ConnectionString = ra source=skinnerm\\NETSDK;" + "initial catalog=Northwind;" + "user _d=riSUser;password=QSPassword; " "persist security info=True;" + "workstation id=SKINNERM;" + "packet siz^-4096"; Информация о строке соединения содержится непосредственно в коде. После добавления этого объекта в проект вы увидите, что объект sqlConnectionl размещается в нижней части окна Visual Studio: ■ DataAccessInVisualStudio - File Edit JTiew Project Build Debm # i.sS Forml.cs[Design]* Forrnl.cs* "SForml 6£J sqlConnectionl Task List Output Ready Свойства этого объекта можно изменить, выбрав его и вызвав диалоговое окно Properties (F4). Выбор данных После того, как вы определили соединение с данными, можно выбрать (или просмот- просмотреть) таблицу в списке доступных таблиц и перетащить ее в активную форму вашего проекта:
Просмотр данных .NET 465 В - 0f Data Connections '■• 'Щ <Add Connection... > Щ G% skinnerrn\netsdk.MattixTest.dbo Й ■ % skinnerm\NET5DK.rJorthwind.QSUser В '■&£ Database Diagrams - % Tables IS- Щ Categories (dbo) £■ Щ CategoryAudit (dbo) t' ■ 1Ш Custorne' " -stomerDemo (a ) Jt Ш CustcrnerDe- ographics (dbo) e ш SJESSuSSGE2S E Щ Employees (dbo) Для примера выберем таблицу Customers. Когда вы перетащите этот объект в свой про- продукт (его можно поместить в форму или палитру элементов управления сервера), в форму будет добавлен объект, производный от SqlDataAd= er или OleDbDataAc- <"ег, если вы не используете SQL Server. Сгенерированный адаптер данных содержит команды д. я ~"ф. INSERT, UPDATE и DELETE. Излишне говорить, что они могут (и, вероятно, должны) oli . изменены для вы- вызова хранимых процедур, а не использования SQL. Однако на данный момент нам вполне достаточно кода, сгенерированного мастером. IDE добавляет в ваш файл .cs следующий код: private System. Data. SqlClient.SqlCommand sqlSelectCommandl; private System..Data.SqlClient.SqlCommand sqllr.s^rtCommandl; System .'Data. SqlClient. SqlCommand sq" "JpdateCommandl ; System.Data.SqlClient.SqlCommand sqIDeleceCommandl; private private private System.Data. SqlClient. sqlDacaAdapterl ; Для каждой SQL-команды определены объект и r^l-'ataAda; : .. Да-iee в файле, в методе Initialize"or.ponent, мастер генерирует код для создания каждой из этих команд и адаптера данных. Код большой, поэтому здесь приводятся только выдержки. Следует рассмотреть два метода, генерируеммх \isual Studio "pdateCommana и Тп- sertConr'a'- 5. Приведем сокращенную версию, содержащую необходимую информацию: П '//< и- sqlInsertCommandl thi.s.sqllnsertCommandl.CommardText = Ъ"INSERT INTO dbo.Customers (Ouscc.nerlD, CompanyName, ContactName, ContactTitle, Address, Ci":y, Region, PostalCode, Country, P..one, Fax) ( VALUES (@CustomerID, @CompanyNaroe, SCOntactName SContactTitle, ©Address, ! " @City, SRegion, SPostalCode, SCountry, @Phone, ©Fax); SELECT CustomerlD, CompanyName, ContactSaaie, ContactTitle, Address, City, Region, PostalCode, Country. Phone, Fax FROM dbo.Customers WHERE (CustomerlD = ?Select2_CustomerID)"; this. sql'InsertCoiranandl. Connection ■= this,sqlConnectionl; // sqlUpdateCommandl this.sqlUpdateCommandl.CommandText = ©"UPDATE dbo. :cistomers SET CustomerlD = SCustomerlD, CompanyName = SCompanyName, ContactName = SContactName, Cc.icaccTitle = SContactTitle, Address = @Address, City = @City, Region = SRegion, PostalCode = SPostalCode, Country = SCountry, Phone = @ Phone, Fax = @Fax WHERE (CustomerlD = @Original_CustomerID) AND (Address = @Original_Address) AND (City = @Original_City) AND (CompanyName = @Original_CompanyName) AND (ContactName = - @Original_ContactName) AND (ContactTitle = @Original_ContactTitle) AND (Country = @Original_Country) "; AND (Fax = SOriginal_Fax) 1 . AND (Phone = @Original_Phone)
466 Глава 12 AND (PostalCode = @Original_Posta2-Code) AND (Region = @Original_Region); SELECT CustomerlD, CompanyName, ContactName, ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax FROM dbo. Customers WHERE (CustomerlD = @Select2_CustomerID)"; this. scjlUpdateCommandl. Connection = thJn.sqlConnectionl; Наибольший интерес представляет сгенерированный SQL. Как для команды Insert, так и для Up- a'.e на самом деле указаны два оператора SQL: один для вставки или обнов- обновления, другой для повторного получения строки из базы данных. Эти на первый взгляд лишние предложения используются для синхронизации дан- данных на клиентской машине с данными на сервере. К вставляемым столбцам могут быть добавлены значения по \"молчанию, либо могут сработать триггеры базы данных, кото- которые обновляют некоторые из столбцов в вставляемой обновляемой записи, поэтому по- повторная синхронизация данных имеет свой смысл. Параметр @Select2_CustomerID, используемый для повторного выбора данных, является той же величиной, которая пере- передается в оператор . - pdate для основного ключа — такое имя было автоматически сгенерировано мастером. Для таблиц, которые содержат столбец ident n Y, сгенерированный SQL применяет значение @@IDENTITY после оператора _ sert. Как уже говорилось в главе 11, использо- использование @@identity для генерации основных ключей может привести к появлению инте- интересных ошибок, поэтому, скорее всего, вы пожелаете изменить этот оператор SQL. Аналогично, если у вас отсутствуют вычисляемые поля, будет накладно заново выбирать все столбцы из оригинальной таблицы в случае, когда что-то было обновлено. Код, сгенерированный мастером, будет работать, однако он далек от оптимального. В реальном проекте вам потребуется заменить многие, если не все, предложения SQL вызовами хранимых процедур. Если после выполнения вставки или обновления записи не требуется повторная синхронизация, то удаление лишнего оператора SQL несколько ускорит работI приложения. Генерация DataSet Теперь, после определения aAdapter. можно создать г ataSet. Для генерации Data- oeu щелкните мышью на Dat _ apter и откройте окно свойств объекта (F4). В нижней части списка свойств имеются три пункта- Configure DataAfer .er.... Configurations Щелкнув мышью на Generate DataSet..., вы сможете указать имя нового объекта Data- Set, а также выбрать таблицы, которые необходимо добавить в этот DataSet. Если вы перетащили в форму из окна Server Explorer несколько таблиц, их можно объединить в одном DataSc":. В действительности создается схема XSD. которая определяет набор данных и каж- каждую включенную в него таблицу. Это аналогично примеру из предыдущей главы, но в данном случае файл XSD создается автоматически: -DataAccessInVisuaBtudk) -о х UJ Solutio :В 1 DataAco. )'A project) udio 1-й R5 ft. |j5| i References bin obj Assemblylnfo.cs DataSeU.xsd *Й DataSetl.cs
Просмотр данных .NET 467 Помимо файла XSD, имеется (скрытый) файл . сь, который определяет несколько безо- безопасных по типу классов. Для просмотра этого сгенерированного файла нажмите на кнопку Show All Files в панели лнструментов, как показано выше, и раскройте файл XSD. Вы увидите файл . cs с тем же именем, что и у файла XSD. В нем определены следующие классы: О Класс, производный от ~iai_aS D Класс, произ юдный от DataT^ ъ для выбранного LataAdapter П Класс, производный от "JataRo . определяющий столбцы, доступные в DataTable 3 Класс, пр Н13ВОДНЫЙ от ventA- , используемый при изменении строки Вы, вероятно, уже догадались, что для генерации этого файла и этих классов исполь- использовался XSD.~.""~ (см. главу 11 Естественно, вы можете обновить фай.. XSD после завершения работы всех масте- мастеров, но не пытайтесь репетировать файл ... . поскольк\' он будет сгенерирован заново при перекомпиляции лроекта, и i( e сделанные изменения будут потеряны. Построение схемы В предыдущей главе был рассмотрен i -шн из способов определения схемы XSD в тексте. Теперь сделаем э. о по-другому. \ isual Studio включает в с бя редактор для создания схем XSD — в меню P'oject выберите Add Class, а затем элемент XML Schema: Categories: Templates: - ] Local ProKct Items „JCode _J Web _J Utility 1 Resources XSLT - e F,le XML Scheme ■- ' Date Form Wcard A fife for creating з schema for XML documentf [ TestScrema.xsd Open Cancel I Help В проект б\дут добавлены два файла: файл .XSD и соответствующий файл .cs. Хотя cxt-via XSD ь данн!- i момент шета, исходный код на О содержит класс, производный от a Schema ГеП<М. Если переключиться из вида Schema в вид XML, можно увидеть шаблон схемы: <?xml version=.0" encoding=nutf-8" ?> <xsd:schema id="TescSchema" targetNaxnespace=" http: / / tempuri. org/TestSchema. xsd" elementFormDe£ault="qualified" xmlns="ht^p://tempuri.org/TestSchema.xsd" xmlns:xsd="hetp://www.w3.org/2001/XMLSchema"> </xsd:scheffla> Сценарий XSD генерирует следующий код на С#: namespace DataAccessInVisualStudio { using System; using System.Data; using ЗуБее....Ри.^1те. Serialization;
468 Глава 12 [System.ComponentModel.DesignerCategoryAttribute("code")] public class TestSchema : System.uata.DataSet { public TestSchema() ( this.InitClassO ; } private TestSchema(Serializationlr.fо info, StreamingContext context) { this.InitClass(); this.GetSerializatioriDatafinfo, context); } protected override bool ShouldSerializeTablesO { return false; .} protected override bqol ShouldSerializeRelationsO { return false; ) private void InitClassО { this. DataSetName = "TestSchema".; this.Namespace = "http://tempuri.org/TestSehema.xsd"; Используем этот код в качестве стартовой точки — он будет изменяться по мере до- добавления элементов в схему XSD. Добавление элемента Первое, что нужно сделать, — добавить новый элемент верхнего уровня. Щелкните правой кнопкой мыши в рабочей области и выберите Add | New Element: «D3taAcce»ln*teualstuc<o - MiaoMR V*ua!Cu>'T[design]- Teftsctitma.K«S Fte у* loots ffirdcn Help <fl- Ki , Debug Po l.«[Desqn TeetSchemajisd j I 9 I j* - ^'lii * ЕЛ т CW».e ley j I] Mew toncJexType b] New jmpleType 7^1 Ki^v д ^ New j i|^ | Generate QataSet Гд PreviewOataSet... На экране появится новый безымянный элемент. Необходимо указать имя элемента - в данном примере вводится Product:
Просмотр данных .NET 469 Когда вы сохраняете файл XSD, модифицируется файл С#. В нем создается несколь- несколько новых классов. Приведенный ниже рисунок показывает основные генерируемые классы и их происхождение: TestSchiema: DataSet - productDatatable. DataTabte, Enumerable \ ProductRow; DataRow ProductRowChangeEvent: EverrtArgs Обсудим наиболее важные для нас аспекты сгенерированного кода: public class TestSchema :' System.Data.DataSet { private ProductDataTable tabieProduct; (System.ComponentModel.Designerberiari'zationVisibilityActr-t-'-te (System.ComponentModei.ri3i3nerSerializationVisibil--y .Content).] public ProductDataTable Product { get { return this.tabieProduct; Создается новая переменная класса Products. 1е. Этот объект возвращается свой- свойством Product и конструируется внутри обновленной функции ;.-.itClass. Пользова- Пользователь этих классов может сконструировать '.ь s^et из класса, содержащегося в этом файле, и применить DataSet. Product = Х1Я получения DataTa- .e таблицы Products. Генерируемая DataTable Приведенный ниже код сгенерирован для DaisTable (Proa г , которая была добавлена в шаблон схемы: public delegate void ProductRowChangeEventHandler (object sender, ProductRowChangeEyent e; ; public class ProductDataTable : DataTable, System.Collections.ZEnumerable { f internal ProductDataTable() : base("Produce") i: _' this.InitClassO ; ) [ System-. ComponentModel. Browsable (f al se) ] public int Count { get { return this.Rows.Count;} } public ProductRow this[int index] { get { return ((ProductRow,) (this.Rows[index]));} •} ,. public event ProductRowChangeEventHandler ProductRowChanged; ., public event ProductRowChangeEventHandler ProductRowChanging; 5 public event ProductRowChangeEventHandler ProductRowDeleted; public event ProductRowChangeEventHandler ProductRowDeleting; Сгенерированный класс productTable является производным от DataTable и включает в себя реализацию IEnumerable. Определены четыре события, использующих объявленного перед классом делегата. Делегату передается экземпляр класса ProductRowChangeEvent, определенного Visual Studio.
470 Глава 12 Сгенерированный код содержит класс, производный от DataRow, который допускает безопасный по типу доступ к столбцам в таблице. Новую строку можно создать одним из двух способов: О Вызвать функцию NewRow (или сгенерированную KjiwProductRow) для получения нового экземпляра класса Row. Передайте нов\то строку в функцию AddRow () (или безопасную по типу AddProduct^ow). О Вызвать функцию AddRow (или сгенерированную Ac jProductRow) и передать ей массив объектов, по одному для каждого столбца в таблице. Функции Ad JProductRow таковы: public void AddProductRow(ProductRow row) t this.Rows.Add(row); } public ProductRow AddProductRow(; { PsoductRow rowProductRow .=? { (ProductRow) (this.NewRow() )'; rowProductRow.IcemAray = new Object[0]; this.Rows.Add{rowProductRow); return rcwProductRow; У Вторая функция не только создает hobvjo строку, но и добавляет ее в коллекцию Rows для DataTable, а затем возвращает этот объект вызывающему. Остальные методы Data- Table предназначены в основном для генерации событий и здесь не обсуждаются. Генерируемая DataRow Сгенерированный класс г г riactRow имеет вид: public class ProductRow : DataRow { private ProductDataTable tableProduct; internal ProductRow(DataRowBuilder rb) : base :b) { this.tableProduct = ((ProductDataTable)(this.^able)); Так как внутри файла XSD еще не определены столбцы", класс Pi "uctRow пуст. Для добавления элемента в шаблон XSD, который станет столбцом в классе ProductRow, щелкните мышью на экране и определите тип Element, а затем наберите имя столбца и тип данных: При сохранении шаблона XSD файл . cs автоматически перекомпилируется. Добав- Добавление элемента Product ID приводит к изменению файла .cs. В классе Prod_ ;t DataTable (производном от DataTable) появляется закрытый член, который представляет собой новый DataColumn: private DataColumn columnProductld; Ему сопутствует свойство ProductlDColumn, как показано ниже. Свойство объявляется как internal: internal DataColumn ProductldColumn { get { return this.columnProductld; )
Просмотр данных .NET 471 Функция AddProductRow () также претерпевает изменения. Она принимает теперь це- целочисленное значение ProductID и сохраняет введенное значение в только что созданном столбце: public ProductRow AddProductRow(int Productld) { ProductRow rowProductRow = ((PioductRow1(this.NewRowf))); rowProductRow.ItemArray = new ^bject[2 {Productld}; this.Rows.Add(rowProductRow); return rowProductRow; } Наконец, в ProductDa ~ le модифицируется метод InitClj ~ (): private void InitClasb ) { this.columnProductld = new DataCoZ-umn ( "Productld", typeof(int , и n System. De~_ .Mappii-gvi pp. Element) ; this.coluimProductlc'.AlDowDBNu- = false; thii.Columns. Add(th ..- lumnProc :Id) ; } Этот код создает новый Dat< .. - и добавляет его в кол. екцию Со1„ ; таблицы DataT'n :. Последний параметр в конструкторе DataColumn определяет, как этот столбец отображается в XML, когда, например, DataSet сохраняют в фалле XML. Класс ProductRow обновляется с целью добавления аксессора для этого столбца: public int Productld { get { return ( (-: :) (this [ttu3. _ac_eProduct.P:E „ductldCo: .. "i; set . this[this. .. 1 .eProduc- . ProcuctldColur"-' = value,- } Генерируемый EventArgs Последний класс, дл ' гяемый в иеходный код. является производным от EventArgs. Этот класс предлагает методы для прямого д( ступа к строке, которая была изменена (или меняется), и илствие, применяемое к этой строке. Другие общие требования Общее требование при < 'тображении данных создать в. плывающее меню для заданной строки. Для этого существуют много различных способ -в. Мы остановимся на том, кото- который упростит требуемый код, особенно при отображении пс ... где выводится на- набор данных с отношениями. Проблема заключается в том, что контекстное меню зависит от того, какая строка выбрана, и эта строка может быть любой из нескольких исходных таблиц внутри набора данных. Так как контекстное меню будет иметь общее назначение, приводимая здесь реализа- реализация использует базовый класс (ContextDataRow), который содержит код для создания меню. Каждый класс, поддерживающий всплывающее меню, является производным от этого базового класса. Когда пользователь щелкает правой кнопкой мыши в любой части строки в DataGrid, мы будем просматривать строку и проверять, является ли она производной от Context- ContextDataRow, и если да, то для нее может быть вызвана функция PopupMenu (). Это можно ре- реализовать с помощью интерфейса, однако в данном случае мы упростим базовый класс. В этом примере будут сгенерированы классы DataRow и DataTable, которые мегут использоваться для безопасного по типу доступа к данным. На этот раз мы напишем К8Д вручную. Будет также показано использование пользовательских атрибутов и отражения. На рисунке приведена иерархия классов для этого примера:
472 Глава 12 DataTable - — "CustomerTable . OrderTable DataRow :ontextDataRow - CustorperRow OrdefRow Полный код примера доступен в каталоге ll_Miscellaneous: using System; u&ing System.Windows -Forms; using System.Data; using System.Data.Sqlclient; using System.Reflection; public class CotttextDataRow : DataRow f public ContextDataRowfDataRowfeuilder builder) : base(builder) public void PopupMeriuf System .V.i:. „ is .Forms. Control parent, int x, int y) { .'/ Используем отражение для получения списка команд всплывающего меню Kemfcerfnfot] members = this.Ge~Cype() FindMertbers(MemberTypes.Method, BindingFlags.Public BindingFlags.Instance, new System.Reflection.MemberFilter(Filter), null); if (members.Length > 0) < if* Создаем контекстное меню ContextMeiu menu = new CpntextMemif.) ; // Теперь проходим тто этим членам и генерируем всплывающее меню // Обратите внимание на приведение Ж Methodlnfo в foreach f,oreach'(Methoalnfp mech in members) // Получаем заголозок для операции из /7 CdijtextMenuAttribute ContextMenuAttribute[] ctx = (ContextMenuAttribute[]) meth.GetCustomAttributes(typeof(Con^extMenuAttribute), true) MenuCommand callback = new MenuCommand(chis, meth); Menultem item = new MenuItemfctxtO1.Caption, new EventHandier(callback.Execute)); item.Defaultltem = ctxfO].Default; menu.MenuItens.Add(item); System.Drawing.Point pc = menu,Show(parent, pt > г Cvstem.Drawing.Point(x,у); private bool Filter(MemberInfo member, object criteria) bool blnclude = false; // Приводим MemberInfo к Methodlnfo
Просмотр данных .NET 473 Methbdlnfo meth = member as Methodlnfo; if (meth!=null) if(meth.ReturnType -= typeof(void)) t ParameterInfо(] parms = meth.GetParametersO ; if (pai-ms. Length == Oj // Проверим, присутствует ли для метода атрибут ' // ContextMenuAttribute object!) atts = meth.GetCustomAttr'ibutes (typeof(ContextMermAttribute), tru3t ; blnclude = (atts.Length ==- 1); Л } return blnclude; Контекстный класс строки данных является производным от Za - aRow и содержит все- всего две функции. Первая, PopupMenu, применяет отражение для просмотра методов, кото- которые соответствуют конкретной сигнатуре, и выводит для пользователя всплывающее меню с этими пунктами. Функция F. It .) используется PopupMenu в качестве делегата при перечислении методов. Она возвращает true, если функция класса удовлетворяет соответствующим соглашениям по вызову: Memberlnfо[] members = this-.GetType( )FindMembers(Me.-nberTypes.Method , BindingFlags.Pubiic 1 BindingFlags.Instar~e, new System.Reflection.MemberFilter(Filter), null); Это оператор используется для фильтрации всех методов текущего объекта и возвращает только те из них, которые соответствуют следующим критериям: О Член должен быть методом О Член должен быть открытым методом □ Член должен возвращать void О Член обязан принимать нулевые параметры О Член должен содержать атрибут ContextMenuAttribute ContextMenuAttribute является пользовательским атрибутом, определенным спе- специально для этого примера. Мы обсудим его после изучения метода PopupMenu: ContextMenu roenu = new ContextMenu (); foreach(MethodInfo meth in members) { // Добавляем элемент меню } System.Drawing.Point pt = new System.Drawing.Point(x,y); menu.Show(parent, pt) ; Создается экземпляр контекстного меню, а затем осуществляется проход в цикле по каждому методу, удовлетворяющему указанному критерию, и добавляется элемент в меню. Меню отображается следующим образом:
474 Глава 12 CompanyNci Something С i ^"o Trujillo E Antonio Mo1- Around the > H'AMTON if -.rout J>_H:BERGS js blaus . B.BLONP' а;воир sJbonap ш;в6ттм BoiidoCo' "- Martin Son .Bon app' Laurence Let- jBottom-Dc l-r' Elizabeth Lin v I Reliieve I Наиболее сложной частью это1"' примера явл* тс часток кода, повторяемый для каждой функции класса, которую геогходимо отобра. i i во всплывающем меню: System.Type ccxtype = typeof ~ r.rextMenuAtt - ContextMenuAttributel] ctx = ContextMenuAt--i--r i, meth.GetCustomAttrib^tes 'typeof (Ccnt^xc: :enuAttribjte) , true.) ; MenuCoirraancl5 callback = new Уэ-uCommand(this -e."'. Menul-em item = new Menul'.er ctx[0] .Captior.. new I2ventKar_aler (ca__back.Exi3jirs ; item.Defaoltlcem = _ -_ .' ■' .refault; menu. Menu I terns. Ace .teni ; Каждый метод содержит атрибут Ccr. .. MenuAi- . "e. Он определяет "хорошее" имя для пункта меню, так как имя метода не может содержа, ь пробелов. Атрибут извлекает- извлекается из метода, после чего о здается новый элемент меню, который добавляется в коллекцию элементов всплывающего меню. Меню в .NET вызываю- е аегатов. аи: посылают ее гщения Windows. Что касается сигнатуры элемент^ меню, то делегат не -овпадает с вы'ранной сигнатурой. Для мето- методов, которые испо. ьзуются в контекста м меню, реалчзован простой управляющий класс lienuCommanc". Он оборачивает вызываемый объект простым объектом, который может быть запущен из меню Создаваемые таблицы и строки В примере XSD (см. вь> не) приведен код, ki торый был сгенерирован с использованием редактора Visual Stud о для созде ш набора классов доступа к данным. Вам, вероятно, интересно знать, как выглядит млнимальнын набор кода для этих классов. Следующий класс показывает треб%емые мет! гы для L аТаЫе: public" class Cus:fcoraerTable : з itaTable .- public CustomerTabls ) : .. .("Customers") ■;-..{ this.Columns.Add("Custo ^r~D", typeoffstrir ■ this. Columns. Add (" Compa- *>: аые" , ypeof (.= ■■_ i ' ' ; this.Columns.Add( "Contact\€"ne", typec Г Ina) ) ; protected override System.1 урн n,rL. dv ' С return ryt;;f (C_.".-<_. F.r. с ; prdtepted overr.de '=>.=>' ■■. r der(DataFoi.'L.. lder buider) return(DataRow) rew C\ ^""отез с з (Ьч4 ' г ; Первое требование lataTab] о состочт в том, 'тт i перекрываете функцию GetRow- Туре. Это используется внутри .NET при генег аг i i-овых ( грок таблицы. Вы должны возвращать тип класса, используемого ;j j ■ p(,i. . \.< ,"\я ллир^ч строки.
Просмотр данных .NET 475 Следующее требование заключается в реализации NewRowFromBuilder, который вы- вызывается средой исполнения при создании новых строк в таблице. Этого достаточно для минимальной реализации. Наша реализация добавляет столбцы в DataTable, поскольку в данном примере заранее известно, какие столбцы будут использоваться. Соответствую- Соответствующий класс Customer Rdj очень простой. Он реализует свойства для каждого столбца в строке и функции, которые отображаются в контекстном меню: public class Cus :omerRow : Context э ow { public CustomerRowfDaterLowBuilder bu- r) : b;_s< 'builder) { } public string CustomerlD { get { return (string)this["uusc :>merID". ;■ set { this["CustomerlD"] = val..;} } // Остальные свойства опушены для у-рощения [ContextMenu("Blacklist Customer")] public void Blacklist!) { // Выполняем некс орые действия } [ContextMenu("Get Contact",Default=true)] public void GetContactO { // Выполняем нек ,,торые дру = ;ействия Этот класс является производным от Contex : ^^aRow, содержащего соответствую- соответствующие методы получения/ установки для свойств с теми же именами, что и у полей. После этого может быть добавлен набор методов, которые используются при отражении класса: [ContextMenu\ "Black: ist Cus ">mer"I public void Blacklist() { // Выполняем некоторое действия Каждый метод, который должен присутствовать в контекстном меню, обязан иметь такую же сигнатуру и содержать пользовательский атрибут ContextMenu. Использование атрибутов Идея, ставшая причиной создания атрибута jnt^xtMenu, заключается в предоставле- предоставлении возможности указания любого имени для данного пункта меню. Реализован также флаг de f аи 11, который используется для указания пункта меню, выбранного по умолчанию. Представим класс атрибута: [AttributeUsage(AttributeTargets.Method,AllowMultiple=false,Inherited=true)] public class ContextMenuAttribute : System.Attribute { public ContextMenuAttribute(string caption) ' ( Caption = caption,- Default = false; } public readonly string Caption; public bool Default,-
476 Глава 12 Атрибут Attr -outeUsage для класса помечает атрибут ContextMenuAttribute как используемый только для методов. Он также указывает, что может существовать только один экземпляр этого объекта для любого метода. Предложение Inherited=true опреде- определяет, может ли атрибут быть указан для метода базового класса и отражен в производном классе. Вероятно, вы сможете придумать несколько других членов, которые можно добавить к этому атрибуту. Вот некоторые примеры: Э Горячая клавиша для пункта меню 3 Изображение О Текст, который необходимо отобразить в панели инструментов, когда мышь наведена на пункт меню О ID контекстной помощи Методы диспетчеризации При отображении меню с помощью .ХЕТ каждый его пункт связывается с обрабатываю- обрабатывающим кодом этого пункта через делегата. Имеются на выбор два варианта реализации ме- механизма привязки пунктов меню к коду Первый - реализовать метод с такой же сигнатурой, что и у System.EventHandler. Он определяется следующим образом: public delegate void EventHandler (Object gender, EventArgs ej ; Второй — определить прокси-класс, который реализует делегата и перенаправляет вызовы в принимающий класс. Этот шаблон известен под именем Command, и именно он используется в данном примере. Командный шаблон разделяет вызывающего и вызываемого при помощи простого промежуточного класса. Вы можете подумать, что это слишком сложно для такого при- примера, однако при этом упрощаются методы для каждой DataRow (так как им не прихо- приходится передавать параметры делегат}) и, кроме того, такая схема является более расширяемой: public class MenuCommar1 public Mer-L.Commar.id(object receiver, Methodlnfo method) Receiver = eceiver; Method = тес--ЭЙ; public void Execute(object sender, EventArgs e) , Method. Invoke. Receiver, new object [] {) ) ,- риЫ i.c readonly ofcrect Receiver; public readonly Meihodlnfo Method,- Класс предоставляет делегата EventHandler (метод Execute), который вызывает требуемый метод для объекта-получателя. В примере демонстрируются два различных типа Row: строки из таблицы Customers и строки из таблицы Orders. Естественно, что варианты обработки каждого из этих типов данных отличаются. На рисунке выше были показаны действия, доступные для строки Customer. Следующий рисунок представляет действия для строки Order:
Просмотр донных .NET 477 Получение выбранной строки Последней головоломкой этого примера является следующее: как узнать, на какой стро- строке в DataSet щелкнул мышью пользователь. Первой вашей мыслью может быть: "это дол- должно быть свойство DataGr id", однако вы не обнаружите там такого свойства. Вы можете обработать координаты мыши, используя обработчик KouseUp , но это поможет только в том случае, если отображаются данные из одной Laf =iTable. Вернемся к заполнению DataGrid и рассмотрим строку: dataGrid.SetDa'aBinding(ds, " dusterэгб") ; Помните раздел, посвященный привязке данных? Этот метод добавляет новый CurrencyManager в 3indingContext, который представляет текущую таблицу и набор данных. DataGrid имеет два члена — DataSi irce и DataMember, которые устанавлива- устанавливаются при вызове SetDataBindinj. DataSourcc в данном случае будет являться DataSet, a DataMember — Customers. Мы имеем источник данных и объект данных и знаем, что эта информация хранится в BindingContext формы. Нам необходимо просмотреть эту информацию: protected void dataGrid_MouseUp(object .sender, MouseEventArgs. e) С II Выполним проверку на нажатие If (e.Button==MouseButtons.Right) { // Вычислим, н& какой строке щелкнул мышью пользователь DataGri'd.hJ'-mesilnfo hfci = dataGrid.HitTest(e.X, e.Y); // Проверим, попал ли пользователь в ячейку if (hfci.Type == DataGrid.HitTestType.Cell) { // Найдем DataRow, соответствующую ячейке, // в которой щелкнул ккшью пользователь После вызова data'^id.HitTes . для выяснения того, где пользователь щелкнул мышью, получим экземпляр Bindir ;KanagerBase для DataGrid: BindingManagerBase bnr.b = this.BindingContext[dataGrid.DataSource, dataGrid.DataMember] ,- Этот код использует DataSource и DataMerrber от DataGrid для указания объекта, который должен быть возвращен. Теперь необходимо найти строку, на которой пользо- пользователь щелкнул мышью, и отобразить контекстное меню. При щелчке правой кнопкой мыши на строке индикатор текущей записи не перейдет на эту строку, но это нас не удовлетворяет. Мы желаем переместить указатель строки и только после этого отобра- отобразить меню. Из объекта HitTestlnf о получен номер строки, поэтому все, что требуется,— это переместить текущую позицию для объекта Bindir.gManagerBase: bmb. Position = hti.Row,- Индикатор строки будет перемещен. Кроме того, если теперь обратиться к классу за строкой, будет получена текущая строка, а не та, что была выбрана последней: DataRowV'ew drv = bmb.Current as DataRowView; if (drv ;= null) { ContextDa ;aRcw ctx = drv.Row as ContextDataRow; if (ctx!=null) ctx.PopupMenu(dataGrid, e.X, e.Y); Поскольку DataGrid отображает элементы из DataSet, объект Current в коллекции BindingManagerBase является DataRowView. Теперь можно получить конкретную строку, оболочкой для которой служит DataRowView, проверить, является ли она ContextDataRow, и вывести меню.
478 Глава 12 В примере были созданы две таблицы данных: Customers и Orders, а также определено отношение между этими таблицами. При нажатии на CustomerOrders вы получаете отфи- отфильтрованный список заказов. Когда вы делаете это, DataGrid меняет DataMember с Custo- Customers на Customers. CustomerOrders, и именно эту информацию использует индексатор BindingContext для получения данных, отображаемых на экране. Заключение В этой главе рассмотрено несколько способов отображения данных в .NET. В Sys- System . Windows. Forms присутствует большое число классов, мы коснулись только наиболее часто используемых. Были описаны DataGi id и многие из его особенностей. В разделе, посвященном XSD, рассмотрена интеграция Visual Studio и схем XML, а так- также представлен созданный вручную пример, показывающий минимальную реализацию. В следующей главе обсуждаются другие возможности .NET, связанные с XML.
ПРОГРАММИСТ — ПРОГРАММИСТУ для профессионалов Платформа .NET предлагает новую среду, в которой можно разрабатывать практически любое приложение, действующее под управлением Windows, а язык С# - новый язык программирования, созданный специально для работы с .NET. В этой книге представлены все основные концепции языка С# и платформы .NET. Полностью описывается синтаксис С#, приводятся примеры построения различных типов приложений с использованием С# - создание приложений и служб Windows, приложений и служб WWW при помощи ASP.NET, а также элементов управления Windows и WWW. Рассматриваются общие библиотеки классов .NET, в частности, доступ к данным с помощью ADO.NET и доступ к службе Active Directory с применением классов DirectoryServices Для кого предназначена эта книга чЭта книга предназначена для опытных разработчиков, возможно, имеющих опыт программирования на VB, C++ или Java, но не использовавших ранее в своей работе язык С# и платформу .NET. Программистам, применяющим современные технологии, книга даст полное представ- представление о том, как писать программы на С# для платформы .NET. Основные темы книги • Все особенности языка С# • С# и объектно-ориентированное программирование • Приложения и службы Windows • Создание web-страниц и web-служб с помощью ASP.NET • Сборки .NET • Доступ к данным при помощи ADO.NET • Создание распределённых приложений с помощью .NET Remoting • Интеграция с COM, COM+ и службой Active Directory FromVB.VC, Java Наша задача - поделиться с вами знаниями опытных программистов и тем самым обеспечить успех на всех стадиях вашей карьеры. Исходные коды и дополнительные материалы к книге доступны по бесплатной подписке на Web-сайте WWW.wrOX.COITI wrox Wrox Press Inc., 29 S. LaSalle St, Suite 520, Chicago, Illinois 60603. USA> Wrox Press Ltd., Arden House, 1102 Warwick Road, Acocks Green, Birmingham. B27 6BH. UK.