
Том 1
Глава 1. Архитектура на платформата .NET и .NET Framework
Глава 3. Обектно-ориентирано програмиране в .NET
Глава 4. Управление на изключенията в .NET
Глава 5. Обща система от типове (Common Type System)
Глава 9. Символни низове (Strings)
Глава 13. Релационни бази от данни и MS SQL Server
Глава 14. Достъп до данни с ADO.NET
Том 2
Глава 15. Графичен потребителски интерфейс с Windows Forms
Глава 16. Изграждане на уеб приложения с ASP.NET
Глава 17. Многонишково програмиране и синхронизация
Глава 18. Мрежово и Интернет програмиране
Глава 19. Отражение на типовете (Reflection).
Глава 20. Сериализация на данни
Глава 21. Уеб услуги с ASP.NET
Глава 22. Отдалечено извикване на методи (Remoting)
Глава 23. Взаимодействие с неуправляван код.
Глава 24. Управление на паметта и ресурсите.
Глава 25. Асемблита и разпространение
Глава 26. Сигурност в .NET Framework
Глава 27. Mono - свободна имплементация на .NET
Глава 28. Помощни инструменти за .NET разработчици
Програмиране за .NET Framework
Светлин Наков и колектив
Александър Русев
Александър Хаджикръстев
Антон Андреев
Бранимир Ангелов
Васил Бакалов
Виктор Живков
Галин Илиев
Георги Пенчев
Деян Варчев
Димитър Бонев
Димитър Канев
Ивайло Димов
Ивайло Христов
Иван Митев
Лазар Кирчев
Манол Донев
Мартин Кулов
Михаил Стойнов
Моника Алексиева
Николай Недялков
Панайот Добриков
Преслав Наков
Радослав Иванов
Светлин Наков
Стефан Добрев
Стефан Захариев
Стефан Кирязов
Стоян Дамов
Тодор Колев
Христо Дешев
Христо Радков
Цветелин Андреев
Явор Ташев
Българска асоциация на разработчиците на софтуер
София, 2004-2005
Програмиране за .NET Framework
© Българска асоциация на разработчиците на софтуер (БАРС), 2005 г.
© Издателство "Фабер", 2005 г.
Настоящата книга се разпространява свободно при следните условия:
Читателите имат право:
- да използват книгата и учебните материали към нея или части от тях за всякакви цели, включително да ги да променят според своите нужди и да ги използват при извършване на комерсиална дейност;
- да използват сорс кода от примерите и демонстрациите, включени към книгата и учебните материали или техни модификации, за всякакви нужди, включително и в комерсиални софтуерни продукти;
- да разпространяват безплатно непроменени копия на книгата и учебните материали в електронен или хартиен вид;
- да разпространяват безплатно оригинални или променени части от учебните материали, но само при изричното споменаване на източника и авторите на съответния текст, програмен код или друг материал.
Читателите нямат право:
- да разпространяват срещу заплащане книгата, учебните материали или части от тях (включително модифицирани версии), като изключение прави само програмният код;
- да премахват настоящия лиценз от книгата или учебните материали.
Всички запазени марки, използвани в тази книга, са собственост на техните притежатели.
Официален сайт:
ISBN 954-775-505-6
|
Национална академия по разработка на софтуер |
|
|
Лекторите » Светлин Наков е автор на десетки технически публикации и няколко книги, свързани с разработката на софтуер, заради което е търсен лектор и консултант. Той е разработчик с дългогодишен опит, работил по разнообразни проекти, реализирани с различни технологии (.NET, Java, Oracle, PKI и др.) и преподавател по съвременни софтуерни технологии в СУ "Св. Климент Охридски". През 2004 г. е носител на наградата "Джон Атанасов" на президента на България Георги Първанов. Светлин Наков ръководи обучението по Java технологии в Академията.
» Мартин Кулов е софтуерен инженер и консултант с дългогодишен опит в изграждането на решения с платформите на Microsoft. Мартин е опитен инструктор и сертифициран от Майкрософт разработчик по програмите MCSD, MCSD.NET, MCPD и MVP и международен лектор в световната организация на .NET потребителските групи INETA. Мартин Кулов ръководи обучението по .NET технологии в Академията. |
Академията » Национална академия по разработка на софтуер (НАРС) е център за професионално обучение на софтуерни специалисти.
» НАРС провежда БЕЗПЛАТНО курсове по разработка на софтуер и съвременни софтуерни технологии в София и други градове.
» Предлагани специалности: § Въведение в програмирането (с езиците C# и Java) § Core .NET Developer § Core Java Developer
» Качествено обучение с много практически проекти и индивидуално внимание за всеки.
» Гарантирана работа! Трудов договор при постъпване в Академията.
» БЕЗПЛАТНО! Учите безплатно във въведителните курсове и по стипендии от работодателите в следващите нива. |

|
Българска асоциация на разработчиците на софтуер (БАРС) е нестопанска организация, която подпомага професионалното развитие на българските софтуерни специалисти чрез образователни и други инициативи. БАРС работи за насърчаване обмяната на опит между разработчиците и за усъвършенстване на техните знания и умения в областта на проектирането и разработката на софтуер. Асоциацията организира специализирани конференции, семинари и курсове за обучение по разработка на софтуер и софтуерни технологии. БАРС организира създаването на Национална академия по разработка на софтуер – учебен център за професионална подготовка на софтуерни специалисти.
|
Отзив от Теодор Милев
Свидетели сме как платформата Microsoft .NET се налага все повече в света на софтуерните технологии. Тази тенденция се наблюдава и в България, където прогресивно нараства броят на проектите, реализирани на базата на .NET. С увеличаване на .NET разработчиците расте и нуждата от качествена техническа литература и учебни материали, които да бъдат използвани при обучението на .NET специалисти.
"Програмиране за .NET Framework" е първата чисто българска книга за Microsoft .NET технологиите. Тя представя на читателя в последователен, структуриран, достъпен и разбираем вид основните концепции за разработка на приложения с .NET Framework и езика C#. Книгата обхваща в детайли всички основни .NET технологии като набляга върху най-важните от тях – ADO.NET, ASP.NET, Windows Forms и XML уеб услуги.
По качество на изложения материал книгата се отличава с високо професионално ниво и превъзхожда повечето преводни издания по темата. Тя е отлично структурирана, а стилът на изложението е лесен за възприемане. Информацията е поднесена с много примери, а това е най-важното за един софтуерен разработчик.
Книгата е написана от широк екип доказани специалисти, работещи в партньорските фирми на Майкрософт – хора с опит в разработката на .NET приложения. Основният автор и ръководител на проекта, Светлин Наков, е изтъкнат .NET специалист, лектор в множество семинари и конференции, търсен консултант и преподавател. Негови са заслугите за курсовете по програмиране за платформа .NET във Факултета по математика и информатика на Софийски университет. Негови са и основните заслуги за целия проект по изготвяне на изчерпателно учебно съдържание и книга по програмиране за .NET Framework.
Светлин Наков е носител на най-голямото отличие в областта на информационните технологии – наградата "Джон Атанасов" на Президента Георги Първанов за принос към развитието на информационните технологии информационното общество. Той е автор на десетки статии и книги за програмиране, а настоящото издание е поредната му добра изява.
Настоящата книга е отлично учебно пособие както за начинаещи, така и за напреднали читатели, които имат желание и амбиции да станат професионални .NET разработчици.
Теодор Милев,
Управляващ директор на "Майкрософт България"
Отзив от Божидар Сендов
Книгата е оригинално българско творение, с нищо неотстъпващо по качество и обем на световните бестселъри с компютърна тематика. Материалът е поднесен достъпно и е богато илюстриран с примери, което я прави не само отлично въведение в платформата .NET за начинаещия, но и отличен справочник за професионалиста-програмист на C#. Читателят може да се запознае в детайли не само с общите принципи, но и с редица тънкости на програмирането за .NET. Широко застъпени са редица "универсални" теми като обектно-ориентирано програмиране, регулярни изрази, XML, релационни бази данни, програмиране в Интернет, многозадачност, сигурност и др.
Книгата се отличава със стегнат и ясен стил на изложението, като е постигнато завидно педагогическо майсторство. Това не бива да ни изненадва – авторите са водещи специалисти с богат опит не само като професионални софтуерни разработчици, но и като преподаватели във Факултета по математика и информатика (ФМИ) на СУ "Св. Климент Охридски". Самата книга в значителна степен се основава на работни лекции, използвани и проверени в поредица от курсове по програмиране за .NET Framework във ФМИ. Сайтът на книгата съдържа над 2000 безплатни слайда, следващи стриктно съдържанието й, а книгата е напълно безплатна в електронния си вариант, което максимално улеснява използването й в съответен курс по програмиране.
Не на последно място, заслужава да се отбележи систематичният опит за превод на всички термини на български език, съобразен с вече наложилата се българска терминология, но и с оригинални идеи при новите понятия.
Работата, която авторите са свършили, е наистина чудесна, а книгата е задължителна част от библиотеката на всеки с интерес към езика C# и изобщо към водещата платформа на Майкрософт .NET.
доц. д-р Божидар Сендов
Факултет по математика и Информатика,
Софийски Университет "Св. Климент Охридски"
Отзив от Стоян Йорданов
"Програмиране за .NET Framework" е уникално ръководство за платформата .NET. Въпреки, че не е учебник по програмиране, книгата е изключително подходяща както за начинаещия програмист, сблъскващ се за пръв път с .NET, така и за опитния разработчик на .NET приложения, целящ да систематизира и попълни знанията си. Всяка тема в "Програмиране за .NET Framework" започва с основите на разглежданите в нея технологии, но към края на темата читателят е вече запознат с детайлите и тънкостите, необходими за успешното им прилагане в практиката.
Обхващайки най-важните аспекти на .NET Framework, книгата започва от основите на езика C# и .NET платформата и постепенно достига до сложни концепции като уеб услуги, сигурност, сериализация, работа с отдалечени обекти, манипулиране на бази данни чрез ADO.NET, потребителски интерфейс с Windows Forms, ASP.NET уеб приложения и т.н. Информацията е поднесена изключително достъпно и подкрепена с многобройни примери и илюстрации. Всяка тема включва и упражнения за самостоятелна работа – неотменим елемент за затвърдяване на придобитите от нея знания.
Авторският колектив включва утвърдени специалисти от софтуерните среди. Въпреки, че авторите са над 30, "Програмиране за .NET Framework" не е просто сборник от статии; напротив – всеки от тях е допринесъл с опита и труда си, за да може книгата да бъде това, което е – добре структурирано и изчерпателно ръководство.
Учебник за студента или справочник за специалиста – "Програмиране за .NET Framework" е задължителна за библиотеката на всеки който има досег с .NET.
Стоян Йорданов,
Software Design Engineer,
Microsoft Corpartion (Redmond)
* Мнението е лично на автора му и не обвързва Microsoft Corporation по никакъв начин
|
Национална академия по разработка на софтуер |
|
|
Лекторите » Светлин Наков е автор на десетки технически публикации и няколко книги, свързани с разработката на софтуер, заради което е търсен лектор и консултант. Той е разработчик с дългогодишен опит, работил по разнообразни проекти, реализирани с различни технологии (.NET, Java, Oracle, PKI и др.) и преподавател по съвременни софтуерни технологии в СУ "Св. Климент Охридски". През 2004 г. е носител на наградата "Джон Атанасов" на президента на България Георги Първанов. Светлин Наков ръководи обучението по Java технологии в Академията.
» Мартин Кулов е софтуерен инженер и консултант с дългогодишен опит в изграждането на решения с платформите на Microsoft. Мартин е опитен инструктор и сертифициран от Майкрософт разработчик по програмите MCSD, MCSD.NET, MCPD и MVP и международен лектор в световната организация на .NET потребителските групи INETA. Мартин Кулов ръководи обучението по .NET технологии в Академията. |
Академията » Национална академия по разработка на софтуер (НАРС) е център за професионално обучение на софтуерни специалисти.
» НАРС провежда БЕЗПЛАТНО курсове по разработка на софтуер и съвременни софтуерни технологии в София и други градове.
» Предлагани специалности: § Въведение в програмирането (с езиците C# и Java) § Core .NET Developer § Core Java Developer
» Качествено обучение с много практически проекти и индивидуално внимание за всеки.
» Гарантирана работа! Трудов договор при постъпване в Академията.
» БЕЗПЛАТНО! Учите безплатно във въведителните курсове и по стипендии от работодателите в следващите нива. |
Том 1
За кого е предназначена тази книга?
Какво представлява .NET Framework?
Фокусът е върху .NET Framework 1.1
Как е представена информацията?
Поглед към съдържанието на книгата
Глава 1. Архитектура на .NET Framework
Глава 3. Обектно-ориентирано програмиране в .NET
Глава 4. Обработка на изключения в .NET
Глава 5. Обща система от типове
Глава 13. Релационни бази от данни и MS SQL Server
Глава 14. ADO.NET и работа с данни
Глава 15. Графичен потребителски интерфейс с Windows Forms
Глава 16. Изграждане на уеб приложения с ASP.NET
Глава 17. Многонишково програмиране и синхронизация
Глава 18. Мрежово и Интернет програмиране
Глава 19. Отражение на типовете (Reflection)
Глава 20. Сериализация на данни
Глава 21. Уеб услуги с ASP.NET
Глава 22. Отдалечено извикване на методи (Remoting)
Глава 23. Взаимодействие с неуправляван код.
Глава 24. Управление на паметта и ресурсите.
Глава 25. Асемблита и разпространение (deployment)
Глава 26. Сигурност в .NET Framework
Глава 27. Mono - свободна имплементация на .NET
Глава 28. Помощни инструменти за .NET разработчици
Константите пишем с главни букви
Член-променливите пишем с префикс "m"
Параметрите на методите пишем с префикс "a"
Курсът по програмиране за платформа .NET в СУ (2002/2003 г.)
Проектът на Microsoft Research и БАРС
Курсът по програмиране за .NET Framework в СУ (2004/2005 г.)
Курсът по програмиране за .NET Framework в СУ (2005/2006 г.)
Българска асоциация на разработчиците на софтуер.
Софийски университет "Св. Климент Охридски"
Права и ограничения на потребителите
Права и ограничения на авторите
Права и ограничения на Microsoft Research
Глава 1. Архитектура на платформата .NET и .NET Framework
Какво представлява платформата .NET?
Архитектура на .NET платформата
.NET Framework и Visual Studio .NET 2003
Интеграция на езиците за програмиране
Common Language Specification (CLS)
Common Language Infrastructure (CLI)
Създаване на потребителски интерфейс
Създаване на инсталационен пакет
VS.NET е силно разширяема среда
Принципи при дизайна на езика C#
Сигурност и надеждност на кода
Създаване на проект, компилиране и стартиране от Visual Studio.NET
Стойностни типове (value types)
Референтни типове (reference types)
Типове дефинирани от потребителя
Изброени типове (enumerations)
Програмни конструкции (statements)
Елементарни програмни конструкции
Програмни конструкции за управление
Дебъгерът на Visual Studio .NET
Извличане на XML документация от C# сорс код
Генериране на HTML документация от VS.NET
Директиви за форматиране на сорс кода
Директиви за условна компилация
Директиви за контрол над компилатора
Документацията на .NET Framework
Глава 3. Обектно-ориентирано програмиране в .NET
Предимства и особености на ООП
Моделиране на обекти от реалния свят
Преизползване на програмния код
Параметри за връщане на стойност (out)
Предаване на променлив брой параметри от различен тип
Предефиниране на оператори – пример
Класове, които не могат да се наследяват (sealed)
Явна имплементация на интерфейс
Наследяване на абстрактни класове
Изобразяване на типовете и връзките между тях
Пространства от имена (namespaces)
Как да организираме пространствата?
Принципи при обектно-ориентирания дизайн
Функционална независимост (loose coupling)
Силна логическа свързаност (strong cohesion)
Глава 4. Управление на изключенията в .NET
Програмна конструкция try-catch
Как CLR търси обработчик за изключенията?.
Прихващане на изключения – пример
Прихващане на изключения на нива – пример
Предизвикване (хвърляне) на изключения
Хвърляне и прихващане на изключения – пример
Хвърляне на прихванато изключение – пример
Дефиниране на собствени изключения
Конструкцията try-catch-finally
try-finally за освобождаване на ресурси
Глава 5. Обща система от типове (Common Type System)
CTS и езиците за програмиране в .NET
Стойностни и референтни типове
Стойностни типове (value types)
Референтни типове (reference types)
Стойностни срещу референтни типове
Стойностни и референтни типове – пример
Защита от неинициализирани променливи
Автоматична инициализация на променливите
Защо стойностните типове наследяват референтния тип System.Object?
Потребителските типове скрито наследяват System.Object
Предефиниране на сравнението на типове
Оператори за работа с типове в C#
Клониране на обекти в .NET Framework
Имплементиране на ICloneable – пример
Опаковане (boxing) и разопаковане (unboxing) на стойностни типове
Опаковане (boxing) на стойностни типове
Разопаковане (unboxing) на опаковани типове
Особености при опаковането и разопаковането
Как работят опаковането и разопаковането?
Пример за опаковане и разопаковане
Аномалии при опаковане и разопаковане
Системни имплементации на IComparable
Имплементиране на IComparable – пример
Интерфейсите IEnumerable и IEnumerator
Имплементиране на IEnumerable и IEnumerator
Какво представляват делегатите?
Делегатите и указателите към функции
Статични или екземплярни методи
Единични (singlecast) делегати
Множествени (multicast) делегати
Разлика между събитие и делегат
Пример за използване на System.EventHandler
Имплементиране на събития в интерфейс
Какво представляват атрибутите в .NET?
Декларативно управление на сигурността
Използване на автоматизирана сериализация на обекти
Създаване на уеб услуги в ASP.NET
Взаимодействие с неуправляван (Win32) код
Синхронизация при многонишкови приложения
Дефиниране на собствени атрибути
Дефиниране на собствен атрибут – пример
Извличане на атрибути от асембли
Какво се случва по време на компилация?
Какво се случва при извличане на атрибут?
Масиви от референтни типове – пример
Инициализиране и достъп до елементите
Инициализиране и достъп до елементите
Създаване на ненулево-базиран масив – пример
Сортиране с IComparer – пример
Колекциите са слабо типизирани
Глава 9. Символни низове (Strings)
Методи за класификация на символите
Символни низове в .NET Framework
Правила за сравнение на символни низове
Методи и свойства на System.String
Ефективно конструиране на низове чрез класа StringBuilder
Проблемът с долепването на низове
Решението на проблема – класът StringBuilder
Използване на StringBuilder – пример
Задаване на първоначален размер за StringBuilder
Сравнение на скоростта на String и StringBuilder – пример
Използване на StringInfo – пример
Използване на форматиращи символи
Извличане на списък от всички култури в .NET Framework – пример
Подредба на байтовете при UTF-16 и UTF-32
Конвертиране със System.Text.Encoding
Работа с Unicode във Visual Studio.NET
За какво се използват регулярните изрази?.
Регулярни изрази и крайни автомати
Пример за регулярен израз в .NET
Escaping при регулярните изрази
Най-важното за работата с регулярни изрази
Шаблонът не е за съвпадение с целия низ
Съвпаденията се откриват в реда на срещане
Търсенето приключва, когато се открие съвпадение.
Търсенето продължава от последното съвпадение
Регулярният израз търси за всички възможности подред
"Мързеливи" метасимволи за количество
По-обстоен пример с разгледаните метасимволи
Регулярните изрази в .NET Framework
Пространството System.Text.RegularExpressions
Няколко основни правила при търсенето
Последователно еднократно търсене с Match(…) и NextMatch()
Тагове за хипервръзки в HTML код – пример
Още нещо за позицията на следващото търсене
Търсене за съвпадения наведнъж с Matches(…) и MatchCollection
Класовете Group и GroupCollection
Как извличаме информацията от групите?
Извличане на хипервръзки в HTML документ – пример
Обратни препратки към именувани групи
Извличане на HTML тагове от документ – пример
Полезни съвета за валидация с регулярни изрази.
Валидни e-mail адреси – пример
Валидни положителни цели числа – пример
Заместване със заместващ шаблон
Специални символи в заместващия шаблон
Разделяне на низ по регулярен израз
Методите Escape(…) и Unescape(…)
Настройки и опции при работа с регулярните изрази
Допълнителни възможности на синтаксиса на регулярните изрази
Символът \G – последователни съвпадения
Групи, които не запазват съвпадение
Метасимволи за преглед напред и назад
Коментари в регулярните изрази
Модификатори на регулярните изрази
Особености и метасимволи, свързани с Unicode
Метасимволите за Unicode категории
Предварително компилиране и запазване на регулярни изрази
Кога да използваме регулярни изрази
Няколко регулярни израза от практиката
Размяна на първите две думи в низ
Парсване на декларации <var>=<value>
Премахване на път от името на файл
Преходни потоци (pass-through streams)
Изчистване на работните буфери
Промяна на текущата позиция в поток
Четене и писане във файлов поток
Пример – замяна на стойност в двоичен файл
Четене от MemoryStream – пример
Писане в MemoryStream – пример
Операции с файлове. Класове File и FileInfo
Работа с директории. Класове Directory и DirectoryInfo
Рекурсивно обхождане на директории – пример
Наблюдение на файловата система
Наблюдение на файловата система – пример
XML (Extensible Markup Language)
Какво представлява един markup език?
Универсална нотация за описание на структурирани данни
XML съдържа метаинформация за данните
XML е световно утвърден стандарт
Прилики между езиците XML и HTML
Разлики между езиците XML и HTML
XML изисква добре дефинирани документи
Пример за лошо дефиниран XML документ
Съхранение на структурирани данни
Повишена необходимост от физическа памет
Дефиниране на пространства от имена
Използване на тагове с еднакви имена – пример
Пространства по подразбиране – пример
Пространства от имена и пространства по подразбиране – пример
XML схеми – защо са необходими?
.NET притежава вградена XML поддръжка
Парсване на XML документ с DOM – пример
Промяна на XML документ с DOM – пример
Построяване на XML документ с DOM – пример
Разлика между pull и push парсер моделите
XmlReader – основни методи и свойства
Класът XmlReader – начин на употреба
XmlValidatingReader – основни методи, свойства и събития
Глава 13. Релационни бази от данни и MS SQL Server
Съхранени процедури (stored procedures)
Системни компоненти на SQL Server 2000
Програмиране за SQL Server 2000
Data Definition Language (DDL)
Data Manipulation Language (DML)
Глава 14. Достъп до данни с ADO.NET
Модели за работа с данни в ADO.NET
Свързан модел (connected model)
Несвързан модел (disconnected model)
Двуслойни приложения (клиент-сървър)
Пространства от имена на ADO.NET
Доставчици на данни (Data Providers) в ADO.NET
Стандартни доставчици на данни в ADO.NET
Компоненти за работа в несвързана среда
Видове автентикация в SQL Server 2000
Символен низ за връзка към база от данни (Connection String)
Реализация на свързан модел в ADO.NET
Кога да използваме свързан модел?
Свързан модел от гледна точка на програмиста
Експлицитно отваряне и затваряне на връзка
Имплицитно отваряне и затваряне на връзка
Използване на метода Dispose()
По-важни свойства на SqlCommand
По-важни методи и свойства на SqlDataReader
Създаване на SqlCommand чрез Server Explorer
Създаване на SqlCommand чрез Toolbox
Необходимост от параметрични заявки
Първичен ключ с пореден номер – извличане
Работа с транзакции в SQL Server
Работа с картинки в база от данни
Съхранение на графични обекти – пример
Работа с големи обеми двоични данни
Типични сценарии за работа в несвързана среда.
Несвързан модел в ADO.NET, XML и уеб услуги
Класове за достъп до данните в несвързана среда
Поддръжка на автоматично свързване
DataTable поддържа списък на всички промени
Използване на ограничения (constraints)
ForeignKey и Unique ограничения
Пример за дефиниране на колона чрез израз
Релации и потребителски интерфейс
Релации и потребителски изрази
Основни методи, използващи релации
Филтриране по версията на данните
Запазване и зареждане на данните от DataSet
ReadXml() и WriteXml() – пример
Архитектура на класа DataAdapter
Адаптерни класове за различните доставчици
Методът Fill() на класа DataAdapter
Свойството MissingSchemaAction
Задаване на съответствие за таблици и колони
Извличане на информация за схемата на източника.
Свойства AcceptChangesDuringFill и ContinueUpdateOnError
Обновяване на данните в източника
Потребителска логика за обновяване на източника
Извличане на обновени стойности в DataSet
Обновяване на свързани таблици
DataSet.GetChanges() и DataSet.HasChanges()
Кога да използваме GetChanges() и HasChanges()?
Грешките в DataSet и DataTable обектите
Несвързан модел – типичен сценарий на работа
Реализация на несвързан модел с DataSet и DataAdapter – пример
Сигурността при работа с бази от данни
Сигурност при динамични SQL заявки
Connection pooling и сигурност
Съхраняване на connection string
Том 2
Глава 15. Графичен потребителски интерфейс с Windows Forms
Глава 16. Изграждане на уеб приложения с ASP.NET
Глава 17. Многонишково програмиране и синхронизация
Глава 18. Мрежово и Интернет програмиране
Глава 19. Отражение на типовете (Reflection).
Глава 20. Сериализация на данни
Глава 21. Уеб услуги с ASP.NET
Глава 22. Отдалечено извикване на методи (Remoting)
Глава 23. Взаимодействие с неуправляван код.
Глава 24. Управление на паметта и ресурсите.
Глава 25. Асемблита и разпространение
Глава 26. Сигурност в .NET Framework
Глава 27. Mono - свободна имплементация на .NET
Глава 28. Помощни инструменти за .NET разработчици
|
Национална академия по разработка на софтуер |
|
|
Лекторите » Светлин Наков е автор на десетки технически публикации и няколко книги, свързани с разработката на софтуер, заради което е търсен лектор и консултант. Той е разработчик с дългогодишен опит, работил по разнообразни проекти, реализирани с различни технологии (.NET, Java, Oracle, PKI и др.) и преподавател по съвременни софтуерни технологии в СУ "Св. Климент Охридски". През 2004 г. е носител на наградата "Джон Атанасов" на президента на България Георги Първанов. Светлин Наков ръководи обучението по Java технологии в Академията.
» Мартин Кулов е софтуерен инженер и консултант с дългогодишен опит в изграждането на решения с платформите на Microsoft. Мартин е опитен инструктор и сертифициран от Майкрософт разработчик по програмите MCSD, MCSD.NET, MCPD и MVP и международен лектор в световната организация на .NET потребителските групи INETA. Мартин Кулов ръководи обучението по .NET технологии в Академията. |
Академията » Национална академия по разработка на софтуер (НАРС) е център за професионално обучение на софтуерни специалисти.
» НАРС провежда БЕЗПЛАТНО курсове по разработка на софтуер и съвременни софтуерни технологии в София и други градове.
» Предлагани специалности: § Въведение в програмирането (с езиците C# и Java) § Core .NET Developer § Core Java Developer
» Качествено обучение с много практически проекти и индивидуално внимание за всеки.
» Гарантирана работа! Трудов договор при постъпване в Академията.
» БЕЗПЛАТНО! Учите безплатно във въведителните курсове и по стипендии от работодателите в следващите нива. |
Ако по принцип не четете уводите на книгите, помислете преди да пропуснете и този. Той е малко по-различен от всички останали, защото тази книга е също малко по-различна от всички останали.
Ако смятате, че ще ви досадим с общи приказки, можете да не се задълбочавате прекалено, но ви препоръчваме поне да преминете през следващите страници "по вентилаторната система", за да разберете какво ви предстои да научите от следващите страници. Ще разберете какво е .NET Framework, за какво служи, какви технологии обхваща и как настоящата книга от една идея се превърна в реалност.
Това е първата чисто българска книга за програмиране с .NET Framework и C#, но за сметка на това е една от най-полезните книги в тази област. Написана от специалисти с опит както в практическата работа с .NET, така и в обучението по програмиране, книгата ще ви даде не само основите на .NET програмирането, но и ще ви запознае с някои по-сложни концепции и ще ви предаде от опита на авторите.
.NET Framework? Ама какво е това? Някаква нова измислица на Microsoft или просто поредния език за програмиране? Да не би да са направили нова версия на C++ или Java? A какъв е тоя език C#? Не мога ли да си пиша на C или C++? Какво е това среда за управлявано изпълнение на код? Не отмина ли вече времето на интерпретираните езици? Защо въобще трябва да сменяме добрите стари платформи с този .NET?
Ако нямате ясен отговор на всички тези въпроси, тази книга е за вас! Ако пък имате – тази книга също е за вас, защото едва ли знаете всичко за програмирането с .NET Framework и едва ли познавате добре всички по-важни технологии, свързани с него.
Тази книга ще ви даде много повече от начални знания. Тя ще ви предаде опит, натрупан в продължение години, и ще ви запознае с утвърдените практики при използването на .NET технологиите.
Тази книга е за всички, които искат да се научат да програмират с .NET Framework и C#, както и за всички, които вече имат основни знания и умения в областта, но искат да ги разширят и да навлязат в някои от по-сложните технологии, с които нямат достатъчно опит.
Книгата е полезна не само за .NET програмисти, но и за всички, които имат желание да се занимават сериозно с разработка на софтуер. В нея се обръща внимание не само на специфичните .NET технологии, но и на някои фундаментални концепции, които всеки програмист трябва добре да знае и разбира.
Тази книга не е подходяща за хора, които никога не са програмирали в живота си. Ако сте абсолютно начинаещ, спрете да четете и просто започнете с друга книга!
В нея няма да намерите обяснения за това какво е променлива, какво е тип данни, какво е условна конструкция, какво е цикъл и какво е функция. Очакваме читателят да е запознат добре с всички тези понятия и с основите на програмирането. Познанията по обектно-ориентирано програмиране (ООП) също ще са полезни, тъй като в книгата не се изясняват в дълбочина теоретичните концепции на ООП, а само средствата за тяхното прилагане в езика C#.
.NET Framework е съвременна платформа за разработка и изпълнение на приложения. Тя предоставя програмен модел, стандартна библиотека от класове и среда за контролирано изпълнение на програмен код.
.NET Framework поддържа различни езици за програмиране и позволява тяхната съвместна работа. .NET приложенията се пишат на езици от високо ниво (C#, VB.NET, Managed C++ и други) и се компилират до междинен език от ниско ниво, наречен IL (Intermediate Language). По време на изпълнение IL програмите (т. нар. управляван код) се компилират до инструкции за текущата хардуерна архитектура, съобразени с текущата операционна система, и след това се изпълняват от микропроцесора.
.NET Framework включва в себе си стандартна библиотека, която съдържа базова функционалност за разработка, необходима за повечето приложения, като вход/изход, връзка с бази данни, работа с XML, изграждане на уеб приложения, използване на уеб услуги, изграждане на графичен потребителски интерфейс и др.
Програмирането за .NET Framework изисква познания на неговите базови концепции (модел на изпълнение на кода, обща система от типове, управление на паметта, масиви, колекции, символни низове и др.), както и познаване на често използваните технологии – ADO.NET (за достъп до бази от данни), Windows Forms (за приложения с графичен потребителски интерфейс), ASP.NET (за уеб приложения и уеб услуги) и др.
Настоящата книга обхваща всички тези концепции и технологии, свързани с разработката на приложения за .NET Framework. Тя има за цел да запознае читателя с принципите на разработка на приложения за Microsoft .NET Framework и да даде широки познания по всички по-важни технологии, свързани с него.
Най-важните теми, които ще бъдат разгледани, са: архитектура на .NET Framework, управлявана среда за изпълнение на код (CLR), езикът C# и реализация на обектно-ориентирано програмиране с неговите средства, обща система от типове (CTS), основна библиотека от класове (Framework Class Library), достъп до бази от данни с ADO.NET, работа с XML, създаване на графичен потребителски интерфейс с Windows Forms и уеб-базирани приложения с ASP.NET. Ще бъде обърнато внимание и на някои по-сложни концепции като отражение на типовете, сериализация, многонишково програмиране, уеб услуги, отдалечено извикване на методи (remoting), взаимодействие с неуправляван код, асемблита, управление на сигурността, по-важни инструменти за разработка и др. Ще бъде разгледана и свободната имплементация на .NET Framework за Linux и други операционни системи (Mono). Накрая ще бъде описана разработката на един цялостен практически проект, който обхваща всички по-важни технологии и демонстрира добрите практики при изграждането на .NET приложения.
Всички теми са базирани на .NET Framework 1.1, Visual Studio .NET 2003 и MS SQL Server 2000. Не се обръща много внимание на новостите в .NET Framework 2.0, Visual Studio 2005 и SQL Server 2005, тъй като по време на разработката на книгата тези продукти и технологии все още не бяха официално излезли на пазара и тяхното бъдеще не беше съвсем ясно.
Въпреки предстоящото излизане на .NET Framework 2.0, настоящата книга си остава изключително полезна, тъй като в същината си версия 2.0 не носи фундаментални промени, а по-скоро разширява вече съществуващите технологии, които ще разгледаме в книгата.
Въпреки големия брой автори, съавтори и редактори, стилът на текста в книгата е изключително достъпен. Съдържанието е представено в добре структуриран вид, разделено с множество заглавия и подзаглавия, което позволява лесното му възприемане, както и бързото търсене на информация в текста.
Настоящата книга е написана от програмисти за програмисти. Авторите са действащи софтуерни разработчици, хора с реален опит както в разработването на софтуер, така и в обучението по програмиране. Благодарение на това качеството на изложението е на много високо ниво.
Всички автори ясно съзнават, че примерният сорс код е едно от най-важните неща в една книга за програмиране. Именно поради тази причина текстът е съпроводен с много, много примери, илюстрации и картинки.
Въобще някой чете ли текста, когато има добър и ясен пример? Повечето програмисти първо гледат дали примерът ще им свърши работа, и само ако нещо не е ясно, се зачитат в текста (това всъщност не е никак добра практика, но такава е реалността). Ето защо многото и добре подбрани примери са един от най-важните принципи, залегнали в тази книга.
Всички примери в книгата са написани на езика C#, въпреки, че .NET Framework поддържа много други езици. Този избор е направен по няколко причини:
- C# е препоръчваният език за програмиране за .NET Framework. Архитектите на езика специално са го проектирали за .NET Framework и са го съобразили с особеностите на платформата още по време на дизайна. C# наследява простотата на Java, мощността на C++ и силните черти на Delphi. Той притежава максимално стегнат и ясен синтаксис.
- В България C# е най-популярният от .NET езиците и се използва най-масово в българските софтуерни компании.
- C# е от семейството на C-базираните езици и синтактично много прилича на Java, C++, C и PHP. Много хора, които не знаят езика, биха разбрали примерите без особени усилия.
- За C# има повече статии в специализираните сайтове и лични дневници (blogs) в Интернет. Общността на C# разработчиците е по-добре развита, отколкото на разработчиците на другите .NET езици.
- Поради голямата популярност на езика C# за него има по-добра поддръжка от инструментите за разработка.
- Езици като C++, Visual Basic и JScript не са проектирани специално за .NET Framework, а са адаптирани допълнително към него чрез редица изменения и добавки. В следствие на това те запазват някои синтактични особености, които не са удобни при работата с .NET.
Ако сега започвате да изучавате .NET Framework, Ви препоръчваме да стартирате от езика C#. След като го овладеете, можете да опитате и другите .NET езици, но за начало C# е най-подходящ.
По принцип езикът C++ може да се използва при програмиране с .NET Framework, но това се препоръчва само при някои много специфични приложения. Този език по първоначален замисъл не е проектиран за .NET платформата и има съвсем друго предназначение. Той е много по-сложен и труден от C# и затова е по-добре да използвате C#, дори ако трябва да го учите от начало. Ако вече знаете C++, няма да ви е трудно да овладеете C# и когато го направите, ще се убедите, че с него се работи много по-лесно.
Въпреки, че езикът Visual Basic .NET (VB.NET) има някои предимства и се използва масово по света, за предпочитане е да ползвате C# при изграждане на .NET приложения. Езикът Visual Basic е масово разпространен по исторически причини (благодарение най-вече на Бил Гейтс). Някои специалисти изказват силно негативни мнения срещу BASIC и произлизащите от него езици, докато други (включително и Microsoft) го подкрепят и препоръчват.
Ще си позволим да цитираме изказването на един от най-известните учени в областта на компютърните науки проф. д-р Едсгар Дейкстра за езика BASIC, от който произлиза VB.NET:
|
|
Практически е невъзможно да научиш на добро програмиране студенти, които са имали предишен досег до езика BASIC – като потенциални програмисти, те са мисловно осакатени, без надежда за възстановяване. |
Горният цитат се отнася за старите версии на езика BASIC. VB.NET е вече съвременен обектно-ориентиран език, който не отстъпва по нищо на C#, освен че има малко по-нетрадиционен синтаксис (в сравнение със семейството на C-базираните езици).
.NET Framework позволява всеки да програмира на любимия си език. Изборът си е лично ваш. Ние можем само да ви дадем препоръки. За целите на настоящата книга авторският колектив е избрал езика C# и препоръчва на читателите да започнат от него.
Книгата се състои от 29 глави, които поради големия обем са разделени в два тома. Том 1 съдържа първите 14 глави, а том 2 – останалите 15. Това важи само за хартиеното издание на книгата. В електронния вариант тя се разпространява като едно цяло.
Нека направим кратък преглед на всяка една от главите и да се запознаем с нейното съдържание, за да разберем какво ни очаква по-нататък.
В глава 1 е представена платформата .NET, която въплъщава визията на Microsoft за развитието на информационните и софтуерните технологии, след което е разгледана средата за разработка и изпълнение на .NET приложения Microsoft .NET Framework.
Обръща се внимание на управлявания код, на езика IL, на общата среда за контролирано изпълнение на управляван код (Common Language Runtime) и на модела на компилация и изпълнение на .NET кода. Разглеждат се още Common Language Specification (CLS), Common Type System (CTS), Common Language Infrastructure (CLI), интеграцията на различни езици, библиотеката от класове Framework Class Library и интегрираната среда за разработка Visual Studio .NET.
Автори на главата са Виктор Живков и Николай Недялков. Текстът е написан с широко използване на лекциите на Светлин Наков по темата и е редактиран от Иван Митев и Светлин Наков.
Глава 2 разглежда езика С#, неговия синтаксис и основни концепции. Представя се средата за разработка Visual Studio .NET 2003 и се демонстрира работата с нейния дебъгер. Отделя се внимание на типовете данни, изразите, програмните конструкции и конструкциите за управление в езика C#. Накрая се демонстрира колко лесно и полезно е XML документирането на кода в С#.
Автор на главата е Моника Алексиева. Текстът е базиран на лекцията на Светлин Наков по същата тема и е редактиран от Панайот Добриков и Преслав Наков.
В глава 3 се прави кратък обзор на основните принципи на обектно-ориентираното програмиране (ООП) и средствата за използването им в .NET Framework и езика C#. Представят се типовете "клас", "структура" и "интерфейс" в C#. Въвежда се понятието "член на тип" и се разглеждат видовете членове (член-променливи, методи, конструктори, свойства, индексатори и др.) и тяхната употреба. Разглежда се наследяването на типове в различните му аспекти и приложения. Обръща се внимание и на полиморфизма в C# и свързаните с него понятия и програмни техники. Накрая се дискутират някои утвърдени практики при създаването на ефективни йерархии от типове.
Автор на главата е Стефан Кирязов. Текстът е написан с широко използване на лекции на Светлин Наков и е редактиран от Цветелин Андреев и Панайот Добриков.
В глава 4 се разглеждат изключенията в .NET Framework като утвърден механизъм за управление на грешки и непредвидени ситуации. Дават се обяснения как се прихващат и обработват изключения. Разглеждат се начините за тяхното предизвикване и различните видове изключения в .NET Framework. Дават се примери за дефиниране на собствени (потребителски) изключения.
Автори на главата са Явор Ташев и Светлин Наков. Текстът е написан с широко използване на лекции на Светлин Наков по темата. Редактор е Мартин Кулов.
В глава 5 се разглежда общата система от типове (Common Type System) в .NET Framework. Обръща се внимание на разликата между стойностни и референтни типове, разглежда се основополагащият тип System.Object и йерархията на типовете, произлизаща от него. Дискутират се и някои особености при работа с типове – преобразуване към друг тип, проверка на тип, клониране, опаковане, разопаковане и др.
Автор на главата е Светлин Наков. Текстът е базиран изцяло на лекцията на Светлин Наков по същата тема и е редактиран от Преслав Наков и Панайот Добриков.
В глава 6 се разглежда референтният тип "делегат". Илюстрирани се начините за неговото използване, различните видове делегати, както и негови характерни приложения. Представя се понятието "събитие" и се обяснява връзката му с делегатите. Прави се сравнение между делегатите и интерфейсите и се дават препоръки в кои случаи да се използват едните и в кои – другите.
Автор на главата е Лазар Кирчев. Текстът е базиран на лекцията на Светлин Наков по същата тема.
В глава 7 се разглежда какво представляват атрибутите в .NET Framework, как се прилагат и къде се използват. Дават се обяснения как можем да дефинираме собствени атрибути и да извличаме приложените атрибути от метаданните на асемблитата.
Автори на главата са Преслав Наков и Панайот Добриков. Текстът е базиран основно на лекцията на Светлин Наков по същата тема и е редактиран от него.
В глава 8 се представят масивите и колекциите в .NET Framework. Разглеждат се видовете масиви – едномерни, многомерни и масиви от масиви (т. нар. назъбени масиви), както и базовият за всички масиви тип System. Array. Дискутират се начините за сортиране на масиви и търсене в тях. Разглеждат се колекциите и тяхната реализация в .NET Framework, класовете ArrayList, Queue, Stack, Hashtable и SortedList, както и интерфейсите, които те имплементират.
Автори на главата са Стефан Добрев и Деян Варчев. Текстът е базиран на лекцията на Светлин Наков по същата тема и е редактиран от него.
В глава 9 се разглежда начинът на представяне на символните низове в .NET Framework и методите за работа с тях. Обръща се внимание на кодиращите схеми, които се използват при съхраняване и пренос на текстова информация. Разглеждат се подробно различните начини за манипулиране на низове, както и някои практически съображения при работата с тях. Демонстрира се как настройките за държава и регион (култура) определят вида на текста, показван на потребителите, и как можем да форматираме изхода в четлив и приемлив вид. Разглеждат се също и начините за преобразуване на вход от потребителя от текст в обект от стандартен тип, с който можем лесно да работим.
Автори на главата са Васил Бакалов и Александър Хаджикръстев. В текста е широко използвана лекцията на Светлин Наков по същата тема. Главата е редактирана от Иван Митев.
В глава 10 се разглеждат регулярните изрази, набиращи все по-голяма популярност сред разработчиците на софтуер при решаването на проблеми, свързани с обработката на текст. Дискутират се произходът и същността на регулярните изрази, техният синтаксис и основните правила при конструирането им. В главата е предложено кратко представяне на основните дейности, при които е подходящо използването на регулярни изрази, и са дадени конкретни насоки как можем да правим това със средствата на .NET Framework. Разглежда се инструментариумът, за работа с регулярни изрази, който стандартната библиотека с класове предоставя, и се описват най-важните методи, съпроводени с достатъчно примери.
Автор на главата е Георги Пенчев. При изготвянето на текста е частично използвана лекцията на Светлин Наков по темата. Технически редактор е Иван Митев.
В глава 11 се разглежда начинът, по който се осъществяват вход и изход от дадена програма в .NET Framework. Представят се различните видове потоци – абстракцията, която позволява връзката на програмата с някакво устройство за съхранение на данни. Обяснява се работата на четците и писачите, които обвиват потоците и така улесняват тяхното използване. Накрая, се прави преглед на средствата, които .NET Framework предоставя за работа с файлове и директории и за наблюдение на файловата система.
Автор на главата е Александър Русев. Текстът е базиран на лекцията на Светлин Наков по същата тема и е редактиран от Галин Илиев и Светлин Наков.
В глава 12 се разглежда работата с XML в .NET Framework. Обяснява се накратко какво представлява езикът XML. Обръща се внимание на приликите и разликите между него и HTML. Разглеждат се приложенията на XML, пространствата от имена и различните схеми за валидация на XML документи (DTD, XSD, XDR). Представят се средствата на Visual Studio .NET за работа с XSD схеми. Разглеждат се особеностите на класическите XML парсери (DOM и SAX) и как те са имплементирани в .NET Framework. Описват се подробно класовете за работа с DOM парсера (XmlNode и XmlDocument) и ролята на класа XmlReader при SAX парсерите в .NET Framework. Обръща се внимание на начина на работа на класа XmlWriter за създаване на XML документи. Дискутират се начините за валидация на XML документи спрямо дадена схема. Разглежда се поддръжката в .NET Framework и на някои други XML-базирани технологии като XPath и XSLT.
Автор на главата е Манол Донев, а редактори са Иван Митев и Светлин Наков. Текстът широко използва лекцията на Светлин Наков по същата тема.
В глава 13 се разглеждат системите за управление на релационни бази от данни. Обясняват се свързаните с тях понятия като таблици, връзки, релационна схема, нормализация, изгледи, ограничения, транзакции, съхранени процедури и тригери. Прави се кратък преглед на езика SQL, използван за манипулиране на релационни бази от данни.
След въведението в проблематиката на релационните бази от данни се прави кратък преглед на Microsoft SQL Server, като типичен представител на RDBMS сървърите. Разглеждат се неговите основни компоненти и инструменти за управление. Представя се използваното от него разширение на езика SQL, наречено T-SQL, и се дискутират основните DDL, DML и DBCC команди. Обръща се внимание на съхранените процедури в SQL Server и се обяснява как той поддържа някои важни характеристики на една релационна база от данни, като транзакции, нива на изолация и др.
Автор на главата е Стефан Захариев. В текста са използвани учебни материали от Бранимир Гюров, Светлин Наков и Стефан Захариев. Редактор е Светлин Наков.
В глава 14 се разгледат подробно двата модела за достъп до данни, реализирани в ADO.NET – свързан и несвързан. Описва се програмният модел на ADO.NET, неговите компоненти и доставчиците на данни. Обяснява се кои класове се използват за свързан достъп до данни, и кои – за несвързан.
При разглеждането на свързания модел за достъп до данни се обръща внимание на доставчикa на данни SqlClient за връзка с MS SQL Server и се обяснява как се използват класовете SqlConnection, SqlCommand и SqlDataReader. Разглежда се работата с параметризирани заявки и използването на транзакции от ADO.NET. Дава се пример за достъп и до други бази от данни през OLE DB. Разглеждат се и някои проблеми при работа с дати и съхранение на графични изображения в базата данни.
При разглеждането на несвързания модел за достъп до данни се дискутират в детайли основните ADO.NET класове за неговата реализация – DataSet и DataTable. Дават се примери и обяснения как се използват ограничения, изрази, релации и изгледи в обектния модел DataSet. Обръща се специално внимание на класа DataAdapter и вариантите за неговото използване при зареждане на данни и обновяване на базата от данни. Разглеждат се подходите за решаване на конфликти при нанасяне на промени в базата данни. Дискутират се и начините за връзка между ADO.NET и XML, а накрая се разглеждат проблемите със сигурността в приложенията, използващи бази от данни.
Автори на главата са Христо Радков (частта за свързания модел) и Лазар Кирчев (частта за несвързания модел). Главата е разработена с широко използване на лекцията на Бранимир Гюров и Светлин Наков по същата тема. Редактори са Светлин Наков и Мартин Кулов.
В глава 15 се разглеждат средствата на Windows Forms за създаване на прозоречно-базиран графичен потребителски интерфейс (GUI) за .NET приложенията. Представят се програмният модел на Windows Forms, неговите базови контроли, средствата за създаване на прозорци, диалози, менюта, ленти с инструменти и статус ленти, както и някои по-сложни концепции като: MDI приложения, data-binding, наследяване на форми, хостинг на контроли в Internet Explorer, работа с нишки във Windows Forms и др.
Автори на главата са Радослав Иванов (по-голямата част) и Светлин Наков. Текстът е базиран на лекцията на Светлин Наков по същата тема.
В глава 16 се разглежда разработката на уеб приложения с ASP.NET. Представят се програмният модел на ASP.NET, уеб формите, кодът зад тях, жизненият цикъл на уеб приложенията, различните типове контроли и техните събития. Показва се как се дебъгват и проследяват уеб приложения. Отделя се внимание на валидацията на данни, въведени от потребителя. Разглежда се концепцията за управление на състоянието на обектите – View State и Session State. Демонстрира се как могат да се визуализират и редактират данни, съхранявани в база от данни. Дискутират се разгръщането и конфигурирането на ASP.NET уеб приложенията в Internet Information Server (IIS) и сигурността при уеб приложенията.
Автор на главата е Михаил Стойнов. Текстът е базиран на лекцията на Михаил Стойнов по същата тема.
В глава 17 се разглежда многозадачността в съвременните операционни системи и средствата за паралелно изпълнение на програмен код, които .NET Framework предоставя. Обръща се внимание на нишките (threads), техните състояния и управлението на техния жизнен цикъл – стартиране, приспиване, събуждане, прекратяване и др.
Разглеждат средствата за синхронизация на нишки при достъп до общи данни, както и начините за изчакване на зает ресурс и нотификация при освобождаване на ресурс. Обръща се внимание както на синхронизационните обекти в .NET Framework, така и на неуправляваните синхронизационни обекти от операционната система.
Изяснява се концепцията за работа с вградения в .NET Framework пул от нишки (thread pool), начините за асинхронно изпълнение на задачи, средствата за контрол над тяхното поведение и препоръчваните практики за работа с тях.
Автор на главата е Александър Русев. Текстът е базиран в голямата си част на лекцията на Михаил Стойнов и авторските бележки в нея.
В глава 18 се разглеждат някои основни средства, предлагани от .NET Framework за мрежово програмиране. Главата започва със съвсем кратко въведение в принципите на работа на съвременните компютърни мрежи и на Интернет и продължава с протоколите, чрез които се осъществява мрежовата комуникация. Обект на дискусия са както класовете за работа с TCP и UDP сокети, така и някои класове, предлагащи по-специфични възможности, като представяне на IP адреси, изпълняване на DNS заявки и др. В края на главата ще се представят средствата за извличане на уеб-ресурси от Интернет и на класовете за работа с e-mail в .NET Framework.
Автори на главата са Ивайло Христов и Георги Пенчев. Текстът широко използва лекцията на Ивайло Христов по същата тема.
В глава 19 се представя понятието Global Assembly Cache (GAC) и отражение на типовете (reflection). Разглеждат се начините за зареждане на асембли. Демонстрира се как може да се извлече информация за типовете в дадено асембли и за членовете на даден тип. Разглеждат се начини за динамично извикване на членове от даден тип. Обяснява се как може да се създаде едно асембли, да се дефинират типове в него и асемблито да се запише във файл по време на изпълнение на програмата.
Автор на главата е Димитър Канев. Текстът е базиран на лекцията на Ивайло Христов по същата тема. Редактор е Светлин Наков.
В глава 20 се разглежда сериализацията на данни в .NET Framework. Обяснява се какво е сериализация, за какво се използва и как се контролира процесът на сериализация. Разглеждат се видовете форматери (formatters). Обяснява се какво е XML сериализация, как работи тя и как може да се контролира изходният XML при нейното използване.
Автор на главата е Радослав Иванов. Текстът е базиран на лекцията на Михаил Стойнов по същата тема. Редактор е Светлин Наков.
В глава 21 се разглеждат уеб услугите, тяхното изграждане и консумация чрез ASP.NET и .NET Framework. Обект на дискусия са основните технологии, свързани с уеб услугите, и причината те да се превърнат в стандарт за интеграция и междуплатформена комуникация. Представят се различни сценарии за използването им. Разглежда се програмният модел за уеб услуги в ASP.NET и средствата за тяхното изграждане, изпълнение и разгръщане (deployment). Накрая се дискутират някои често срещани проблеми и утвърдени практики при разработката на уеб услуги чрез .NET Framework.
Автори на главата са Стефан Добрев и Деян Варчев. В текста са използвани материали от лекцията на Светлин Наков по същата тема. Технически редактор е Мартин Кулов.
В глава 22 се разглежда инфраструктурата за отдалечени извиквания, която .NET Framework предоставя на разработчиците. Обясняват се основите на Remoting технологията и всеки един от нейните компоненти: канали, форматери, отдалечени обекти и активация. Дискутират се разликите между различните типове отдалечени обекти. Обясняват се техният жизнен цикъл и видовете маршализация. Стъпка по стъпка се достига до създаването на примерен Remoting сървър и клиент. Накрая се представя един гъвкав и практичен начин за конфигуриране на цялата Remoting инфраструктура чрез конфигурационни файлове.
Автор на главата е Виктор Живков. В текста са използвани материали от лекцията на Светлин Наков. Редактори са Иван Митев и Светлин Наков.
Глава 23 разглежда как можем да разширим възможностите на .NET Framework чрез употреба на предоставените от Windows приложни програмни интерфейси (API). Дискутират се средствата за извикване на функционалност от динамични Win32 библиотеки и на проблемите с преобразуването (маршализацията) между Win32 и .NET типовете.
Обръща се внимание на връзката между .NET Framework и COM (компонентният модел на Windows). Разглеждат се както извикването на COM обекти от .NET код, така и разкриването на .NET компонент като COM обект. Демонстрира се и технологията IJW за използване на неуправляван код от програми, написани на Managed C++.
Автор на главата е Мартин Кулов. Текстът е базиран на неговата лекция по същата тема. Технически редактор е Галин Илиев.
В глава 24 се разглежда писането на правилен и ефективен код по отношение използването на паметта и ресурсите в .NET Framework. В началото се прави сравнение на предимствата и недостатъците на ръчното и автоматичното управление на памет и ресурси. След това се разглежда по-обстойно автоматичното им управление с фокус най-вече върху системата за почистване на паметта в .NET (т. нар. garbage collector). Обръща се внимание на взаимодействието с нея и практиките, с които можем да й помогнем да работи възможно най-ефективно.
Автори на главата са Стоян Дамов и Димитър Бонев. Технически редактор е Светлин Наков.
В глава 25 се разглежда най-малката съставна част на .NET приложенията – асембли, различните техники за разпространение на готовия софтуерен продукт на клиентските работни станции и някои избрани техники за създаване на инсталационни пакети и капаните, за които трябва да се внимава при създаване на инсталационни пакети.
Автор на тази глава е Галин Илиев. В текста е използвана частично лекцията на Михаил Стойнов. Технически редактор е Светлин Наков.
В глава 26 се разглежда как .NET Framework подпомага сигурността на създаваните приложения. Това включва както безопасност на типовете и защита на паметта, така и средствата за защита от изпълнение на нежелан код, автентикация и оторизация, електронен подпис и криптография. Разглеждат се технологиите на .NET Framework като Code Access Security, Role-Based Security, силно-именувани асемблита, цифрово подписване на XML документи (XMLDSIG) и други.
Автори на главата са Тодор Колев и Васил Бакалов. В текста е широко използвана лекцията на Светлин Наков по същата тема. Технически редактор е Светлин Наков.
В глава 27 се разглежда една от алтернативите на Microsoft .NET Framework – проектът с отворен код Mono. Обясняват се накратко начините за инсталиране и работа с Mono, използването на вградените технологии ASP.NET и ADO.NET, както и създаването на графични приложения. Дават се и няколко съвети и препоръки за писането на преносим код.
Автори на главата са Цветелин Андреев и Антон Андреев. Текстът е базиран на лекцията на Антон Андреев по същата тема. Технически редактор е Светлин Наков. Коректор е Соня Бибиликова.
В глава 28 се разглеждат редица инструменти, използвани при разработката на .NET приложения. С тяхна помощ може значително да се улесни изпълнението на някои често срещани програмистки задачи. Изброените инструменти помагат за повишаване качеството на кода, за увеличаване продуктивността на разработка и за избягване на някои традиционни трудности при поддръжката. Разглеждат се в детайли инструментите .NET Reflector, FxCop, CodeSmith, NUnit (заедно с допълненията към него NMock, NUnitAsp и NUnitForms), log4net, NHibernate и NAnt.
Автори на главата са Иван Митев и Христо Дешев. Текстът е по техни авторски материали. Редактор е Светлин Наков.
В глава 29 се дискутира как могат да се приложат на практика технологиите, разгледани в предходните теми. Поставена е задача да се разработи един сериозен практически проект – система за запознанства в Интернет с възможност за уеб и GUI достъп.
При реализацията на системата се преминава през всичките фази от разработката на софтуерни проекти: анализиране и дефиниране на изискванията, изготвяне на системна архитектура, проектиране на база от данни, имплементация, тестване и внедряване на системата.
При изготвяне на архитектурата приложението се разделя на три слоя – база от данни (която се реализира с MS SQL Server 2000), бизнес слой (който се реализира като ASP.NET уеб услуга) и клиентски слой (който се реализира от две приложения – ASP.NET уеб клиент и Windows Forms GUI клиент).
Ръководител на проекта е Ивайло Христов. Автори на проекта са: Ивайло Христов (отговорен за Windows Forms клиента), Тодор Колев и Ивайло Димов (отговорни за уеб услугата и базата данни) и Бранимир Ангелов (отговорен за ASP.NET уеб клиента). Инсталаторът на проекта е създаден от Галин Илиев. Технически редактори на кода са Мартин Кулов, Светлин Наков, Стефан Добрев и Деян Варчев.
Автори на текста са Ивайло Христов, Тодор Колев, Ивайло Димов и Бранимир Ангелов. Редактор на текста е Светлин Наков.
Тъй като настоящият текст е на български език, ще се опитаме да ограничим употребата на английски термини, доколкото е възможно. Съществуват обаче три основателни причини да използваме и английските термини наред с българските им еквиваленти:
- По-голямата част от техническата документация за .NET Framework е на английски език (повечето книги и в частност MSDN Library) и затова е много важно читателите да знаят английския еквивалент на всеки използван термин.
- Много от използваните термини не са пряко свързани с .NET и са навлезли отдавна в програмисткия жаргон от английски език (например "дебъгвам", "компилирам" и "плъгин"). Тези термини ще бъдат изписвани най-често на кирилица.
- Някои термини (например "framework" и "deployment") са трудно преводими и трябва да се използват заедно с оригинала в скобки. В настоящата книга на места такива термини са превеждани по различни начини (според контекста), но винаги при първо срещане се дава и оригиналният термин на английски език.
С цел уеднаквяване на стила на кода във всички примери от книгата, в примерите и демонстрациите от лекциите, както и в практическия проект, е въведена конвенция за кода, която включва редица препоръки за форматирането на кода, имената на типове, членове и променливи, елементи от потребителския интерфейс и други. Ще обясним по-важните от тях:
Примери:
|
private const int MAX_VALUE = 4096; private const string INPUT_FILE_NAME = "input.xml"; |
Това е утвърдена практика, възприета от повечето програмисти на C, C++, Java и C#.
Примери:
|
private Hashtable mUsersProfiles; private ArrayList mUsers; |
Тази конвенция не е стандартна, но тъй като Microsoft нямат официална препоръка по този въпрос, ние възприехме тази конвенция за именуване на член-променливите, за да ги отличаваме от останалите променливи. Префиксът "m" произхожда от думата "member" (член).
Пример:
|
public void IsLoginValid(string aUserName, string aPassword) { // ... } |
Тази конвенция също не е стандартна, но ние я възприехме, за да можем лесно да отличаваме параметрите в методите от останалите променливи, което често пъти е много полезно. Префиксът "a" произхожда от думата "argument" (аргумент на метод).
Възприели сме конвенция за именуване на идентификаторите, която е близка до официалните препоръки на Microsoft (за случаите, в които Microsoft са дали препоръки) и е съобразена с принципите за именуване на член-променливи и параметри, които вече разгледахме. Ето как изглежда тази конвенция:
|
Идентификатор |
Стил |
Пример |
|
пространство от имена (namespace) |
Pascal Case |
System.Windows.Forms |
|
тип (клас, структура, ...) |
Pascal Case |
TextWriter |
|
интерфейс (interface) |
Pascal Case, префикс "I" |
ISerializable |
|
изброен тип (enum type) |
Pascal Case |
FormBorderStyle |
|
изброена стойност (enum value) |
Pascal Case |
FixedSingle |
|
поле само за четене (read-only field) |
Pascal Case |
UserIcons |
|
поле-константа (constant) |
UPPERCASE |
MAX_VALUE |
|
свойство (property) |
Pascal Case |
BorderColor |
|
събитие (event) |
Pascal Case |
SizeChanged |
|
метод (method) |
Pascal Case |
ToString() |
|
член-променлива (field) |
Pascal Case, префикс "m" |
mUserProfiles |
|
статична член-променлива (static field) |
Pascal Case, префикс "m" |
mTotalUsersCount |
|
параметър на метод (parameter) |
Pascal Case, префикс "a" |
aFileName |
|
локална променлива (local variable) |
Camel Case |
currentIndex |
При именуване на контроли използваме Pascal Case и представка, която съответства на техния тип. Не слагаме префикс "m", когато контролата е член-променлива:
|
Контрола |
Пример |
|
Button |
ButtonOk, ButtonCancel |
|
Label |
LabelCustomerName |
|
TextBox |
TextBoxCustomerName |
|
Panel |
PanelCustomerInfo |
|
Image |
ImageProduct |
Използваме множествено число за именуване на таблици (например Users, Countries, StudentsCourses, …). При имената на колоните в таблица използваме Pascal Case (например UserName, MessageSender, UserId и т.н.).
Служебните думи в езика SQL (например SELECT, CREATE TABLE, FROM, INTO, ORDER BY и др.) изписваме с главни букви.
Историята на тази книга е дълга и интересна.
Няколко години след официалното излизане на .NET платформата, през 2002 г. .NET Framework вече беше навлязъл широко на пазара и много български фирми разработваха .NET приложения. Езикът C# и .NET платформата вече бяха добре познати сред софтуерните специалисти, но по университетите все още никой не преподаваше тези технологии.
В този момент в Софийски университет възникна курсът "Програмиране за платформа .NET".
Курсът "Програмиране за платформа .NET" в Софийски университет беше организиран през летния семестър на учебната 2002/2003 г. от група студенти с изявен интерес към .NET технологиите, някои от които имаха вече натрупан сериозен практически опит като .NET разработчици.
Преподавателският екип беше в състав Светлин Наков (работещ тогава в Мусала Софт), Стоян Йорданов (работещ тогава в Рила Солюшънс), Георги Иванов (работещ тогава във WebMessenger) и Николай Недялков (работещ тогава в Информационно обслужване).
Курсът (http://www.nakov.com/dotnet/2003/) обхващаше всички основни технологии, свързани с .NET Framework. Интересът към него беше много голям. Над 300 студента преминаха обучението, което беше с обем 60 учебни часа. Много от тях след това започнаха професионалната си кариера като .NET програмисти.
По време на семестъра бяха разработени авторски учебни материали за повечето от темите, които по-късно бяха използвани при изготвянето на лекции по "Програмиране за .NET Framework", на които е базирана настоящата книга.
Две години по-късно Microsoft Research отправиха предложение към Софийски университет за участие в академичен проект за създаване на учебно съдържание и учебни материали по дисциплини, изучаването на които е базирано на технологиите на Microsoft.
Екипът на Светлин Наков, съвместно с Българска асоциация на разработчиците на софтуер и Софийски университет предложиха проект за разработка на изчерпателно учебно съдържание и провеждане на университетски курсове по "Програмиране за .NET Framework". Проектът беше одобрен и частично финансиран от Microsoft Research.
Така започна съставянето на учебните материали, върху които е базирана настоящата книга. За година и половина бяха изработени повече от 2000 PowerPoint слайда по 26 теми, съдържащи над 600 примера, около 200 демонстрации на живо и над 300 задачи за упражнения. Учебните материали са с много високо качество и предоставят задълбочена информация по всички по-важни технологии, свързани с програмирането с .NET Framework. По някои от темите лекциите се получиха значително по-добри от официалните учебни материали на Microsoft (т. нар. Microsoft Official Curriculum). Лекциите са достъпни за свободно изтегляне от сайта на книгата.
По разработените вече учебни материали през зимния семестър на 2004/2005 г. беше проведен курс във Факултета по математика и информатика на Софийски университет с продължителност 90 учебни часа.
Курсът (http://www.nakov.com/dotnet/) беше организиран от Светлин Наков и неговия екип – Бранимир Гюров, Мартин Кулов, Георги Иванов, Михаил Стойнов и Ивайло Христов. Интересът към курса отново беше голям и стотици студенти избраха да преминат обучението. Мнозина от тях след това започнаха работа като .NET програмисти във водещи български софтуерни компании.
Няколко месеца след приключване на курса започна писането и на настоящата книга по материалите, използвани в лекциите.
През зимния семестър на 2005/2006 г. във Факултета по математика и информатика на Софийски университет "Св. Климент Охридски" отново се организира курс по .NET Framework (http://www.devbg.org/dotnetcourse/) с продължителност 90 учебни часа.
Преподавателският екип е съставен от представители на авторския колектив, разработил настоящата книга: Светлин Наков, Ивайло Христов, Михаил Стойнов, Галин Илиев, Васил Бакалов, Стефан Захариев, Радослав Иванов, Антон Андреев, Стефан Кирязов и Виктор Живков.
Курсът се провежда по официалните лекции и учебни материали, разработени по съвместния проект между Microsoft Research, Софийски университет и БАРС, които са достъпни за свободно изтегляне от сайта на курса.
Настоящата книга се използва като официален учебник в курса.
Първоначално идеята беше да се разпишат като текст изготвените вече лекции и да се компилира учебник за курсовете по програмиране за .NET Framework. По-късно проектът силно се разрасна и в него се включиха над 30 души. Появиха се допълнителни теми, появиха се и множество допълнения към обхванатите в лекциите теми.
Настоящата книга се разпространява напълно безплатно в електронен вид по лиценз, който позволява използването й за всякакви цели, включително и в комерсиални проекти. Книгата се разпространява и в хартиен вид срещу заплащане, което покрива разходите по отпечатването и разпространението й, без да се реализира печалба.
Екипът, написал настоящата книга, е съставен от хора, които имат силен интерес към .NET технологиите и желаят безвъзмездно да споделят своя опит като участват в написването на една или няколко от темите. Някои от участниците в екипа са бивши студенти, посещавали курсовете по .NET Framework в Софийски университет, други са членове на Софийската .NET потребителска група (www.sofiadev.org), а трети – разработчици, които от някъде са научили за проекта. Всички автори, съавтори и редактори от екипа по разработката на книгата са програмисти с реален практически опит.
Участниците в проекта дадоха своя труд безвъзмездно, без да получат материални или други облаги, защото съзнаваха липсата на добра книга за .NET Framework на български език и имаха силно желание да помогнат на своите настоящи и бъдещи колеги да навлязат с много по-малко усилия в .NET технологиите.
Написването на книгата отне около 6 месеца. Екипът беше ръководен от Светлин Наков, който има богат опит с писането на статии, презентации и книги и притежава добри технически познания по .NET Framework. Екипът се събираше на всеки 2 седмици за да дискутира напредъка по задачите и проблемите, възникнали по време на работата по проекта.
Работата по всяка тема изискваше нейният автор да предава по 10-15 страници на всеки 2 седмици. Този подход доведе до намаляване на риска от закъснение на работата по темите, и позволи проблемите да бъдат идентифицирани и решавани още при възникването им. В крайна сметка проектът завърши успешно, макар и доста след планираните първоначално срокове.
По време на работата възникваха проблеми, породени от голямото натоварване на авторите на работното им място. Някои автори трудно успяваха да спазят обещаните срокове (а други дори никога не са ги спазвали). По време на поправителната сесия някои студенти имаха сериозни трудности. Въпреки това само един участник, който се включи в проекта, в последствие се отказа. Всички останали написаха успешно своите теми.
За улесняване на съвместната работа бе използвана системата за екипна работа по проекти, предлагана свободно от портала sciforge.org. За целите на книгата в SciForge беше регистриран и използван проект "Книга за .NET Framework", който все още е публично достъпен от адрес http://sciforge.org/projects/dotnetbook/. Беше използвана системата за контрол на версиите Subversion, форумът и пощенският списък (mailing list), предлагани от SciForge.
За да се уеднаквят стиловете и форматирането във всички глави, беше разработено специално "ръководство за писателите", което дефинираше строги правила, свързани със стила на изказ, структурирането на текста, форматирането на кода, примерите, таблиците, схемите, картинките и т.н. Бяха разработени конвенции за кода, речник на преводните думи и други полезни стандарти. За всяка глава беше направен шаблон за MS Word 2003, в който авторите трябваше да пишат. Всички тези усилия силно ограничиха различията в стила и форматирането между отделните глави на книгата.
Всяка тема, след написването й, беше редактирана и редактирана от поне един редактор. Първоначално всички редакции и рецензии се извършваха от ръководителя на проекта Светлин Наков, но по-късно към редактирането се присъединиха и други участници. В резултат на общите усилия съдържанието на всички теми е на добро техническо ниво и добре издържано откъм стил.
Авторският колектив се състои от над 30 души – автори, съавтори, редактори и други. Ще представим всеки от тях с по няколко изречения (подредбата е по азбучен ред).
Александър Русев е програмист във фирма JCI (www.jci.com), където се занимава с разработка на софтуер за леки автомобили. Завършил е Технически университет – София, специалност компютърни системи и технологии. Александър се е занимавал и с разработка на софтуер за мобилни телефони. Професионалните му интереси включват Java технологиите и .NET платформата. Можете да се свържете с Александър по e-mail: arussev@gmail.com.
Александър Хаджикръстев е софтуерен архитект със сериозен опит в областта на проектирането и разработката на уеб базирани системи и e-commerce приложения. Той е сътрудник и консултант на PC Magazine България (www.sagabg.net/PCMagazine/) и почетен член на Българската асоциация на софтуерните разработчици (www.devbg.org). Александър има дългогодишен опит като ръководител на софтуерни проекти във фирми, базирани в България и САЩ. Професионалните му интереси са свързани с проектирането и изграждането на .NET приложения, разработването на експертни системи и софтуер за управление и автоматизация на бизнес процеси.
Антон Андреев работи като ASP.NET уеб разработчик във фирма TnDSoft (www.tndsoft.com). Той се интересува се от всичко, свързано с компютрите и най-вече с .NET и Linux. Като ученик се е занимавал с алгоритми и е участвал в олимпиади по информатика. Завършил е математическа гимназия и езикова гимназия с английски език, а в момента е студент в специалност информатика във Факултета по математика и информатика (ФМИ) на Софийски университет "Св. Климент Охридски". Работил е и като системен администратор във ФМИ и сега продължава да подпомага проектите на факултета, разработвайки нови сайтове. Неговият личен сайт е достъпен от адрес: http://debian.fmi.uni-sofia.bg/~toncho/portfolio/. Можете да се свържете с Антон по e-mail: anton.andreev@fmi.uni-sofia.bg.
Бранимир Ангелов е софтуерен разработчик във фирма Gugga (www.gugga.net) и студент във Факултета по Математика и информатика на Софийски университет "Св. Климент Охридски", специалност компютърни науки. Неговите професионални интереси са в областта на обектно-ориентирания анализ, моделиране и програмиране, уеб технологиите и в частност изграждането на RIA (Rich Internet Applications) и разработката на софтуер за мобилни устройства. Бранимир е печелил грамоти и отличия от различни състезания, както и първо място на Националната олимпиада по информационни технологии, на която е бил и жури година по-късно.
Васил Бакалов е студент, последен курс, в Американския университет в България, специалност Информатика. Той е председател на студентския клуб по информационни технологии и е студент-консултант на Microsoft България за университета. В рамките на клуба се занимава с управление на проекти и консултации по изпълнението им. Като студент-консултант на Microsoft България Васил подпомага усилията на Microsoft да поддържа тясна връзка със студентите и да ги информира и обучава по най-новите й продукти и технологии. Васил работи и като сътрудник на PC Magazine България от няколко години и има редица статии и коментари в изданието. В университета той предлага и изготвя план за курс по практическо изучаване на роботика, като разширение на обучението по изкуствен интелект, който е одобрен и внедрен. Той работи и с няколко ИТ фирми, където изгражда решения, базирани на .NET платформата. Притежава професионална сертификация от Microsoft. Можете да се свържете с Васил по e-mail: dotnetbook@vassil.info.
Виктор Живков е софтуерен инженер в Интерконсулт България (www.icb.bg). В момента е студент в Софийски Университет "Св. Климент Охридски", специалност информатика. Професионалните му интереси са основно в областта на решенията, базирани на софтуер от Microsoft. Виктор има сериозен опит в работата с .NET Framework, Visual Studio .NET и Microsoft SQL Server. Той участва в проекти за различни информационни системи, главно за Норвегия. Членува в БАРС от 2005 година. За връзка с Виктор можете да използвате неговия e-mail: viktor.zhivkov@gmail.com.
Деян Варчев е старши уеб разработчик във фирма Vizibility (www.vizibility.net). Неговите отговорности включват проектирането и разработката на уеб базирани приложения, използващи последните технологии на Microsoft, проучване на новопоявяващи се технологии и планиране на тяхното внедряване в производството, както и обучение на нови колеги. Неговите професионални интереси са свързани тясно с технологиите на Microsoft – .NET платформата, SQL Server, IIS, BizTalk и др. Деян е студент по информатика във Факултета по математика и информатика на Софийски университет "Св. Климент Охридски".
Димитър Бонев е софтуерен разработчик във фирма Formula Telecom Solutions (www.fts-soft.com). Той отговаря за разработването на уеб базирани приложения за корпоративни клиенти, както и за някои модули и инструменти, свързани с вътрешния процес на разработка във фирмата. Професионалните му интереси са насочени предимно към .NET платформата, методологията extreme programming и софтуерния дизайн. Димитър е завършил ВВВУ "Г. Бенковски", специалност компютърна техника. Той има богат опит в разработването на софтуерни решения, предимно с технологиите на Microsoft и Borland.
Димитър Канев е разработчик на софтуер във фирма Медсофт (www.medsoft.biz). Той е завършил Факултета по математика и информатика на Софийски университет "Св. Климент Охридски", специалност информатика. Професионалните му интереси са основно в областта на решенията, базирани на софтуер от Microsoft. Димитър има сериозен опит в работата с Visual Studio .NET, Microsoft SQL Server и ГИС системи. Работил е в проекти за изграждане на големи информационни системи, свързани с ГИС решения, и експертни системи за медицински лаборатории.
Галин Илиев е ръководител на проекти и софтуерен архитект в българския офис на Technology Services Consulting Group (www.wordassist. com). Галин е участвал в проектирането и разработването на големи информационни системи, Интернет сайтове с управление на съдържанието, допълнения и интеграция на MS Office със системи за управление на документи. Той притежава степен бакалавър по мениджмънт и информационни технологии, а също и сертификация MCSD за Visual Studio 6.0 и Visual Studio .NET. Той има сериозен опит с работата с Visual Studio .NET, MS SQL Server, MS IIS и MS Exchange. Личният му сайт е достъпен от адрес www.galcho.com, а e-mail адресът му е Iliev@galcho.com.
Георги Пенчев е софтуерен разработчик във фирма Symex България (www.symex.bg), където отговаря за разработка на финансово ориентирани графични Java приложения и на Интернет финансови портали с Java и PHP. Участвал е в изграждането на продукти за следене и обработка на борсови индекси и котировки за Българската фондова борса. Георги е студент по информатика във Факултета по математика и информатика на Софийски университет "Св. Климент Охридски". Професионалните и академичните му интереси са насочени към Java и .NET технологиите, биоинформатикатa, теоретичната информатика, изкуствения интелект и базите от знания. През 2004 и 2005 г. е асистент в курса по "Информационни технологии" за студенти с нарушено зрение и в практическия курс по "Структури от данни и програмиране" в Софийски университет. Можете да се свържете с Георги по e-mail: pench_wot@yahoo.com.
Иван Митев е софтуерен разработчик във фирма EON Technologies (www.eontechnologies.bg). Той е завършил Факултета по математика и информатика на Софийски университет "Св. Климент Охридски", специалност информатика. Иван е участвал в проектирането и реализацията на множество информационни системи, основно ГИС решения. Професионалният му опит е в разработки предимно с продукти и технологии на Microsoft. Основните интереси на Иван са в създаването на качествени и ефективни софтуерни решения чрез използването на подходящи практики, технологии и инструменти. Технически уеблог, който той поддържа от началото на 2004 година, е с акцент върху .NET програмирането и е достъпен на адрес http://immitev.blogspot.com. Можете да се свържете с Иван по e-mail: immitev@gmail.com.
Ивайло Димов е софтуерен разработчик във фирма Gugga (www.gugga.com). Неговите интереси са в областта на обектно-ориентираното моделиране, програмиране и анализ, базите от данни, уеб приложенията и приложения, базирани на Microsoft .NET Framework. В момента Ивайло е студент във Факултета по математика и информатика на Софийски университет "Св. Климент Охридски", специалност Компютърни науки. Той е сертифициран от Microsoft разработчик и е печелил редица грамоти и отличия от състезания по програмиране. През 2004 г. е победител в Националната олимпиада по информационни технологии и е участвал в журито на същата олимпиада година по-късно.
Ивайло Христов е преподавател в Софийски университет "Св. Климент Охридски", където води курсове по "Програмиране за .NET Framework", "Качествен програмен код", "Увод в програмирането", "Обектно-ориентирано програмиране" и "Структури от данни в програмирането". Неговите професионални интереси са в областта на .NЕТ технологиите и Интернет технологиите. Като ученик Ивайло е участник в редица национални състезания и конкурси по програмиране и е носител на престижни награди и отличия. Той участва в екип, реализирал образователен проект на Microsoft Research в областта на .NET Framework. Личният сайт на Ивайло е достъпен от адрес: www.ivaylo-hristov.net.
Лазар Кирчев е завършил Факултета по математика и информатика на Софийски университет "Св. Климент Охридски" и в момента е дипломант в специализация "Информационни системи". Той работи в Института за паралелна обработка на информацията към БАН по съвместен проект между Факултета по математика и информатика и БАН за изграждане на grid система. Неговите интереси включват .NET платформата, grid системите и базите от данни.
Манол Донев е софтуерен разработчик във фирма telerik (www.telerik. com). Той е част от екипа, който разработва уеб-базираната система за управление на съдържание Sitefinity (www.sitefinity.com). Манол е студент във Факултета по математика и информатика на Софийски университет "Св. Климент Охридски", специалност Информатика. Неговите професионални интереси обхващат най-вече .NET технологиите (в частност ASP.NET уеб приложения, XML и уеб услуги). Можете да се свържете с Манол по e-mail: manol.donev@gmail.com.
Мартин Кулов е изпълнителен директор на фирма КодАтест (www. codeattest.com), в която разработва системи за управление на качеството и автоматизация на софтуерното производство. Той има дългогодишен професионален опит като разработчик и ръководител в различни по големина проекти за частния и обществения сектор. Интересите му са в областта на продуктите и технологиите на Microsoft. Мартин е сертифициран от Microsoft разработчик по програмите MCSD и MCSD.NET (Charter Member). Той е магистър инженер при Факултета по комуникационна техника и технологии на Технически университет – София. През 2004 г. той участва като лектор в курсовете "Програмиране за .NET Framework" и "Качествен програмен код" в Софийски университет "Св. Климент Охридски". Мартин е лектор и на семинари на Microsoft, свързани с .NET технологиите и разработката на софтуер. Той е почетен член на Българската асоциация на разработчиците на софтуер и член на SofiaDev .NET потребителската група. Можете да се свържете с него по e-mail: martin@codeattest.com или чрез неговия личен уеблог: http://www. codeattest.com/blogs/martin/.
Михаил Стойнов е софтуерен разработчик във фирма MPS (www.mps.bg), която е подизпълнител на Siemens A.G. Той се занимава професионално с програмиране за платформите Java и .NET Framework от няколко години. Участва като лектор в преподавателския екип на курсовете "Програмиране за .NEТ Framework" и "Качествен програмен код". Той е студент-консултант на Майкрософт България за Софийски университет през последните 2 години и подпомага разпространението на най-новите продукти и технологии на Microsoft в университета. Михаил е бил лектор на международни конференции за ГИС системи. Интересите му обхващат разработка на уеб приложения, приложения с бази от данни, изграждане на сървърни системи и участие в академични дейности.
Моника Алексиева е софтуерен разработчик във фирма Солвер / Мидакс (www.midax.com). В момента следва специалност информатика във Факултета по математика и информатика на Софийски университет "Св. Климент Охридски". Моника има професионален опит в разработката за .NET Framework с езика C# и е сертифициран от Microsoft разработчик за .NET платформата. Нейните интереси са в областта на технологиите за изграждането на графичен потребителски интерфейс и разработката на приложения за мобилни устройства. През 2004 година Моника е асистент по "Структури от Данни" в Софийски университет.
Николай Недялков е президент на Асоциацията за информационна сигурност (www.iseca.org) която е създадена с цел прилагане на най-добрите практики за осигуряване на информационната сигурност на национално ниво и при извършването на електронен бизнес. Николай е професионален разработчик на софтуер, консултант и преподавател с дългогодишен опит. Той е автор на статии и лектор на множество конференции и семинари в областта на софтуерните технологии и информационна сигурност. Преподавателският му опит се простира от асистент по "Структури от данни в програмирането", "Обектно-ориентирано програмиране със C++" и "Visual C++" до лектор в курсовете "Мрежова сигурност", "Сигурен програмен код", "Интернет програмиране с Java", "Конструиране на качествен програмен код", "Програмиране за платформа .NET" и "Разработка на приложения с Java". Интересите на Николай са концентрирани върху техническата и бизнес страната на информационната сигурност, Java и .NET технологиите и моделирането и управлението на бизнес процеси в големи организации. Николай има бакалавърска степен от Факултета по математика и информатика на Софийски университет "Св. Климент Охридски". Като ученик е дългогодишен състезател по програмиране, с редица призови отличия. През 2004 г. е награден от Президента на България Георги Първанов за приноса му към развитието на информационните технологии и информационното общество. Той е почетен член на БАРС. Личният му сайт е достъпен от адрес: www.nedyalkov.com.
Панайот Добриков е софтуерен архитект в SAP A.G., Java Server Technology (www.sap.com), Германия и е отговорен за координацията на софтуерните разработки в SAP Labs България. Той е завършил Факултета по математика и информатика на Софийски университет "Св. Климент Охридски", специалност информатика. Панайот е дългогодишен участник (като състезател и ръководител) в ученически и студентски състезания по програмиране и е носител на много престижни награди в страната и чужбина. Той е автор на книгите "Програмиране = ++Алгоритми;" (www. algoplus.org) и "Java Programming with SAP Web Application Server", както и на десетки научно-технически публикации. През периода 2001-2003 води курсовете "Проектиране и анализ на компютърни алгоритми" и "Прагматика на обектното програмиране" в Софийски университет. Можете да се свържете с Панайот по e-mail: dobrikov@gmail.com.
Преслав Наков е аспирант по изкуствен интелект в Калифорнийския университет в Бъркли (www.berkeley.edu), САЩ. Неговият професионален опит включва шестгодишна работа като софтуерен разработчик във фирмите Комсофт (www.comsoft.bg) и Рила Солюшънс (www.rila.bg). Интересите му са в областта на компютърната лингвистика и биоинформатикатa. Преслав получава магистърската си степен по информатика от Софийски университет "Св. Климент Охридски". Той е носител е на бронзов медал от Балканиада по информатика, заемал призови места в десетки национални състезания по програмиране като ученик и студент. Състезател е, а по-късно и треньор на отбора на Софийския университет, участник в Световното междууниверситетско състезание по програмиране (ACM International Collegiate Programming Contest). Той е асистент в множество курсове във Факултета по математика и информатика на Софийски университет, лектор-основател на курсовете "Проектиране и анализ на компютърни алгоритми" и "Моделиране на данни и проектиране на бази от данни". Преслав е автор на книгите "Основи на компютърните алгоритми" и "Програмиране = ++Алгоритми;" (www.algoplus.org). Той има десетки научни и научнопопулярни публикации в престижни международни и национални издания. Той е първият носител на наградата "Джон Атанасов" за принос към развитието на информационните технологии и информационното общество, учредена от президента на България Георги Първанов.
Радослав Иванов е софтуерен разработчик във фирма Медсофт (www. medsoft.biz) и студент в специалност информатика във Факултета по математика и информатика на Софийски университет "Св. Климент Охридски". Професионалните му интереси са в областта на информационната сигурност и продуктите и технологиите на Microsoft.
Светлин Наков е директор на Националната академия по разработка на софтуер (http://academy.devbg.org), където обучава софтуерни специалисти за практическа работа в ИТ индустрията. Той е хоноруван преподавател по съвременни софтуерни технологии в Софийски университет "Св. Климент Охридски", където води курсове по "Проектиране и анализ на компютърни алгоритми", "Интернет програмиране с Java", "Мрежова сигурност", "Програмиране за .NET Framework" и "Качествен програмен код". Светлин има сериозен професионален опит като софтуерен разработчик и консултант. Неговите интереси обхващат Java технологиите, .NET платформата и информационната сигурност. Той е завършил бакалавърската и магистърската си степен във Факултета по математика и информатика на Софийски университет "Св. Климент Охридски". Като ученик и студент Светлин е победител в десетки национални състезания по програмиране и е носител на 4 медала от международни олимпиади по информатика. Той има десетки научни и технически публикации, свързани с разработката на софтуер, в български и чуждестранни списания и е автор на книгите "Интернет програмиране с Java" и "Java за цифрово подписване на документи в уеб". През 2003 г. той е носител на наградата "Джон Атанасов" на фондация Еврика. През 2004 г. получава награда "Джон Атанасов" от президента на България Георги Първанов за приноса му към развитието на информационните технологии и информационното общество. Светлин е един от учредителите на Българската асоциация на разработчиците на софтуер (www.devbg.org) и понастоящем неин председател.
Стефан Добрев е старши уеб разработчик във фирма Vizibility (www.vizibility.net). Той отговаря за голяма част от .NET продуктите, разработвани в софтуерната компания, в това число уеб базирана система за изграждане на динамични сайтове и управление на тяхното съдържание, уеб система за управление на контакти и др. Негова отговорност е и внедряването на утвърдените практики и методологии за разработка на софтуер в производствения процес. Професионалните му интереси са насочени към уеб технологиите, в частност ASP.NET, XML уеб услугите и цялостната разработка на приложения, базирани на .NET Framework. Стефан следва информатика във Факултета по математика и информатика на Софийски университет "Св. Климент Охридски".
Стефан Кирязов е софтуерен разработчик във фирма Verix (www.verix.bg). Той се занимава професионално с разработка на .NET решения за бизнеса и държавната администрация. Опитът му включва изграждане на уеб и настолни приложения с технологии на Microsoft, а също и Java и Oracle. Завършил е Факултета по математика и информатика на Софийски университет "Св. Климент Охридски", специалност информатика. Неговите професионални интереси включват архитектура, дизайн и методологии за разработка на големи корпоративни приложения. За контакти със Стефан можете да използвате неговия e-mail: skiryazov@verix.bg.
Стефан Захариев работи като софтуерен разработчик в Интерконсулт България (www.icb.bg), където е отговорен за създаването на инструменти за автоматизиране на процеса на разработка. Той има дългогодишен опит в създаването на ERP системи, който натрупва при работата си в различни фирми в България. Основните му интереси са свързани със системите за управление на бази от данни, платформата .NET, ORM инструментите, J2ME, както и Borland Delphi. При завършването си на средното образование в "Технологично училище – Електронни системи", печели отличителна награда за цялостни постижения. През 2005 г. завършва "Технически университет – София", където се дипломира като бакалавър във факултета по "Компютърни системи и управление". Той членува в БАРС и в Софийската .NET потребителска група Можете да се свържете със Стефан по e-mail: stephan.zahariev@gmail.com.
Стоян Дамов е софтуерен консултант, пич, поет и революционер. Можете да се свържете с него по e-mail: stoyan.damov@gmail.com или от неговия личен сайт: http://spaces.msn.com/members/stoyan/.
Тодор Колев е софтуерен разработчик в Gugga (www.gugga.com) и студент във Факултета по математика и информатика на Софийски университет "Св. Климент Охридски", специалност Информатика. Неговите професионални интереси са в областта на обектно-ориентирания анализ, моделиране и програмиране, уеб технологиите, базите данни и RIA (Rich Internet Applications). Тодор е дългогодишен участник в състезания по информатика и информационни технологии, печелил редица грамоти и отличия, както и сребърен медал на международна олимпиада по информационни технологии. Той е носител на първо място от националната олимпиада по информационни технологии и е участвал в журито на същата олимпиада година по-късно. Тодор има множество разработки в сферата на уеб технологиите и е участвал в изследователски екип в Масачузетският технологичен институт (MIT). Той е сертифициран Microsoft специалист.
Христо Дешев е разработчик на ASP.NET компоненти във фирма telerik (www.telerik.com). Той е завършил Американския университет в България, специалност информатика. Основните му интереси са в областта на подобряването на процеса на разработка на софтуер. Той е запален привърженик на Agile методологиите, основно на Extreme Programming (XP). Професионалният му опит е предимно в разработката на решения с кратък цикъл за обратна връзка, високо покритие от тестове и почти пълна автоматизация на всички нива от работния процес.
Христо Радков е управител на фирма за софтуерни консултантски услуги Calisto ID (www.calistoid.com). Той е бакалавър от английската специалност "Manufacturing Engineering" в Технически Университет – София и магистър по информационни и комуникационни технологии във Факултета по математика и информатика на Софийски университет "Св. Климент Охридски". Христо има дългогодишен опит с различни сървъри за бази от данни и сериозен опит с различни технологии на Microsoft, Borland, Sun и Oracle. Участник и ръководител е в проекти за изграждане на няколко големи и няколко по-малки информационни системи, динамични Интернет сайтове и др. Под негово ръководство е създаден най-успешния складово-счетоводен софтуер за фармацевтични предприятия в страната. Като ученик Христо има множество участия и награди от олимпиади по математика в страната и чужбина.
Цветелин Андреев е софтуерен разработчик във фирма Komero Technologies (www.komero.net). Той отговаря основно за UNIX базираните решения и за модули, свързани с вътрешния процес на разработка. В момента Цветелин е студент във Факултета по математика и информатика на Софийски университет "Св. Климент Охридски" и е професионално сертифициран от Sun. Неговите интереси са основно в областта на Java и UNIX технологиите, но обхващат и области от .NET платформата, изкуствен интелект, мрежова сигурност, анализ на изисквания, софтуерни архитектури и дизайн. Личният сайт на Цветелин е достъпен от адрес: www.flowerlin.net.
Явор Ташев е софтуерен разработчик във фирма TND Soft (www.tndsoft.com). Той е завършил Факултета по математика и информатика на Софийски университет "Св. Климент Охридски", специалност информатика. Участвал е в разработката на големи корпоративни сайтове и комуникационни системи, базирани на технологиите и платформите на Microsoft. Интересите му са насочени към .NET платформата, Java и изкуствения интелект. Професионалният му опит е свързан предимно с .NET Framework, Visual Studio .NET, Microsoft SQL Server и Microsoft Internet Information Server.
Настоящата книга стана реалност благодарение на много хора и няколко организации, които помогнаха и допринесоха за проекта. Нека изкажем своята благодарност и уважение към тях.
На първо място трябва да благодарим на
главния организатор и ръководител на проекта, Светлин Наков, който успя да
мотивира над 30 души да участват в начинанието и успя да ги ръководи успешно
през всичките месеци на работата по проекта. Той успя да реализира своята идея
за създаване на чисто българска книга за програмиране с .NET Framework най-вече
благодарение на всички доброволни участници, които дариха своя труд за проекта
и отделиха от малкото си свободно време за да споделят своите знания и опит
безвъзмездно, за каузата.
Авторският колектив е наистина главният виновник за съществуването на тази книга. Текст с такъв обем и такова качество не може да бъде написан от един или двама автора за по-малко от няколко години, а до тогава информацията може вече да остаряла.
Идеята за участие на толкова много автори се оказа успешна, макар и координацията между тях да не беше лесна. Въпреки, че отделните глави от книгата са писани от различни автори, те следват единен стил и високо качество. Всички глави са добре структурирани, с много заглавия и подзаглавия, с много и подходящи примери, с добър стил на изказ и еднакво форматиране.
Проектът получи силна подкрепа от Българската асоциация на разработчиците на софтуер (БАРС), тъй като е в синхрон с нейните цели и идеи.
БАРС официално държи правата за издаване и разпространение на книгата в хартиен вид, но няма право да реализира печалба от тази дейност. Асоциацията чрез своите контакти успя да намери финансиране за отпечатването на книгата, както и хостинг за нейния уеб сайт и форум.
В ранните си фази, когато бяха изготвени лекциите за курса "Програмиране за .NET Framework", проектът получи подкрепа и частично финансиране от Microsoft Research. Ако не беше тази подкрепа, вероятно нямаше да се стигне до създаването на лекциите и до написването на книгата.
Порталът за организиране на работата в екип SciForge.org даде своя принос към проекта, като предостави среда за съвместна работа, включваща система за контрол над версиите, форум, пощенски списък (mailing list) и някои други средства за улеснение на работата.
Благодарностите са отправени главно към създателя на портала и негов главен администратор Калин Наков (www.kalinnakov.com), който указваше редовно съдействие в случай на технически проблеми.
Факултетът по математика и информатика (ФМИ) на Софийски университет "Св. Климент Охридски" подпомогна проекта главно в началната му фаза, като подкрепи предложението на преподавателския екип от курса "Програмиране за платформа .NET" за участие в конкурса на Microsoft Research.
Благодарностите са отправени към ст. ас. Елиза Стефанова (която оформи изключително убедително текста на предложението за проекта към Microsoft Research) и доц. Магдалина Тодорова (която пое ролята на административен ръководител при взаимоотношенията с Microsoft).
По-късно, когато проектът на MS Research приключи и започна работата по настоящата книга, ФМИ предостави зали и техника за провеждане на регулярните срещи на авторския колектив.
Софтуерната компания telerik (www.telerik.com) подкрепи проекта чрез осигуряване на финансиране за отпечатване на книгата на хартия. Изказваме благодарности от името на целия авторски колектив.
Изказваме благодарности още към:
- Георги Иванов, ръководител на проекти във фирма Sciant (www.sciant.com), участник в преподавателския екип на курсовете по "Програмиране за .NET Framework". Участник в създаването на лекциите, по които е изградена настоящата книга.
- Стоян Йорданов, софтуерен инженер в Microsoft Corporation, Redmond (www.microsoft.com), участник в преподавателския екип на курсовете по "Програмиране за .NET Framework". Участник в създаването на лекциите, по които е изградена настоящата книга.
- Бранимир Гюров, частичен съавтор на една от главите на книгата, участник в преподавателския екип на курса "Програмиране за .NET Framework". Участник в създаването на лекциите, на които се основава настоящата книга.
- Невена Партинова, графичен дизайнер. Благодарности за изготвянето на корицата на книгата и за цялото търпение по време на продължителните дискусии за графичния дизайн и цветовата гама.
- Михаил Балабанов, преводач и автор на спецификации за превод на софтуер, участник в превода на OpenOffice.org. Благодарности за помощта при превода на някои технически термини.
- Никола Касев, взел участие при създаването на лекциите, по които е изградена настоящата книга.
- Свилена Момова, частичен съавтор на една от главите на книгата.
- Веселин Райчев, частичен съавтор на една от главите на книгата.
Официалният уеб сайт на книгата "Програмиране за .NET Framework" е достъпен от адрес: http://www.devbg.org/dotnetbook/. От него можете да изтеглите цялата книга в електронен вид, лекциите, на които тя е базирана, както и сорс кода на практическия проект от глава 29, за който има специално изготвена инсталираща програма.
Към книгата е създаден и дискусионен форум, който се намира на адрес: http://www.devbg.org/forum/index.php?showforum=30. В него можете да дискутирате всякакви технически и други проблеми, свързани с книгата, да отправяте мнения и коментари и да задавате въпроси към авторите.
Книгата и учебните материали към нея се разпространяват свободно по следния лиценз:
1. Настоящият лиценз дефинира условията за използване и разпространение на комплект учебни материали и книга по "Програмиране за .NET Framework", разработени от екип под ръководството на Светлин Наков (www.nakov.com) с подкрепата на Българска асоциация на разработчиците на софтуер (www.devbg.org) и Microsoft Research (research.microsoft.com).
2. Учебните материали се състоят от:
- презентации;
- примерен сорс код;
- демонстрационни програми;
- задачи за упражнения;
- книга (учебник) по програмиране за .NET Framework с езика C#.
3. Учебните материали са достъпни за свободно изтегляне при условията на настоящия лиценз от официалния сайт на проекта:
http://www.devbg.org/dotnetbook/
4. Автори на учебните материали са лицата, взели участие в тяхното изработване. Всеки автор притежава права само над продуктите на своя труд.
5. Потребител на учебните материали е всеки, който по някакъв начин използва тези материали или части от тях.
1. Потребителите имат право:
- да използват учебните материали или части от тях за всякакви цели, включително да ги да променят според своите нужди и да ги използват при извършване на комерсиална дейност;
- да използват сорс кода от примерите и демонстрациите, включени към учебните материали или техни модификации, за всякакви нужди, включително и в комерсиални софтуерни продукти;
- да разпространяват безплатно непроменени копия на учебните материали в електронен или хартиен вид;
- да разпространяват безплатно оригинални или променени части от учебните материали, но само при изричното споменаване на източника и авторите на съответния текст, програмен код или друг материал.
2. Потребителите нямат право:
- да разпространяват срещу заплащане учебните материали или части от тях (включително модифицирани версии), като изключение прави само програмният код;
- да премахват настоящия лиценз от учебните материали.
1. Всеки автор притежава неизключителни права върху продуктите на своя труд, с които взима участие в изработката на учебните материали.
2. Авторите имат право да използват частите, изработени от тях, за всякакви цели, включително да ги изменят и разпространяват срещу заплащане.
3. Правата върху учебните материали, изработени в съавторство, са притежание на всички съавтори заедно.
4. Авторите нямат право да разпространяват срещу заплащане учебни материали или части от тях, изработени в съавторство, без изричното съгласие на всички съавтори.
Ръководството на Българска асоциация на разработчиците на софтуер (БАРС) има право да разпространява учебните материали или части от тях (включително модифицирани) безплатно или срещу заплащане, но без да реализира печалба от продажби.
Microsoft Research има право да разпространява учебните материали или части от тях по всякакъв начин – безплатно или срещу заплащане, но без да реализира печалба от продажби.
Светлин Наков,
24.09.2005 г.
|
Национална академия по разработка на софтуер |
|
|
Лекторите » Светлин Наков е автор на десетки технически публикации и няколко книги, свързани с разработката на софтуер, заради което е търсен лектор и консултант. Той е разработчик с дългогодишен опит, работил по разнообразни проекти, реализирани с различни технологии (.NET, Java, Oracle, PKI и др.) и преподавател по съвременни софтуерни технологии в СУ "Св. Климент Охридски". През 2004 г. е носител на наградата "Джон Атанасов" на президента на България Георги Първанов. Светлин Наков ръководи обучението по Java технологии в Академията.
» Мартин Кулов е софтуерен инженер и консултант с дългогодишен опит в изграждането на решения с платформите на Microsoft. Мартин е опитен инструктор и сертифициран от Майкрософт разработчик по програмите MCSD, MCSD.NET, MCPD и MVP и международен лектор в световната организация на .NET потребителските групи INETA. Мартин Кулов ръководи обучението по .NET технологии в Академията. |
Академията » Национална академия по разработка на софтуер (НАРС) е център за професионално обучение на софтуерни специалисти.
» НАРС провежда БЕЗПЛАТНО курсове по разработка на софтуер и съвременни софтуерни технологии в София и други градове.
» Предлагани специалности: § Въведение в програмирането (с езиците C# и Java) § Core .NET Developer § Core Java Developer
» Качествено обучение с много практически проекти и индивидуално внимание за всеки.
» Гарантирана работа! Трудов договор при постъпване в Академията.
» БЕЗПЛАТНО! Учите безплатно във въведителните курсове и по стипендии от работодателите в следващите нива. |
- Познания по програмиране
- Езици за програмиране
- Среди за разработка на софтуер
- Какво е .NET?
- Архитектура на платформата Microsoft .NET
- Какво е .NET Framework?
- Архитектура на .NET Framework
- Common Language Runtime (CLR)
- Управляван код
- Междинен език IL
- Модел за изпълнение на IL кода
- Асемблита и метаданни
- .NET приложения
- Домейни на приложението
- Common Language Specification (CLS), Common Type System (CTS)
- Common Language Infrastructure (CLI) и интеграцията на различни езици
- Framework Class Library
- Интегрирана среда за разработка Visual Studio .NET
В настоящата тема ще представим платформата .NET, която въплъщава визията на Microsoft за развитието на информационните и софтуерните технологии, след което ще разгледаме средата за разработка и изпълнение на .NET приложения Microsoft .NET Framework. Ще обърнем внимание на управлявания код, на езика IL, на общата среда за контролирано изпълнение на управляван код (Common Lnaguage Runtime) и на модела на компилация и изпълнение на .NET кода. Ще разгледаме още Common Language Specification (CLS), Common Type System (CTS), Common Language Infrastructure (CLI), интеграцията на различни езици, библиотеката от класове Framework Class Library и интегрираната среда за разработка Visual Studio .NET.
Microsoft дефинират платформата .NET като съвкупност от технологии, които свързват хората с информацията – навсякъде, по всяко време, от всяко устройство. Това определение звучи като маркетингова пропаганда, но .NET е не само технология, тя е и идеология. Платформата въплъщава визията на Microsoft, че информацията трябва да бъде максимално достъпна за хората.
.NET платформата осигурява стандартизирана инфраструктура за разработка, използване, хостинг и интеграция на .NET приложения и XML уеб услуги, базирана на .NET сървърите на Microsoft, средствата за разработка (.NET Framework и Visual Studio .NET), идеологията на smart клиентите и т. нар. .NET Building Block Services.
Визията на Microsoft за .NET е да създадат платформа, която да може да обединява хетерогенна инфраструктура от сървъри, да интегрира бизнес процесите на различни компании по стандартен начин, и да предоставя на потребителите достъп до информацията, която им е нужна, по всяко време, от всяко място и от всяко устройство. Както ще видим по-нататък, Microsoft са направили голяма крачка напред към реализирането на тази визия, като са поставили една стабилна технологична основа за разработка и изпълнение на приложения – Microsoft .NET Framework.
|
|
Разграничайвате понятията "платформа .NET" и ".NET Framework"! .NET платформата е визията на Microsoft за развитието на технологиите и осигурява глобална инфраструктура за реализацията на тази визия. .NET Framework е само част от .NET платформата – тази част, която е насочена към разработчиците на софтуер. Тя осигурява среда за разработка и контролирано изпълнение на .NET приложения и предоставя програмен модел и библиотеки от класове за разработка, независима от езиците за програмиране. Имайте предвид, че много често под .NET се подразбира не платформата .NET, а средата .NET Framework, например ".NET език", ".NET приложение" и т. н. В настоящата книга също ще подразбираме под .NET не .NET платформата, а .NET Framework. |
Платформата .NET обединява в себе си четири технологични и идеологически компонента: инфраструктурата от сървъри .NET Enterprise Servers, средствата за разработка .NET Framework и Visual Studio .NET 2003, глобалните услуги .NET Building Block Services и идеологията .NET Smart Clients:

Всеки един от изброените компоненти на .NET платформата е достатъчно обемна тема, за да й се посвети цяла отделна книга, но нашата цел е само да се запознаем накратко с посочените технологии и идеологии, без да навлизаме в подробности. Нека сега ги разгледаме една по една.
.NET Enterprise Servers предоставят сървърната инфраструктура на .NET платформата и същевременно среда за изпълнение, управление и интеграция на XML уеб услуги.
Ключовите характеристики на .NET Enterprise сървърите са:
- Силна поддръжка на XML – всички .NET сървъри използват широко XML стандарта за представяне и обмяна на информация.
- Висока надеждност – ключова характеристика, изключително важна за бизнеса.
- Добра скалируемост – възможност за поемане на огромно натоварване при необходимост.
- Оркестрация на бизнес процесите в приложенията и услугите (business process orchestration) – дава се възможност за схематично дефиниране на работните процеси по утвърдени стандарти (като BPEL) и контролираното им изпълнение, наблюдение и управление.
- Повишена сигурност – сигурността е основна архитектурна концепция при .NET сървърите.
- Лесно управление – леснота за администриране, настройка, наблюдение и управление на работата на сървърите.
Microsoft разработват сървърни продукти от много години и в момента предлагат цяло семейство от специализирани сървъри, насочени към различни бизнес нужди. Ще дадем съвсем кратко описание на най-важните от тях:
- Microsoft Windows Servers Family – представлява фамилия сървърни операционни системи (като Windows 2000 Server и Windows 2003 Server).
- Microsoft Internet Information Server – представлява уеб сървър, който е част от Windows. Служи за хостинг на уеб сайтове със статично и динамично съдържание.
- Microsoft SQL Server – служи за управление на релационни бази от данни, многомерни данни и XML.
- Microsoft BizTalk Server – използва се за интеграция и оркестрация на бизнес процеси, услуги и системи.
- Microsoft Exchange – позволява координация на съвместната работа в организации. В частност осигурява поддръжката на пощенски услуги (e-mail).
- Microsoft SharePoint Portal Server – позволява сътрудничество и споделяне на информация в реално време. Улеснява конкурентната работа с общи документи и работата в екип.
- Microsoft Host Integration Server – позволява интеграция на стари системи.
- Microsoft Application Center – осигурява хостинг, управление и мониторинг на критични за бизнеса приложения.
- Microsoft Content Management Server – служи за изграждане, поддръжка и управление на уеб съдържание.
- Microsoft Mobile Information Server – позволява интеграция с мобилни приложения.
- Microsoft Internet Security and Acceleration Server – контрол и защита на връзката с Интернет. Предоставя защитна стена (firewall) с възможност за филтриране и анализ на трафика на различни нива.
- Microsoft Commerce Server – използва се за реализация на приложения за електронна търговия.
.NET Framework е софтуерна платформа за разработка и изпълнение на .NET приложения. Тя представлява предоставя програмен модел и стандартна библиотека с класове за разработка на приложения и унифицирана среда за изпълнение на управляван код. Поддържа различни езици за програмиране и позволява тяхната съвместна работа.
.NET Framework съществува в два варианта:
- .NET Framework – пълна версия.
- .NET Compact Framework – съкратена версия за изпълнение върху мобилни устройства. Създадена е специално за устройства с ограничени хардуерни ресурси.
Visual Studio .NET 2003 представлява цялостна интегрирана среда за разработка на .NET приложения. Позволява създаване на различни видове приложения, писане на програмен код, изпълнение и дебъгване на приложения, изграждане на потребителски интерфейс и др. VS.NET предоставя единна среда за всички технологии и за всички програмни езици, поддържани стандартно от .NET Framework (C#, VB.NET, C++ и J#).
.NET Building Block Services са съвкупност от XML уеб услуги, насочени към крайния потребител. Основната им задача е да осигуряват персонализиран достъп до данните на даден потребител по всяко време и от всякакво устройство. За целта се използват отворени стандарти и протоколи за комуникация.
.NET Building Block Services са създадени с цел да позволяват лесна интеграция с други услуги и приложения и да позволяват връзка между тях. Ето няколко области, в които има изградени такива Building Block услуги:
- автентикация – на базата на .NET Passport
- доставка на съобщения
- съхранение на лични потребителски данни – документи, контакти, електронна поща, календар, любими сайтове и други
- съхранение на настройки на приложения, които потребителят използва.
Smart clients представлява архитектурна концепция, която позволява изграждането на клиентски приложения, които:
- предоставят гъвкав потребителски интерфейс (за разлика от уеб приложенията и WAP приложенията)
- консумират XML уеб услуги (чрез които си осигуряват връзка с останалия свят и обменят данни със сървърите, които съхраняват и обработват техните данни)
- могат да работят в online и offline режим (като синхронизират данните си когато са online)
- имат възможност да се самообновяват (и това може да става автоматично, с минимални усилия от страна на потребителя).
Смарт клиентите предоставят алтернатива на клиент-сървър приложенията и уеб приложенията. Като концепция те не са непременно обвързани с .NET. Има, например, реализация на smart клиент архитектури, базирани на Java платформата.
.NET платформата предоставя специализирана инфраструктура, която подпомага и улеснява реализацията на smart client приложения.
.NET smart клиентите работят както върху обикновени настолни компютри, така и върху различни преносими устройства: мобилни телефони, hand held устройства, вградени системи и т. н.
Основната им задача е да предоставят достъп до информацията, нужна на потребителя, навсякъде, по всяко време и във вид, удобен за потребителя.
.NET Framework и неговия вариант за мобилни приложения .NET Compact Framework предлагат възможности за разработка на smart client приложения за много разнообразни устройства.
До момента направихме преглед на .NET платформата и разгледахме компонентите, от които тя се състои. Сега ще разгледаме в детайли .NET Framework, неговата архитектура и модела за изпълнение на приложения, който тя използва.
.NET Framework e среда за разработка и изпълнение на приложения за .NET платформата. Тя предоставя програмен модел, библиотеки от типове и единна инфраструктура за разработка на приложения и поддържа различни езици за програмиране.
Приложенията, базирани на .NET Framework, се компилират до междинен код (на езика IL) и се изпълняват контролирано от средата за изпълнение на .NET Framework. Компилираният .NET код се нарича още управляван код и може да работи без да се прекомпилира върху различни платформи, за които има имплементация за .NET Framework (Windows, Linux, FreeBSD).
Можем да разделим .NET Framework на два основни компонента:
- Common Language Runtime (CLR) – средата, в която се изпълнява управляваният код на .NET приложенията. Представлява виртуална машина, която контролирано изпълнява .NET кода и осигурява различни услуги, като управление на сигурността, управление на паметта и др.
- Framework Class Library (FCL) – представлява основната библиотека от типове, които се използват при изграждането на .NET приложения. Съдържа основната функционалност за разработка, необходима за повечето приложения, като вход/изход, връзка с бази данни, работа с XML, изграждане на уеб приложения, използване на уеб услуги, изграждане на графичен потребителски интерфейс и др. Стандартните класове и типове от FCL можем да използваме навсякъде, където има инсталиран .NET Framework.
Архитектурата на .NET Framework често пъти се разглежда на нива, както това е направено на следната схема:

Ще разгледаме отделните слоеве един по един и ще обясним тяхната роля в .NET Framework. Ще започнем от най-долния.
Операционната система управлява ресурсите, процесите и потребителите на машината. Тя предоставя и някои услуги на приложенията като например: COM+, MSMQ, IIS, WMI и други.
Средата, която изпълнява .NET приложенията (CLR), е обикновен процес в операционната система и се управлява от нея, както останалите процеси.
Най-често операционната система, която изпълнява CLR е Microsoft Windows, но .NET Framework има имплементации и за други операционни системи (например проектът Mono).
Общата среда за изпълнение Common Language Runtime (CLR) управлява процеса на изпълнение на .NET код. Тя се грижи за заделяне и освобождаване на паметта, управлява конкурентността, грижите за сигурността на приложенията и изпълнява други важни задачи, свързани с изпълнението на кода. Ще обърнем специално внимание на CLR малко по-нататък.
Base Class Library (BCL) е част от стандартната библиотека на .NET Framework – Framework Class Library (FCL).
BCL представлява богата обектно-ориентирана библиотека с основни класове, които осигуряват базова системна функционалност. BCL осигурява вход-изход, работа с колекции, символни низове, мрежови ресурси, сигурност, отдалечено извикване, многонишковост и др.
Технологиите ADO.NET, XML, ASP.NET и Windows Forms не са част от BCL, тъй като те са по-скоро допълнителни библиотеки, отколкото базова системна функционалност.
Слоят на ADO.NET и XML предоставя удобен начин за работа с релационни и други бази от данни и средства за обработка на XML. ADO.NET поддържа два моделa на работа с данни – свързан и несвързан. XML поддръжката реализира DOM модела и модел, подобен на SAX, за достъп до XML. Ще разгледаме в детайли XML и ADO.NET в темите "Работа с XML" и "Достъп до данни с ADO.NET".
ASP.NET и Windows Forms изграждат слоя за интерфейс към крайния потребител на приложенията и ни предоставят богата функционалност за създаване на уеб и Windows базиран потребителски интерфейс, както и уеб услуги. ASP.NET позволява по лесен начин да бъдат изграждани гъвкави динамични уеб сайтове и уеб приложения и уеб услуги. Windows Forms позволява изграждане на прозоречно-базиран графичен потребителски интерфейс с богати възможности.
ASP.NET и Windows Forms използват компонентно-базирана архитектура и благодарение на нея позволяват изграждане на потребителския интерфейс визуално, чрез сглобяване на компоненти в специално разработени за това редактори, предоставени от средите за разработка. Ще разгледаме в детайли технологиите Windows Forms и ASP.NET в темите "Графичен потребителски интерфейс с Windows Forms", "Изграждане на уеб приложения с ASP.NET" и "Уеб услуги с ASP.NET".
.NET Framework позволява на разработчика да използва различни езици за програмиране, както и да интегрира в едно приложение компоненти, разработвани на различни езици. Възможно е дори клас, написан на един език, да бъде наследен и разширен от клас, написан на друг език.
Microsoft .NET Framework поддържа стандартно езиците C#, VB.NET, Managed C++ и J#, но трети доставчици предлагат допълнително .NET версия на още много други езици, като Pascal, Perl, Python, Fortran, Cobol и други.
Съвместимостта на езиците за програмиране в .NET Framework се дължи на архитектурни решения, които ще разгледаме в детайли след малко.
След като се запознахме накратко с архитектурата на .NET Framework, нека сега разгледаме в детайли и най-важният компонент от нея – CLR.
Common Language Runtime (CLR) е сърцето на .NET Framework. Той представлява среда за контролирано изпълнение на управляван код. На практика CLR е тази част от .NET Framework, която изпълнява компилираните .NET програми в специална изолирана среда.
В своята същност CLR представлява виртуална машина, която изпълнява инструкции, на езика IL (Intermediate Language), езикът до който се компилират всички .NET езици. CLR е нещо като виртуален компютър, който обаче не изпълнява асемблерен код за процесор Pentium, AMD или някакъв друг, а IL код.
Има голямо сходство между .NET CLR и Java Virtual Machine, но между двете технологии и много разлики. По предназначение те служат за едно също нещо – да изпълняват код за някакъв виртуален процесор. В .NET това е IL кода, а при Java платформата – т. нар. Java bytecode. Основната разлика между IL и bytecode е, че IL е език от по-високо ниво, а това позволява да бъде компилиран много по-ефективно от Java bytecode.
Отговорностите на CLR включват:
- Изпълнение на IL кода. Реално IL инструкциите, преди да бъдат изпълнени за първи път, се компилират до инструкции за текущия процесор и след това се изпълняват от системния процесор. Този процес на междинно компилиране до машиннозависим (native) код се нарича JIT компилация (Just-In-Time compilation).
- Управление на паметта и ресурсите на приложенията. CLR включва в себе си система за заделяне на памет и система за почистване на неизползваната памет и ресурси (т. нар. garbage collector). Управлението на паметта при .NET приложенията се извършва в голяма степен автоматизирано и в повечето случаи програмистът не трябва да се грижи за освобождаване на заделената памет. Ще разгледаме в детайли как .NET Framework управлява паметта в темата "Управление на паметта и ресурсите".
- Осигуряване безопасността на типовете. .NET Framework е среда за контролирано изпълнение на програмен код (managed execution environment). Тя не позволява директен достъп до паметта, не позволява директна работа с указатели, не позволява преобразуване от един тип към друг, който не е съвместим с него, не позволява излизане от границите на масив, както и всякакви други опасни операции. По тази причина .NET се нарича управлявана среда – защото тя управлява изпълнението на кода и по този начин предпазва програмите от много досадни проблеми, които възникват при неуправляваните среди.
- Управление на сигурността. NET Framework има добре изградена концепция за сигурност на различни нива. От една страна .NET приложенията могат да се изпълняват с различни права. Правата могат да се задават от администраторите чрез т. нар. политики за сигурност. CLR следи дали кодът, който се изпълнява, спазва зададената политика за сигурност и не позволява тя да бъде нарушена. Тази техника се нарича "code access security". От друга страна .NET Framework поддържа и средства за управление на сигурността, базирана на роли (role-based security). Ще разгледаме в детайли всички тези техники и средства в темата "Сигурност в .NET Framework".
- Управление на изключенията. .NET Framework е изцяло обектно-ориентирана среда за разработка и изпълнение на програмен код. В нея механизмът на изключенията е залегнал като основно средство за управление на грешки и непредвидени ситуации. Една от задачите на CLR е да се грижи за изключенията, които възникват по време на изпълнение на кода. При настъпване на изключение CLR има грижата да намери съответния обработчик и да му предостави управлението. Ще разгледаме в детайли всичко това в темата "Управление на изключенията в .NET".
- Управление на конкурентността. CLR контролира паралелното изпълнението на нишки (threads) като за целта си взаимодейства с операционната система. Повече за работата с нишки ще научим в темата "Многонишково програмиране и синхронизация".
- Взаимодействие с неуправляван код. CLR осигурява връзка между управляван (.NET) код и неуправляван (Win32) код. За целта той изпълнява доста сложни задачи, свързани с конвертиране на данни, синхронизация, прехвърляне на извиквания, взаимодействие с компонентния модел на Windows (COM) и много други. Ще разгледаме в детайли тези проблеми в темата "Взаимодействие с неуправляван код".
- Подпомагане процесите на дебъгване (debugging) и оптимизиране (profiling) на управлявания код. CLR осигурява инфраструктура и средства за реализацията на дебъгване и оптимизиране на кода от външни специализирани програми.
Управляваният код (managed code) е кодът, който се изпълнява от CLR. Той представлява поредица от IL инструкции, които се получават при компилацията на .NET езиците. По време на изпълнение управляваният код се компилира допълнително до машиннозависим код за текущата платформа и след това се изпълнява директно от процесора.
Управляваният код (.NET код) се различава значително от неуправлявания код (например Win32 кода).
Управляваният код е машиннонезависим, т. е. може да работи на различни хардуерни архитектури, процесори и операционни системи, стига за тях да има имплементация на CLR.
Неуправляваният код е машиннозависим, компилиран за определена хардуерна архитектура и определен процесор. Например програмите, написани на езика C, се компилират до неуправляван код за определена архитектура.
Ако компилираме една C програма за Embedded Linux върху платформа StrongARM, ще получим неуправляван машиннозависим (native) код за Linux за тази платформа. Кодът ще съдържа инструкции за микропроцесор StrongARM и ще използва системни извиквания към операционната система Embedded Linux. Съответно на друга платформа няма да може да работи без прекомпилация на сорс кода на C програмата.
По същия начин, ако компилираме една C програма за Windows върху архитектура x86, ще получим неуправляван код за процесор x86 (примерно Pentium, Athlon и т.н.), който използва системни извиквания към Windows. Този код се нарича Win32 код и може да работи само върху 32-битова Windows операционна система. За да се стартира върху друга платформа, трябва да се компилира.
При управлявания код нещата стоят по различен начин. Ако компилираме една C# програма за платформа .NET Framework 1.1, ще получим управляван, машиннонезависим IL код, който може да работи върху различен хардуер. Кодът реално ще е компилиран за платформа CLR 1.1 и ще се състои от IL инструкции за виртуалния процесор на CLR и ще използва системни извиквания към .NET Base Class Library.
Управляваният код лесно може да бъде пренесен върху различни платформи без да се променя или прекомпилира. Така например програма на C#, която е компилирана под Windows до управляван IL код, може да се изпълнява без промени както върху Windows под .NET Framework, така и върху Linux под Mono, а също и върху мобилни устройства под Windows CE и .NET Compact Framework.
Управляваният код се самоописва чрез метаданни и носи в себе си описание на типове данни, класове, интерфейси, свойства, полета, методи, параметри на методите и други, както и описание на библиотеки с типове, описание на изисквания към сигурността при изпълнение и т. н. Това дава голяма гъвкавост на разработчика и възможност за динамично зареждане, изследване и изпълнение на функционалност, компилирана като управляван (IL) код.
Неуправляваният код стандартно не съдържа метаданни и това силно затруднява динамичното зареждане и изпълнение на неуправлявана функционалност.
Управляваният код задължително е обектно-ориентиран, докато за неуправлявания няма такова изискване. Всички .NET езици са обектно-ориентирани. Всички .NET програми се компилират до класове и други типове от общата система от типове на .NET Framework. Всички данни, използвани от управлявания код, са наследници (в смисъла на обектно-ориентираното програмиране) на базовия тип System.Object. Ще разгледаме това в подробности в темата "Обща система от типове".
Управляваният код е защитен от неправилна работа с паметта и типовете и това го прави по-сигурен и високо надежден. Управляваният код не може да извършва неправилен достъп до паметта, достъп до чужда памет и неправилна работа с типове. Това предпазва програмисти от много досадни проблеми, присъщи при писането на неуправляван код, като загуба на памет, достъп до неинициализирана памет, повторно освобождаване на памет, работа с невалиден указател и т.н.
До управляван код се компилират всички .NET езици. Това дава възможност за широко взаимодействие между код, писан на различни езици за програмиране. Възможно е дори клас, написан на един .NET език, да бъде наследен и разширен от клас, написан на друг .NET език.
За .NET Framework няма значение на какъв език е бил написан кода преди да бъде компилиран. Всичкият код се компилира до IL и се изпълнява от CLR по еднакъв начин.
Управлението на паметта е една от важните задачи на CLR. Идеята за автоматизирано управление на паметта е залегнала в .NET Framework на дълбоко архитектурно ниво. Целта е да се улесни разработчика като се освободи от досадната задача сам да следи за освобождаването на заделената памет.
CLR, като средата за изпълнение, управлява заделянето на памет, инициализирането й, и автоматичното й освобождаването посредством garbage collector.
Динамично заделените обекти се разполагат в динамичната памет, в тъй наречения "managed heap". След като техния живот завърши и те вече не са необходими на приложението, системата за почистване на паметта (garbage collector) освобождава заеманата от тях памет автоматично. По този начин се избягват най-често срещаните проблеми като загуба на памет и достъп до освободена или неинициализирана памет. Повече за управлението на паметта в .NET Framework ще научим в темата "Управление на паметта и ресурсите".
Важна особеност при работата с управляван код е, че при него няма указатели. Вместо указатели се работи с референции, които са силно типизирани и се управляват автоматично. Референцията (reference) прилича на указател, но не е просто адрес в паметта, а има тип, т. е. тя е указател към определен тип данни и не може да сочи към място в паметта, където няма инстанция на този тип.
Междинният език Intermediate Language (IL), е език за програмиране от ниско ниво, подобен на асемблерните езици. За разлика от тях, обаче, IL е от много по-високо ниво, отколкото асемблерите за съвременните микропроцесори.
IL е обектно-ориентиран език. Той разполага с инструкции за заделяне на памет, за създаване на обект, за предизвикване и обработка на изключения, за извикване на виртуални методи и други инструкции, свързани с обектно-ориентираното програмиране.
Тъй като не е процесорно-специфичен, IL предоставя голяма гъвкавост и възможност за изпълнение на кода върху различни платформи чрез компилиране до съответния за платформата машинен език.
Възможна е и предварителна компилация до код за текущата платформа, но тази техника не носи голяма полза и рядко се използва.
Имплементацията на IL в .NET Framework се нарича MSIL (Microsoft Intermediate Language). IL може да има и други имплементации в други платформи и среди за изпълнение на .NET код.
Езикът IL е стандартизиран от организацията ECMA и в съответния стандарт се нарича CIL (Common Intermediate Language).
|
|
Често пъти термините IL и MSIL се използват като взаимозаменяеми и затова винаги трябва да имате предвид, че става въпрос за кода, който се изпълнява от CLR – машинният код, получен при компилацията на .NET езиците. |
За да илюстрираме по-добре казаното до тук, нека разгледаме една проста програмка, написана на MSIL – класическият пример "Hello world!":
|
.method private hidebysig static void Main() cil managed { .entrypoint // Code size 11 (0xb) .maxstack 8 ldstr "Hello, world!" call void [mscorlib]System.Console::WriteLine(string) ret } // end of method HelloWorld::Main |
Всичко, което прави тази MSIL програма, е да изведе съобщението "Hello, world!" на конзолата. Тя дефинира един статичен метод без параметри с име Main, в който извиква с параметър "Hello, world!" статичния метод WriteLine() от класа System.Console, който отпечатва посочения текст.
Вече споменахме няколко пъти междинния код IL и обяснихме, че .NET езиците (C#, VB.NET и т. н.) се компилират до него, а след това полученият код се изпълнява от CLR.
Сега ще разгледаме детайлно процеса на компилиране и изпълнение на .NET приложенията. Ще изясним как се извършва компилирането на програми от високо ниво, как се получава IL код, как този код се записва в специален файлов формат (асембли) и как след това компилираните асемблита се изпълняват от CLR като се компилират междувременно до машинен код от JIT компилатора.
Целият този процес е изобразен схематично на фигурата:

Изходният код на .NET програмите може да е написан на предпочитания от нас .NET език, например C#, VB.NET, Managed C++ или друг. За да го компилираме до IL управляван код, използваме компилатора за съответния език. В резултат получаваме асембли.
Асемблито представлява изпълним файл, съдържащ .NET управляван код и метаданни, които описват съдържанието на асемблито. Метаданните съдържат имената на класовете и типовете в асемблито, информация за членовете на класовете (методи, полета, свойства и други).
Едно асембли може да бъде изпълним файл (.exe файл) или динамична библиотека (.dll файл). Изпълнимите файлове съдържат допълнителна информация, която подпомага началното им стартиране (например входна точка на изпълнение).
При изпълнение на дадено асембли CLR го зарежда в паметта и анализира метаданните му. Извършват се различни проверки на кода – дали е коректен спрямо IL стандарта, дали има необходимите права за изпълнение и др.
След това управляваният IL код преминава през специфичния за текущата платформа JIT компилатор и се компилира до машинен код за текущия процесор. Компилираният вече код след това се изпълнява директно от процесора.
JIT компилаторът не компилира в началото цялото асембли, а само методът, от който започва изпълнението му. След това при опит за изпълнение на некомпилиран метод, той се компилира. Така кодът се компилира само при нужда и това осигурява добро бързодействие. Забавянето е незначително и скоростта на изпълнение на управлявания код на практика е почти еднаква със скоростта на изпълнение на неуправлявания код.
Предимството на JIT компилацията е, че може да оптимизира кода за текущата хардуерна платформа по най-добрия начин. Например ако е наличен най-мощният процесор на Intel или AMD и CLR поддържа този процесор, той ще компилира IL кода по начин, оптимизиран специално за него, и ще използва пълните му възможности. При неуправляваният код това не е възможно, защото кодът се компилира така, че да работи върху всички процесори, без да използва пълните възможности на текущата хардуерна платформа. По тази причина в някои случаи управляваният код може да е дори по-бърз от неуправлявания въпреки нуждата от JIT компилация, която отнема време.
Когато разполагаме с компилирано асембли и искаме да го изпълним, имаме право на избор кога да компилираме IL кода до машинен код. Това може да стане по време на изпълнение (посредством JIT компилатора) и предварително (с прекомпилация за текущата платформа).
Прекомпилацията на асемблита се извършва с инструмента ngen.exe, който е стандартна част от .NET Framework.
Общата среда за изпълнение CLR се състои от доста модули, всеки от които изпълнява конкретна задача. Схематично архитектурата можем да представим по следния начин:

Ще разгледаме всеки от посочените компоненти съвсем накратко, тъй като функциите им са от много ниско ниво и рядко ще ни се налага да взаимодействаме директно с тях:
- Base Class Library Support – предоставя системни услуги, необходими за работата на Base Class Library (BCL).
- Thread Support – предоставя услуги за манипулация на нишки в .NET приложенията – създаване на нишка, управление на състоянието на нишка, синхронизация и др.
- COM Marshaler – грижи се за комуникацията с COM обекти. Осигурява извикването на COM сървъри от .NET код и извикването на .NET код от COM. Негова грижа са прехвърлянето на заявки, преобразуването на данни, управлението на жизнения цикъл на COM обектите и др.
- Type Checker – осъществява проверка на типовете за съответствие при извикване и поддържа класовите йерархии.
- Exception Manager – грижи се за управление на изключенията –предизвикване на изключение, прихващане, обработване и др.
- Security Engine – отговаря за проверките на сигурността при изпълнение на кода.
- Debug Engine – осигурява функционалност, свързана с дебъгването и оптимизирането на управляван код.
- JIT Compiler – един от най-важните модули – по време на изпълнение компилира IL кода в специфичен за процесора код.
- Code Manager – управлява изпълнението на кода.
- Garbage Collector – управлява паметта автоматичното почистване на паметта и ресурсите. Контролира живота на обектите.
- Class Loader – служи за зареждане на класове и типове. Използва се при началното изпълнение на приложението, както и при динамично зареждане на код по време на изпълнение.
Нека сега разгледаме по-подробно как CLR изпълнява IL кода. Изпълнението на кода, както можем да видим от схемата по-долу, е итеративен процес, който се състои от много стъпки.
При изпълнение на метод от едно асембли Class Loader подсистемата на CLR зарежда всички нужни за неговата работа класове и типове. В зависимост от това дали кодът е вече компилиран до машинен или не Class Loader предава кода за директно изпълнение или го компилира с JIT компилатора (при първо извикване на всеки метод).

Преди JIT компилацията се извършва процес, известен като верификация. Той проверява дали IL кодът е безопасен – дали не се опитва да осъществява директен достъп до паметта, дали не се опитва да заобикаля механизмите за сигурност и т. н. Ако системният администратор е определил кода за сигурен (trusted) неговото верифициране може да бъде се прескочено.
JIT компилаторът създава специфичен за машината код (native код), който се изпълнява директно от процесора. Този машинен код съдържа в себе си много допълнителни инструкции, чрез които си взаимодейства със CLR. Целта е кодът да се изпълнява по контролиран начин, за да не нарушава принципите за сигурност и надеждност, но без да се забавя излишно заради всички допълнителни проверки.
При изпълнението на кода, при достъп до ресурси, при извикване на системни библиотеки и в много други случаи се извършват проверки на сигурността (чрез т. нар. security engine).
Ако трябва да бъде извикан некомпилиран метод, този метод се връща в JIT компилатора и така се затваря цикълът на компилация до машинен код. Като резултат от описания алгоритъм не се налага компилиране на един и същ метод повече от веднъж и освен това, ако някой метод не се извиква никъде в приложението, той въобще не се компилира от JIT компилатора.
Асемблитата са най-малката самостоятелна градивна единица в .NET Framework. Те представляват наследници на познатите ни .exe и .dll файлове и съдържат IL изпълним код, метаданни и ресурси:

За разлика от неуправляваните изпълними файлове, асемблитата са самоописващи се и носят в себе си информация за всички класове, типове и ресурси, които съдържат, както и информация за сигурността, за зависимост от външни компоненти и др. Тази информация се нарича метаданни.
Асемблитата имат собствена версия и дефинират изисквания към правата, свързани със сигурността, на потребителя или процеса, който ги изпълнява. Те могат да имат и цифров подпис, положен от създателя им, чрез който се осигурява повишена сигурност.
С вграждането на версия в самия файл на асемблито се разрешава проблемът, известен като "DLL Hell". Той се състои в следното: Когато няколко приложения зависят от един и същ споделен файл и от точно определена негова версия, с досегашните технологии в Windows при поява на нова версия на този файл, старият файл се презаписва и на практика няма гаранция, че новата версия е 100% съвместима със старата. В резултат по "магически" начин някое от приложенията, което е използвало старата версия, престава да работи.
Например при Win32 приложенията може да се случи при инсталиране на нов софтуер част от старите приложения, които са работели до този момент, да спрат да работят. Причината е в това, че новият софтуер презаписва някоя от библиотеките, които старите приложения са използвали, с по-нова версия, която може да не е съвместима със старата.
Тъй като при асемблитата версията се задава за всяко едно от тях и се записва освен в метаданните на асемблито и в неговото файлово име, при поява на нова версия не се появяват конфликти между приложенията.
Всяко асембли може да посочи точно кое асембли му е необходимо и точно в коя негова версия. Освен, че могат да съществуват едновременно няколко различни версии на едно асембли, те могат и да се изпълняват едновременно. Така е възможно в един и същи момент да работят и старите и новите приложения и всяко приложение да използва версията на общите асемблита, с която е предвидено да работи.
Всяко асембли съдържа т. нар. манифест, в който се описват зависимостите, които има с други асемблита. В него се определя и политиката за избор на версия, в случай че има повече от една за някое от реферираните асемблита.
Както вече споменахме, всички асемблита съдържат метаданни. Метаданните описват различни характеристики на асемблитата и съдържимото в тях:
- име на асемблито (например System.Windows.Forms)
- версия, състояща се от 4 числа (например 1.0.5000.0)
- локализация, описваща език и култура (например неутрална или en-US или bg-BG)
- цифров подпис на създателя (незадължителен)
- изисквания към правата за изпълнение
- зависимости от други асемблита (описани с точното име и версия)
- експортирани типове
- списък със дефинираните класове, интерфейси, типове, базови класове, имплементирани интерфейси и т.н.
- списък с дефинираните ресурси
Освен тези данни за всеки клас, интерфейс или друг тип, който е дефиниран в асемблито, се съдържа и следната информация:
- описание на член-променливите, свойствата и методите в типовете
- описание на параметри на методите, връщана стойност на метода за всеки метод
- приложени атрибути към асемблито, методите и другите елементи от кода
Във всяко асембли може да има секция, в която се намира неговият изпълним код (IL кодът). Тази секция не е задължителна. Вече разгледахме какво представлява IL кодът и как се изпълнява, така че няма да се спираме отново на това.
В ресурсната секция на асемблито могат да бъдат добавяни различни ресурси (иконки, картинки, локализирани низове и др.), необходими на приложението. Ресурсите могат да се пакетират във файла на асемблито, заедно с изпълнимия код и могат да се извличат от него по време на изпълнение. Тази секция не е задължителна.
За удобство имаме възможност да създаваме и асемблита, които се състоят от няколко файла, както и сателитни асемблита с различна култура. Ще разгледаме асемблитата по-детайлно в темата "Асемблита и разпространение".
Тъй като асемблитата са основната единица за разгръщане (deployment) в .NET Framework, ще се спрем накратко върху различните видове асемблита според начина им на разгръщане – частни и споделени.
Частните асемблита (private assemblies) се използват само от едно приложение и се записват в неговата директория или в нейна поддиректория. Те са лесни за разгръщане тъй като могат да се разпространяват чрез просто копиране и вмъкване (copy/paste). При тях контролът на версиите е по-лесен и не се изисква цифров подпис на създателя или силно име.
Споделените асемблита от своя страна са достъпни за всички приложения. Те се инсталират в специална област, наречена Global Assembly Cache (GAC). Всяко приложение, което реферира външно асембли търси споделените асемблита в тази област. Това поведение може да се контролира чрез манифеста, който задава правилата за търсене на нужните асемблита и версии. За да се определи уникално всяко асембли използва т. нар. силно име (strong name). To включва:
- име на асемблито
- версия
- локализация
- цифров подпис на създателя
Пример за силно име на асембли е идентификаторът:
|
["myDll, Version=1.0.0.1, Culture=neutral, PublicKeyToken= 9b35aa32c18d4fb1"] |
Повече за разгръщане на приложения и асемблита можете да намерите в темата "Асемблита и разпространение".
.NET приложенията се състоят от едно или повече асемблита, в които се съдържат техният код и ресурси. Те представляват изпълними единици, които могат да бъдат конфигурирани.
В зависимост от вида си .NET приложенията могат да бъдат самостоятелни или обвързани с други услуги или приложения. Например уеб приложенията не са самостоятелни и се изпълняват в средата на ASP.NET, докато конзолните приложения могат да се изпълняват самостоятелно.
За разлика от повечето Win32 приложения, .NET приложенията могат да бъдат инсталирани с просто копиране (XCOPY deployment), без да се налага регистриране на отделните им компоненти (регистрирането се налага само, ако искаме да позволим неуправлявани компоненти да могат да достъпват нашите асемблита). При .NET приложенията не се използва Windows Registry за регистрация на компонентите.
Всяко приложение използва собствена политика за зареждане на свързаните с него асемблита и намирането на нужната им версия. При липса на изрично указана такава първо се търси подходящо асембли в директориите на приложението и после в GAC. Всяко едно приложение може да използва различна версия на дадено асембли без да се влияе от останалите и, както вече споменахме, новите версии не предизвикват конфликти.
Поради всички посочени качества инсталирането, поддържането, обновяването и деинсталирането на .NET приложения е лесно и безопасно за останалите приложения.
В Windows и в .NET Framework се въвежда понятието "преносим изпълним файл" (Portable Executable или PE). Това са файлове, които съдържат информация за себе си, съдържат своя изпълним код и необходимите за работата си ресурси. Структурата на РЕ файловете може да се онагледи със следната фигура:

РЕ хедърът съдържа описание за вида на самия РЕ файл – дали той е изпълним или е библиотека с типове.
След него CLR хедърът дава нужната на CLR информация за изпълнение на самото асембли.
Останалите елементи са ни вече познати от структурата на асемблитата и затова няма да се спираме на тях отново.
Понятието РЕ представлява обобщение на двата файлови формата – .exe и .dll (става дума единствено за Windows платформи – при другите имплементации на CLI е много вероятно тези формати да се описват по друг начин.)
Application domain (домейн на приложението) е ново понятие, което се въвежда с .NET Framework. То представлява допълнително ниво на изолация между отделни .NET приложения, изпълнявани в един и същ процес на операционната система.
За да се ограничат възможните проблеми, свързани с манипулиране на паметта, в операционната система всеки процес разполага със собствена памет, с която работи, и няма право да чете или пише в паметта на друг процес. Ако се налага такова взаимодействие, то се извършва индиректно например чрез прокси обекти.
Както вече знаем, всяко .NET приложение изисква CLR да бъде зареден в паметта. Ако трябва да го зареждаме за всяко .NET приложение, което стартираме, това ще предизвика голямо ненужно натоварване и неефективно използване на ресурсите.
Като решение на този проблем идва концепцията за обединяване на няколко .NET приложения в един процес от операционната система. Това, обаче крие рискове приложенията да си пречат едно на друго. Необходим е начин за изолиране на приложенията едно от друго в рамките на процеса. Точно такава е задачата на домейните на приложенията (application domains) – те ни позволяват да изпълняваме няколко приложения в един и същ процес и същевременно ни дават пълна изолация между тях. По този начин намаляваме броя на процесите, спестяваме разход на процесорно време за зареждане на CLR и прехвърляне между процесите и намаляваме количеството на използваната памет и елиминираме повторното зареждане на едни и същи библиотеки.
.NET Framework използва вътрешно домейни на приложението за много цели, например за да изолира едно от друго отделните ASP.NET уеб приложения в рамките на един уеб сървър.
Една от най-добрите черти на .NET Framework е възможността за интеграция на множество езици за програмиране. Тя позволява да работим на предпочитания от нас език и да не губим възможността за използване на други езици в рамките на нашето решение.
За .NET Framework няма значение на какъв език е написан даден клас или компонент, стига езикът да поддържа общата езикова спецификация – CLS (Common Language Specification), т.е. да е един от .NET езиците.
Интеграцията на различни езици в .NET Framework е възможна благодарение на три важни стандарта:
- Common Language Specification (CLS)
- Intermediate Language (IL)
- Common Type System (CTS)
Ще разгледаме тези стандарти един по един с изключение на IL, тъй като вече се запознахме с него.
CLS дефинира общите и задължителни характеристики, които един програмен език трябва да притежава, за да бъде съвместим с останалите .NET езици. Тази спецификация има за цел да минимизира разликите между .NET езиците.
CLS, например, налага ограничението да се прави разлика межди малки и главни букви в имената на типовете и техните публични членове, методи, свойства и събития. Ако нарушим това правило, нашият код ще се компилира, но ще загуби съвместимостта си с CLS и другите .NET езици.
Друго ограничение, което CLS налага, e езиците да бъдат обектно-ориентирани. Това означава, че за да бъде направен даден език съвместим с CLS и .NET Framework, той трябва да бъде разширен да поддържа класове, интерфейси, свойства, изключения и всички останали елементи на обектно-ориентираното програмиране с .NET.
Повечето .NET езици поддържат много повече възможности от тези, които изисква CLS. Поради това трябва да сме внимателни при създаването на класове и други типове и да подхождаме с ясната идея дали искаме те да са CLS съвместими или не.
Общата система от типове в .NET Framework представлява формална спецификация на типовете данни, използвани в различните .NET езици за програмиране. CTS описва различните .NET типове (примитивни типове данни, класове, структури, интерфейси, делегати, атрибути и др.). В CTS се описват съдържанието и начина на дефиниране на типовете, модификаторите за достъп, начините за наследяване, времето на живот на обектите и много други технически характеристики.
CTS ни гарантира съвместимостта на данните между отделните езици за програмиране. Например типът String в С# е същия като String във Visual Basic .NET. Това позволява кодът, писан на различни езици, да си обменя свободно данни, защото данните са съвместими с CTS.
CTS дефинира двата типа обекти – референтни и стойностни, според това как се пазят в паметта и как се манипулират. CTS налага задължението всички типове да наследяват системния тип System.Object, дори и примитивните. Благодарение на това извикването "5.ToString()" е напълно валидно в езика C#.
Ще опишем съвсем накратко референтните и стойностните типове в CTS, а в по-големи детайли относно тях ще навлезем в темата "Обща система от типове".
Референтни типове (reference types) са всички класове, масиви, интерфейси и делегати. Класът String също е референтен тип. Техните инстанции представляват типово-обезопасени указатели към паметта, в която са записани данните за определен обект.
Инстанциите на референтните типове се съхраняват в динамичната памет (managed heap) и подлежат на почистване от системата за събиране на боклука (garbage collector). При предаване като параметър, те се предават по референция (адрес).
Стойностни типове (value types) са структурите и примитивните типове (като int, float, char и други). Този тип обекти се съхраняват в стека и се унищожават при излизане от обхват. При предаване като параметър, се предават по стойност (освен, ако изрично не е указано друго).
Спецификацията за общата инфраструктура на .NET езиците CLI (Common Language Infrastructure) е стандартизираната част от CLR. Нейната цел е още по-мащабна от идеята да се интегрират различните езици за програмиране – става дума за междуплатформена съвместимост. За целта тя е стандартизирана от организациите ECMA и ISO (стандарт ISO 23271:2003).
В CLI се описва как приложения написани на различни езици да могат да се изпълняват в различни среди без да се налага да се променят или прекомпилират.
CLI стандартизира следните компоненти на .NET Framework:
- Common Language Specification (CLS)
- Common Type System (CTS)
- Common Intermediate Language (CIL)
- Начина за управление на изключения в .NET
- Форматите за асемблита и метаданни
- Части от .NET Framework Class Library
Имплементацията на CLI стандарта за Windows е Microsoft .NET Framework, а за UNIX и Linux е Mono. С учебна цел Майкрософт разпространяват официално имплементация на CLI с отворен код, т. нар. Shared Source CLI (http://msdn.microsoft.com/net/sscli/).
Microsoft предлагат компилатори и поддръжка във Visual Studio .NET 2003 за следните езици:
- C# - препоръчителният език за програмиране под .NET Framework. Съвременен обектно-ориентиран език, подобен на C++ и Java, разработен специално за .NET Framework.
- Visual Basic .NET – обновена версия на езика Microsoft Visual Basic, адаптирана към .NET Framework.
- C++ (managed/unmanaged) – езикът C++ по идея е език от доста по-ниско ниво в сравнение със C# и VB.NET. Той е адаптиран към .NET Framework чрез множество разширения, допълнения и ограничения и е наречен Managed C++. Езикът продължава да съществува и като неуправляван език, който не е съвместим с .NET и се нарича Unmanaged C++.
- J# – езикът J# е създаден за да позволи по-лесното прехвърляне на Java приложения към C#. Той спазва синтаксиса на езика Java, на използва както стандартните библиотеки на Java платформата, така и стандартните библиотеки на .NET (Framework Class Library).
- JScript.NET – езикът JScript.NET е представител на слабо типизираните скриптови езици от фамилията ECMAScript (като JavaScript, VBScript и JScript), но е адаптиран към .NET Framework. Използва се за изпълнение на скриптове в някои уеб браузъри и някои други приложения.
Допълнително освен стандартните .NET езици трети доставчици са разработили съвместими с .NET Framework компилатори за Perl (ActiveState Perl for .NET), Python, Pascal (Borland Delphi 2005), Smalltalk, APL, COBOL, Eiffel, Haskell, Scheme и др.
Можем да използваме най-удобния ни език и да го смесваме с други езици в рамките на едно приложение. Имаме възможност да наследяваме безпроблемно типове, дефинирани на друг програмен език. Дори можем да използваме ефективно системата за изключения и тяхната обработка между езиците.
Интеграцията на езиците за програмиране в .NET Framework е вградена и не се налага да правим "акробатики" за да я използваме. Това е възможно поради единните система от типове, програмен модел и библиотеки от класове.
Както вече споменахме, C# е препоръчваният език за програмиране за .NET Framework. Този език е специално проектиран от архитектите на .NET Framework и е съобразен с особеностите на платформата още по време на дизайна. Именно по тази причина в настоящата книга всички примери и програмен код са написани на C#.
C# компилаторът е част от стандартния пакет на Microsoft .NET Framework SDK. C# е нов език, който се появява за пръв път в .NET и представлява смесица между C++ и Java, с елементи от Delphi. Проектиран е от екипа на Андерс Хейлсбърг, създателят на средата за бърза разработка на приложения Delphi, който е работил дълги години като архитект в Borland, а по-късно се присъединява към Microsoft.
C# е съвременен обектно-ориентиран език, силно типизиран, с широка поддръжка на идеите на компонентно-ориентирания подход за разработка. C# поддържа синтаксис за дефиниране и използване на свойства и събития, които играят важна роля при дефинирането и използването на компоненти.
C# е наследник на езика C++, но не наследява от него всичко, а само част от синтаксиса и някои негови силни страни (например предефинирането на оператори). По идея C# е проектиран да бъде лесен за използване като Java, но мощен като C++ и до голяма степен тази идея е осъществена.
В C# е премахната нуждата от допълнителни файлове като хедъри, IDL дефиниции и други, познати ни в повечето езици като C и С++. Езикът няма никакви ограничения в употребата си – еднакво добре можем да програмираме Windows, уеб или конзолни приложения, услуги (services) или библиотеки.
Заради силната типизация в С# всичко е обект – всеки един от типовете, дефинирани било в .NET Framework, било от нас, директно или индиректно наследяват базовия тип System.Object.
Самият език С# е стандартизиран от ЕСМА и ISO още преди да бъде реализирана финалната му версия в .NET Framework.
Както вече обяснихме, настоящата книга разглежда работата с .NET Framework в контекста на езика С#, така че от тук нататък често ще срещаме правоъгълни области с примерен код, като следващата:
|
using System;
namespace HelloCSharp { class HelloCSharp { static void Main() { Console.WriteLine("Hello, C#!"); } } } |
Примерът дефинира нашата първа програма на C# – класическата програмка "Hello, World!", която е адаптирана за C# и се е превърнала в "Hello, C#!". Сега няма да обясняваме в детайли как работи тя, защото ще направим това по-късно, в темата "Въведение в C#".
Framework Class Library (FCL) е стандартната библиотека на .NET Framework. В нея се съдържат няколко хиляди дефиниции на типове, които предоставят богата функционалност.
FCL съдържа средства, които позволяват на програмистите да разработват различни видове приложения:
- Windows приложения с прозоречно-базиран графичен потребителски интерфейс
- Уеб-базирани приложения
- Конзолни приложения
- Приложения за мобилни устройства
- XML уеб услуги
- Windows услуги
- Библиотеки с компоненти
Основните библиотеки, от които се състои FCL, са:
- Base Class Library – библиотека съдържаща основните средства, нужни за разработване на приложения. Дефинира работа с вход и изход, многозадачност, колекции, символни низове и интернационализация, достъп до мрежови ресурси, сигурност, отдалечено извикване и други.
- ADO.NET и XML – осигуряват достъп до бази данни и средства за обработка на XML.
- ASP.NET – предоставя ни рамкова среда (framework) за разработка на уеб приложения с богата функционалност, както и средства за създаване и консумиране на уеб услуги.
- Windows Forms – служи за основа при разработването на Windows приложения с прозоречно-базиран графичен потребителски интерфейс. Windows Forms се базира на вградените в Windows средства за изграждане на графичен потребителски интерфейс.
По-нататък ще обърнем специално внимание на всички тези библиотеки и ще разгледаме средствата, които те предлагат, в дълбочина.
За да се работи по-лесно с това голяма многообразие от типове, което FCL предлага, типовете са разделени в отделни асемблита и допълнително са разпределени в пространства от имена (namespaces) според своето предназначение и взаимовръзка.
Да разгледаме основните пространства от имена от FCL и тяхното предназначение:
- System – съдържа основни типове, използвани от всяко .NET приложение. В пространството System се намира, например, базовият за всички типове в .NET Framework клас System.Object, както и класът System.Console, който позволява вход и изход от конзолата.
- System.Collections – в това пространство се намират често използвани типове за управление на колекции от обекти: стек, опашка, хеш таблица и други.
- System.IO – съдържа типовете, които осигуряват входно-изходните операции в .NET Framework – потоци, ресурси от файловата система и други.
- System.Reflection – съдържа типове, които служат за достъп до метаданните по време на изпълнение на кода. Чрез тях е възможна реализацията на динамично зареждане и изпълнение на код.
- System.Runtime.Remoting – имплементира технология, която позволява отдалечен достъп до обекти и данни по прозрачен за програмиста начин.
- System.Runtime.Serialization – обединява типове, отговорни за процеса на сериализация и десериализация на обекти (запазване на състоянието на обект и по-късното му възстановяване).
- System.Security – в това пространство се намират типовете, които се използват за управление на сигурността. Те позволяват защита на данни и ресурси, определяне и проверка на текущите права на потребителя и други.
- System.Text – типовете от това пространство предоставят функционалност за обработка на текст, промяна на кодовата му таблица и други услуги, свързани с конвертиране на данни и интернационализация на приложенията.
- System.Threading – дефинира типове, осигуряващи достъп до нишки и свързаните с тях операции, като например синхронизация.
- System.Xml – съдържа типове за работа с XML и технологиите, свързани с него.
Освен тези общодостъпни пространства от имена, разполагаме и с още някои, които са достъпни за различните типове приложения:
- System.Web.Services – дефинира типовете, използвани за изграждането и консумирането на уеб услуги.
- System.Web.UI – съдържа стандартни средства и компоненти за изграждане на уеб приложения.
- System.Windows.Forms – съдържа типове, използвани при създаването на Windows приложения с графичен потребителски интерфейс.
До момента се запознахме с .NET Framework, с нейната архитектура, със средата за контролирано изпълнение на управляван код CLR и с основните библиотеки на .NET Framework.
Време е да разгледаме и средата за разработка на .NET приложения, която Microsoft предоставят на разработчиците. Това е продуктът Microsoft Visual Studio .NET (VS.NET).
VS.NET е една от водещите в световен мащаб интегрирани среди за разработка на приложения (IDE – Integrated Development Environment). С негова помощ можем да извършваме всяка една от типичните задачи, свързани с изграждането на едно приложение – писане на код, създаване на потребителски интерфейс, компилиране, изпълняване и тестване, дебъгване, проследяване на грешките, създаване на инсталационни пакети, разглеждане на документацията и други.
Пакетът Visual Studio .NET 2003 поддържа стандартно езиците за програмиране Microsoft C# .NET, Microsoft Visual Basic .NET, Microsoft C++ .NET (managed/unmanaged) и Microsoft Visual J#. За да ползвате език, различен от тези, които Microsoft предлага стандартно, трябва да инсталирате нужните добавки към VS.NET.
Текстовият редактор за код на VS.NET поддържа всички утвърдени съвременни функции на редакторите за сорс код – синтактично оцветяване за по-лесно визуално възприемане на кода и намаляване на грешките, автоматично довършване на започнат израз, автоматично извеждане на помощна информация по време на писане, средства за навигация по кода и много други.
Поддържа се IntelliSense функционалност за подсказване на имена на класове, методи и променливи. Тя предоставя огромно улеснение за навлизащите тепърва .NET програмисти, тъй като позволява те да разгледат на място възможностите и да изберат от списък тази, която ги интересува. Така се спестяват усилия, време за изписване на името и се намалява значително вероятността за досадни "правописни" грешки.
Следващата илюстрация дава нагледна представа за редактора на код на Visual Studio .NET 2003:

Visual Studio .NET 2003 предоставя удобен за работа графичен дизайнер за потребителски интерфейси. С него за няколко минути можем да изградим дизайна на даден потребителски интерфейс, независимо дали става дума за Windows Forms прозорец, уеб страница или интерфейс за мобилни приложения.
Това, което виждаме във VS.NET докато изграждаме потребителския интерфейс, е почти същото, което и потребителят ще види, когато стартира приложението. Начинът на работа с различните технологии за представяне на потребителски интерфейс е много подобен и това допълнително улеснява разработчиците и повишава тяхната продуктивност.
Добавянето на визуални компоненти (или потребителски контроли) става чрез влачене и пускане (drag and drop), а след това ни остава само да настроим нужните свойства на обекта със желаните от нас стойности и да добавим обработчици към някои от събитията.
Ето изглед от VS.NET в момент на редактиране на диалог от Windows Forms приложение:

Visual Studio .NET 2003 предлага унифициран начин за работа с компилаторите за различните езици. Не се налага да използваме командния ред и да знаем дългия списък с инструкции към компилатора за да можем да компилираме кода и да създаваме асемблита. Достатъчно е на натиснем [Shift+Ctrl+B] за да компилираме цялото решение с всички проекти в него.
При компилация VS.NET автоматично създава нужните асемблита и ресурсни файлове. Tя се грижи и за сателитните асемблита (ако има такива), опреснява референциите към външни файлове и класове и изпълнява още много други задачи.
Освен синтактичните грешки, процесът на компилация улавя и някои семантични. Допълнително той може да показва и предупредителни съобщения за съмнителен или недобър код. Можем да контролираме нивото на филтриране на тези предупреждения и дори да настроим средата да ги счита за грешки и да прекъсва процеса на компилация заради тях.
Едно ограничение, което VS.NET има, е че то не може да създава многофайлови асемблита. Ако това наистина ни се наложи трябва да използваме инструмента Assembly Linker (al.exe).
VS.NET предлага два режима на компилация:
- Debug – в този режим компилаторът създава дебъг символи за всички методи в приложението и ги записва в отделен файл с разширение .pdb. Чрез него можем да извършваме проследяване на грешките (debugging). Този режим на компилация е препоръчителен за процеса на разработване и тестване на приложението.
- Release – в този режим на компилация VS.NET създава код, готов за продукция и разпространение до клиентите. От него са отстранени всички функции свързани с дебъгване и тестване. Не се генерират .pdb файлове и като цяло има по-добра производителност от Debug версията. За сметка на това възможностите за откриване грешки са намалени.
Тъй като Visual Studio .NET интегрира в себе си разработването на приложения с различни технологии, ние можем да стартираме по унифициран начин всеки един от типовете приложения. Това, което трябва да направим, е единствено да натиснем бутона Start (или Debug/Start). Средата проверява дали има промени във файловете на проекта, ако има такива, тя прекомпилира приложението и след това стартира съответния процес.
VS.NET поддържа т. нар. решения (solutions). В едно решение може да има един или повече проекта. Например в една система може да 3 проекта – уеб услуга, Windows Forms клиент и ASP.NET уеб приложение.
Имаме възможност да указваме проект, който да се стартира при стартиране на решението. Допълнително можем да укажем да се стартират множество проекти при натискане на бутона Start. Тази опция е много удобна при разработване на клиент-сървър приложения, тъй като не ни се налага ръчно да стартираме всеки компонент.
Тестването на приложенията може да се извършва веднага след стартирането. Тъй като Visual Studio .NET 2003 се "закача" в дебъг режим към процеса на стартираното приложението, можем да дебъгваме лесно и бързо своя код.
Процесът на проследяване на грешки, или както по-често го наричаме дебъгване, се осъществява много лесно със Visual Studio .NET 2003. Средата ни предоставя множество вградени в нея инструменти, с които да извършваме тази задача. Инструментите са достъпни от менюто Debug и включват следните възможности:
- Breakpoints – списък със зададените точки на прекъсване (break points). Можем да премахваме, създаваме и настройваме параметрите на всяка точка поотделно.
- Running documents – списък с всички файлове, които се използват от приложението в момента. Използва се главно при дебъгване на уеб приложения.
- Call stack – показва ни стекът на извикванията на методите до дадения момент. Перфектен е за анализ на програмна логика и намиране на мястото, където е възникнало изключение.
- Autos – показва всички променливи, които са в момента в обхват.
- Local – показва всички локални променливи.
- Immediate/Command Window – позволява ни да изпълняваме инструкции и да променяме стойности на променливи по време на изпълнение. Предоставя множество мощни възможности, които обаче излизат извън рамките на нашата тема.
- Watch – показва списък с всички променливи, които сме заявили, че искаме да наблюдаваме. Чрез него можем и да променяме техните стойности по време на изпълнение на програмата.
- Quick Watch – показва стойността на избрана при дебъгване променлива.
- Step control – дава ни средства за постъпково изпълнение на кода ред по ред и стъпка по стъпка. Можем да избираме реда на изпълнение; да изпълняваме методи като влизаме в тях или ги изчакваме да завършват и преминаваме към следващата стъпка; можем да контролираме и кои редове от кода се изпълняват и при нужда да местим курсора на изпълнение напред и назад.
- Exception control – можем да задаваме дали нашето приложение да влиза в дебъг режим при възникване на изключение и да спира веднага след мястото на възникване на изключението без да сме сложили точка на прекъсване.
Освен тези удобства имаме възможност да разглеждаме съдържанието на паметта и регистрите в "суров" вид и да извършваме декомпилация на кода (disassembling).
Освен стандартните шаблони за всеки език за програмиране Visual Studio .NET 2003 ни предлага и шаблони за инсталационни пакети. Така се затваря цикълът на разработка на приложения. Можем да използваме готовите стандартни форми и технологии на инсталация и/или да добавим свои собствени към инсталационния пакет. След като сме завършили и тази стъпка, можем да разпространяваме своето приложение до крайните потребители.
Технологиите на инсталиране, които ни предлага Visual Studio .NET 2003, са приложими за почти всякакви приложения, независимо от това дали са конзолни, Windows Forms, уеб приложения или библиотеки с типове. Процеса на създаване на инсталационни пакети е разгледан подробно в темата "Асемблита и разпространение".
При инсталиране на Visual Studio .NET с него се инсталира неговата документация и по желание документацията на Microsoft .NET Framework (т.нар. MSDN Library).
Те автоматично се интегрират в средата за разработка и позволяват да получим т. нар. контекстно-ориентирана помощ. Например, докато използваме даден клас от .NET Framework, VS.NET ни показва неговата документация в прозореца Dynamic Help. Интегрираната помощна система във VS.NET позволява при натискане на клавиша [F1] да получим информация за текущия клас, свойство или компонент, който е на фокус. Това значително улеснява разработчика.
Интегрираната среда за разработка Microsoft Visual Studio .NET е така проектирана, че лесно да може да се разширява с допълнителни модули и нови възможности. Съществуват стотици добавки (plug-ins) за VS.NET, които добавят поддръжка на нови езици и нови технологии, подпомагат процеса на разработка по различни начини, добавят интеграция с други продукти и т. н. Някои от тях са свободни, докато други са комерсиални продукти. Благодарение на добре документираните програмни интерфейси за интеграция с VS.NET програмистите могат да добавят и собствени добавки за средата.
1. Опишете накратко платформата Microsoft .NET. Кои са основните принципи, които са заложени в нея? Избройте четирите компонента, от които тя се състои.
2. Какво представляват .NET Enterprise сървърите? Избройте някои от тях. Какво представлява .NET Framework? От какви компоненти се състои? Какво е Visual Studio .NET? За какво служат .NET Building Block услугите? Какво са .NET Smart клиентите? Какво е характерно за тях?
3. Опишете накратко .NET Framework. От какви компоненти се състои?
4. Какво представлява средата за контролирано изпълнение на програмен код Common Language Runtime (CLR)?
5. Какво представлява Framework Class Library (FCL)? Каква функционалност предлага тя?
6. Какво е управляван код? Има ли причина да бъде използван вместо традиционния машиннозависим код? Какво е характерно за междинния език IL?
7. Какво представляват .NET асемблитата (assemblies)? Каква информация съдържат метаданните в асемблитата? Какво представляват .NET приложенията? Какво е област на приложението (application domain)?
8. Какво е Common Language Specification (CLS)? Защо е необходима тази спецификация? Какво описва тя?
9. Какво представлява общата система от типове в .NET Framework (Common Type System)? Защо е необходима тя?
10. Избройте няколко от .NET езиците. Какво е общото между тях? Какво е специфичното за всеки от тях?
11. Избройте основните пакети от Framework Class Library (FCL). За какво служат те?
1. Светлин Наков, Архитектура на платформата .NET и .NET Framework – http://www.nakov.com/dotnet/lectures/Lecture-1-MS.NET-Framework-Architecture-v1.03.ppt
2. Jeffrey Richter, Applied Microsoft .NET Framework Programming, Microsoft Press, 2002, ISBN 0735614229
3. MSDN, Common Language Runtime Overview – http://msdn.microsoft. com/library/en-us/cpguide/html/ cpconcommonlanguageruntimeoverview.asp
4. MSDN, Compiling to MSIL – http://msdn.microsoft.com/library/en-us/ cpguide/html/cpconMicrosoftIntermediateLanguageMSIL.asp
5. MSDN, Application Domains Overview – http://msdn.microsoft.com/ library/en-us/cpguide/html/cpconapplicationdomainsoverview.asp
|
Българска асоциация на разработчиците на софтуер (БАРС) е нестопанска организация, която подпомага професионалното развитие на българските софтуерни специалисти чрез образователни и други инициативи. БАРС работи за насърчаване обмяната на опит между разработчиците и за усъвършенстване на техните знания и умения в областта на проектирането и разработката на софтуер. Асоциацията организира специализирани конференции, семинари и курсове за обучение по разработка на софтуер и софтуерни технологии. БАРС организира създаването на Национална академия по разработка на софтуер – учебен център за професионална подготовка на софтуерни специалисти.
|
- Добро познаване на поне един език за програмиране от високо ниво (С, С++, Java, Pascal/Delphi, Perl, Python, PHP или друг)
- Базови познания за архитектурата на .NET Framework
- Принципи при дизайна на езика
- Нашата първа програма на C#
- Типове данни в C#. Примитивни типове данни. Изброен тип
- Декларации. Изрази. Оператори. Програмни конструкции
- Елементарни програмни конструкции. Съставни конструкции
- Конструкции за управление – условни конструкции, конструкции за цикъл, конструкции за преход. Специални конструкции
- Коментари в програмата
- Вход и изход от конзолата
- Дебъгерът на Visual Studio .NET
- XML документация в C# кода
В настоящата тема ще разгледаме езика С#, ще се запознаем с неговите основни концепции, ще напишем и компилираме първата си C# програма. Ще се запознаем със средата за разработка Visual Studio .NET 2003 и ще демонстрираме работата с нейния дебъгер. Ще отделим внимание на типовете данни, изразите, програмните конструкции и конструкциите за управление в езика C#. Накрая ще демонстрираме колко лесно и полезно е XML документирането на кода в С#.
Настоящата тема има за цел да запознае читателя с конкретните синтактични правила на езика C# и неговите програмни конструкции без да претендира за изчерпателност. В нея няма да обясняваме какво е променлива, функция, цикъл и т. н., а ще се фокусираме върху реализацията на тези езикови примитиви в C#. Очаква се читателят да владее основите на програмирането с поне един език от високо ниво, а тази тема ще му помогне да премине към C#.
С# е съвременен, обектно-ориентиран и типово обезопасен език за програмиране, който е наследник на C и С++. Той комбинира леснотата на използване на Java с мощността на С++.
Създаден от екипа на Андерс Хейлсбърг, архитектът на Delphi, С# заимства много от силните страни на Delphi – свойства, индексатори, компонентна ориентираност. С# въвежда и нови концепции – разделяне на типовете на два вида – стойностни (value types) и референтни (reference types), автоматично управление на паметта, делегати и събития, атрибути, XML документация и други. Той е стандартизиран от ECMA и ISO.
C# е специално проектиран за .NET Framework и е съобразен с неговите особености. Той е сравнително нов, съвременен език, който е заимствал силните страни на масово използваните езици за програмиране от високо ниво, като C, C++, Java, Delphi, PHP и др.
Преди да се запознаем със синтаксиса и програмните конструкции в C#, нека първо разгледаме основните принципи, залегнали при проектирането му.
Езикът C# е насочен към компонентно-ориентираното програмиране, при което софтуерът се изгражда чрез съединяване на различни готови компоненти и описание на логиката на взаимодействие между тях.
При проектирането на .NET Framework и езика C# компонентният подход е залегнал на най-дълбоко архитектурно ниво. .NET Framework дефинира общ компонентен модел, който установява правилата за изграждане и използване на компоненти за всички .NET приложения. Езикът C# поддържа класове, интерфейси, свойства, събития и други средства за описание на компонентите, както и средства за тяхното използване. В темата "Графичен потребителски интерфейс с Windows Forms" ще дискутираме по-задълбочено компонентния модел на .NET Framework.
С# е обектно-ориентиран език за програмиране. В него залягат основните принципи на обектно-ориентираното програмиране, като капсулация на данните, наследяване и полиморфизъм.
В .NET Framework всички типове данни наследяват системния тип System. Object и придобиват от него някои общи методи, свойства и други характеристики.
В следствие на това в C# всички данни се третират като обекти. Дори примитивните типове, чрез въвеждането на автоматичното им опаковане (boxing) и разопаковане (unboxing) се превръщат в обекти. Например, 5.ToString() е валидно извикване в C#, защото 5 се опакова и се разглежда като обект от тип System.Object, на който се извиква метода ToString().
По идея .NET Framework и C# са проектирани за да осигурят висока сигурност и надеждност на изпълнявания софтуер. .NET Framework предоставя среда за контролирано изпълнение на управляван код, с което прави невъзможно възникването на някои от най-неприятните проблеми, свързани с управлението на паметта, неправилното преобразуване на типове и др. C# наследява всички тези характеристики от .NET Framework и добавя към тях някои допълнителни механизми за предпазване на програмистите от често срещани грешки.
С# е силно типизиран и типово обезопасен. В него не се използват указатели към паметта, които създават много проблеми в по-старите езици за програмиране. Вместо тях се използват специални силно типизирани указатели, които се наричат референции (references). Използването на референции вместо указатели решава проблемите, които възникват от неправилната работа с указатели и директния достъп до паметта. В .NET Framework управлението на паметта се извършва почти изцяло от CLR.
Всъщност в C# може да се използват указатели (като тези в C и C++) чрез запазената дума unsafe, но това не е се препоръчва в масовия случай, защото лишава програмата от типова обезопасеност и позволява неправилна работа с паметта.
В С# не може да се излезе от границите на масив или символен низ. При опит да бъде направено това, се получава изключение, което може да бъде прихванато и обработено. В езици като C, C++ и Pascal излизането от границите на масив води до достъп до памет, използвана от други данни и най-често пъти предизвиква сривове или неочаквано поведение на програмата.
При създаване на клас, структура или друг тип C# компилаторът не позволява да останат неинициализирани член-данни. Това защитава програмиста от възможността да работи с неинициализирани данни.
Макар С# да не инициализира автоматично локалните променливи, компилаторът предупреждава за неправилното им използване. Например следният код ще предизвика грешка при опит за компилация:
|
int value; value = value + 5; |
Преобразуването на типове също е безопасно. CLR не позволява да се извърши невалидно преобразуване на типове – да се преобразува променлива от даден тип към променлива от тип, който не е съвместим с първия. При опит да бъде направено това, възниква изключение.
Неявното преобразуване на типове е разрешено само за съвместими типове, когато не е възможна загуба на информация. При явно преобразуване на типове, ако те не са съвместими, се хвърля InvalidCastException по време на изпълнение. Например следният код предизвиква изключение по време на изпълнение:
|
object a = "This will raise InvalidCastException!"; int b = (int) a; |
В C# чрез запазената дума checked могат да се отделят блокове код, в които аритметичните операции се проверяват за препълване на типовете и ако това се случи, се хвърля OverflowException. Това е много полезно, защото за разлика от С++, където при такива ситуации се получава грешен резултат, в С# може да се реагира адекватно на такава специфична ситуация. Ето един пример, при който CLR засича препълване на типа int:
|
checked { int i = 100000; int j = i*i; // OverflowException is thrown } |
Една от целите на езика C# е да позволи с малко усилия да се пише надежден код. С цел да се намалят грешките от припокриване на виртуални методи са въведени запазените думи new и override, чрез които да се контролира припокриването на виртуален метод, който е в базов клас при наследяване. Каква е разликата между двете? При полиморфизъм (обект от базовия клас е създаден като обект от наследника), ако е използвана new като модификатор на метода в наследника, ще се извика функцията на базовия клас, а при използване на override – функцията на наследника.
В .NET Framework заделянето и използването на паметта се управлява автоматично от CLR (Common Language Runtime). Стойностните типове, които ще разгледаме по-подробно в темата "Обща система от типове", се пазят в стека, докато референтните – в т. нар. "динамична памет" (managed heap), за която се грижи системата за почистване на паметта (garbage collector).
Системата за почистване на паметта е част от CLR и нейна задача е да освобождава периодично паметта и ресурсите, заделени за обекти, които не се използват повече от приложението. Такива обекти могат да бъдат най-разнообразни: данни в динамичната памет, масиви, символни низове, а също и файлове, буфери в паметта, връзки към бази данни и др.
Грижата за паметта е трудна и сложна задача, но благодарение на CLR тя не е задължение на .NET програмистите. На нея ще обърнем специално внимание в темата "Управление на паметта и ресурсите".
Обработката на грешки, които могат да възникнат по време на изпълнение на програмата, в .NET Framework се реализира чрез използване на изключения. Механизмът на изключенията позволява да се съобщи за възникнал проблем или неочаквана ситуация и за нея да може да се реагира адекватно.
Изключенията представляват обекти от клас Exception или производен на него клас и съдържат информация за възникналата грешка. Например, при опит за деление на нула CLR прихваща проблема и предизвиква изключението DivideByZeroException, а при опит за излизане от границите на масив възниква ArgumentOutOfRangeException. Работата с изключения също ще бъде дискутирана в детайли в темата "Управление на изключенията в .NET".
В .NET Framework са въведени т. нар. сигурност на ниво достъп до кода (code access security) и сигурност, базирана на роли (role-based security). Чрез тях се осъществява контрол на достъпа до ресурси от програмата. Например, ако трябва да се извика системна функция или да се пише във файл, кодът трябва да има права да го направи. Сигурността на ниво достъп до кода оставя CLR да взима решения, докато при сигурност, базирана на роли, програмата може да реагира различно спрямо ролята и правата на потребителя.
В С# няма разделяне на хедър файлове и файлове с имплементация, както в C и C++. Това спестява много проблеми и улеснява поддръжката на сорс кода. В C# всичкият програмен код на даден клас е в един файл.
Програмите на С# представляват съвкупност от дефиниции на класове, структури и други типове. Във всяка C# програма някой от класовете съдържа метод Main() – входна точка за програмата.
Приложенията могат да се състоят от много файлове, а в един файл може да има няколко класове, структури и други типове.
Класовете логически се разполагат в пространства от имена (namespaces). Те от своя страна могат да се систематизират в йерархия, т.е. едно пространство от имена може да съдържа както класове, така и други пространства от имена. Едно пространство от имена може да е разположено в няколко файла и дори в няколко асемблита. Например, в пространството от имена System се съдържа пространството от имена Xml. Пространството System.Xml от своя страна е разделено в две различни асемблита - System.Xml.dll и System.Data.dll.
Във Visual Studio .NET има инструмент, наречен "Object Browser", чрез който могат да се разгледат йерархиите на пространствата от имена в проекта, какво съдържат те и в кои файлове се намират.
Всяка книга за запознаване с даден програмен език започва обикновено с програмката "Hello, world!". Ние няма да правим изключение от този принцип и ще започнем по подобен начин – с програмката "Hello, C#". Ето как изглежда нейният сорс код:
|
HelloCSharp.cs |
|
using System;
class HelloCSharp { static void Main() { Console.WriteLine("Hello, C#"); } } |
На първия ред директивата using System указва, че се използва пространството от имена System (т.е. всички класове, структури и други типове, декларирани в него). Тя е като #include в C++, като import в Java и като uses в Delphi.
Следва декларацията на клас с ключова дума class. Този клас се състои от един единствен метод – методът static void Main(), който е входна точка на програмата. Когато този метод завърши, завършва и програмата.
В метода Main() се извиква метода WriteLine(…) на класа Console, намиращ се в пространството от имена System. Класът Console осигурява средства за вход и изход от конзолата. Чрез него отпечатваме на конзолата текста "Hello, C#".
Програми на C# могат да се компилират от командния ред чрез компилатора csc.exe, който е стандартна част от .NET Framework и е достъпен от директорията, в която е инсталиран той. При стандартна инсталация тя е C:\Windows\Microsoft.NET\Framework\v1.1.4322.
Можем да компилираме сорс кода на примерната програма по следния начин: Използвайки командния интерпретатор (cmd.exe) се придвижваме до директорията, където се намира файлът HelloCSharp.cs. След това можем да го компилираме със следната команда:
|
csc HelloCSharp.cs |
За да бъде намерен компилаторът csc.exe, е необходимо в текущия път (в променливата PATH от средата) да е включена директорията на .NET Framework.
Ако компилацията премине успешно, в резултат се получава файлът HelloCSharp.exe, който представлява .NET асембли, записано като изпълним файл.
Стартирането на получения изпълним (.exe) файл става както всички останали изпълними файлове, например чрез следната команда:
|
HelloCSharp.ехе |
Резултатът от изпълнението на нашата първа C# програмка представлява един текстов ред:
|
Hello, C# |
Резултатът от компилирането и изпълнението на примерната програма е показан на следващата картинка:

Ще покажем как може да се използва интегрираната среда за разработка на приложения Microsoft Visual Studio .NET за изпълнение на предходната примерна програмка. Ще създадем нов проект (конзолно приложение), ще го компилираме и изпълним. Трябва да преминем през следните стъпки:
1. Стартираме Visual Studio .NET.
2. От меню File избираме New Project. Избираме Visual C# Projects | Console Application. Избираме име и местоположение за проекта:

Visual Studio .NET създава за нас един Solution и един проект в него, съдържащ няколко файла. Файлът, в който можем да пишем нашия код, се отваря автоматично.
3. Въвеждаме примерната програмка. Можем да сменим името на файла Class1.cs с HelloCSharp.cs чрез клавиша [F2], натиснат в момент, в който е активен файлът Class1.cs от Solution Explorer:

4. За да компилираме, натискаме [Shift+Ctrl+B] или избираме менюто Build | Build Solution. Ето как изглежда VS.NET в този момент:

5. За да стартираме приложението, натискаме [Ctrl+F5] или избираме от менюто Debug | Start Without Debugging. В резултат приложението се изпълнява в нов конзолен прозорец и след приключване на работата му VS.NET ни приканва да натиснем някакъв клавиш, за да затвори прозореца:

Можем да стартираме приложението и само с [F5], но тогава то ще се изпълни в режим на дебъгване и след приключване на работата му прозорецът, в който е изведен резултата, веднага ще се затвори и няма да го видим.
Езикът C# дефинира следните запазени думи, които се използват в конструкциите и синтаксиса на езика:
|
abstract |
as |
base |
bool |
break |
byte |
|
case |
catch |
char |
checked |
class |
const |
|
continue |
decimal |
default |
delegate |
do |
double |
|
else |
enum |
event |
explicit |
extern |
false |
|
finally |
fixed |
float |
for |
foreach |
goto |
|
if |
implicit |
in |
int |
interface |
internal |
|
is |
lock |
long |
namespace |
new |
null |
|
object |
operator |
out |
override |
params |
private |
|
protected |
public |
readonly |
ref |
return |
sbyte |
|
sealed |
short |
sizeof |
stackalloc |
static |
string |
|
struct |
switch |
this |
throw |
true |
try |
|
typeof |
uint |
ulong |
unchecked |
unsafe |
ushort |
|
using |
virtual |
void |
volatile |
while |
|
Ще видим за какво служат повечето от тях постепенно, в процеса на запознаване с езика C#, с обектно-ориентираното програмиране в .NET Framework, с общата система от типове и в някои други теми.
Типовете данни в C# биват два вида – типове по стойност (value types) и типове по референция (reference types). Типовете по стойност (стойностни типове) директно съдържат своята стойност и се съхраняват в стека. Те се предават по стойност. Типовете по референция (референтни типове) представляват силно типизирани указатели към стойност в динамичната памет. Те се предават по референция (адрес) и се унищожават от garbage collector, когато не се използват повече от програмата.
Типовете биват още примитивни (вградени, built-in) типове и типове, дефинирани от потребителя.
Типовете по стойност (стойностни типове) са примитивните типове, изброените типове и структурите. Например:
|
int i; // примитивен тип int enum State { Off, On } // изброен тип (enum) struct Point { int x, y; } // структура (struct) |
Типовете по референция (референтни типове) са класовете, интерфейсите, масивите и делегатите. Например:
|
class Foo: Bar, IFoo {...} // клас interface IFoo: IBar {...} // интерфейс string[] a = new string[5]; // масив delegate void Empty(); // делегат |
На всички типове в C# съответстват типове от общата система от типове (Common Type System – CTS) на .NET Framework. Например, на примитивния C# тип int съответства типа System.Int32 от CTS.
Примитивните типове данни в C# (built-in data types) биват:
- byte, sbyte, int, uint, long, ulong – цели числа
- float, double, decimal – реални числа
- char – Unicode символи
- bool – булев тип (true или false)
- string – символен низ (неизменима последователност от Unicode символи)
- object – обект (специален тип, който се наследява от всички типове)
Типовете дефинирани от потребителя биват класове, структури, изброени типове, интерфейси и делегати:
|
class Foo: Bar, IFoo {...} // клас struct Point { int x, y; } // структура interface IFoo: IBar {...} // интерфейс delegate void Empty(); // делегат |
Вече се сблъскахме с класовете в C# в примерната програма "Hello, C#". Повече за тях, както и за структурите и интерфейсите ще научим в темата "Обектно-ориентирано програмиране в .NET".
Има два типа преобразувания на примитивните типове – преобразуване по подразбиране (implicit conversion) и изрично преобразуване (explicit conversion).
В C# преобразуването по подразбиране е позволено, когато е безопасно. Например, от int към long, от float към double, от byte към short:
|
short a = 10; int b = a; // implicit type conversion from short to int |
Изричното преобразуване се използва, когато преобразуваме към по-малък тип или типовете не са директно съвместими. Например, от long към int, от double към float, от char към short, от int към char, от sbyte към uint:
|
int a = 10; short b = (short) a; // explicit type conversion |
В С# има специална ключова дума checked, която указва при препълване да се получава System.OverflowException вместо грешен резултат. Ключовата дума unchecked действа противоположна на checked. Ето пример за преобразуване на типове с използване на тези ключови думи:
|
byte b8 = 255; short sh16 = b8; // implicit conversion int i32 = sh16; // implicit conversion float f = i32; // implicit - possible loss of precision! double d = f; // implicit conversion checked { byte byte8 = (byte) sh16; // explicit conversion // OverflowException is possible! ushort ush16 = (ushort) sh16; // explicit conversion // OverflowException is possible if sh16 is negative! } unchecked { uint ui32 = 1234567890; sbyte sb8 = (sbyte) ui32; // explicit conversion // OverflowException is not thrown in unchecked mode } |
Изброените типове в C# се състоят от множество именувани константи. Дефинират се със запазената дума enum и наследяват типа System.Enum. Ето пример за изброен тип, който съответства на дните от седмицата:
|
public enum Days { Saturday, Sunday, Monday, Tuesday, Wednesday, Thursday, Friday }; |
Изброените типове се използват за задаване на една измежду няколко възможности. Вътрешно се представят с int, но може да се зададе и друг числов тип.
Изброените типове са силно типизирани – те не се превръщат в int, освен експлицитно.
Ето пример как може да бъде използван даден изброен тип:
|
Days today = Days.Friday; if (today == Days.Friday) { Console.WriteLine("Днес е петък."); } |
Както се вижда, инстанциите на изброените типове могат да приемат една от дефинираните в тях стойности.
Изброените типове могат да се използват и като съвкупност от битови флагове чрез атрибута [Flags]. Ето пример за изброен тип, който може да приема за стойност комбинация от дефинираните в него константи:
|
[Flags] public enum FileAccess { Read = 1, Write = 2, Execute = 4, ReadWrite = Read | Write }
// ...
Console.WriteLine( FileAccess.ReadWrite | FileAccess.Execute);
// The result is: "ReadWrite, Execute" |
Какво представляват атрибутите и как се използват ще разгледаме по-детайлно в темата "Атрибути". Засега трябва да знаем, че чрез тях може да се асоциира допълнителна информация към типовете.
Използването на изброени типове осигурява по-високо ниво на абстракция и по този начин сорс кодът става по-разбираем и по-лесен за поддръжка.
В .NET Framework широко се използват изброени типове. Например, изброения тип ConnectionState, намиращ се в пространство от имена System.Data, характеризира състоянието на връзка към база от данни, създадена чрез ADO.NET (зададено е и числовото съответствие на всяко едно от състоянията):
|
public enum ConnectionState { Closed = 0, Open = 1, Connecting = 2, Executing = 4, Fetching = 8, Broken = 16 } |
Идентификаторите в С# се състоят от последователности от букви, цифри и знак за подчертаване като винаги започват с буква или знак за подчертаване. В тях малките и главните букви се различават. Идентификаторите могат да съдържат Unicode символи, например:
|
int алабала_портокала = 42; bool \u1027\u11af = true; |
Microsoft препоръчва се следната конвенция за именуване:
- PascalCase – за имена на класове, пространства от имена, структури, типове, методи, свойства, константи
- camelCase – за имена на променливи и параметри
Въпреки, че е възможно, не се препоръчва да се използват идентификатори на кирилица или друга азбука, различна от латинската.
Декларациите на променливи в C# могат да са няколко вида (почти като в C++, Java и Delphi) – локални променливи (за даден блок), член-променливи на типа и константи. Ето пример:
|
int count; string message; |
Член-променливите могат да имат модификатори, например:
|
public static int mCounter; |
Константите в С# биват два вида – константи, които приемат стойността си по време на компилация (compile-time константи) и такива, които получават стойност по време на изпълнение на програмата (runtime константи).
Compile-time константите се декларират със запазената дума const. Те задължително се инициализират в момента на декларирането им и не могат да се променят след това. Те реално не съществуват като променливи в програмата. По време на компилация се заместват със стойността им. Например:
|
public const double PI = 3.1415926535897932; const string COMPANY_NAME = "Менте Софт"; |
Runtime константите се декларират като полета с модификатора readonly. Представляват полета на типа, които са само за четене. Инициализират се по време на изпълнение (в момента на деклариране или в конструктора на типа) и не могат да се променят след като веднъж са инициализирани. Например:
|
public readonly DateTime NOW = DateTime.Now; |
Операторите в С# са много близки до операторите в C++ и Java и имат същите действие и приоритет. Те биват:
- Аритметични: +, -, *, /, %, ++, --
- Логически: &&, ||, !, ^, true, false
- Побитови операции: &, |, ^, ~, <<, >>
- За слепване на символни низове: +
- За сравнение: ==, !=, <, >, <=, >=
- За присвояване: =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
- За работа с типове: as, is, sizeof, typeof
- Други: ., [], (), ?:, new, checked, unchecked, unsafe
В C# операторите могат да се предефинират. В темата "Обектно-ориентирано програмиране в .NET" ще видим как точно става това.
Програмен код, който се изчислява до някаква стойност, се нарича израз (expression). Изразите в C# имат синтаксиса на C++ и Java. Например:
|
a = b = c = 20; // израз със стойност 20 (а+5)*(32-a)%b // израз с числова стойност "ала" + "бала" // символен израз (string) Math.Cos(Math.PI/x) // израз с реална стойност typeof(obj) // израз от тип System.Type (int) arr[idx1][idx2] // израз от тип int new Student() // израз от тип Student (currentValue <= MAX_VALUE) // булев израз |
Програмните конструкции (statements) имат синтаксиса на C++ и Java. Те биват няколко вида:
Елементарните програмни конструкции са най-простите елементи на програмата. Например:
|
// присвояване (<променлива> = <израз>) sum = (a+b)/2;
// извикване на метод PrintReport(report);
// създаване на обект student = new Student("Светлин Наков", 3, 42688); |
Съставните програмни конструкции се състоят от няколко други конструкции, оградени в блок. Например:
|
{ Report report = GenerateReport(period); report.Print(); } |
Конструкциите за управление, както в повечето езици за програмиране, биват условни конструкции, конструкции за цикъл, за преход и т. н. В C# синтаксисът на тези конструкции е много близък до синтаксиса на C++ и Java.
Условните конструкции в С# са if, if-else и switch. Техният синтаксис е еднакъв със синтаксиса им в C, C++ и Java.
if и if-else конструкциите за разлика от С и С++ могат да приемат единствено булево условие. Не са позволени целочислени стойности, които да играят ролята на true, ако са различни от 0 и false – иначе. Ето няколко примера за условна конструкция:
|
if (orderItem.Ammount > ammountInStock) { MessageBox.Show("Not in stock!", "error"); }
if (Valid(order)) { ProcessOrder(order); } else { MessageBox.Show("Invalid order!", "error"); } |
Ново в switch конструкцията за разлика от C и C++ е, че позволява изразът, по който се осъществява условието, да бъде от тип string или enum. Например:
|
switch (characterCase) { case CharacterCasing.Lower: text = text.ToLower(); break; case CharacterCasing.Upper: text = text.ToUpper(); break; default: MessageBox.Show("Invalid case!", "error"); break; } |
Конструкцията switch се различава от реализацията си в С++. В С# не се разрешава "пропадане" (fall-through). Пропадането в switch конструкциите може да доведе до грешки. Независимо от удобствата, които предлага тази възможност, дизайнерите на езика С# са преценили, че рискът за грешка поради пропускане на break е по-голям, затова всеки case етикет трябва задължително да завършва с break.
Конструкциите за повторение (iteration statements) са for-цикъл, while-цикъл, цикъл do-while и цикъл foreach – за обработка на колекции. Техният синтаксис е еднакъв със синтаксиса им в C, C++ и Java. Изключение прави foreach цикълът, който няма еквивалент в C и C++. Ето няколко примера:
Пример за for-цикъл:
|
// Отпечатваме числата от 1 до 100 и техните квадрати for (int i=1; i<=100; i++) { int i2 = i*i; Console.WriteLine(i + " * " + i + " = " + i2); } |
Пример за while-цикъл:
|
// Изчисляваме result = a^b result = 1; while (b > 0) { result = result * a; b--; } |
Пример за цикъл do-while:
|
// Четем символи до достигане на край на ред do { ch = ReadNextCharacter(stream); } while (ch != '\n'); |
Операторът foreach е приложим за масиви, колекции и други типове, които поддържат интерфейса IEnumerable или имaт метод за извличане на итератор (enumerator).
Пример за цикъл foreach:
|
string[] names = GetNames();
// Отпечатваме всички елементи на масива names foreach (string name in names) { Console.WriteLine(name); } |
Конструкциите за преход в C# са: break, continue – които се използват в цикли, goto – за безусловен преход и return – за връщане от метод. Те работят по същия начин, като в C, C++ и Java.
Пример за използване на конструкцията break:
|
// Търсим позицията на даден елемент target в масива a[] int position = -1; for (int i=0; i<a.length; i++) { if (a[i] == target) { position = i; break; } } return position; |
Конструкциите за управление на изключенията в С# са: throw – за предизвикване на изключение, try-catch – за прихващане на изключение, try-finally – за сигурно изпълнение на завършваща секция и try-catch-finally – за прихващане на изключение със завършваща секция.
Пример за предизвикване и прихващане на изключение:
|
// ... public static void ThrowException() { // ... throw new System.Exception(); } // ... public static void Main() { try { ThrowException(); } catch(System.Exception e) { // ... } finally { // ... } } |
Методът ThrowException() предизвиква изключение от тип Exception, което може да бъде прихванато, ако функцията, която го предизвиква, се намира в try-catch блок. В такъв случай може да се извършат действия в catch блока, в зависимост от информацията, която носи това изключение.
Конструкциите try-finally и try-catch-finally се използват най-вече за освобождаване на ресурси, които се използват в тялото им. Независимо дали възникне изключение при работата с даден ресурс, той трябва да бъде освободен накрая – това обикновено се прави във finally блок, който се изпълнява независимо дали се минава през catch блока. Блокът finally се изпълнява дори и да има return в catch или try блок.
В темата "Управление на изключенията в .NET" ще разгледаме подробно работата с изключения, техните особености и препоръките за правилна работа с тях.
Специалните конструкции в С# са: lock – за синхронизирано изпълнение, checked, unchecked – за контрол на аритметичните препълвания, unsafe – за директен достъп до паметта чрез указатели, fixed – за фиксиране на местоположението в паметта при работа с неуправляван код.
Коментарите биват два вида - коментар за част от програмен ред и блоков коментар.
Ето пример за коментар на един ред:
|
Order orders[]; // Съдържа всички поръчки на потребителя |
Ето пример и за блоков коментар:
|
/* Изтриваме всички поръчки, за които някой артикул не е наличен в необходимото количество. Изтриване реално не се извършва, а само се променя статуса на Canceled */ foreach (Order order in customer.Оrders) { if (!AllItemsInStock(order)) { order.Status = OrderStatus.Canceled; } } |
В .NET Framework за вход и изход от конзолата се използват стандартни класове от BCL (Base Class Library). Входът и изходът от конзолата се осъществяват чрез класа Console, намиращ се в пространство от имена System.
Класът System.Console предоставя основната функционалност, от която се нуждаят конзолните приложения (console applications), които четат и пишат на екрана. Ако конзолата не съществува (например в Windows Forms и уеб-базираните приложения), писането в конзолата няма никакъв ефект и не се предизвикват изключения.
Всяко конзолно приложение при стартиране получава от операционната система три стандартни потока – за вход, изход и за грешки. При изпълнение от конзолата тези три потока автоматично се асоциират със самата нея. За достъп до стандартния вход, стандартния изход и стандартния изход за грешки в .NET Framework се използват свойствата In, Out и Error на класа Console.
Входът от конзолата се осъществява чрез два метода на класа Console - Read() и ReadLine(). Методът Read() чете единичен символ от стандартния вход и го връща като int стойност или връща -1, ако няма повече символи. Методът ReadLine() чете цял символен ред и връща string или стойност null ако е достигнат края на входа.
И двата метода са синхронни (блокиращи) операции т. е. при извикване блокират, докато не бъде прочетен някакъв символ (ред).
Методът Read() има една особеност – той не връща управлението след всеки въведен символ, а връща прочетените символи наведнъж един след друг едва след като се натисне [Enter]. По тази причина този метод не е удобен за интерактивен вход от клавиатурата при конзолни приложения.
Ето един пример за използването на метода Read():
|
while (true) { int i = Console.Read(); if (i == -1) { break; } char c = (char) i; Console.WriteLine ("Echo: {0}", c); } |
В практиката за въвеждане на стойности от конзолата по-често се използва методът ReadLine(). Ето пример за неговата употреба:
|
string s = Console.ReadLine(); Console.WriteLine("You enetered: {0}", s); |
Изходът към конзолата се осъществява чрез два метода на класа Console - Write(…) и WriteLine(…), които печатат на конзолата подадените като параметри данни, с разликата, че WriteLine(…) преминава на нов ред след като отпечата текста. Методите приемат string, int, float, double и други типове данни.
Write(…) и WriteLine(…) приемат и параметрични форматиращи низове, които позволяват печатане на текст чрез шаблони, които се попълват от подадените параметри. На форматиращите низове ще обърнем специално внимание в темата "Символни низове".
Ето един пример за вход и изход от конзолата, който илюстрира и използването на форматиращи низове:
|
int a = Int32.Parse(Console.ReadLine()); int b = Int32.Parse(Console.ReadLine()); Console.WriteLine("{0} + {1} = {2}", a, b, a+b); // (въвеждаме съответно 2 и 3 като вход от конзолата) // Резултат: 2 + 3 = 5
Console.WriteLine( "Днес е {0:dd.MM.yyyy} г.", DateTime.Now); // Резултат: Днес е 13.05.2004 г.
Console.WriteLine("Цена: {0,12:C}", 27); // Резултат: Цена: 27,00 лв // (точният формат зависи от текущите езикови настройки)
string name = Console.ReadLine(); Console.WriteLine("Хей, {0}, часът е {1:HH:mm}!", name, DateTime.Now); // (въвеждаме "Наков") // Резултат: Хей, Наков, часът е 16:43! |
Сега ще илюстрираме как се използва дебъгерът на Visual Studio .NET. Ще покажем поставяне на точка за спиране (breakpoint), изпълнение на програмата в дебъг режим, проследяване на изпълнението на програмата и следене на стойностите на променливите по време на изпълнение.
Ще си поставим за задача да напишем програма, която намира всички трицифрени числа, сумата от цифрите на които е стойността 25. Можем да решим задачата по следния начин:
|
Digits.cs |
|
using System;
public class Digits { static void Main() { for (int d1=1; d1<=9; d1++) { for (int d2=0; d2<=9; d2++) { int d3 = 25 - d1 - d2; if ((d3 >= 0) && (d3 <= 9)) { int n = d1*100 + d2*10 + d3; Console.WriteLine(n); } } } } } |
За да проследим изпълнението на примерната програма, ще изпълним следните стъпки:
1. Стартираме Visual Studio .NET.
2. Създаваме ново конзолно приложение с име Digits.sln. Въвеждаме в него сорс кода от примера Digist.cs.
3. За да го компилираме натискаме [Shift]+[Ctrl]+[B].
4. Слагаме точка на прекъсване (breakpoint) с мишката върху първия ред от най-вътрешния програмен блок (щракваме малко вляво от самия ред и редът се маркира по специфичен начин):

5. Стартираме програмата от съответния бутон за стартиране от лентата с инструменти на Visual Studio .NET. Програмата ще започне да се изпълнява и когато се достигне реда с точката на прекъсване, Visual Studio .NET ще спре изпълнението и ще влезе в дебъг режим.
6. От менюто Debug можем да разгледаме по-интересните функции на дебъгера на Visual Studio .NET. Можем да добавим точки на прекъсване (breakpoints), да следим стойностите на променливите, да проследяване на изпълнението на кода по различен начин (Step Into, Step over, Step out), да добавим променливи за следене (Add Watch), да следим стека за изпълнение на програмата и т.н. Можем да разгледаме представянето на променливите в паметта – като изберем Debug | Windows | Memory. Много полезен е и прозорецът Command Window, в който не само може да се види стойност на променлива в дебъг режим, но и да се изпълни някакъв метод и да се види върнатата от него стойност. С [Shift] + [F9] при маркирана променлива може да се извика прозорец за нейното наблюдение (Quick Watch).
Точките на прекъсване могат да бъдат асоциирани с някакво условие (conditional breakpoints) и да спират изпълнението на програмата само ако това условие е истина.
Ето как изглежда работното пространство на Visual Studio .NET по време на проследяване на изпълнението на програмата след спиране в точката на прекъсване и преминаване към следващия оператор с [F10]:

Всяка програма на С# се компилира до междинен език IL (Intermediate Language). Microsoft предоставя стандартен инструмент за разглеждане на този, генериран от компилаторите на С#, код. Това е инструментът Microsoft .NET Framework Disassembler (ILDASM). С тази деасемблираща програма можем да отворим всяко .NET асембли и да разгледаме неговите пространства от имена, класове, типове и код.
Инструментът ildasm.exe е стандартна част от Microsoft .NET Framework SDK. Обикновено .NET Framework SDK идва заедно с Visual Studio .NET и се намира в директория C:\Program Files\Microsoft Visual Studio .NET 2003\SDK\v1.1\Bin. Нека илюстрираме как се използва той. За целта трябва да изпълним следните стъпки:
1. Стартираме командния интерпретатор cmd.exe:
|
Start | Programs | Accessories | Command Prompt |
2. Отиваме в директорията, където се намира компилираната програма, например, програмата от предходния пример Digits.exe:
|
cd "C:\DotNet-course-lectures\Lecture-2-Introduction-to-CSharp\Demo-3-Digits\bin\Debug" |
3. Извикваме от командната линия инструмента ILDASM (ildasm.exe) и му подаваме като параметър компилираната програма Digits.exe:
|
ildasm Digits.exe |
4. Навигирайки по дървото, което ILDASM показва за асемблито Digits.exe, можем да видим как изглежда MSIL кодът за конструктора на класа Digits и за метода му Main():

XML документацията в C# програмите представлява съвкупност от коментари, започващи с ///. Тя може да съдържа различни XML тагове – например, таг описващ връщана стойност на метод, таг за препратки към други методи и др. Като идея XML документацията прилича на JavaDoc в Java.
XML документацията значително улеснява поддръжката – документацията е част от кода, а не стои във външен файл. Поддържа се лесно и ползата от нея е видима още при разработката на приложението – Visual Studio .NET показва краткото описание на даден метод, ако той е документиран чрез вградената XML документация, при задействане на IntelliSense. C# компилаторът може да извлича XML документацията като XML файл за по-нататъшна обработка.
Ето пример за използване на XML документация:
|
/// <summary> /// The main entry point for the application. /// </summary> /// <param name="args">The command line arguments</param> static void Main(string[] args) { // ... }
/// <summary>Calculates the square of a number</summary> /// <param name="num">The number to calculate</param> /// <returns>The calculated square</returns> /// <exception cref="OverflowException">Thrown when the /// result is too big to be stored in an int</exception> /// <seealso cref="System.Int32" /> public static int square(int num) { // ... } |
Ето по-важните тагове, използвани в XML документацията в C#:
- <summary>…</summary> – кратко описание за какво се отнася даден тип, метод, свойство и т.н. Visual Studio .NET показва това описание при задействане на IntelliSense.
- <remarks>…</remarks> – подробно описание на даден тип, метод, свойство и т.н. Visual Studio .NET показва това описание в областта Object Browser.
- <param name="…">…</param> – описание на един от параметрите на даден метод.
- <returns>…</returns> – описание на връщаната от даден метод стойност.
- <exception cref="…">…</exception> – описание на изключение, което може да възникне в даден метод.
- <seealso cref="…"/> – препратка към информация, свързана с текущото описание.
- <value>…</value> – описание на свойство (property).
Сега ще покажем как чрез C# компилатора може да се извлече документацията от C# файл в отделен XML файл. Нека имаме следната програма на C#, която използва XML документация:
|
MainClass.cs |
|
using System;
namespace XMLCommentsDemo { /// <summary> /// MainClass is a sample illustrating how to use XML /// documentation in C#. /// </summary> class MainClass { /// <summary>Calculates the square of a number</summary> /// <param name="num">The number to calculate</param> /// <returns>The calculated square</returns> /// <exception cref="OverflowException">Thrown when the /// result is too big to be stored in an int</exception> /// <seealso cref="System.Int32" /> public static int Square(int num) { checked { return num*num; } }
/// <summary> /// The main entry point for the application. /// </summary> /// <param name="args">The command line arguments</param> static void Main(string[] args) { Console.WriteLine("3*3 = " + Square(3)); } } } |
За да извлечем документацията от тази програма, трябва да изпълним следните стъпки:
1. Стартираме командния интерпретатор cmd.exe:
|
Start | Programs | Accessories | Command Prompt |
2. Отиваме в директорията, където се намира сорс кода на програмата. Нека тя е Demo-6-XML-Comments:
|
cd "C:\DotNet-course-lectures\Lecture-2-Introduction-to-CSharp\Demo-6-XML-Comments" |
3. Извикваме компилатора на C#, за да компилира файла MainClass.cs, като му задаваме опцията за извличане на XML документацията в отделен файл:
|
csc MainClass.cs /doc:MainClassComments.xml |
4. Отваряме получения .xml файл с Internet Explorer, за да разгледаме съдържанието му.
Ето как полученият XML файл:
|
MainClassComments.xml |
|
<?xml version="1.0"?> <doc> <assembly> <name>MainClass</name> </assembly> <members> <member name="T:XMLCommentsDemo.MainClass"> <summary> MainClass is a sample illustrating how to use XML documentation in C#. </summary> </member> <member name="M:XMLCommentsDemo.MainClass.Square( System.Int32)"> <summary>Calculates the square of a number</summary> <param name="num">The number to calculate</param> <returns>The calculated square</returns> <exception cref="T:System.OverflowException">Thrown when the result is too big to be stored in an int</exception> <seealso cref="T:System.Int32"/> </member> <member name="M:XMLCommentsDemo.MainClass.Main( System.String[])"> <summary> The main entry point for the application. </summary> <param name="args">The command line arguments</param> </member> </members> </doc> |
Сега ще покажем как чрез Visual Studio .NET може да се генерира HTML документация за даден проект на C# по XML коментарите в неговия сорс код. Във вид на HTML документацията е много по-удобна за четене и разглеждане.
За целта трябва да изпълним следните стъпки:
1. Отваряме с Visual Studio .NET проект, в който сме използвали XML документиране, например проекта Demo-6-XML-Comments.sln, който съдържа кода от предходния пример.
2. От меню Tools избираме Build Comment Web Pages…. Указваме директория, където да се генерира HTML документацията, и натискаме бутона [OK]. Visual Studio .NET ще генерира в посочената директория съвкупност от HTML файлове, които документират нашия проект и съдържат XML коментарите от сорс кода му, подредени в подходящ за разглеждане вид.
3. Разглеждаме HTML документацията, която Visual Studio .NET е генерирал. Можем да навигираме по пространствата от имена, типовете от проекта и отделните му методи:

Нека сега разгледаме някои по-важни директиви на т. нар. предпроцесор. Преди компилация C# програмите преминават през процес на обработка, който идентифицира кода, който трябва да бъде компилиран при условна компилация. Този процес се изпълнява от предпроцесора. Програмно върху предпроцесора можем да указваме влияние чрез т.нар. директиви – запазени думи, започващи със символа #.
В С# са въведени директиви за форматиране на сорс кода - #region и #endregion, които ограждат блок от кода, който се "свива" от редактора на Visual Studio .NET:
|
#region Windows Form Designer generated code /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { // ... } #endregion |
Visual Studio .NET редакторът много често слага региони, за да отдели автоматично-генерирания код от сорс кода, писан от програмиста. Директивите #region и #endregion се игнорират от C# компилатора и се използват единствено от средите за разработка.
Директивите #define и #ifdef служат за условна компилация. Чрез тях може да се укаже на компилатора да компилира кода по различен начин според процесора, платформата и въобще средата, в която се извършва компилацията. Чрез #if, #else, #elif, #endif се задават границите на блоковете за условна компилация и съответните условия (знаци) за компилиране. Директивите #define и #undef дефинират знаци за условна компилация, според които се определя кой от блоковете за условна компилация да се разглежда.
Следният пример показва как могат да се използват директивите на предпроцесора за условна компилация:
|
#define DEBUG #define VC_V7
using System; public class MyClass { public static void Main() { #if (DEBUG && !VC_V7) Console.WriteLine("Only DEBUG is defined"); #elif (!DEBUG && VC_V7) Console.WriteLine("Only VC_V7 is defined"); #elif (DEBUG && VC_V7) Console.WriteLine("DEBUG and VC_V7 are defined"); #else Console.WriteLine("DEBUG and VC_V7 are not defined"); #endif } } |
Ето и резултата от изпълнението на примера:

Директивите #warning и #error предизвикват предупреждения и грешки по време на компилация. Например следната програма на C# се компилира успешно, но с предупреждение:
|
#define DEBUG public class MyClass { public static void Main() { #if DEBUG #warning DEBUG symbol is defined #endif } } |
Програмирането с .NET Framework е немислимо без неговата документация. Затова нека сега разгледаме какво представлява тя и как можем да я използваме при търсене на помощна информация по време на разработката на .NET приложения.
Документацията на .NET Framework се съдържа в "Microsoft MSDN Library".
MSDN Library е система, която предоставя пълен набор от технически документи, описващи продуктите, инструментите и технологиите за разработка на Microsoft (в частност .NET Framework и C#), както и средства за навигация и търсене в тях. MSDN Library съдържа технически ръководства, справочна информация, статии, примери и други ресурси за софтуерни разработчици.
MSDN Library е достъпен безплатно в on-line вариант от Интернет сайта за разработчици на Microsoft – http://msdn.microsoft.com/library/. Продуктът се разпространява и за локална инсталация заедно с партньорските програми на Microsoft.
За пример ще покажем как можем да намерим подробна информация за форматиращите низове в .NET Framework и тяхното използване. За целта стартираме MSDN Library и търсим "composite formatting":

Документацията на .NET Framework е част от MSDN Library и се разпространява заедно с VS.NET и .NET Framework SDK.
Когато бъде инсталирана, документацията за .NET Framework, тя се интегрира във VS.NET и може да се използва директно от него. Например, ако се нуждаем от помощна информация за метода WriteLine(…) на класа Console, натискаме [F1] във Visual Studio .NET докато курсорът е върху този метод. Отваря се нов прозорец, в който са описани параметрите, типа на връщаната стойност, типовете изключения, които може да предизвика описвания метод, в кое пространство от имена се намира и др.
Ето как изглежда описанието на метода WriteLine(…) на класа Console:

1. Съставете програма на C#, която въвежда от конзолата име на студент и го поздравява в стил "Здравей, <име>!".
2. Съставете програма на C#, която въвежда коефициентите на квадратно уравнение и пресмята реалните му корени.
3. Напишете програма, която намира всички символни низове, които се състоят от точно 5 малки латински букви и са симетрични спрямо средата си.
4. Проследете работата на програмата от задача 3 с дебъгера на Visual Studio .NET.
5. Променете програмата от задача 3, така че да намира само тези низове, които съдържат четен брой гласни букви. Колко са тези низове?
6. Добавете XML документация в програмата от задача 5 и генерирайте HTML документация от Visual Studio .NET.
7. Напишете програма, която намира сумарната стойност на група фактури. Програмата трябва да въвежда последователно от конзолата сумите на фактурите (реални числа със знак) докато стигне до празен ред. Сумарната стойност на фактурите трябва да се отпечата в 10-символно поле, дясно подравнена, с точност 2 знака след десетичната запетая (потърсете в документацията подходящ форматиращ стринг).
8. Напишете програма, която прочита прост числен израз, състоящ се от реални числа, свързани с операциите "+" и "-", и изчислява и отпечатва стойността му.
1. Светлин Наков, Въведение в C# – http://www.nakov.com/dotnet/ lectures/Lecture-2-Introduction-to-CSharp-v1.0.ppt
2. MSDN Training, Programming C# (MOC 2124C), Module 2: Overview of C#
3. MSDN Training, Programming C# (MOC 2124C), Module 3: Using Value-Type Variables
4. Jessy Liberty, Programming C#, Second Edition, O’Reilly, 2002, ISBN 0-596-00309-9
5. Svetlin Nakov, .NET Framework Overview – http://www.nakov.com/ publications/Nakov-DotNET-Framework-Overview-english.ppt
6. MSDN, C# Keywords – http://msdn.microsoft.com/library/en-us/csref/ html/vclrfcsharpkeywords_pg.asp
7. MSDN, C# Built-in Types Table – http://msdn.microsoft.com/library/en-us/csref/html/vclrfbuiltintypes.asp
8. MSDN, Common Type System Overview – http://msdn.microsoft.com/ library/en-us/cpguide/html/cpconcommontypesystemoverview.asp
9. MSDN, Enumerations – http://msdn.microsoft.com/library/en-us/cpguide/ html/cpconEnumerations.asp
10. MSDN, C# Operators – http://msdn.microsoft.com/library/en-us/csref/ html/vclrfCSharpOperators.asp
11. MSDN, Statements (C# Programmer's Reference) – http://msdn.microsoft.com/library/en-us/csref/html/vclrfstatements.asp
12. MSDN, XML Documentation Tutorial (C# Programmer's Reference) – http://msdn.microsoft.com/library/en-us/csref/html/vcwlkxmldocumentationtutorial.asp
13. MSDN, C# Preprocessor Directives - http://msdn.microsoft.com/ library/en-us/csref/html/vclrfPreprocessorDirectives.asp
14. MSDN, Composite Formatting – http://msdn.microsoft.com/library/en-us/cpguide/html/cpconcompositeformatting.asp
15. MSDN, Console Class (.NET Framework) – http://msdn.microsoft.com/ library/en-us/cpref/html/frlrfsystemconsoleclasstopic.asp
- Познаване на принципите на обектно-ориентираното програмиране
- Познаване на поне един обектно-ориентиран език за програмиране – C++, Java, C#, Object Pascal/Delphi
- Предимства и особености на ООП.
- Основни принципи на ООП. Основни понятия
- ООП и .NET Framework
- Членове на клас
- Член-променливи (полета). Константни полета
- Методи (член-функции)
- Статични членове
- Конструктори. Статичен конструктор
- Предаване на параметрите
- Свойства. Индексатори
- Предефиниране на оператори
- Наследяване
- Интерфейси. Абстрактни класове
- Виртуални членове. Предефиниране и скриване.
- Клас диаграми
- Принципи при обектно-ориентирания дизайн
- Пространства от имена (namespaces)
В настоящата тема ще направим кратък обзор на основните принципи на обектно-ориентираното програмиране (ООП) и средствата за използването им в .NET Framework и езика C#. Ще се запознаем с типовете "клас", "структура" и "интерфейс" в C#. Ще въведем понятието "член на тип" и ще се разгледаме видовете членове (член-променливи, методи, конструктори, свойства, индексатори и др.) и тяхната употреба. Ще се спрем и на наследяването на типове в различните му аспекти и приложения. Ще обърнем внимание и на полиморфизмът в C# и свързаните с него понятия и програмни техники. Накрая ще обсъдим някои утвърдени практики при създаването на ефективни йерархии от типове.
Обектно-ориентираното програмиране се е наложило като стандарт при почти всички съвременни езици за програмиране. То предоставя мощно средство за моделиране на обектите от реалния свят и взаимоотношенията между тях, позволява добро структуриране на програмния код и улеснява неговото преизползване. Благодарение на капсулацията на данните, чрез която се скриват имплементационните детайли и се намалява сложността на софтуера, както и на възможностите за наследяване на свойства и действия и за работа с абстрактни данни и изпълнение на абстрактни операции, ООП е се е утвърдило като предпочитан подход при създаване на големи приложения и библиотеки.
В основата на ООП стоят обектите, моделиращи обекти от реалния свят и взаимодействията между тях. Това позволява изграждането на софтуерни системи, които въпреки сложността си са разбираеми и в следствие на това - лесни за разширяване и поддръжка. Даден обект, представящ същност (entity) от реалния свят, би могъл (почти) без изменения да играе ролята на същия физически обект в друга софтуерна система.
Обектите притежават атрибути, които описват свойствата на им, и операции – възможните действия, които могат да се извършват с обекта.
Едно от основните предимства на обектно-ориентирания подход е, че позволява лесно преизползване на програмния код (code reuse). Това се постига с помощта на наследяване и полиморфизъм, които ни позволяват да дефинираме общите свойства и действия за множество от типове обекти само в един от тях.
Трите основни принципа на ООП са капсулация на данните, наследяване и полиморфизъм. Те са основните характеристики, които определят един език за програмиране като обектно-ориентиран.
Основна концепция в ООП е обектът да се разглежда като "черна кутия" – използващите обекта "виждат" само атрибутите и операциите, които са присъщи на обекта от реалния свят, без да се интересуват от конкретната им реализация – клиентът на обекта трябва да знае само какво може да прави обектът, а не как го прави. В такъв смисъл "капсулация" означава скриване на ненужните детайли за обектите и откриване към външния свят само на важните техни характеристики и свойства.
Обектите в ООП съдържат своите данни и средствата за тяхната обработката, капсулирани като едно цяло.
Ако един обект съдържа всички свойства и действия на друг, първият може да го наследи. По този начин наследеният обект освен собствените си атрибути и операции приема и тези на "родителя" си (базовия клас), като така се избягва повторното им дефиниране и се позволява създаването на йерархии от класове, моделиращи по естествен начин зависимостите от реалността.
За да изясним това понятие, ще си послужим с класическият в OOП пример за класа от обекти Animal, който представлява абстракция за множеството от всички животни. Всички обекти от този клас имат общи характеристики (например цвят и възраст) и обща функционалност, например операциите Eat и Sleep, докато за класът Dog, представляващ множеството от всички кучета, които също са животни, би могъл да предоставя операциите Eat, Sleep и Bark. Удачно е класът Dog да наследи Animal – тогава той ще съдържа описание само на собственото си действие Bark, докато тези на Eat и Sleep ще получи от базовия си клас.
Чрез наследяването се постига специализация, или конкретизация на класовете, тъй като базовият клас представлява категория от обекти по-обща от тази на наследяващите го. Ако си послужим с горния пример, множествата на кучетата и котките са подмножества на множеството от всички животни.
Може да се каже, че наследяването моделира "is-a" отношението между обектите, например можем да твърдим, че кучето е животно, тъй като то "може да прави" всичко, което и животното и притежава всички животински характеристики (цвят, възраст и т.н.).
Полиморфизъм буквално означава приемането на различни форми от един обект. Нека е даден базов клас, представящ категория от обекти, които реализират общо действие, което се наследява от множество класове, описващи по-тесни категории. Въпреки, че те всички споделят това действие, те могат да го реализират по различен начин. Когато разполагаме с обекти от базовия клас, знаем че всички те реализират това действие, независимо на кой наследен клас принадлежат. Поради това можем да го използваме без да се интересуваме от конкретната му реализация. Например, ако класът Animal предоставя действието Talk и разгледаме наследяващите го класове Dog и Cat, всеки от тях го реализира по конкретен начин. И ако имаме животно, което издава звук и то е куче – ще лае, а ако е котка – ще мяучи.
Полиморфизмът позволява унифицираното извършване на действие над различни обекти, които го реализират. В този случай издаването на звук от животно е полиморфно действие – такова, което се реализира по различен начин в различните наследници на базовия клас.
Без да претендираме за изчерпателност ще даден кратка дефиниция за основните понятия от ООП, които ще използваме по-нататък. Ако откривате, че повечето от тези термини са ви напълно непознати, ви препоръчваме първо да се запознаете с принципите на обектно-ориентираното програмиране от някоя специализирана книга по ООП, а след това да продължите нататък. В настоящата тема ще направим преглед на реализацията на ООП в .NET Framework, а не на ООП като идеология.
Класовете са категории от обекти, споделящи общи свойства и операции, които могат да се извършват върху тях. Например класът "студент" представя множеството от всички студенти. Класът не съществува реално като физическа същност, а по-скоро можем да го разгледаме като описание на неговите обекти.
Обект наричаме конкретен елемент от даден клас (инстанция), например студентът Тодор Георгиев, трети курс, ядрена физика в СУ.
Процесът на създаване на обект от даден клас е инстанциране. Обектите, създадени при инстанциране на даден клас, се наричат негови инстанции. Например в резултат от инстанцирането на класа "студент" можем да получим обекта "Иван Петров", който е инстанция на класа "студент".
Свойство се нарича видима за външния свят характеристика (атрибут) на обектите от даден клас. Например свойства на класа "студент" са личните имената му, личните му данни, оценките му и др.
Метод е действие, което всички обекти от даден клас могат да извършват. Например всички обекти от класа "студент" могат да извършват действието "явяване на изпит".
Интерфейсът е описание на съвкупност от действия, които даден обект може да извършва. Ако един обект може да извършва всички действия от даден интерфейс, казваме че обектът реализира, или имплементира интерфейса. Класът "студент", например, би могъл да реализира интерфейса "учащ" съдържащ действието "учене".
Наследяване в ООП наричаме възможността един клас, наричан наследник, да придобие свойства и действия на друг клас – родител (базов клас). Например класът "прекъснал студент" би могъл да наследи класа "студент", като към наследените методи и свойства добави собствени, например "получаване на призовка от военните власти".
Абстракция на данните наричаме възможността да работим с данни без да се интересуваме от тяхното вътрешно представяне, а само от операциите, които можем да извършваме над тях. Удачно е този подход да се осъществи чрез използването на интерфейси.
Структури от данни, които дефинират група от операции, но не разкриват информация как са имплементирани тези операции, се наричат абстрактни структури от данни.
Абстракцията на действията е възможността да изпълняваме действия, без да се интересуваме от конкретната им реализация. Обикновено се постига чрез полиморфизъм. Например ако извикваме даден метод от даден клас през неговия базов клас или интерфейс, ние реално извикваме абстрактно действие от базовия клас, което е реализирано в класа-наследник.
В .NET Framework обектно-ориентираният подход е залегнал на най-дълбоко архитектурно ниво. Всеки тип, дефиниран от потребителя, и всички типове от Common Type System (CTS) наследяват System.Object или негов наследник.
В някои обектно-ориентирани езици се използват примитивни типове данни (булеви, числови, символни), които в езиците от .NET Framework са също наследници на System.Object.
Всички .NET езици са обектно-ориентирани и приложенията се пишат изцяло обектно-ориентирано – няма глобални функции и всички действия се извършват или чрез създаване на обекти и с използване на методите и свойствата им, или чрез използване на статични членове (тях ще разгледаме малко по-нататък).
В предходната глава въведохме понятието тип и разделихме типовете в C# на типове стойностни и референтни. Следва да представим една по-подробна класификация на типовете данни в .NET Framework. Те биват:
- класове
- делегати
Понятието "клас" от ООП се реализира в .NET Framework чрез класове (classes) и структури (structs).
Не трябва да бъркаме понятието клас от концепциите на ООП с понятието клас в .NET Framework. Разликата е тънка – класът в .NET действително е клас според ООП терминологията, но обратното не е вярно. ООП терминът клас се реализира и по още един начин – чрез структури.
Основната разлика между класовете и структурите в .NET Framework е, че структурите са стойностни типове, докато класовете са референтни типове. Структурите по-интуитивно моделират данни, от които се очаква поведение като на примитивни типове, докато класовете по-добре моделират обекти от реалния свят, които могат да извършват определени действия.
Тъй като типовете по стойност в общия случай се създават в стека за изпълнение на програмата, структурите е добре да съдържат малки по-обем данни, а по-големите количества е удачно да се обработват с помощта на класове, инстанциите на които съхраняват членовете си в динамичната памет.
Някои обектно-ориентирани езици позволяват използването на множествено наследяване – възможността един клас да приеме методи и свойства от няколко родителя. При проектирането на .NET Framework е взето решение това да не се допуска.
Една от причините в .NET Framework да няма множествено наследяване е, че множественото наследяване води до конфликти, например ако един клас наследи елемент с едно и също име от повече от един родител.
От друга страна множественото наследяване води до по-сложни и трудно разбираеми йерархии – такива, образуващи граф, докато при наследяването от единствен родител се получава дърво.
В .NET Framework приемането на характеристики и поведение от повече от една същности от реалния свят се осъществява чрез реализиране на няколко интерфейса едновременно, при което обаче не може да се наследят данни или програмен код, а само дефиниции на действия.
Класовете в C# са основните единици, от които се състоят програмите. Те моделират обектите от реалния свят и могат да дефинират различни членове (член-променливи, методи, свойства и др.). Нека видим как изглежда един примерен клас на езика C#:
|
class Student { // Private member declarations private string mFirstName; private string mLastName; private string mStudentId;
// Constant private const double PI = 3.1415926535897932384626433;
// Constructor public Student(string aStudentId) { mStudentId = aStudentId; }
// Property public string FirstName { get { return mFirstName; } set { mFirstName = value; } }
// Read-only property public string StudentId { get { return mStudentId; } }
// Method public string StoreExamResult( string aSubject, double aGrade) { // ... } } |
В горния пример е дефиниран класът Student, илюстриращ някои от видовете членове, които класовете могат да реализират – капсулираните полета mFirstName, mLastName и mStudentId, константата PI, конструкторът Student(…), свойствата FirstName и StudentId и методът StoreExamResult(…). С течение на темата ще се запознаем по-отблизо с всеки от тези видове членове.
В .NET типовете "клас" и "структура", като реализация на понятието клас от ООП, могат да съдържат в себе си членове (members), подобно на други обектно-ориентирани езици като Java и C++. Членовете могат да бъдат от един от следните видове:
- полета, или член-променливи (fields)
- константи (constants)
- методи, или член-функции (methods)
- свойства (properties)
- индексатори (indexers)
- оператори (operators)
- конструктори (constructors)
- вложени типове (класове, структури, изброени типове и др.)
Множеството от типове, които могат да "виждат" определен член на даден клас се определя от видимостта. Правилното задаване на видимостта на членовете е ключов момент в разработването на йерархии от класове, тъй като основен принцип в ООП е клиентът на класа да вижда само това, което му е необходимо, и нищо повече. Следва описание на нивата на видимост в .NET Framework.
Глобална видимост – членовете с такова ниво на достъп могат да се достъпват от всеки тип.
Това са членовете, видими от всички типове, дефинирани в асемблито, в което е дефиниран дадения, a също и от наследниците на типа.
Членове, които се достъпват от всички типове, дефинирани в асемблито, в което е дефиниран дадения.
Членове, видими само от наследниците на дадения тип.
Капсулирани членове, видими единствено в рамките на типа.
Данните, с които инстанцията на класа работи, се съхраняват в член-променливи (или още полета). Те се дефинират в тялото на класа и могат да се достъпват от други видове членове – методи, конструктори, индексатори, свойства. В следващия пример ще покажем няколко декларации на член-променливи, за които в последствие ще дадем обяснения.
|
class Student { private string mFirstName; private string mLastName; private string mStudentId; private int mCourse = 1; private string mSpeciality; private Course[] mCoursesTaken;
// Avoid missing the visibility modifier string mRemarks = "(няма забележки)"; } |
Дефиницията на всяко поле започва с ниво на видимост. Допустими са всички по-горе изброени нива на видимост, но в примера са използвани само private, защото скриването на полетата от използващите класа, т.е. указването на видимост private или protected, е утвърдена практика в ООП. Когато искаме да предоставим данните на класа на околния свят в .NET е прието вместо полета с ниво на достъп "public" да се използват свойства, на които ще се спрем малко по-късно. Степента на видимост може и да не бъде определена явно, както е в последния ред за полето mRemarks от примера и в този случай се подразбира private. Тази практика не се препоръчва, защото води до по-неясен код.
Следващият елемент от дефиницията на член-променлива е типът, който се указва задължително. Може да бъде произволен .NET тип от CTS или дефиниран от потребителя.
След типа следва името на дефинираното поле, чрез което се обръщаме към него. То представлява идентификатор, т. е. последователност от unicode символи – главни и малки букви, цифри, -(тире) и _(подчертаващо тире), незапочваща с цифра или тире.
Имената на полетата и въобще на членовете в .NET Framework могат да бъдат идентични със съществуващи имена на типове или пространства от имена (на тях ще се спрем в края на темата). Например класът Student може да има свойство със същото име Student. Могат да бъдат и запазени думи, но само ако бъдат предшествани от @. Допуска се и използването на нелатински букви в имената, но не се препоръчва.
При дефиницията на поле можем да му зададем стойност, както в примера това е направено за mCourse и mRemarks. Ако началната стойност бъде пропусната, на член-променливата се задава стойност по подразбиране. За референтните типове това е null, а за стойностните типовете е 0 или неин еквивалент (например false за boolean). В .NET Framework всички членове и променливи се инициализират автоматично. Това намалява грешките, възникващи заради използването на неинициализирани променливи.
Константните полета (или само константи) много приличат на обикновените полета, но имат някои особености. Нека обърнем внимание на следния пример, който показва няколко дефиниции на константи:
|
public class MathConstants { public const string PI_SYMBOL = "π"; public const double PI = 3.1415926535897932385; public const double SQRT2 = 1.4142135623731; } |
От примера виждаме, че дефиницията на константа е дефиницията на поле с добавена ключовата дума const. Има и някои други разлики.
При декларирането на константно поле е задължително да се предостави стойност. Освен това стойността на константата не може да бъде променяна по време на работата с типа, в който е дефинирана – може само да бъде прочетена. Константите реално не съществуват като полета в типа, а съществуват само в сорс кода и се заместват със стойността им по време на компилация. Поради тази причина const декларациите в C# се наричат още compile-time константи, т. е. константи, които съществуват само по време на компилацията.
Друг специален вид полета, подобни на константите, са полетата само за четене (read-only fields). Те се различават от константните по това, че стойността им освен при дефиницията може да бъде зададена и в конструктор, но от там нататък не може да бъде променяна. Член-променлива само за четене се декларира, като се използва запазената дума readonly, като в примера:
|
class ReadOnlyDemo { private readonly int mSize;
public ReadOnlyDemo(int aSize) { mSize = aSize; // cannot be further modified! } } |
За разлика от константите, полетата само за четене са реални полета в типа, които обаче, задължително трябва да се инициализират в конструктора на класа или при деклариране, защото след това не може да им бъде присвоявана стойност и биха останали с подразбиращата се. Поради тази причина те се наричат още run-time константи, т. е. константи, които се инициализират по време на изпълнение на програмата.
Методите (или още член-функции) дефинират операции за типа, в който са дефинирани. Те могат да боравят с членовете му, независимо от степента им на видимост, да ги достъпват и променят (освен полетата обявени като константни или само за четене).
В C# функции могат да бъдат дефинирани единствено като членове на клас или структура, за разлика от други обектно-ориентирани езици, където се използват глобални функции – такива, които не са обвързани с конкретен тип и са общодостъпни. В C# функции, които се достъпват без да е нужна инстанция на даден клас, се дефинират като статични. На тях ще се спрем след малко.
Подобно на полетата, и методите могат да имат ниво на видимост. И синтактично, и от гледна точка на стила на програмиране, на методите е допустимо да се зададе коя да е от възможните нива на видимост, тъй като те представляват действията с типа и за някои от тях е необходимо да бъдат видими за околния свят, а за други – не. Отново подразбиращото се ниво на видимост е private, но е препоръчително да се декларира изрично.
Методите могат да приемат параметри и да връщат стойност. Параметрите имат тип, който може да бъде всеки валиден .NET тип. Върнатата стойност може да бъде също от всеки възможен тип, а може и да отсъства. Нека обърнем внимание на следния пример:
|
class MethodsDemo { public void SayHiGeorgi() { SayHi("Гошо"); }
public void SayHiPeter() { SayHi("Пешо"); }
private void SayHi(string aName) { if (aName == null || aName == "" ) { return; } Console.WriteLine("Здравей, {1}", aName); }
public int Multiply(int x, int y) { return x * y; } } |
Първите два метода, SayHiGeorgi() и SayHiPeter(), не приемат никакви параметри и не връщат стойност. Третият, SayHi(string aName), приема един параметър от тип string и не връща стойност. Последният, Multiply(int x, int y), приема два параметъра от тип int и връща стойност също от тип int.
В дефинициите на първите три метода от примера забелязваме ключовата дума void – тя се използва при методи, които не връщат стойност. За методи, които връщат стойност, вместо ключовата дума void се указва типа на връщаната стойност.
В последния метод забелязваме как се употребява ключовата дума return за връщане на стойност. Същата ключова дума използваме и за прекратяване на изпълнението на метод, който не връща стойност, както в метода SayHi(aName).
В C# е допустимо един тип да има два и повече метода с едно и също име, но с някои ограничения. Ще въведем понятие, свързано с използването на едно и също име за няколко метода. Комбинацията от името, броя и типа на параметрите на метод наричаме сигнатура. Ако два метода имат едно и също име, те задължително трябва да се различават по сигнатура. Следващият пример илюстрира дефинирането на три метода с еднакви имена:
|
int Sum(int a, int b) { return a + b; }
int Sum(int a, int b, int c) { return a + b + c; }
long Sum(long a, long b, long c) // avoid this { return a + b + c; } |
Горните дефиниции са напълно валидни – първите два метода се различават по броя на параметрите си, а вторият и третият – по типа.
|
|
Трябва да сме особено внимателни с дефиниции като последните две и е препоръчително да се избягват, тъй като не е очевидно кой метод ще бъде извикан при обръщение като int sumTest = sum(1,2,3). Компилаторът по никакъв начин не ни предупреждава за двусмислието. В горния пример ще бъде извикан първият метод – sum(int a, int b, int c). |
Както вече споменахме, в C# функции, които могат да се извикват без да е нужна инстанция на клас, се реализират като статични (или общи) методи. Това става, като в дефиницията им включим ключовата дума static. Статичните членове се споделят от всички инстанции и се използват за пресъздаване на свойства и действия, които са постоянни за всички обекти от дадения клас. Достъпът до статичните членове на типа се извърша директно, а не през инстанция, както в следващия пример:
|
class Bulgaria { private static int mNumberOfCities = 267;
public static int NumberOfCities { get { return mNumberOfCities; } }
public static void AddCity(string aCityName) { mNumberOfCities++; // ... }
// ...
static void Main() { Console.WriteLine( "В България има {0} града.", Bulgaria.NumberOfCities); } } |
В примера видяхме дефинирането и използването на статични полета, методи и свойства. Използвахме статичните свойства без да инстанцираме класа Bulgaria никъде.
|
|
Важна особеност, която трябва да имаме предвид при използването на статични методи и свойства, е че те могат да използват само статични полета. Полетата, които са обвързани с инстанция могат да се достъпват само в нейния контекст, а статичните методи и свойства са независими от инстанцията. |
Статичните полета на типа много приличат на глобалните променливи в по-старите езици за програмиране като C, C++ и Pascal. Както глобалните променливи, статичните полета са достъпни от цялото приложение и имат само една инстанция.
От членовете на типа, освен полетата, свойствата и методите също и конструкторите, индексаторите и събитията могат да бъдат статични. Константите също са общи за всички инстанции на типа, но не могат да бъдат статични. Деструкторите също не могат да бъдат статични, докато операторите задължително са.
Конструкторите се използват при създаване на обекти и служат за инициализация, или начално установяване на състоянието на полетата на обекта. Механизмът на работа и синтаксисът за дефиниране на конструкторите в C# са подобни на други обектно-ориентирани езици, като Java и C++ с някои особености, на които ще обърнем внимание. Допуска се използването на повече от един конструктор, като конструкторите трябва да се различават по броя и/или типа на параметрите. Възможно е и да не се дефинира конструктор и в такъв случай компилаторът създава подразбиращ се – публичен, с празно тяло и без параметри.
Съществуват три възможности за инициализацията на полетата на обекта – да бъдат инициализират в конструктор, при декларацията им или да нямат изрично зададена стойност.
Инициализациите, описани в тялото на конструктора се изпълняват по време на изпълнението този конструктор – при създаване на обект от съответния клас с ключовата дума new в C#.
Инициализациите, дефинирани при декларацията на полетата се изпълня-ват директно преди конструктора. Можем да приемем, че при компилацията инициализациите на полетата се добавят в началото на всеки конструктор. Всъщност C# компилаторът прави точно това скрито от програмиста – поставя код, който инициализира всички член-променливи на типа във всички негови конструктори.
Полетата, които нямат зададена начална стойност, получават стойност по подразбиране (нулева стойност). Това поведение се изисква от спецификацията на езика C# и не зависи от конкретната имплементация на компилатора.
Със следващия пример ще разгледаме примерни дефиниции на конструктори на базов клас с един наследник:
|
class Student { private string mName; private int mStudentId; private string mPosition = "Student";
public Student(string aName, int aStudentId) { mName = aName; mStudentId = aStudentId; }
public Student(string aName) : this(aName, -1) { }
public static void Main() { Student s = new Student("Бай Киро", 12345); } }
public class Kiro : Student { public Kiro() : base("Бай Киро", 12345) { }
// ... } |
Забелязваме употребата на ключовите думи this и base след дефиницията на конструкторите на класа. Те представляват съответно обръщения към друг конструктор на същия клас и към конструктор на базовия клас, като в скобите се изреждат параметрите, които се подават на извиквания конструктор. В примера е използване наследяване, на което ще с спрем в детайли след малко (класът Kiro наследява класа Student).
В следващата демонстрация ще си послужим с инструмента IL DASM (ildasm.exe), който е част от .NET Framework SDK, за да разгледаме MSIL кода, който C# компилаторът генерира за класа Student, който дефинирахме в примера по-горе. С това упражнение не само ще се запознаем с работата с инструмента, но и ще забележим особеностите в генерирания код, свързани с полетата със зададена стойност при декларацията. Ето стъпките, които трябва да направим:
1. Отваряме Demo-1-Constructors.sln, елементарен Visual Studio .NET проект с единствен C# файл, който съдържа кода от горния пример. Компилираме проекта.
2. Стартираме командния интерпретатор към Visual Studio .NET. Не използваме стандартния cmd.exe, а този, който се намира в Start -> Programs -> Microsoft Visual Studio 2003 -> Visual Studio Tools, защото той се стартира с регистрирани пътища към .NET инструментите, които се използват от командния ред.
3. Избираме директорията, където се намира изпълнимият файл, получен при компилиране на проекта – Demo-1-Constructors.exe. Ако не сме променили настройките на Visual Studio .NET, това ще е директорията <директория на проекта>\bin\Debug.
4. Извикваме от командния ред инструмента ildasm и му подаваме като параметър компилираното приложение:
|
ildasm Demo-1-Constructors.exe |
Ето как изглежда прозорецът на инструмента, в който е заредено асемблито от приложението, когато разпънем всички елементи от дървото:

IL DASM показва дърво за асемблито, в което различаваме класа Student и членовете му. Ако се придвижим по дървото до конструкторите на класа, можем да изследваме техния IL код, както е показано на следващата картинка:

В кода, генериран за конструктора с един параметър, се вижда обръщението към този с два параметъра. Ако повторим същото действие и с втория конструктор, можем да наблюдаваме и неговия IL код (на картинката по-долу).
Забелязваме, че задаването на стойност на полетата с инициализация при декларацията реално се извършва в началото на втория конструктор. Реално тези полета се инициализират и от първия конструктор, защото той извиква втория.

В този пример ще представим един популярен шаблон в обектно-ориентирания дизайн – клас, който може да има най-много една инстанция в рамките на цялото приложение. Такъв клас наричаме singleton. За реализирането на такива класове се използва следният подход:
|
public sealed class Singleton { private static Singleton mInstance = null;
private Singleton() { }
public static Singleton Instance { get { if (mInstance == null) { mInstance = new Singleton(); } return mInstance; } } } |
Целта на задаването на private видимост за конструктора на класа е за да не могат да се създават инстанции освен от членове на класа, както в случая статичното свойство Instance. В дефиницията на класа е използвана ключовата дума sealed, която указва, че класът не може да бъде наследяван.
Горният пример само демонстрира използването на sealed класове и частен конструктор. В реална ситуация при реализацията на singleton шаблона трябва да се вземе предвид, че е възможно няколко нишки (threads) едновременно да се опитат да извлекат инстанцията на singleton класа и да се получи нежелано поведение. Затова обикновено реализацията на този шаблон изисква допълнителни усилия за нишково обезопасяване на работата на класа. На работата с нишки ще обърнем специално внимание в темата "Многонишково програмиране и синхронизация".
Конструкторите, подобно на други видове членове на класа, могат да бъдат обявени за статични, с тази особеност че статичният конструктор може да бъде най-много един и не може да приема параметри и модификатори за достъп.
Статичният конструктор се използва за инициализация на статичните членове и се извиква автоматично. Извикването на статичният конструктор се извършва "зад кулисите" от CLR. Това става по време на изпълнението на програмата и моментът на стартирането му не е точно определен. Това, което е сигурно, е че статичният конструктор е вече извикан когато се създаде първата инстанция на класа или когато се достъпи някой негов статичен член. В рамките на програмата, статичният конструктор може да бъде извикан най-много веднъж.
В следващия пример ще разгледаме класа SqrtPrecalculated, който използва статичен конструктор:
|
class SqrtPrecalculated { public const int MAX_VALUE = 10000; private static int[] mSqrtValues; // static field
// Static constructor static SqrtPrecalculated() { mSqrtValues = new int[MAX_VALUE + 1]; for (int i = 0; i <= MAX_VALUE; i++) mSqrtValues[i] = (int) Math.Sqrt(i); }
// Static method public static int GetSqrt(int aValue) { return mSqrtValues[aValue]; }
static void Main() { Console.WriteLine(GetSqrt(1000)); } } |
Класът SqrtPrecalculated служи за бързо изчисляване на корен квадратен. Той предоставя статичния метод SqrtPrecalculated(), който връща цялата част на квадратния корен на аргумента си.
За по-голямо бързодействие всички квадратни корени на числата от 0 до 10000 се изчисляват предварително в статичния конструктор и после се използват наготово. Множеството от стойностите се съхранява в статичното поле mSqrtValues[], което се инициализира в статичния конструктор, който се изпълнява преди първия опит за достъп до класа.
Ще илюстрираме поведението на статичните конструктори в .NET Framework, като с помощта на дебъгера на VS.NET наблюдаваме как преди да започне да бъде използван даден клас се изпълнява първо статичният му конструктор.
Ще използваме дебъгера на Visual Studio .NET за да проследим изпълнението на кода от горния пример, който се съдържа в приложението Demo-2-TestStaticConstructor от демонстрациите. Ще изпълним последователно следните стъпки:


Свойствата са членове на класовете, структурите и интерфейсите, които обикновено се използват за да контролират достъпа до полетата на типа.
Свойствата приличат на член-променливите по това, че имат име, по-което се достъпват, и стойност от някакъв предварително определен тип. От гледна точка на синтаксиса за достъп до тях, свойствата изглеждат по същият начин както полетата. Разликата се състои в това, че свойства съдържат код, който се изпълнява при обръщение към тях, т. е. извършват действия. Свойствата могат да бъдат и статични.
Свойствата могат да имат два компонента (accessors):
- код за прочитане на стойността (get accessor)
- код за присвояване на стойността (set accessor)
Когато създаваме свойства можем да предоставим дефиниции на двата компонента, както и на само един от тях, но задължително трябва да е дефиниран поне единият. Според предоставените компоненти делим свойствата на три вида:
- Свойства само за чете (read only) - такива, които дефинират само код за прочитане на стойността им.
- Свойства за четене и писане (read and write) - когато имат и двата компонента.
- Свойства само за писане (write only) - когато е предоставен само код за присвояване на стойност.
Ще дефинираме класа Person за да илюстрираме дефинирането и използването на свойства:
|
public class Person { private string mName; private DateTime mDateOfBirth;
// Property Name of type string public string Name { get { return mName; } set { if ((value != null) && (value.Length > 0)) { mName = value; } else { throw new ArgumentException("Invalid name!"); } } }
// Property DateOfBirth of type DateTime public DateTime DateOfBirth { get { return mDateOfBirth; } set { if ((value.Year >= 1900) && (value.Year <= DateTime.Now.Year)) { mDateOfBirth = value; } else { throw new ArgumentOutOfRangeException( "Invalid date of birth!"); } } }
// Read-only property Age of type int public int Age { get { DateTime now = DateTime.Now; int yearsOld = now.Year - mDateOfBirth.Year; DateTime birthdayThisYear = new DateTime(now.Year, mDateOfBirth.Month, mDateOfBirth.Day, mDateOfBirth.Hour, mDateOfBirth.Minute, mDateOfBirth.Second); if (DateTime.Compare(now, birthdayThisYear) < 0) { yearsOld--; } return yearsOld; } } }
// Property usage example class PropertiesDemo { static void Main() { Person person = new Person(); person.Name = "Svetlin Nakov"; person.DateOfBirth = new DateTime(1980, 6, 14); Console.WriteLine("{0} is born on {1:dd.MM.yyyy}.", person.Name, person.DateOfBirth); Console.WriteLine("{0} is {1} years old.", person.Name, person.Age); } } |
В примерния клас виждаме дефинициите на две свойства за четене и писане - Name от тип string и DateOfBirth от тип DateTime, както и едно само за четене – Age от тип int.
Можем да доловим различните аспекти на употребата на свойства - едно свойство може да бъде просто обвивка около поле на типа, но може и да реализира по-сложна логика. Например свойствата Name и DateOfBirth в примера просто връщат стойността на полетата, които обвиват, или я задават след съответните проверки за валидност. Свойство може да бъде и абстракция на данни, извличането и съхранението на които би могло да бъде свързано със сложна обработка. Опростен пример за това е Age, което връща стойност, резултат от извършване на изчисления, в случая разликата между текущата дата и рождената дата на лицето.
Ще си изясним работата със свойства като проследим хода на програмата по време на достъпа до тях. За целта ще си послужим с кода от примера, който се съдържа в приложението Demo-4-Properties от демонстрациите. Той съдържа горния пример. Нека изпълним следните стъпки:


Това ни показва, че зад операцията "присвояване на стойност" на свойството стои кодът му за присвояване.

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

|
|
В режим на дебъгване прозорецът, в който се изпълнява приложението, се затваря веднага след приключване на изпълнението на кода и резултатът трудно може да бъде видян. Ако искаме да видим отпечатания резултат, трябва или да сложим точка на прекъсване преди края на Main() метода, или да се придвижим до последната операция стъпка по стъпка или да изпълним програмата не с [F5], а с [Ctrl-F5]. |
Като стартираме ildasm и разгледаме с него IL кода за класа Person, забелязваме нещо много интересно – в класа Person има методи с префикс set_, отговарящи на компонентите за присвояване на дефинираните от нас свойства, и методи с префикс get_, които съответстват на компонентите за връщане на стойност.
На практика след компилация get и set частите на свойствата са се превърнали в методи, а достъпът до тях се е превърнал в операции за извикване на метод. Това е начинът, по който C# компилаторът компилира свойствата – превръща ги в методи, а достъпът до тях превръща в извиквания на методи.
Ето как изглежда класът Person в инструмента IL DASM:

Индексаторите в C# (indexers) са членове на класовете, структурите и интерфейсите, които предоставят индексиран достъп до данни на типа, подобно на достъпа до елементите на масив.
Индексаторите по синтаксис и семантика много приличат на свойства, но получават като параметър индекс на елемент, с който да работят. На практика, те представляват свойства, приемащи параметър и дори в някои .NET езици, например VB.NET, синтаксисът на декларирането им е същият като при свойствата.
За да си изясним най-лесно как се дефинират индексатори, да разгледаме следния пример:
|
private object[] mElements;
public object this[int index] { get { return mElements[index]; } } |
Виждаме, че дефиницията на индексатор прилича на тази на свойство, но има и някои разлики. На индексатора не се задава име, а вместо него се задава запазената дума this.
Достъпът до индексатор на обект се извършва посредством името на променливата от типа, дефиниращ индексатора, последвана от индекса в квадратни скоби, също както се извършва достъпа до елемент на масив, например myArrayList[5].
Позовавайки се на начина, по който се обръщаме към индексаторите, можем да ги разглеждаме като средство за предефиниране на оператора []. Използването на индексатори позволява интуитивен достъп до обекти, които се състоят от множество компоненти, каквито са масивите и колекциите.
За да илюстрираме по-пълно дефинирането и използването на индексатори, ще използваме следващия пример. Ще дефинираме клас, който имитира поведението на масив от 32 стойности, всяка от които е или 0 или 1:
|
struct BitArray32 { private uint mValue;
// Indexer declaration public int this [int index] { get { if (index >= 0 && index <= 31) { // Check the bit at position index if ((mValue & (1 << index)) == 0) return 0; else return 1; } else { throw new ApplicationException(String. Format("Index {0} is invalid!", index)); } } set { if (index < 0 || index > 31) throw new ApplicationException( String.Format("Index {0} is invalid!", index));
if (value < 0 || value > 1) throw new ApplicationException( String.Format("Value {0} is invalid!", value));
// Clear the bit at position index mValue &= ~((uint)(1 << index));
// Set the bit at position index to value mValue |= (uint)(value << index); } } }
class IndexerTest { static void Main() { BitArray32 arr = new BitArray32();
arr[0] = 1; arr[5] = 1; arr[5] = 0; arr[25] = 1; arr[31] = 1;
for (int i=0; i<=31; i++) { Console.WriteLine("arr[{0}] = {1}", i, arr[i]); } } } |
Класът BitArray32 представлява масив от битове с 32 елемента, който вътрешно съхранява стойностите им в едно 32-битово поле. Елементите му достъпваме посредством дефинирания индексатор по същия начин, по който достъпваме елементите на вградените в CTS масиви. На масивите в .NET Framework ще се спрем в темата "Масиви и колекции".
Виждаме компонентите за прочитане и присвояване на стойността, които извършват проверка дали индексът е в съответния диапазон, след което чрез битови операции осъществяват достъп до посочения като параметър бит. При невалидни параметри се предизвиква изключение, чрез което се уведомява извикващия код за проблема. На изключенията ще се спрем подробно в темата "Управление на изключенията в .NET".
За да проследим работата на индексатора ще си послужим с приложението от демонстрациите Demo-5-Indexers.sln, което съдържа кода от горния пример. Ще изпълним следните стъпки:





В .NET Framework се допуска дефинирането на индексатори, приемащи повече от един параметър. Примерно обръщение към такъв индексатор е конструкцията personInfo["Бай Иван", 68]. Възможно е в един тип да се дефинират и няколко индексатора с различен набор от параметри. Индексаторите не могат да бъдат статични, тъй като реализират индексиране в рамките на дадена инстанция.
Ето още един пример за индексатор, който приема два параметъра от тип символен низ и връща целочислена стойност:
|
class DistanceCalculator { public int this[string aTown1, string aTown2] { get { if (aTown1.Equals("София") && aTown2.Equals("Варна")) return 470; else throw new ApplicationException("Unknown distance!"); } } }
class DistanceTest { static void Main() { DistanceCalculator dc = new DistanceCalculator(); Console.WriteLine("Разстоянието между {0} и {1} е {2} " + "километра.", "София", "Варна", dc["София", "Варна"]); } } |
В примера е реализиран клас, който по дадени имена на два град връща разстоянието между тях. Разбира се, тази функционалност не е реализирана напълно, но целта на примера е да се илюстрира работата с индексатори, а не да се даде завършен проект, който работи.
Структурите в .NET Framework представляват съвкупност от полета с данни. Te приличат много на класовете, но за разлика то тях са стойностни типове. Инстанциите на структурите имат поведение като примитивните числени типове – разполагат в стека за изпълнение на програмата, предават се по стойност и се унищожават при излизане от обхват.
За разлика от структурите класовете са типове по референция и се разполагат в динамичната памет заради което създаването и унищожаването им е по-бавно. При предаване като параметри се предава само техният адрес в динамичната памет (т. нар. референция).
Структурите, както и класовете, могат да дефинират конструктори, полета, свойства, индексатори и други членове.
Въпреки, че синтаксисът на езика C# го допуска, не се препоръчва в структурите да има методи с логика. Структурите трябва да се използват за да съхраняват някаква структура от данни (съвкупност от полета).
При правилна употреба заместването на класове със структури може значително да увеличи производителността. Ще се спрем по-подробно на класовете и структурите в темата "Обща система от типове".
Структурите се дефинират по същия начин, както и класовете, но вместо запазената дума "class" се използва запазената дума "struct".
За да демонстрираме работата със структури, ще дадем няколко примера:
|
struct Point { public int mX, mY; }
struct Color { public byte mRedValue; public byte mGreenValue; public byte mBlueValue; }
struct Square { public Point mLocation; public int mSize; public Color mBorderColor; public Color mSurfaceColor; } |
Както виждаме, структурите много приличат на класове, но основното им предназначение е да съхраняват данни.
В C# има три различни режима на предаване на параметрите. Ще ги разгледаме накратко, след което ще се спрем по-подробно на всеки от тях и ще илюстрираме разликите между тях с примери. Параметрите при извикване на метод могат да се предават по следните начини:
- out (изходни параметри за връщане на стойност)
Параметрите могат да не бъдат инициализирани преди предаването им. Инициализацията се извършва от извиквания метод, а преди нея достъпът е само за писане и в тялото на метода, и в кода, който го извиква.
- ref (входно-изходни параметри за предаване по референция)
Промените, които методът прави по подадените му по референция параметри, изменят истинските стойности на параметрите, а не техни копия от стека, и за това са видими от кода, извикал метода.
- in (входни параметри за предаване по стойност)
Това е подразбиращият се режим на предаване на параметрите в C#. При изпълнението на метода в стека се записват стойностите на параметрите, с които методът работи и след излизането от тялото му, когато се изтрие върха на стека, промените в параметрите остават изгубени, като стойността на локална променлива, излязла от обхват.
Предаването на out параметри се задейства, като маркираме параметъра с ключовата дума out, и в дефиницията на метода, и при извикването му. Целта на тези параметри е не методът да приема като входни данни тяхната стойност, а единствено да я инициализира и да я върне като резултат от изпълнението си. По тази причина те се наричат изходни параметри.
Изходните параметри се предават по адрес в случай на стойностен тип и по адрес на референцията (двоен указател) в случай на референтен тип. Благодарение на това при промяна на стойността им в даден метод тази промяна директно се отразява на променливата, подадена от извикващия метод.
Тъй като върнатата стойност от даден метод в C# може да бъде само една, използвайки out параметри можем да върнем на кода, извикал метода, повече стойности. Нека разгледаме следния пример за да илюстрираме връщането на стойност чрез out параметри:
|
public struct Point { public int mX, mY;
public Point(int aX, int aY) { mX = aX; mY = aY; } }
public struct Dimensions { public int mWidth, mHeight; public Dimensions(int aWidth, int aHeight) { mWidth = aWidth; mHeight = aHeight; } }
public class Rectangle { private int mX, mY, mWidth, mHeight;
public Rectangle(int aX,int aY, int aWidth,int aHeight) { mX = aX; mY = aY; mWidth = aWidth; mHeight = aHeight; }
public void GetLocationAndDimensions( out Point aLocation, out Dimensions aDimensions) { aLocation = new Point(mX, mY); aDimensions = new Dimensions(mWidth, mHeight); } }
class TestOutParameters { static void Main() { Rectangle rect = new Rectangle(5, 10, 12, 8);
Point location; Dimensions dimensions;
// location and dimension are not previously initialized rect.GetLocationAndDimensions( out location, out dimensions);
Console.WriteLine("({0}, {1}, {2}, {3})", location.mX, location.mY, dimensions.mWidth, dimensions.mHeight); // Result: (5, 10, 12, 8) } } |
В горния пример са дефинирани структурите Point и Dimensions, които методът GetLocationAndDimensions(…) на класа Rectangle използва за да връща чрез изходните си параметри техни инстанции.
Трябва да обърнем внимание на употребата на ключовата дума out и на това, че променливите location и dimensions не са инициализирани никъде в тялото на метода Main(…). Ако параметрите не бяха указани като такива за връщане на стойност, това не би било допустимо – получава се грешка при компилация "Use of unassigned local variable".
Примерът извлича с едно извикване на метод две стойности – местоположението и размерите на даден правоъгълник, като ги записва в инстанции на структурите Point и Dimensions.
Предаването на параметрите по референция се активира като добавим ключовата дума ref към описанието на даден параметър в дефиницията на метода и при извикването му. Такива параметри се наричат входно-изходни.
При предаване на параметри по референция при стартиране на метода в стека не се записват копия на стойностите на параметрите, а указатели към адреса в паметта на оригиналните им стойности. Така извиканият метод може както да чете информация от подадените му параметри, така и да ги изменя и да връща стойности на кода, който го е извикал.
Параметрите поп референция се предават по адрес в случай на стойностен тип и по адрес на референцията (двоен указател) в случай на референтен тип. Благодарение на това при промяна на стойността им в даден метод тази промяна директно се отразява на променливата, подадена от извикващия метод. Ще илюстрираме това с пример:
|
public struct Point { internal int mX, mY;
public static void IncorrectMultiplyBy2(Point aPoint) { aPoint.mX *= 2; aPoint.mY *= 2; }
public static void MultiplyBy2(ref Point aPoint) { aPoint.mX *= 2; aPoint.mY *= 2; }
static void Main() { Point p = new Point(); p.mX = 5; p.mY = -8; Console.WriteLine("p=({0},{1})", p.mX, p.mY); // 5,-8 IncorrectMultiplyBy2(p); Console.WriteLine("p=({0},{1})", p.mX, p.mY); // 5,-8 MultiplyBy2(ref p); Console.WriteLine("p=({0},{1})", p.mX, p.mY); // 10,-16 } } |
При изпълнение на примера се вижда, че в тялото на метода Main(…) не се отразяват промените в предадения в подразбиращия се режим (в случая по стойност) параметър p при извикването на метода IncorrectMultiplyBy2(…). Когато, обаче, параметърът е маркиран като ref, методът MultiplyBy2(…) успява да удвои членовете му, тъй като този метод променя директно подадената стойност, а нейно копие.
В подразбиращия се режим при извикване на метод му се подават копия от стойностите на параметрите. Реално стойностните типове се предават по стойност (предава се тяхно копие), а референтните типове се предават по референция (предава се копие на тяхната адреса в динамичната памет, към който сочат).
В по-горния пример видяхме как въпреки промяната в тялото на метода IncorrectMultiplyBy2(…) предадената по стойност променлива p не измени реалната си стойност. Трябва да обърнем внимание, че p е от стойностен тип (инстанция на структурата Point). Ако p беше референтен тип, промените в членовете му щяха да бъдат видими за кода, извикал метода. Защо това е така, въпреки че предаваме параметъра по стойност? На този въпрос ще си отговорим след като разгледаме следващия пример:
|
public class ClassPoint { internal int mX, mY;
public static void MultiplyBy2(ClassPoint aPoint) { aPoint.mX *= 2; aPoint.mY *= 2; }
public static void IncorrectErase(ClassPoint aPoint) { aPoint = null; }
static void Main() { ClassPoint p = new ClassPoint(); p.mX = 5; p.mY = -8; Console.WriteLine("p=({0},{1})", p.mX, p.mY); // 5,-8 MultiplyBy2(p); Console.WriteLine("p=({0},{1})", p.mX, p.mY); // 10,-16 IncorrectErase(p); Console.WriteLine("p=({0},{1})", p.mX, p.mY); // 10,-16 } } |
Забелязваме, че при обръщението към метода MultiplyBy2(…) дори и без да указваме, че параметърът се предава по референция, полетата на p успешно се удвояват. Това е така, защото класът ClassPoint е референтен тип и променливата от този тип представлява указател към паметта, където е записана същинската стойност на обекта.
При извикване на метод с предаване на параметрите по стойност в стека се прави копие на подадената променлива, която в случая е указател (референция) и операциите с това копие изменят реално оригиналната стойност на променливата в динамичната памет.
При промяна на даден параметър, подаден по стойност, например при изменянето на параметъра aPoint в тялото на метода IncorrectErase(…), се изменя единствено копираният на стека указател, а не реалната стойност на обекта. Затова при излизане от метода IncorrectErase(…), въпреки че за параметъра aPoint е зададена стойност null, променливата p не е променена и сочи към обекта, който е бил подаден при извикването.
В C# можем да дефинираме методи с променлив брой параметри. Пример за такъв метод, който неведнъж сме ползвали в нашите примери, е Console.WriteLine(…). Предаването на променлив брой параметри в C# се реализира чрез следния синтаксис:
|
static int Sum(params int[] aValues) { int retval = 0; foreach(int arg in aValues) { retval += arg; } return retval; }
static void Main() { int sum = Sum(1, 2, 3, 4, 5); Console.WriteLine(sum); // The result is 15 } |
В горния пример дефинирахме метода Sum(…), който изчислява сумата на произволен брой цели числа от тип int. Указваме, че методът приема произволен брой параметри със служебната дума params. Тя може да се използва най-много веднъж в дефиницията на даден метод и задължително се прилага към последния изреден параметър, който трябва да бъде масив, приемащ множеството от параметрите. На метода от примера могат да бъдат подадени както произволен брой променливи от тип int, така и масив от тип int, т. е. допустими извиквания са както Sum(1,2,3), така и Sum(new Object[]{1,2,3}).
В някои случаи е възможно да се нуждаем от метод, който да приема произволен брой параметри, но не задължително от един и същ тип. Например такъв би могъл да бъде методът, който изчислява сума на произволен брой целочислени параметри, включително и такива, зададени като символни низове. За този метод допустими обръщения биха били sum(1, 2, 3) както и sum(1, "2", 3).
За да реализираме такъв метод, можем да предаваме параметрите чрез масив от по-общ тип, например чрез масив от инстанции на типа System.Object, който е базов тип за всички типове в .NET Framework. Така можем да работим с произволно множество от параметри, чиито тип различаваме с помощта на оператора за принадлежност към тип is. В нашия случай за да използваме параметри от тип символен низ, първо ги конвертираме към желания целочислен тип. Ето примерна реализация на описаната идея:
|
int Sum(params Object[] values) { int retval = 0; foreach(Object arg in values) { if (arg is int) { retval += (int)arg; } else if (arg is string) { retval += int.Parse((string)arg); } } return retval; } |
В този вариант методът Sum(…) извлича целочислената стойност от низа, когато се натъкне на такъв. Забелязваме употребата на оператора is, който връща true ако първият му аргумент "е" от типа, подаден като втори аргумент и false в противен случай.
В примера сме използвали операциите (int)arg и (string)arg, които наричаме преобразуване на типове. На тях ще се спрем след малко, когато разглеждаме предефинирането на оператори и наследяване.
Както и в други обектно-ориентирани езици (например C++), в C# някои оператори могат да бъдат предефинирани. Могат да се предефинират унарни (приемащи един аргумент) и бинарни (приемащи два аргумента) оператори, действащи върху дефинирани от потребителя типове. Предефинирането на оператори се извършва, като разработчикът предоставя собствена имплементация за действието на вградените оператори върху дефинираните от него типове.
Операторите, освен с броя на аргументите си, се характеризират с приоритет и асоциативност. Когато се съставят изрази, съдържащи прилагане на повече от един оператор, редът на прилагането им се определя от приоритета – операторите се прилагат в реда на намаляване на приоритета им. Нека например разгледаме израза a*b+c. В този случай умножението ще се извърши преди събирането, тъй като е с по-висок приоритет, т.е. ако a=1, b=2 и c=5 резултатът ще бъде 7.
Ако имаме израз, който прилага много пъти един и същ оператор, редът на прилагането на тези оператори не може да се определи с помощта на приоритета им. В такъв случай той зависи от асоциативността, която може да бъде лява и дясна. Например, ако имаме израза 1024 / 128 / 8, Резултатът от този израз е 1, тъй като операторът / е лявоасоциативен, т.е. се прилага от ляво на дясно.
В .NET Framework може да се предефинира действието на операторите върху дефинираните от потребителя типове, но не и техните приоритет и асоциативност.
По долу даден е списък с всички оператори в C#, изреден по ред на приоритета им, намаляващ от ляво надясно и отгоре надолу:
- основни: (x) x.y f(x) a[x] x++ x-- new typeof sizeof checked unchecked
- унарни: + - ! ++x --x (T)x
- мултипликативни: * / %
- адитивни: + -
- побитови (bitshift): << >>
- за сравнение: < > <= >= is as
- за равенство: == !=
- логически: & ^ |
- условни: && || c?x:y
- за присвояване: = += -= *= /= %= <<= >>= &= ^= |=
Не всички оператори в C# могат да се предефинират. Предефинируеми са унарните оператори +, -, !, ~, ++, --, true и false, бинарните +, -, *, /, %, &, |, ^, <<, >>, ==, !=, >, <, >= и <=, и операторите за явно (имплицитно) и неявно (експлицитно) преобразуване на типове.
Дефиницията на предефиниран оператор в C# представлява дефиниция на статичен метод, приемащ един или два параметъра и връщащ някакъв резултат, към който е указана ключовата дума operator.
За да илюстрираме предефинирането на оператори ще дефинираме тип "обикновена дроб" (Fraction), който съдържа в себе си обикновена дроб (съставена от числител и знаменател). Ще предефинираме всички основни математически операции за работа с обикновени дроби (събиране, изваждане, умножение, деление и т.н.), както и някои други оператори, които улесняват работата с типа Fraction. Ето една примерна реализация:
|
FractionsTest.cs |
|
public struct Fraction { private long mNumerator; private long mDenominator;
public Fraction(long aNumerator, long aDenominator) { // Cancel the fraction and make the denominator positive long gcd = GreatestCommonDivisor( aNumerator, aDenominator); mNumerator = aNumerator / gcd; mDenominator = aDenominator / gcd;
if (mDenominator < 0) { mNumerator = -mNumerator; mDenominator = -mDenominator; } }
private static long GreatestCommonDivisor( long aNumber1, long aNumber2) { aNumber1 = Math.Abs(aNumber1); aNumber2 = Math.Abs(aNumber2); while (aNumber1 > 0) { long newNumber1 = aNumber2 % aNumber1; aNumber2 = aNumber1; aNumber1 = newNumber1; } return aNumber2; }
public static Fraction operator +(Fraction aF1, Fraction aF2) { long num = aF1.mNumerator*aF2.mDenominator + aF2.mNumerator*aF1.mDenominator; long denom = aF1.mDenominator*aF2.mDenominator; return new Fraction(num, denom); }
public static Fraction operator -(Fraction aF1, Fraction aF2) { long num = aF1.mNumerator*aF2.mDenominator - aF2.mNumerator*aF1.mDenominator; long denom = aF1.mDenominator*aF2.mDenominator; return new Fraction(num, denom); }
public static Fraction operator *(Fraction aF1, Fraction aF2) { long num = aF1.mNumerator*aF2.mNumerator; long denom = aF1.mDenominator*aF2.mDenominator; return new Fraction(num, denom); }
public static Fraction operator /(Fraction aF1, Fraction aF2) { long num = aF1.mNumerator*aF2.mDenominator; long denom = aF1.mDenominator*aF2.mNumerator; return new Fraction(num, denom); }
// Unary minus operator public static Fraction operator -(Fraction aFrac) { long num = -aFrac.mNumerator; long denom = aFrac.mDenominator; return new Fraction(num, denom); }
// Explicit conversion to double operator public static explicit operator double(Fraction aFrac) { return (double) aFrac.mNumerator / aFrac.mDenominator; }
// Operator ++ (the same for prefix and postfix form) public static Fraction operator ++(Fraction aFrac) { long num = aFrac.mNumerator + aFrac.mDenominator; long denom = aFrac.mDenominator; return new Fraction(num, denom); }
// Operator -- (the same for prefix and postfix form) public static Fraction operator --(Fraction aFrac) { long num = aFrac.mNumerator - aFrac.mDenominator; long denom = aFrac.mDenominator; return new Fraction(num, denom); }
public static bool operator true(Fraction aFraction) { return aFraction.mNumerator != 0; }
public static bool operator false(Fraction aFraction) { return aFraction.mNumerator == 0; }
public static implicit operator Fraction(double aValue) { double num = aValue; long denom = 1; while (num - Math.Floor(num) > 0) { num = num * 10; denom = denom * 10; } return new Fraction((long)num, denom); }
public override string ToString() { if (mDenominator != 0) { return String.Format("{0}/{1}", mNumerator, mDenominator); } else { return ("NaN"); // not a number } } }
class FractionsTest { static void Main() { Fraction f1 = (double)1/4; Console.WriteLine("f1 = {0}", f1); Fraction f2 = (double)7/10; Console.WriteLine("f2 = {0}", f2); Console.WriteLine("-f1 = {0}", -f1); Console.WriteLine("f1 + f2 = {0}", f1 + f2); Console.WriteLine("f1 - f2 = {0}", f1 - f2); Console.WriteLine("f1 * f2 = {0}", f1 * f2); Console.WriteLine("f1 / f2 = {0}", f1 / f2); Console.WriteLine("f1 / f2 as double = {0}", (double)(f1 / f2)); Console.WriteLine( "-(f1+f2)*(f1-f2/f1) = {0}", -(f1+f2)*(f1-f2/f1)); } } |
Горният пример дефинира клас, представляващ обвивка на обикновена дроб, или иначе казано, той моделира множеството на рационалните числа. За да могат обектите от типа Fraction действително да имат поведение като на числа, той предефинира унарните оператори -, ++, --, true, false и бинарните +, -, * и /.
Забелязваме също предефинирането на явно преобразуване от Fraction към double и имплицитно от double към Fraction. Добре е да обърнем внимание на това, че вида на преобразуването не е избран случайно. Явно преобразуване се дефинира, когато имаме конвертиране със загуба, тъй като изисква изрично упоменаване на преобразованието. В горния пример конвертирането към double е такова, защото някои рационални числа не могат да бъдат представени с плаваща запетая без загуба на точност. Ако дефинираме преобразуването към double като имплицитно би било възможно по невнимание да присвоим дроб на число с плаваща запетая, но като изискваме изрично преобразуване компилаторът не допуска потенциално опасната операция. Тъй като конвертирането на число с плаваща запетая към рационално винаги може да се извърши без загуба няма нужда да го определяме като явно.
Виждаме, че е допустимо и предефинирането на операторите true и false. Това позволява използването на инстанции от тип Fraction в булеви изрази. Най-лесно това може да се илюстрира с един прост пример. Нека разгледаме следната модифицирана версия на метода ToString() на Fraction:
|
public override string ToString() { if (this) { return String.Format("{0}/{1}", mNumerator, mDenominator); } else { return ("0"); } } |
Така промененият метод, освен че връща текстовото представяне на дробта, също и проверява дали тя е нулева дроб и стойност 0 в този случай. При изчисляването на стойността на булевият израз (this) се изпълнява тялото на предефинирания оператор true.
Използвайки дебъгера на Visual Studio .NET ще проследим изпълнението на кода от примера. За целта:


Ще се спрем отново на понятието наследяване поради особената му важност в обектно-ориентираното програмиране. Няма да обясняваме теоретичната страна наследяването, тъй като това е извън обхвата на настоящата тема. Ще обясним само как да извършваме наследяване на класове със средствата на езика C#.
В C# синтаксиса и семантиката на наследяването са близки до тези в други езици за обектно-ориентирани езици, като C++ и Java. За да направим даден клас Derived наследник на даден друг клас Base, трябва след декларацията на класа Derived да сложим двоеточие, следвано от името на класа Base. За да илюстрираме това, ще разширим един от примерите, които разгледахме по-горе в темата:
|
class Student { private string mName; private int mStudentId; private string mPosition = "Student";
public Student(string aName, int aStudentId) // ...
public Student(string aName) : this(aName, -1) // ...
public void PrintName() { Console.WriteLine("Student name: {0}", mName); } }
public sealed class Kiro : Student { public Kiro() : base("Бай Киро", 12345) { }
public void Oversleep() { //... }
static void Main() { Student tosho = new Student("Тошо", 54321); Kiro kiro1 = new Kiro(); Student kiro2 = new Kiro(); // Kiro kiro3 = new Student("Бай Киро", 12345); // invalid! tosho.PrintName(); kiro1.PrintName(); // kiro2.Oversleep(); ((Kiro)kiro2).Oversleep(); } } |
Виждаме, че класът Kiro наследява класа Student, с което приема от него всички негови полета, свойства, методи и други членове. Разбира се, наследените членове са достъпни за класа Kiro, само ако не са били обявени като private в базовия клас Student.
Трябва да обърнем внимание на третия ред от метода Main(…):
|
Student kiro2 = new Kiro(); |
В него създаваме обект от тип Kiro, но го присвояваме на променлива от тип Student. Тази операция е напълно коректна, тъй като присвояването на обект от наследен тип в променлива от базов тип е позволено. Обратното, обаче, не е в сила и ако разкоментираме втория ред, приложението не би се компилирало.
Обръщението kiro1.PrintName() е също напълно валидно, тъй като класът Kiro наследява всички членове на базовия клас Student и затова съдържа дефиницията на метода PrintName().
В дефиницията на класа Kiro забелязваме употребата на ключовата дума sealed. С нея указваме, че Kiro не може да бъде наследяван от друг клас. Това е пример как чрез забраняването на наследяване можем да създаваме йерархии от класове по-близки до реалните обекти, които представяме. В конкретния пример е удачно да маркираме класа като sealed, тъй като той представлява категория, която не може повече да се конкретизира (Kiro е клас, който съответства на един конкретен обект от действителността, а не на група различни обекти).
В някои обектно-ориентирани езици, като например C++, се допуска наследяване на структури. В C# и в другите .NET езици това не е позволено.
|
|
Структурите в .NET Framework не могат да се наследяват по между си и не могат да наследяват и да бъдат наследявани от класове. |
Нека направим един прост експеримент, за да онагледим невъзможността за наследяване на структури. Със следния код ще създадем една тривиална структура:
|
public struct TestStruct { } |
Отново с помощта на инструмента ildasm получаваме MSIL кода за тази проста структура:

Забелязваме, че структурата TestStruct наследява от System.ValueType и, което в нашия случай е по-интересно, в дефиницията й фигурира модификаторът sealed. Това указва на компилатора, че този тип не може да бъде наследен. Следната ситуация, при която се опитваме да наследим структура от клас, е също недопустима и предизвиква грешка при опит за компилация:
|
public class TestClass { }
public struct AnotherTestStruct : TestClass { } |
Нека сега разгледаме конвертирането (casting) на обект от даден тип към обект от друг тип. При класове в отношение наследник-наследен можем да конвертираме нагоре по йерархията (upcasting) и надолу по йерархията (downcasting). Нека обясним тези две понятия.
С операцията Student kiro2 = new Kiro() от по-горния пример присвояваме обект от клас Kiro на променлива от клас Student, т.е. конвертираме (преобразуваме) обекта към класа Student. В този случай използваме конвертиране нагоре (upcasting), тъй като Student е базов клас на Kiro или, иначе казано, се намира по-горе в йерархията. Тази операция е напълно допустима, тъй като kiro2 действително е студент.
В нашия пример следващият ред
|
Kiro kiro3 = new Student("Бай Киро", 12345); |
е коментиран, тъй като операцията, която там се опитваме да извършим, е недопустима и този код не би могъл да се компилира, тъй като обектът, който конструираме посредством new Student("Бай Киро", 12345) не е инстанция на класа Kiro (въпреки че го наподобява по стойностите на полетата, той не съдържа метода Oversleep()).
С обръщението (Kiro)kiro2 разглеждаме обекта kiro2 като обект от тип Kiro. Тази операция наричаме конвертиране надолу, или downcasting. Типът на израза в скобите е Kiro и заради това можем свободно да извикаме метода Oversleep(), защото въпреки, че е сочен от променлива от тип Student, този израз фактически е инстанция на класа Kiro и съдържа имплементация на метода. На долната илюстрация виждаме, че и Visual Studio .NET разпознава типа на израза като ни предоставя членовете му в падащото меню за автоматично завършване на израза:

|
|
В C# конвертирането надолу е синтактично валидна операция, независимо дали обектът, който конвертираме, е действително от въпросния наследяващ типа. Например, закоментираното обръщение Kiro kiro3 = new Student("Бай Киро", 12345) би могло да се зададе във вида Kiro kiro3 = (Kiro)new Student("Бай Киро", 12345), което се компилира успешно от C# компилатора без дори да генерира предупреждение, тъй като по време на компилация не е известно дали типовете са съвместими. При изпълнението на този код, обаче, въпросното преобразуваме ще предизвика изключение System.InvalidCastException, тъй като конструираният обект не е от тип Kiro или съвместим с него тип. |
Интерфейсите описват функционалност (група методи, свойства, индексатори и събития), която се поддържа от множество обекти. Подобно на класовете и структурите те се състоят от членове, но се различават от тях по това, че дефинират само прототипите на членовете си, без конкретната им реализацията.
От интерфейсите не могат да се създават обекти чрез инстанциране. Интерфейсите се реализират от класове или структури, които имплементират всички дефинирани в тях членове. Конкретните имплементации на даден интерфейс вече могат да се инстанцират и да се присвояват на променливи от тип интерфейс.
Интерфейсите могат да съдържат методи, свойства, индексатори и събития. В интерфейс не могат да се дефинират конструктори, деструктори, полета и вложени типове и не могат да се предефинират оператори.
Интерфейсите в C# не могат и да съдържат и константи, за разлика от други обектно-ориентирани езици, като Java, където това е допустимо.
Към членовете на интерфейс не може да се прилагат модификатори на достъпа – по подразбиране всички членове са с глобална видимост, все едно е указан модификатор public. Интерфейс може да наследи един или повече други интерфейса, като е възможно да предефинира или скрива техните членове. За пример да разгледаме няколко дефиниции на интерфейси:
|
GeometryInterfaces.cs |
|
interface IMovable { void Move(int aDeltaX, int aDeltaY); }
interface IShape { void SetPosition(int aX, int aY); double CalculateSurface(); }
interface IPerimeterShape : IShape { double CalculatePerimeter(); }
interface IResizable { void Resize(int aWeight); void Resize(int aWeightX, int aWeightY); void ResizeByX(int aWeightX); void ResizeByY(int aWeightY); }
interface IDrawableShape : IShape, IResizable, IMovable { void Delete();
Color Color { get; set; } } |
Дефинирахме следните интерфейси: IMovable, IShape, IPerimeterShape, IResizable и IDrawableShape. Те илюстрират дефинирането на методи и свойства в интерфейс, както и наследяването между интерфейси (което може да бъде и множествено, както е например при IDrawableShape).
Тъй като не съдържат данни и описана функционалност, интерфейсите не могат да се инстанцират, а само да се реализират (имплементират) от класове и структури, от които вече могат да се създават инстанции.
Реализирането на интерфейс е операция, подобна на наследяването, с тази особеност, че реализиращият интерфейса тип в общия случай трябва да предостави реализации за всички членове на интерфейса. Ето примерна реализация на някои от дефинираните в горния пример интерфейси:
|
GeomertyImplementation.cs |
|
public class Square : IShape { private int mX, mY, mSize;
public Square(int aX, int aY, int aSize) { mX = aX; mY = aY; mSize = aSize; }
public void SetPosition(int aX, int aY) // From IShape { mX = aX; mY = aY; }
public double CalculateSurface() // Derived from IShape { return mSize * mSize; } }
public struct Rectangle : IShape, IMovable, IResizable { private int mX, mY, mWidth, mHeight;
public Rectangle(int aX, int aY, int aWidth, int aHeight) { mX = aX; mY = aY; mWidth = aWidth; mHeight = aHeight; }
public void SetPosition(int aX, int aY) // From IShape { mX = aX; mY = aY; }
public double CalculateSurface() // Derived from IShape { return mWidth * mHeight; }
public void Move(int aDeltaX, int aDeltaY) // From IMovable { mX += aDeltaX; mY += aDeltaY; }
public void Resize(int aWeight) // Derived from IResizable { mWidth = mWidth * aWeight; mHeight = mHeight * aWeight; }
public void Resize(int aWeightX, int aWeightY) // IResizable { mWidth = mWidth * aWeightX; mHeight = mHeight * aWeightY; }
public void ResizeByX(int aWeightX) // From IResizable { mWidth = mWidth * aWeightX; }
public void ResizeByY(int aWeightY) // From IResizable { mHeight = mHeight * aWeightY; } }
public class Circle : IPerimeterShape { private int mX, mY, mRadius;
public Circle(int aX, int aY, int aRadius) { mX = aX; mY = aY; mRadius = aRadius; }
public void SetPosition(int aX, int aY) // From IShape { mX = aX; mY = aY; }
public double CalculateSurface() // From IShape { return Math.PI * mRadius * mRadius; }
public double CalculatePerimeter() // From IPerimeterShape { return 2 * Math.PI * mRadius; } } |
В този пример виждаме как класът Square реализира интерфейса IShape и как класът Rectangle реализира едновременно няколко интерфейса: IShape, IMovable и IResizable. Класът Circle реализира интерфейса IPerimeterShape, но понеже този интерфейс е наследник на IShape, това означава, че Circle на практика имплементира едновременно интерфейсите IShape и IPerimeterShape. Забележете, че всички методи от интерфейсите са декларирани като публични. Това се изисква по спецификация, защото всички методи в даден интерфейс са публични (въпреки, че нямат модификатор public). Няма да дискутираме как работят самите имплементации, защото това е извън целите на примера.
Имплементирането на интерфейс много прилича на наследяване. Можем да считаме, че то действително е особен вид наследяване, защото също задава "is-a" релация между интерфейса и типа, който го реализира. Например, в сила са твърденията че квадратът и правоъгълникът са форми, а кръгът също е форма, и освен това има периметър.
След като реализирането на интерфейс създава "is-a" релация, можем да говорим и за множество от обекти от тип интерфейс – това са инстанциите на всички класове, които реализират интерфейса пряко или косвено (реализирайки интерфейс, който го наследява), както и техните наследници.
Интересно в горния пример е, че типът Rectangle не е клас, а структура. Това илюстрира една разлика между наследяването на клас и реализирането на интерфейс – второто може да се извърши и от структура.
|
|
Въпреки, че е възможно, не е препоръчителна практика структурите да реализират функционалност и да имплементират интерфейси. Структурите трябва да се използват за съхранение на проста съвкупност от полета. Ако случаят не е такъв, трябва да се използва клас. |
Чрез следващия пример ще демонстрираме създаването на обекти от тип интерфейс. Реално ще създаваме обекти от типове, които наследяват даден интерфейс:
|
GeometryTest.cs |
|
class GeomertyTest { public static void Main() { Square square = new Square(0, 0, 10); Rectangle rect = new Rectangle(0, 0, 10, 12); Circle circle = new Circle(0, 0, 5); if (square is IShape) { Console.WriteLine("{0} is IShape", square.GetType()); } if (rect is IResizable) { Console.WriteLine("{0} is IResizable", rect.GetType()); }
IShape[] shapes = {square, rect, circle}; foreach (IShape shape in shapes) { shape.SetPosition(5, 5); if (shape is IPerimeterShape) { Console.WriteLine("{0} is IPerimeterShape", shape); } } } } |
В горния пример създадохме масив от обекти от тип IShape и към всички приложихме действието SetPosition(…) полиморфно, т. е. без да се интересуваме от точния им тип – единствено знаем, че обектите поддържат методите от интерфейса. Кодът от примера се компилира и изпълнява без грешка и отпечатва следния резултат:
|
Square is IShape Rectangle is IResizable Circle is IPerimeterShape |
Виждаме, че макар и да не можем директно (с конструктор) да създадем обект от тип интерфейс, можем през променлива от този тип да достъпваме обекти от класовете, които го реализират.
Друго интересно явление, което наблюдаваме в горния пример, е че можем да използваме интерфейс, за да приложим полиморфизъм, като полиморфното действие се извършва от типовете, реализиращи интерфейса, независимо дали са класове или структури.
Отново ще обърнем внимание на запазената дума is, която представихме при разглеждането на предаването на произволен брой параметри. Обръщението <обект> is <тип> връща true ако обектът е от дадения тип и false в противен случай. Трябва да имаме предвид, че обектите от тип-наследник са обекти и от базовия тип, за това <обект> is <базов_тип> винаги връща true.
В горния пример това обръщение се среща три пъти, като при първите два от тях по време на компилация получаваме предупреждение "The given expression is always of the provided type" – съобщение, с което сме напълно съгласни. Действително, типът на обектите circle и rect се определя по време на компилация и още тогава е известно, че проверяваното условие е винаги истина.
За обръщението в тялото на цикъла не получаваме предупреждение и в този случай на употреба виждаме истинската мощ на оператора is – проверка за типа на обект, който не е известен в момента на компилация.
Както споменахме по-рано в тази тема, класовете и структурите могат да имплементират по повече от един интерфейс. Това би могло да създаде конфликт, ако един тип имплементира няколко интерфейса, съдържащи методи с еднакви сигнатури. Да разгледаме следния пример:
|
public interface I1 { void Test(); }
public interface I2 { void Test(); void AnotherTest(); }
public class TestImplementation : I1, I2 { public void Test() { Console.WriteLine("Test() called"); } } |
Горният код е допустим в C#, но използването му не се препоръчва. То създава затруднения, от една страна, защото не е ясно в кой интерфейс е дефиниран методът Test() в класа TestImplementation, и от друга, защото няма възможност да предостави различни имплементации за метода от различните интерфейси.
За да се справим с описания проблем можем да използваме явната имплементация на интерфейси (explicit interface implementation). В C# можем да дефинираме в един тип два метода с еднаква сигнатура, стига поне единият от тях да е явна имплементация на метод от интерфейс. Явна имплементация се задава, като изрично се укаже на кой интерфейс принадлежи имплементираният член, както в примера:
|
public class TestExplicit : I1, I2 { void I1.Test() { Console.WriteLine("I1.Test() called"); }
void I2.Test() { Console.WriteLine("I2.Test called"); }
void I2.AnotherTest() { Console.WriteLine("I2.AnotherTest called"); }
public void Test() { Console.WriteLine("TestExplicit.Test() called"); }
public static void Main() { TestExplicit t = new TestExplicit();
t.Test(); // Prints: TestExplicit.Test() called
I1 i1 = (I1) t; i1.Test(); // Prints: I1.Test() called
I2 i2 = (I2) t; i2.Test(); // Prints: I2.Test() called } } |
Виждаме как при явна имплементация на интерфейс трябва да укажем името на интерфейса в дефиницията на реализирания член, а за да го достъпим трябва да преобразуваме обекта към интерфейса. Методите, принадлежащи на явно имплементирани интерфейс, не могат да бъдат публични или да имат друг модификатор за достъп. Те винаги private.
|
|
Не е позволено да имплементираме явно само някои членове от един интерфейс. В горния пример ако променим дефиницията на метода I2.AnotherTest() на public void AnotherTest(), компилаторът ще съобщи за грешка. |
При изпълнение на примерния код се получава следният резултат:
|
TestExplicit.Test() called I1.Test() called I2.Test called |
Абстрактните класове приличат на интерфейсите по това, че те не могат да се инстанцират, защото могат да съдържат дефиниции на неимплементирани методи, но за разлика от интерфейсите могат да съдържат и описани действия. Абстрактният клас реално е комбинация между клас и интерфейс – частично имплементиран клас, който дефинира имплементация за някои от методите си, а други оставя абстрактни, без имплементация.
За пример нека разгледаме следния абстрактен клас:
|
AbstractTest.cs |
|
public abstract class Car { public void Move() { // Move the car }
abstract public int TopSpeed { // Retrieve the top speed in Kmph get; }
public abstract string BrandName { get; } } |
Дефинирахме клас, който реализира само един от членовете си – метода Move() и дефинира други два, без да ги реализира – свойствата BrandName и TopSpeed.
Абстрактните класове, подобно на интерфейсите, ни помагат по-адекватно да моделираме зависимости от реалният свят, защото чрез тях могат да се представят абстрактни същности. В нашия пример невъзможността за инстанциране на класа Car има смисъл, тъй като и в реалността не можем да имаме кола с неопределена марка.
Ключовата дума abstract в декларацията на класа го определя като абстрактен. Виждаме, че тя може да се приложи и към член. Абстрактни могат да бъдат методите, свойствата, индексаторите и събитията.
|
|
Абстрактните членове не могат да имат имплементация, както и член, който не е абстрактен, не може да бъде оставен без такава. |
Ако в един клас е дефиниран абстрактен член, класът задължително трябва да бъде обявен за абстрактен. В противен случай получаваме грешка при компилация. Обратното не е задължително – допустимо е да имаме абстрактен клас, на който всички членове са дефинирани.
Тъй като абстрактните класове са класове, те имат същата структура - същият набор от членове (полета, константи, вложени типове и т. н.), същите модификатори на видимостта и дори същите механизми за наследяване, но с някои особености. Нека разширим предходния пример:
|
AbstractTest.cs |
|
public class Trabant : Car { public override int TopSpeed { get { return 120; } }
public override string BrandName { get { return "Trabant"; } } }
public class Porsche : Car { public override int TopSpeed { get { return 250; } }
public override string BrandName { get { return "Porsche"; } } }
public class AbstractTest { static void Main() { Car[] cars = new Car[] {new Trabant(), new Porsche()}; foreach (Car car in cars) { Console.WriteLine("A {0} can go {1} Kmph", car.BrandName, car.TopSpeed); } } } |
При изпълнението на този код получаваме следния резултат:
|
A Trabant can go 120 Kmph A Porsche can go 250 Kmph |
Виждаме, че въпреки че абстрактният клас не може да се инстанцира директно, обектите от наследяващите го класове могат да се разглеждат като обекти от неговия тип. По показания начин можем да използваме абстрактни базови класове, за да задействаме полиморфизъм, или, казано по-общо, да създадем абстрактен корен на дърво от класове.
В примера ползвахме ключовата дума override, с която указваме, че даден метод в класа наследник припокрива (замества) оригиналния наследен метод от базовия си клас. В случая базовия клас не предоставя имплементация за припокритите методи, така че припокриването е задължително. Ще разгледаме ключовата дума override и нейното действие след малко. Нека сега продължим с абстрактните класове.
Възможно е абстрактен клас, съдържащ абстрактни членове, да бъде наследен, без всичките му абстрактни членове да бъдат реализирани. Възможно е също клас, който имплементира абстрактните членове на абстрактния си родител, да дефинира допълнително и свои членове, също абстрактни. В този случай класът-наследник също трябва да бъде деклариран като абстрактен, защото съдържа абстрактни членове.
Тези възможности правят още по-гъвкав инструментариума за създаване на йерархии от класове и моделиране на реалния свят. Ще илюстрираме тази възможност със следното разширение на предходния пример:
|
AbstractTest.cs |
|
abstract public class TurboCar : Car { protected Boolean mTurboEnabled = false;
public void EnableTurbo() { mTurboEnabled = true; }
public void DisableTurbo() { mTurboEnabled = false; } }
public class TrabantTurbo : TurboCar { override public int TopSpeed { get { return mTurboEnabled ? 220 : 120; } }
override public string BrandName { get { return "Trabant Turbo"; } } }
public class AbstractTest { static void Main() { TurboCar turboCar = new TrabantTurbo(); Console.WriteLine("A {0} can go {1} Kmph", turboCar.BrandName, turboCar.TopSpeed);
turboCar.EnableTurbo(); Console.WriteLine( "A {0} can go {1} Kmph with turbo enabled", turboCar.BrandName, turboCar.TopSpeed); } } |
Създадохме класа TrabantTurbo, който реализира абстрактните свойства, индиректно наследени от класа TurboCar. Класът TurboCar е разширение на класа Car, който също като него е абстрактен, но предоставя допълнителна функционалност за включване на режим "турбо".
|
|
Ако един клас наследи от абстрактен и не предостави дефиниции за всички негови абстрактни членове, той трябва задължително също да бъде обявен за абстрактен. |
След изпълнението на примера получаваме следния резултат:
|
A Trabant Turbo can go 120 Kmph A Trabant Turbo can go 220 Kmph with turbo enabled |
В дефинициите на членовете в горните примери забелязваме употребата на запазената дума override. Без нея те не биха могли да бъдат компилирани. Това е така, защото въпросните членове са виртуални.
Виртуалните членове са един по-особен вид членове, без които полиморфизмът би бил неосъществим. Тяхната особеност проличава при наследяване – на наследяващите класове се дава възможност вместо изцяло да пресъздадат даден наследен виртуален метод, просто да предоставят своя имплементация на същия. Така, ако работим с обект от наследения клас през референция към базовия, той ще разполага с имплементациите, които наследникът е предоставил. Ще си изясним този механизъм при разглеждането на предефиниране и скриване на виртуални членове.
Виртуални членове се дефинират, като в дефиницията им се укаже ключовата дума virtual. Всички абстрактни членове, включително и тези, дефинирани в интерфейсите (и те са абстрактни, тъй като нямат имплементация), са винаги виртуални. Поради тази причина в някои обектно-ориентирани езици за програмиране (например в C++) абстрактните членове се наричат още "чисто виртуални".
При дефиниране на виртуален член в тип-наследник, чиято сигнатура съвпада с член, дефиниран в някои от базовите типове, той може или да се предефинира (да му се даде нова имплементация) или да се "скрие".
Когато се използва ключовата дума override, се реализира предефиниране на виртуалния член, а когато се използва ключовата дума new – скриване, което е и опцията, която се подразбира когато не се укаже никаква ключова дума.
|
|
Когато в наследен клас се предефинира виртуален член на базовия, този член е виртуален и в наследения клас. |
Най-лесно ще доловим разликата между скриването и предефинирането на членове, като първо обърнем внимание на следната модификация на по-горния пример. В нея вместо абстрактен сме използвали нормален, конкретен клас, който съдържа дефиниции на свойствата, връщащи подразбиращи се стойности и вместо override сме използвали new:
|
NonAbstractTest.cs |
|
public class Car { public virtual int TopSpeed { // Retrieve the top speed in Kmph get { return -1; // Default value } }
public virtual string BrandName { get { return "unknown"; // Default value } } }
public class Trabant : Car { new public int TopSpeed { get { return 120; } }
new public string BrandName { get { return "Trabant"; } } }
public class Porsche : Car { new public int TopSpeed { get { return 250; } }
new public string BrandName { get { return "Porsche"; } } }
public class NonAbstractTest { static void Main() { Car[] cars = new Car[] {new Trabant(), new Porsche()}; foreach (Car car in cars) { Console.WriteLine("A {0} can go {1} Kmph", car.BrandName, car.TopSpeed); } } } |
При изпълнението на този код получаваме следния, донякъде разочароващ, резултат:
|
A unknown can go -1 Kmph A unknown can go -1 Kmph |
Причината резултатът да се разминава с очакванията ни е, че при скриването на членовете наследяващият клас не предоставя своята дефиниция на базовия. Така, когато достъпваме обект от наследен клас през референция към обект от базовия, разполагаме само с неговите собствени реализации (на базовия клас). Поради това не можем да използваме полиморфизъм – когато достъпваме обект от базов клас, независимо от специфичният му тип, винаги ще ползваме имплементацията, дефинирана в базовия, т. е. той може приеме само една форма.
Трябва да отбележим, че ако пропуснем запазената дума new, поведението на кода ще бъде същото, но ще получим предупреждение от компилатора "The keyword new is required on '<method_name>' because it hides inherited member".
Ако в горния пример заменим new с override, ще задействаме механизма на полиморфизма и резултатът ще бъде следния:
|
A Trabant can go 120 Kmph A Porsche can go 250 Kmph |
Ако в горния пример пропуснем да обявим членовете TopSpeed и BrandName като виртуални, ще получим същия разочароващ резултат, както и преди:
|
A unknown can go -1 Kmph A unknown can go -1 Kmph |
Виждаме, че при използването на полиморфизъм има много варианти да сбъркаме и да получим неправилно поведение. Затова можем да запомним следното правило:
|
|
За да действа полиморфизмът, трябва полиморфният метод в базовия тип да е виртуален (да е обявен като virtual, abstract или да е член на интерфейс) и в класа наследник да е имплементиран с override. |
Клас диаграмите са стандартно графично средство за изобразяване на йерархии от типове, предоставено ни от езика за моделиране UML (Unified Modeling Language). Ще се запознаем съвсем накратко с клас диаграмите без да претендираме за изчерпателност, тъй като моделирането с UML е необятна тема, на която са посветени хиляди страници и тази материя е извън обхвата на настоящата тема.
При многократно наследяване е възможно да се получат йерархии, които са големи и сложни и по тази причина са трудни за възприемане. Чрез клас диаграмите се създава визуална представа за взаимовръзките между типовете и така се улеснява възприемането им. С помощта на клас диаграмите можем да погледнем системата, която разработваме "от птичи поглед", което ни помага да си създадем значително по-ясна представа за нея, отколкото ако преглеждаме множество файлове със сорс код.
В UML клас диаграмите типовете се изобразяват като правоъгълници, в които са изписани членовете им, евентуално с отбелязана степен на видимост пред името: + за public, # за protected и - за private. Ето един пример (класът Rectangle):

Правоъгълникът, изобразяващ даден тип, обикновено е разделен на три части – най-горната съдържа името му, средната съдържа полетата му и най-долната съдържа неговите методи.
Наследяването на клас и имплементирането
на интерфейс се изобразява със затворена стрелка (
),
като стрелките, обозначаващи наследяване и имплементиране се различават по това,
че първите обикновено са плътни, а вторите – пунктирани:

В примера класът FilledRectangle наследява класа Rectangle, а класът Square имплементира интерфейса ISurfaceCalculatable, а.
Връзките между типовете се изобразяват с отворена
стрелка (
).
Тези връзки се наричат още асоциациационни връзки (association links).
Асоциационните връзки могат да бъдат три вида (асоциация, агрегация, композиция). Асоциация е просто някаква връзка между два типа, примерно даден студент използва даден компютър (асоциацията е между студента и компютъра). Агрегация означава че даден клас съдържа много инстанции на даден друг клас, но вторият може да съществува отделно и без първия, примерно една учебна група се състои от много студенти, но студентите могат да съществуват и самостоятелно, без да са в дадена учебна група. Композиция между два класа означава, че един клас се използва като съставна част от друг и не може да съществува без него, примерно един правоъгълник се състои от 4 страни, но страните не могат да съществуват самостоятелно без правоъгълника.
Връзките композиция и агрегация могат да имат множественост, например "1 към 1", "1 към много" и т.н. Пример за множественост на връзка е връзката между студент и учебна дисциплина (например "1 към много" – 1 студент изучава много учебни дисциплини).
Следният пример представлява проста диаграма и илюстрира основните елементи, които ни предоставя UML нотацията за изграждане на клас диаграми:

По затворените стрелки разбираме, че класовете Square и Rectangle наследяват Shape и имплементират интерфейса ISurfaceCalculatable, а те от своя страна са наследени съответно от FilledSquare и FilledRectangle.
Виждаме също как с отворени стрелки е изобразена връзката "тип съдържа инстанция на друг тип като свой член", както например класът FilledRectangle съдържа инстанция на структурата Color.
Пространствата от имена (namespaces) са средство за организиране на кода в софтуерните проекти. Те съдържат дефиниции на класове, структури, изброени типове и други пространства от имена, като по този начин осигуряват логическо групиране на множества от типове. Пространствата от имена не могат да съдържат дефиниции на функции и данни, тъй като езиците от .NET Framework са строго обектно-ориентирани и такива дефиниции се допускат само в тялото на типовете.
Пространства от имена в C# се дефинират и използват подобно на пространствата от имена в C++ и на пакетите в Java. Задават се с ключовата дума namespace последвана от името на пространството и множеството от дефиниции на типове, оградено във фигурни скоби, както е показано на примера по-долу:
|
namespace SofiaUniversity { // Type definitions ... } |
Тази дефиниция може да присъства в повече от един файл, като по този начин се създава пространство, което е физически разпределено в различните файлове.
Достъпът до дефинираните в тялото на пространство типове се осъществява по два начина – чрез използване на пълно име на типа и с използването на ключовата дума using.
Пълно име наричаме името на типа предшествано от името на пространството, в което се намира, разделени с точка. Например ако класът AdministrationSystem е дефиниран в пространството SofiaUniversity, тогава пълното му име е AdministrationSystem.SofiaUniversity. По този начин се обръщаме към имена на типове, дефинирани в пространства, различни от текущото.
Използването на пространства от имена позволява дефинирането на типове с едно и също име, стига те да са в различни пространства. Посредством използването на пълни имена се разрешават конфликтите, породени от еднаквите имена на типовете. Например клас с име Config може да е дефиниран както в пространството SofiaUniversity. DataAccess, така и в SofiaUniversity.InternetUtilities. Ако е необходимо в даден клас да бъдат използвани едновременно и двата класа, те се достъпват с пълните си имена: SofiaUniversity.DataAccess.Config и SofiaUniversity.InternetUtilities.Config.
Директивата using <namespace_name>, поставена в началото на файла, позволява директно използване на всички типове от указаното пространство само чрез краткото им име. Пример за това е следният фрагмент от кода, който се генерира автоматично от Visual Studio .NET при създаването на нов файл:
|
using System; |
Това обръщение прави достъпно за програмата основното пространство от имена на .NET Framework – System, което съдържа някои типове, които се използват постоянно – Object, String, Int32 и др.
Както вече споменахме, пространствата от имена могат да съдържат и дефиниции на други пространства. По този начин можем да създаваме йерархии от пространства от имена, в които да разполагаме типовете, които дефинираме.
Подпространства могат да бъдат дефинирани в тялото на пространството родител, но могат да бъдат създадени и в отделен файл. В такъв случай се използва пълно име на пространство от имена. То представлява собственото име на пространството предшествано от родителите му, разделени с точки, както например System.Windows.Forms. Пълното име на тип, дефиниран в подпространство, трябва да съдържа пълното му име, например System.Windows.Forms.Form.
Следва да илюстрираме дефинирането на една простра структура от пространства от имена:
|
namespace SofiaUniversity.Data { public struct Faculty { // ... } public class Student { // ... } public class Professor { // ... } public enum Specialty { // ... } }
namespace SofiaUniversity.UI { public class StudentAdminForm : System.Windows.Forms.Form { // ... } public class ProfessorAdminForm : System.Windows.Forms.Form { // ... } } namespace SofiaUniversity { public class AdministrationSystem { public static void Main() { // ... } } } |
В примера по-горе виждаме дефинициите на основното пространство SofiaUniversity и подпространствата му SofiaUniversity.Data и SofiaUniversity.UI, в които сме дефинирали нашите потребителски типове, например класовете SofiaUniversity.AdministrationSystem и SofiaUniversity.UI.StudentAdminForm, и структурата SofiaUniversity. Data.Faculty.
Използвайки директивата using можем да включваме пространства, зададени с пълното им име. Тази директива включва единствено това пространство, което споменаваме изрично, но не и неговите подпространства. Например, ако укажем using System.Windows няма да имаме директен достъп до класа System.Windows.Forms.Form.
Използвайки ключовата дума using можем да задаваме също и псевдоними на пълните имена на пространствата, както например:
|
using WinForms = System.Windows.Forms;
namespace SofiaUniversity.UI { public class StudentAdminForm : WinForms.Form { // ... }
// ... } |
Основната цел на използването на пространства от имена е създаването на добре организирани и структурирани софтуерни системи. За целта трябва да разделяме типовете, които дефинираме, в пространства, чиято структура отговаря на логическата организация на обектите с които работим. Ако се придържаме към някои прости принципи при изграждането на структури от пространства и типове, можем да създадем значително по-ясни и интуитивни за възприемане проекти без да рискуваме вместо това допълнително да си усложним живота.
Изключително полезно е да разпределяме типовете, които дефинираме, в пространства от имена. Това е задължително, ако те са много на брой, например над 20, защото прекалено много елементи на едно място са по-трудни за възприемане не само в програмирането. Можем да създаваме и вложени пространства, но само ако е необходимо - не трябва да изпадаме и в другата крайност, защото ако създаваме прекалено много пространства от имена ще се окажем с излишно сложна структура от пространства, която няма да направи организацията в проекта ни по-ясна, даже напротив.
Добре е логическата организация в системите, които разработваме, да отговаря на физическата – публичните типове да създаваме във файлове, носещи тяхното име, а за пространствата – директории с тяхното име, в които да се поместват типовете им. Когато създаваме вложени пространства, е добре да ги създаваме като поддиректории на тези на родителите им пространства. Така само с един поглед на структурата на проекта в Solution Explorer на Visual Studio .NET добиваме представа за нея.
За проекта от примера по-горе е удачно да организираме типовете във файлове по следния начин:

Виждаме, че класът Student от пространството SofiaUniversity.Data е разположен във файла Student.cs от поддиректорията Data на директория SofiaUniversity от нашия проект. По същия принцип класът ProfessorAdminForm се намира във файла SofiaUniversity/UI/ ProfessorAdminForm.cs.
При такава организация е много лесно да се запознаем визуално и с логическата, и с физическата структура на компонентите, които изграждат проекта ни. Когато двете не се разминават, навигацията в сорс кода на системата и като цяло работата с нея се улеснява значително.
Ще разгледаме няколко много важни принципа за ефективно проектиране на типове, които всеки добър софтуерен разработчик трябва да познава и прилага. Тези принципи не се отнасят само за езика C#, а са важни концепции при проектирането и изграждането на софтуер. Те намират приложение дори не само в софтуерното инженерство, но и във всички инженерни дисциплини като цяло.
Когато създаваме софтуерни системи целим да опростим работата по разработването, поддържането и развиването им. Това постигаме като се придържаме към ясна и разбираема структура на системата, близка до проблемната област, към която е ориентирана тази система. Добре направеният обектно-ориентиран дизайн намалява значително усилията за изучаване на системата при извършването на промени. За да го постигнем, е необходимо да се съобразяваме с няколко основни принципа, които ще разгледаме сега.
Когато създаваме типове, които минимално зависят един от друг, можем да променяме всеки от тях без да е необходимо задълбочено познаване на цялата система. Към този принцип за функционална независимост трябва да се придържаме и когато дефинираме членовете на един тип. Ако минимизираме взаимозависимостите в системите, които разработваме, ще можем много по-лесно да използваме вече създадените модули, типове и методи в други проекти.
При проектирането на типове трябва да следваме принципа, че даден тип трябва да има ясна цел и да зависи минимално от останалите типове. Тази независимост улеснява поддръжка, опростява дизайна и позволява по-лесно преизползване на кода.
Трябва да се стремим типовете да издават възможно най-малко тайни за това как са имплементирани вътрешно. Потребителите на даден тип трябва да виждат като публични само свойствата и методите, които ги засягат, а останалите трябва да са скрити. Това намалява сложността на системата, защото намалява общия брой детайли, за които потребителят на даден тип трябва да мисли, когато иска да го използва. Скриването на имплементационните детайли (чрез капсулация) позволява промяната в имплементацията на даден тип без да се променя никой от типовете, който го използва.
Тъй като клас диаграмите показват връзките между типовете, те ни помагат да идентифицираме нивото на независимост между тях. Използвайки клас диаграми можем чисто визуално да преценим дали типовете, които използваме, имат прекалено много зависимости помежду си.
Действията, които даден метод или клас извършва, трябва да бъдат логически свързани, да са насочени към решаването на една обща задача (не няколко логически несвързани задачи). Това свойство, е известно още като модулност. За да имаме ефективна модуларизация в проекта, който разработваме, трябва всички типове да предоставят ясен интерфейс, който е възможно най-прост и не съдържа излишни методи и свойства. Необходимо е още всички методи в типовете да са свързани логически и да имат имена, които ясно подсказват за какво служат. Не трябва да имаме типове, които имат няколко несвързани логически отговорности и изпълняват разнородни задачи. Това е признак на лош дизайн и води до много проблеми при поддръжката на системата.
Препоръчително е всеки тип, с който работим, както и всеки негов метод, да е свързан с решаването на обща задача и всяко действие, което се извършва да е стъпка или елемент от решаването й. Така системите които изграждаме, ще бъдат много по-разбираеми, и от там лесни за разширяване и поддръжка. Силната свързаност намалява сложността в проектите като спомага за ефективното разделяне на отговорностите в системата.
1. Формулирайте основните принципи на обектно-ориентираното програмиране. Дефинирайте понятията клас, обект, атрибут, метод, енкапсулация на данните, абстракция на данните и действията, наследяване, полиморфизъм.
2. Дефинирайте клас Student, който съдържа като private полета данните за един студент – трите имена, ЕГН, адрес (постоянен и временен), телефон (стационарен и мобилен), e-mail, курс, специалност, ВУЗ, факултет и т.н. Използвайте изброен тип (enumeration) за специалностите, ВУЗ-овете и факултетите. Дефинирайте свойства за достъп до полетата на класа.
3. Дефинирайте няколко конструктора за класа Student, които приемат различни параметри (пълните данни за студента или само част от тях). Неизвестните данни запълвайте с 0 или null.
4. Добавете в класа Student статично поле, което съдържа количеството инстанции, създадени от този клас от стартирането на програмата до момента. За целта променете по подходящ начин конструкторите на класа, така че да следят броя създадени инстанции.
5. Направете класа Student структура. Какви са разликите между клас и структура?
6. Направете нов клас StudentsTest, който има статичен метод за отпечатване на информацията за един или няколко студента. Методът трябва да приема променлив брой параметри.
7. Добавете към класа StudentsTest няколко статични полета от тип Student и статичен конструктор, който създава няколко инстанции на структурата Student с някакви примерни данни и ги записва в съответните статични полета.
8. Създайте интерфейс IAnimal, който моделира животните от реалния свят. Добавете към него метод Talk(), който отпечатва на конзолата специфичен за животното писък, булево свойство Predator, което връща дали животното е хищник и булев метод CouldEat(IAnimal), който връща дали животното се храни с посоченото друго животно. За проверка на типа животно използвайте оператора is.
9. Създайте класове, които имплементират интерфейса IAnimal и моделират животните "куче" и "жаба".
10. Създайте абстрактен клас Cat за животното "котка", който имплементира частично интерфейса IAnimal.
11. Създайте класове Kitten и Tomcat за животните "малко котенце" и "стар котарак", които наследяват абстрактния клас Cat и имплементират неговите абстрактни методи
12. Създайте клас CrazyCat, наследник на класа Tomcat за животното "луда котка", което издава кучешки звуци при извикване на виртуалния метод Talk().
13. Реализирайте клас със статичен метод, който инстанцира по един обект от всеки от класовете, поддържащи интерфейса IAnimal, и им извиква виртуалния метод Talk() през интерфейса IAnimal. Съответстват ли си животинските писъци на животните, които ги издават?
14. Направете всички полета на структурата Student с видимост private. Добавете дефиниции на свойства за четене и писане за всички полета.
15. Направете свойството за достъп до ЕГН полето от структурата Student само за четене. Направете и полето за ЕГН само за четене. Не забравяйте задължително да го инициализирате от всички конструктори на структурата.
16. Напишете клас, който представя комплексни числа и реализира основните операции с тях. Класът трябва да съдържа като private полета реална и имагинерна част за комплексното число и да предефинира операторите за събиране, изваждане, умножение и деление. Реализирайте виртуалния метод ToString() за улеснение при отпечатването на комплексни числа.
17. Реализирайте допълнителни оператори за имплицитно преобразуване на double в комплексно число и експлицитно преобразуване на комплексно число в double.
18. Добавете индексатор в класа за комплексни числа, който по индекс 0 връща реалната част, а по индекс 1 връща имагинерната част на дадено комплексно число.
19. Организирайте всички дефинирани типове в няколко пространства от имена.
20. Направете конструкторите на структурата Student да подава изключения при некоректно зададени данни за студент.
21. Добавете предизвикване на изключения в класа за комплексни числа, където е необходимо.
1. Светлин Наков, Обектно-ориентирано програмиране в .NET – http://www.nakov.com/dotnet/lectures/Lecture-3-Object-Oriented-Concepts-v1.0.ppt
2. Jeffrey Richter, Applied Microsoft .NET Framework Programming, Microsoft Press, 2002, ISBN 0735614229
3. Tom Archer, Andrew Whitechapel, Inside C#, 2-nd Edition, Microsoft Press, 2002, ISBN 0735616485
4. Erika Ehrli Cabral, OOPs Concepts in .NET Framework – http://www.c-sharpcorner.com/Code/2005/June/OOPSand.NET1.asp
5. MSDN Training, Programming C# (MOC 2124C), Module 5: Methods and Parameters
6. MSDN Training, Programming C# (MOC 2124C), Module 7: Essentials of Object-Oriented Programming
7. MSDN Training, Programming C# (MOC 2124C), Module 9: Creating and Destroying Objects
8. MSDN Training, Programming C# (MOC 2124C), Module 10: Inheritance in C#
9. MSDN Training, Programming C# (MOC 2124C), Module 12: Operators, Delegates, and Events
10. MSDN Training, Programming C# (MOC 2124C), Module 13: Properties and Indexers
11. MSDN Library – http://msdn.microsoft.com/
12. Visual Case Tool – UML Tutorial, The Class Diagram – http://www. visualcase.com/tutorials/class-diagram.htm
13. Steve McConnell, Code Complete, 2nd Edition, Microsoft Press, 2004, ISBN 0735619670
|
Национална академия по разработка на софтуер |
|
|
Лекторите » Светлин Наков е автор на десетки технически публикации и няколко книги, свързани с разработката на софтуер, заради което е търсен лектор и консултант. Той е разработчик с дългогодишен опит, работил по разнообразни проекти, реализирани с различни технологии (.NET, Java, Oracle, PKI и др.) и преподавател по съвременни софтуерни технологии в СУ "Св. Климент Охридски". През 2004 г. е носител на наградата "Джон Атанасов" на президента на България Георги Първанов. Светлин Наков ръководи обучението по Java технологии в Академията.
» Мартин Кулов е софтуерен инженер и консултант с дългогодишен опит в изграждането на решения с платформите на Microsoft. Мартин е опитен инструктор и сертифициран от Майкрософт разработчик по програмите MCSD, MCSD.NET, MCPD и MVP и международен лектор в световната организация на .NET потребителските групи INETA. Мартин Кулов ръководи обучението по .NET технологии в Академията. |
Академията » Национална академия по разработка на софтуер (НАРС) е център за професионално обучение на софтуерни специалисти.
» НАРС провежда БЕЗПЛАТНО курсове по разработка на софтуер и съвременни софтуерни технологии в София и други градове.
» Предлагани специалности: § Въведение в програмирането (с езиците C# и Java) § Core .NET Developer § Core Java Developer
» Качествено обучение с много практически проекти и индивидуално внимание за всеки.
» Гарантирана работа! Трудов договор при постъпване в Академията.
» БЕЗПЛАТНО! Учите безплатно във въведителните курсове и по стипендии от работодателите в следващите нива. |
- Базови познания за архитектурата на .NET Framework
- Базови познания за езика C#
- Какво е изключение в .NET?
- Прихващане
- Свойства
- Йерархия и видове
- Предизвикване (хвърляне)
- Дефиниране на собствени
- Препоръчвани практики
В настоящата тема ще разгледаме изключенията в .NET Framework като утвърден механизъм за управление на грешки и непредвидени ситуации. Ще обясним как се прихващат и обработват. Ще разгледаме начините за хвърляне на изключение. Ще се запознаем накратко с различните видове изключения в .NET Framework. Ще дадем примери за дефиниране на собствени (потребителски) изключения.
В обектно-ориентираното програмиране (ООП) изключенията представляват мощно средство за централизирана обработка на грешки и необичайни ситуации. Те заместват в голяма степен процедурно-ориентирания подход, при който всяка функция връща като резултат от изпълнението си код на грешка (или неутрална стойност ако не е настъпила грешка).
В ООП кодът, който извършва дадена операция, обикновено предизвиква изключение, когато в него възникне проблем и операцията не може да бъде изпълнена успешно. Методът, който извиква операцията може да прихване изключението и да обработи грешката или да пропусне изключението и да остави то да бъде прихванато от извикващият го метод. Така не е задължително грешките да бъдат обработвани непосредствено от извикващия код, а могат да се оставят за тези, които са го извикали. Това дава възможност управлението на грешките и необичайните ситуации да се извършва на много нива.
Друга основна концепция при изключенията е тяхната йерархична същност. Изключенията в ООП са класове и като такива могат да образуват йерархии посредством наследяване. При прихващането на изключения може да се обработват наведнъж цял клас от грешки, а не само дадена определена грешка (както е в процедурното програмиране).
В ООП се препоръчва чрез изключения да се управлява всяко състояние на грешка или неочаквано поведение, възникнало по време на изпълнението на една програма.
Изключенията в .NET са класическа имплементация на изключенията от ООП, макар че притежават и допълнителни възможности, произтичащи най-вече от предимствата на управлявания код.
В .NET Framework управлението на грешките се осъществява предимно чрез изключения. Всички операции от стандартната библиотека на .NET (Framework Class Library) сигнализират за грешки посредством хвърляне (throw, raise) на изключение. .NET програмистите трябва да се съобразяват с изключенията, които биха могли да възникнат и да предвидят код за тяхната обработка в някой от извикващите методи.
Изключение може да възникне поради грешка в нашия код или в код който извикваме (примерно библиотечни функции), при изчерпване на ресурс на операционната система, при неочаквано поведение в .NET средата (примерно невъзможност за верификация на даден код) и в много други ситуации.
В повечето случаи едно приложение е възможно да се върне към нормалната си работа след обработка на възникнало изключение, но има и ситуации в които това е невъзможно. Такъв е случаят при възникване на някои runtime изключения. Пример за подобна изключителна ситуация е, когато една програма изчерпа наличната работна памет. Тогава CLR хвърля изключение, което сигнализира за настъпилия проблем, но програмата не може да продължи нормалната си работа и единствено може да запише състоянието на данните, с които работи (за да минимизира загубите), и след това да прекрати изпълнението си.
Всички изключения в .NET Framework са обекти, наследници на класа System.Exception, който ще разгледаме в детайли след малко. Всъщност, съществуват и изключения, които не отговарят на това изискване, но те са нестандартни и възникват рядко. Тези изключения не са съвместими със CLS (Common Language Specification) и не могат да се предизвикат от .NET езиците (C#, VB.NET и т. н.), но могат да възникнат при изпълнение на неуправляван код.
Изключенията носят в себе си информация за настъпилите грешки или необичайни ситуации. Тази информация може да се извлича от тях и е много полезна за идентифицирането на настъпилия проблем. В .NET Framework изключенията пазят в себе си името на класа и метода, в който е възникнал проблемът, а ако асемблито е компилирано с дебъг информация, изключенията пазят и името на файла и номера на реда от сорс кода, където е възникнал проблемът.
Когато възникне изключение, изпълнението на програмата спира. CLR средата запазва състоянието на стека и търси блока от кода, отговорен за прихващане и обработка на възникналото изключение. Ако не го намери в границите на текущия метод, го търси в извикващия го метод. Ако и в него не го намери, го търси в неговия извикващ и т. н. Ако никой от извикващите методи не прихване изключението, то се прихваща от CLR, който показва на потребителя информация за възникналия проблем.
Изключенията улесняват писането и поддръжката на надежден програмен код, като дават възможност за обработката на проблемните ситуации на много нива. В .NET Framework се позволява хвърляне и прихващане на изключения дори извън границите на текущия процес.
Работата с изключения включва две основни операции – прихващане на изключения и предизвикване (хвърляне) на изключения. Нека разгледаме първо прихващането на изключения в езика C#.
В C# изключенията се прихващат с програмната конструкция try-catch:
|
try { // Do some work that can raise an exception } catch (SomeExceptionClass) { // Handle the caught exception } |
Кодът, който може да предизвика изключение, се поставя в try блока, а кодът, отговорен за обработка му – в catch блока.
Catch блокът може да посочи т. нар. филтър за прихващане на изключения или да го пропусне. Филтърът представлява име на клас, поставен в скобки като параметър на catch оператора. В горния пример филтърът задава прихващане на изключения от класа SomeExceptionClass и всички класове, негови наследници. Ако филтърът бъде пропуснат, се прихващат всички изключения, независимо от типа им:
|
try { // Do some work that can raise an exception } catch { // Any exception is caught here } |
Изразът catch може да присъства няколко пъти съответно за различните типове изключения, които трябва да бъдат прихванати, например:
|
try { // Do some work that can raise an exception } catch (SomeExceptionClass) { // Handle the SomeExceptionClass and its descendants } catch (OtherExceptionClass) { // Handle the OtherExceptionClass and its descendants } |
Когато възникне изключение, CLR търси "най-близкия" catch блок, който може да обработи типа на възникналото изключение. Първо се претърсва try-catch блокът от текущия метод, към който принадлежи изпълняваният в момента код (ако има такъв блок). Последователно се обхождат асоциираните с него catch блокове, докато се намери този, чийто филтър съответства на типа на възникналото изключение.
Ако това претърсване пропадне, се извършва същото претърсване за следващия try-catch блок, ограждащ текущия (ако има такъв). Този блок може да се намира в текущия метод, в извикващия го метод или в някой от методите, които са извикали него. Ако търсенето отново пропадне, се търси следващия try-catch блок и се проверяват неговите филтри дали улавят възникналото изключение. Търсенето продължава докато се намери първият подходящ обработчик на възникналото изключение или се установи, че няма изобщо такъв.
Търсенето може да обходи целия стек на извикване на методите и да не успее да намери catch блок, който да обработи изключението. В такъв случай изключението се обработва от CLR (появява се съобщение за грешка).
Нека разгледаме един прост пример:
|
static void Main() { string s = Console.ReadLine();
try { Int32.Parse(s); Console.WriteLine("You entered valid Int32 number {0}", s); } catch (FormatException) { Console.WriteLine("Invalid integer number!"); } catch (OverflowException) { Console.WriteLine("Number too big to fit in Int32!"); } } |
В този пример програма очаква да се въведе цяло число. Ако потребителят въведе нещо различно, ще възникне изключение.
Извикването на метода Int32.Parse(s) може да предизвика различни изключения и затова е поставено в try блок, към който са асоциирани няколко catch блока.
Ако вместо число се подаде някаква произволна комбинация от символи, при извикването на метода Int32.Parse(s) ще възникне изключението System.FormatException, което ще бъде прихванато и обработено от първия catch блок.
Ако потребителят въведе число, по-голямо от максималната стойност за типа System.Int32, при извикването на Int32.Parse(s) ще възникне System.OverflowException, чиято обработка се извършва от втория catch блок.
Всеки catch блок е подобен на метод който приема точно един аргумент от определен тип изключение. Този аргумент може да бъде зададен само с типа на изключението, както е в по-горния пример, а може да се зададе и променлива:
|
catch (OverflowException ex) { // Handle the caught exception } |
Тук посредством от променливата еx, която е инстанция на класа System.OverflowException, можем да извлечем допълнителна информация за възникналото изключение.
Нека сега разгледаме един по-сложен пример за прихващане на изключения – прихващане на изключения на няколко нива:
|
static void Main() { try { int result = Calc(100000, 100000, 1); Console.WriteLine(result); } catch (ArithmeticException) { Console.WriteLine("Calculation failed!"); } }
static int Calc(int a, int b, int c) { int result; try { checked { result = a*b/c; } } catch (DivideByZeroException) { result = -1; } return result; } |
В този пример изключенията се прихващат на 2 нива – в try-catch блок в метода Calc(…) и в try-catch блок в метода Main(), извикващ Calc(…).
Ако методът Calc(…) бъде извикан с параметри (0, 0, 0), ще се получи деление на 0 и изключението DivideByZeroException ще бъде прихванато и обработено в try-catch блока на Calc(…) метода и съответно ще се получи стойност -1.
Ако, обаче, методът Calc(…) бъде извикан с параметри (100000, 100000, 1), ще се получи препълване на типа int, което в checked блок ще предизвика ArithmeticOverflowException. Това изключение няма да бъде хванато от catch филтъра в Calc(…) метода и CLR ще провери следващия catch филтър. Това е try-catch блокът в метода Main(), от който е извикан методът Calc(…). CLR ще открие в него е подходящ обработчик за изключението (catch филтърът за класа ArithmeticException, на който класът ArithmeticOverflowException е наследник) и ще го изпълни. Резултатът ще е отпечатване на съобщението "Calculcation failed!".
Възможно е по някаква причина в Calc(…) метода да възникне изключение, което не е наследник на ArithmeticException (например OutOfMemoryException). В такъв случай то няма да бъде прихванато от никой от catch филтрите и ще се обработи от CLR.
Изключенията в .NET Framework са обекти. Класът System.Exception е базов клас за всички изключения в CLR. Той дефинира свойства, общи за всички .NET изключения, които съдържат информация за настъпилата грешка или необичайна ситуация.
Ето и някои често използвани свойства:
- Message – текстово описание на грешката.
- StackTrace – текстова визуализация на състоянието на стека в момента на възникване на изключението. Дава информация за това в кой метод в кой файл и на кой ред във файла е възникнало изключението. Имената на файловете и редовете са налични само при компилиране в дебъг режим.
- InnerException – изключение, което е причина за възникване на текущото изключение (ако има такова). Например имаме метод който чете от файл и после форматира прочетените данни. Ако по време на четенето възникне изключение то може да бъде прихванато и да се хвърли ново изключение от друг, собствено дефиниран тип, като прихванатото изключение се присвои на свойството InnerException. Целта е обработчикът на изключението да получи информация както за възникналия проблем, така и за неговия първопричинител. Чрез свойството InnerException изключенията могат да се свързват във верига, която съдържа последователно всички изключения, които са причинили изключението в нейното начало.
Ще илюстрираме употребата на свойствата с един пример:
|
using System;
class ExceptionsTest { public static void CauseFormatException() { string s = "an invalid number"; Int32.Parse(s); }
static void Main(string[] args) { try { CauseFormatException(); } catch (FormatException fe) { Console.Error.WriteLine( "Exception caught: {0}\n{1}", fe.Message, fe.StackTrace); } } } |
Свойството StackTrace е изключително полезно при идентифициране на причината за изключението. Резултатът от примера е информация за прихванатото в Main() метода изключение, отпечатана върху стандартния изход за грешки:
|
Exception caught: Input string was not in a correct format. at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) at System.Int32.Parse(String s) at ExceptionsTest.CauseFormatException() in c:\consoleapplication1\exceptionstest.cs:line 8 at ExceptionsTest.Main(String[] args) in c:\consoleapplication1\exceptionstest.cs:line 15 |
Имената на файловете и номерата на редовете са достъпни само ако сме компилирали с дебъг информация. Ако компилираме по-горния пример в Release режим, ще получим много по-бедна информация от свойството StackTrace:
|
Exception caught: Input string was not in a correct format. at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) at ExceptionsTest.Main(String[] args) |
Превключването между Debug и Release на компилация става много лесно от лентата с инструменти за компилация във VS.NET:

Изключенията са класове и като такива могат да се наследяват и да образуват йерархии. Както вече знаем, всички изключения в .NET Framework наследяват класа System.Exception. Този клас има няколко важни наследника, от които обектната йерархия продължава в няколко посоки. Това се вижда от следната диаграма:

Някои изключения директно наследяват System.Exception, например класовете System.SystemException и System.ApplicationException.
Системните изключения, които се използват от стандартните библиотеки на .NET и вътрешно от CLR, наследяват класа System.SystemException. Ето някои от тях:
- System.ArithmeticException – грешка при изпълнението на аритметична операция, например деление на 0, препълване на целочислен тип и др.
- System.ArgumentException – невалиден аргумент при извикване на метод.
- System.NullReferenceException – опит за достъп до обект, който има стойност null.
- System.OutOfMemoryException – паметта е свършила.
- System.StackOverflowException – препълване на стека. Обикновено възниква при настъпване на безкрайна рекурсия.
- System.IndexOutOfRangeException – опит за излизане от границите на масив.
Изключенията дефинирани от потребителя трябва да наследяват класа System.ApplicationException. Така потребителските програми ще предизвикват изключения само от този тип или негови наследници. Това дава възможност да се разбере дали проблемът е на ниво потребителски код или е свързан със системна грешка. Възможно е потребителски-дефинирано изключение да наследи и директно System.Exception, а не System. ApplicationException, но има много спорове дали това е добра практика. Някои експерти твърдят, че наследяването на ApplicationException усложнява излишно йерархията, докато други смятат, че е по-важно да се разграничават системните от потребителските изключения.
Както вече знаем, при прихващане на изключения от даден клас се прихващат и изключенията от всички негови наследници. Затова е важна подредбата на catch блоковете. Например конструкцията:
|
try { // Do some works that can raise an exception } catch (System.ArithmeticException) { // Handle the caught arithmetic exception } |
прихваща освен ArithmeticException и изключенията OverflowException и DivideByZeroException. В този пример всичко е наред, но нека разгледаме следния код:
|
static void Main() { string s = Console.ReadLine(); try { Int32.Parse(s); } catch (Exception) // Трябва да е най-накрая { Console.WriteLine("Can not parse the number!"); } catch (FormatException) // Този код е недостижим { Console.WriteLine("Invalid integer number!"); } catch (OverflowException) // Този код е недостижим { Console.WriteLine("The number is too big!"); } } |
В този пример има недостижим код, защото първият catch блок ще се изпълнява за всички типове изключения, тъй като той прихваща базовия тип System.Exception. По тази причина по-специфичните блокове след него няма да се изпълнят никога.
|
|
catch блоковете трябва да са подредени така, че да започват от изключенията най-ниско в йерархията и да продължават с по-общите. Така ще бъдат обработени първо по-специфичните изключения и след това по общите. В противен случай кодът за по-специфичните никога няма да се изпълни. |
Управлявания .NET код може да предизвика само изключения, наследници на System.Exception. Неуправляваният код може да предизвика и други изключения. За прихващане на всички изключения в C# се използва следната конструкция:
|
try { // Do some work that can raise an exception } catch { // Handle the caught exception } |
Използването на тази конструкция е опасно и трябва да се използва само в краен случай, когато е наистина е наложително, защото прихващането на всякакви изключения може да доведе до неочаквани резултати.
Досега разгледахме как се прихващат изключения, които са предизвикани от някой друг. Нека сега разгледаме как ние можем да предизвикваме изключения, които някой друг да прихваща.
Предизвикването (хвърляне) на изключения (throwing, raising exceptions) има за цел да уведоми извикващия код за възникването на даден проблем. Тази техника се използва при настъпване на грешка или необичайна ситуация в даден програмен фрагмент. Под "необичайна ситуация" се има предвид ситуация, която разработчикът е предвидил като евентуално възможна, но която не се случва при нормалната работа, примерно опит за намиране на корен квадратен от отрицателно число.
При една такава необичайна ситуация изпълнението на програмата е нормално да продължи, а извикващият текущия метод трябва да бъде информиран за проблема, за да може да реагира по подходящ начин.
За да се хвърли изключение, с което да се уведоми извикващият код за даден проблем в C# се използва оператора throw, на който се подава инстанция на класа на изключението. Най-често се изисква създаване на обект от някой наследник на класа System.Exception, в който се поставя описание на възникналия проблем. Ето един пример, в който се хвърля изключение ArgumentException:
|
throw new ArgumentException("Invalid argument!"); |
Обикновено преди да бъде хвърлено изключение, то се създава чрез извикване на конструктора на класа, на който то принадлежи. Почти всички изключения дефинират следните два конструктора:
|
Exception(string message); Exception(string message, Exception InnerException); |
Първият конструктор приема текстово съобщение, което описва възникналият проблем, а вторият приема и изключение, причинител на възникналия проблем.
При хвърляне на изключение CLR прекратява изпълнението на програмата и обхожда стека до достигане на catch блок за съответното изключение (целият процес беше описан подробно преди малко).
Ето един пример за хвърляне и прихващане на изключение:
|
public static double Sqrt(double aValue) { if (aValue < 0) { throw new System.ArgumentOutOfRangeException( "Sqrt for negative numbers is undefined!"); } return Math.Sqrt(aValue); }
static void Main() { try { Sqrt(-1); } catch (ArgumentOutOfRangeException ex) { Console.Error.WriteLine("Error: " + ex.Message); } } |
В него е дефиниран метод, който извлича корен квадратен от реално число с двойна точност. При подаване на отрицателен аргумент методът хвърля ArgumentException. В Main() метода изключението се прихваща и се отпечатва грешка.
В catch блокове прихванатите изключения могат да се хвърлят отново. Пример за такова поведение е следният програмен фрагмент:
|
public static int Calculate(int a, int b) { try { return a/b; } catch (DivideByZeroException) { Console.WriteLine("Calculation failed!"); throw; } }
static void Main() { try { Calculate(1, 0); } catch (Exception ex) { Console.WriteLine(ex); } } |
В метода Calculate(…) прихванатото аритметично изключение се обработва като се отпечатва на конзолата "Calculation failed!" и след това се хвърля отново (чрез израза throw;). В резултат същото изключение се прихваща и от try-catch блока в Main() метода.
В .NET Framework програмистите могат да дефинират собствени класове за изключения и да създават класови йерархии с тях. Това осигурява много голяма гъвкавост при управлението на грешки и необичайни ситуации. В по-големите приложения изключенията се разделят в логически в категории и за всяка категория се дефинира по един базов клас, а за конкретните представители на категориите се дефинира по един клас-наследник. Ето един пример:

В примера се създава по един абстрактен базов клас за категорията изключения, свързани с клиентите (CustomerException) и за категорията изключения, свързани с поръчките (OrderException). Наследниците на OrderException и CustomerException също могат да се подреждат в класова йерархия и да дефинират собствени подкатегории.
При работата на приложението, използващо класовата йерархия от примера могат да се прихващат наведнъж всички грешки, свързани с клиентите или само някои конкретни от тях. Това дава добра гъвкавост при управлението на грешките.
Добре е да се спазва правилото, че йерархиите трябва да са широки и плитки, т.е. класовете на изключения трябва да са производни на тип, който се намира близо до System.Exception, и трябва да бъдат не повече от две или три нива надълбоко. Ако дефинираме тип за изключение, който няма да бъде базов за други типове, маркираме го като sealed, а ако не искаме да бъде инстанциран директно, го правим абстрактен.
За дефинирането на собствени изключения се наследява класът System. ApplicationException и му се създават подходящи конструктори и евентуално му се добавят и допълнителни свойства, даващи специфична информация за проблема. Препоръчва се винаги да се дефинират поне следните два конструктора:
|
MyException(string message); MyException(string message, Exception InnerException); |
Въпреки, че не е задължително, силно се препоръчва имената на изключенията да завършват на "Exception", например OrderException, CustomerNotFoundException, InvalidCredentialsException и т. н.
Веднъж дефинирани, собствените класове за изключения могат да се ползват по същия начин, както и системните изключения.
Ще даден един пример за собствено изключение, което се използва при парсването на текстов файл. То съдържа в себе си специфична информация за проблем, възникнал при парсването – име на файла, номер на ред, съобщение за грешка и изключение-причинител на проблема:
|
class ParseFileException : ApplicationException { private string mFileName; private long mLineNumber;
public string FileName { get { return mFileName; } }
public long LineNumber { get { return mLineNumber; } }
public ParseFileException(string aMessage, string aFileName, long aLineNumber, Exception aCauseException) : base( aMessage, aCauseException) { mFileName = aFileName; mLineNumber = aLineNumber; }
public ParseFileException(string aMessage, string aFileName, Exception aCauseException) : this( aMessage, aFileName, 0, aCauseException) { }
public ParseFileException(string aMessage, string aFileName) : this(aMessage, aFileName, null) { } } |
В класа ParseFileException няма нищо сложно. Той наследява System. Exception и дефинира две полета (име на файл и номер на ред), две свойства за достъп до тях и няколко конструктора за инициализация на класа по различен набор от параметри.
Понеже всички инстанции на ParseFileException се създават чрез извикване (директно или индиректно) на базовия конструктор на класа ApplicationException, то при подаване на изключение-причинител, то ще бъде записано в свойството InnerException, което се наследява от класа System.Exception. По същия начин подаденото текстово описание на проблема ще се запише в наследеното свойство Message.
Ето как изключението ParseFileException може да бъде използвано в програма, която по даден текстов файл, съдържащ цели числа (по 1 на ред), намира тяхната сума:
|
static long CalculateSumOfLines(string aFileName) { StreamReader inF; try { inF = File.OpenText(aFileName); } catch (IOException ioe) { throw new ParseFileException(String.Format( "Can not open the file {0} for reading.", aFileName), aFileName, ioe); }
try { long sum = 0; long lineNumber = 0; while (true) { lineNumber++; string line; try { line = inF.ReadLine(); } catch (IOException ioe) { throw new ParseFileException( "Error reading from file.", aFileName, lineNumber, ioe); }
if (line == null) break; // end of file reached
try { sum += Int32.Parse(line); } catch (SystemException se) { throw new ParseFileException(String.Format( "Error parsing line '{0}'.", line), aFileName, lineNumber, se); } } return sum; } finally { inF.Close(); } }
static void Main() { try { long sumOfLines = CalculateSumOfLines(@"c:\test.txt"); Console.WriteLine("The sum of lines={0}", sumOfLines); } catch (ParseFileException pfe) { Console.WriteLine("File name: {0}", pfe.FileName); Console.WriteLine("Line number: {0}", pfe.LineNumber); Console.WriteLine("Exception: {0}", pfe); } } |
В кода са използвани класове за работа с текстови файлове и потоци от пространството с имена System.IO, които ще разгледаме подробно в темата "Вход и изход". Засега нека се съсредоточим върху използването на изключения, а не върху работата с файлове.
В примера при възникване на проблем при четенето от файла или с формата на данните, прочетени от него, се хвърля изключението ParseFileException. В него се задава подходящо съобщение за грешка, записват се името на файла, номерът на реда, където е възникнал проблема, и изключението-причинител на проблема.
Ако стартираме приложението в момент, в който файлът c:\test.txt липсва, ще получим следния резултат:
|
File name: c:\test.txt Line number: 0 Exception: ParseFileException: Can not open the file c:\test.txt for reading. ---> System.IO.FileNotFoundException: Could not find file "c:\test.txt". File name: "c:\test.txt" at System.IO.__Error.WinIOError(Int32 errorCode, String str) at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, Boolean useAsync, String msgPath, Boolean bFromProxy) at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize) at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks, Int32 bufferSize) at System.IO.StreamReader..ctor(String path) at System.IO.File.OpenText(String path) at Test.CalculateSumOfLines(String aFileName) in c:\demos\ParseFileExceptionDemo.cs:line 52 --- End of inner exception stack trace --- at Test.CalculateSumOfLines(String aFileName) in c:\demos\ParseFileExceptionDemo.cs:line 56 at Test.Main() in c:\demos\ParseFileExceptionDemo.cs:line 106 |
Както се вижда, възникнало е изключение ParseFileException, а причината за него е изключението System.IO.FileNotFoundException.
Съхраняването на началната причина за възникване на изключението при подаване на изключение от по-високо ниво на абстракция (както в горния пример) е добра практика, защото дава на разработчика по-богата информация за възникналия проблем.
В примера изключението ParseFileException е от по-високо ниво на абстракция, отколкото FileNotFoundException и дава по-богата информация на разработчика.
Когато възникне изключение, изпълнението на програмата спира и управлението се предава на най-близкия блок за обработка на изключения. Това означава, че кодът който е между фрагмента, породил изключението и началото на блока за обработка на изключението няма да се изпълни. Да разгледаме следния фрагмент:
|
StreamReader reader = File.OpenText("example.txt"); string fileContents = reader.ReadToEnd(); reader.Close(); |
В него се отваря за четене даден файл, след това се прочита цялото му съдържание и накрая се затваря. Ако по време на четенето от файла настъпи някакъв проблем, последният ред няма да се изпълни и файлът ще остане отворен. Това води до загуба на ресурси и ако се случва често, свободните ресурси малко по малко ще намаляват и в един момент ще се изчерпат. Програмата ще започне да се държи странно и най-вероятно ще приключи работата си аварийно.
Проблемът е в това, че при възникване на изключение редовете, които следват реда, в който е настъпило изключението, въобще не се изпълняват. Това може да причини лоши последствия като загуба на ресурси, оставяне на обекти в невалидно състояние, неправилен ход на изпълняваните алгоритми и др.
Проблемът може да бъде решен чрез програмната конструкция try-finally в C#:
|
try { // Do some work that can raise an exception } finally { // This block will always execute } |
Тя осигурява гарантирано изпълнение на зададен програмен блок независимо дали в блока преди него възникне изключение или не. Конструкцията има следното поведение:
- Ако в try блока не възникне изключение, след завършването на изпълнението му, се изпълнява веднага след него и finally блокът.
- Ако в try блока възникне изключение, изпълнението на try блока ще се прекъсне и CLR ще започне да търси обработчик за възникналото изключение. В този случай има две възможности:
o CLR намира обработчик за изключението. Тогава първо ще се изпълни finally блокът и едва след това намереният от CLR обработчик.
o CLR не намира подходящ обработчик. Тогава първо CLR ще обработи изключението (ще даде някакво съобщение за грешка) и след това ще изпълни finally блока.
Може да изглежда сложно, но всъщност не е. Важното нещо, което трябва да запомним е, че finally блокът се изпълнява винаги, независимо от това какво се е случило в try блока. Останалите детайли не са чак толкова важни.
Конструкцията try-finally може да се комбинира с конструкцията try-catch. Така се получава try-catch-finally конструкцията, която работи по следния начин:
- Ако в try блока не възникне изключение, се изпълняват последователно try и finally блоковете.
- Ако в try блока възникне изключение, което може да се улови от catch филтрите на try-catch-finally конструкцията, първо се изпълнява съответният catch блок, а след него се изпълнява и finally блокът.
- Ако в try блока възникне изключение, което не отговаря на catch филтрите от try-catch-finally конструкцията, CLR търси подходящ catch филтър в стека за изпълнение на програмата за да обработи изключението. Отново има две възможности:
o CLR намира обработчик за изключението. Тогава първо ще се изпълни finally блокът и едва след това намереният от CLR обработчик.
o CLR не намира подходящ обработчик. Тогава първо CLR ще обработи изключението (ще даде някакво съобщение за грешка) и след това ще изпълни finally блока.
За повече яснота да разгледаме един пример:
|
try { int a = 0; int b = 1/a; } catch (ArithmeticException) { Console.WriteLine("ArithmeticException caught."); } finally { Console.WriteLine("Finally block executed."); } |
В примера в try блока възниква аритметично изключение заради делението на 0, то се обработва веднага от catch блока и накрая се изпълнява finally блокът. При изпълнението на примера се получава следният резултат:

Нека сега разгледаме пример, в който възниква изключение, което не се прихваща никъде в програмата:
|
try { int a = 0; int b = 1/a; } finally { Console.WriteLine("Finally block executed."); } |
В случая CLR вътрешно ще прихване изключението, ще отпечата съобщение за грешка и едва след това ще изпълни finally блока:

Да разгледаме и още един пример:
|
try { try { int a = 0; int b = 1/a; } finally { Console.WriteLine("Finally block executed."); } } catch (ArithmeticException) { Console.WriteLine("ArithmeticException caught."); } |
При неговото изпълнение ще настъпи аритметично изключение в try блока от try-finally конструкцията. CLR ще потърси и ще намери подходящ обработчик за него в try-catch конструкцията. Понеже CLR е намерил обработчик, първо ще бъде изпълнен finally блока, а след това обработчикът на изключението. Резултатът ще бъде следния:

В блока, който се изпълнява задължително (finally блока), може да се съдържа код за освобождаване на ресурси, който трябва да се изпълни винаги. Така се осигурява почистване след всяка успешно започната операция, преди да се върне управлението на извикващия блок или да продължи да се изпълнява кодът след finally блока. Ето един пример:
|
/// <summary> /// Returns the # of first line from the given text file /// that contains given pattern or -1 if such line is not found /// </summary> static int FindInTextFile(string aPattern, string aFileName) { int lineNumber = 0; StreamReader inF = File.OpenText(aFileName); try { while (true) { string line = inF.ReadLine(); if (line == null) break; // end of file reached lineNumber++; if (line.IndexOf(aPattern) != -1) return lineNumber; } return -1; } finally { inF.Close(); // The file will never remain opened } } |
В примера е реализиран метод, който търси даден текст в даден текстов файл и връща номера на реда, в който е намерен текстът. Понеже е използвана конструкцията try-finally след отварянето на файла, каквото и да се случи по време на търсенето, файлът накрая ще бъде затворен.
Ако не възникне изключение по време на търсенето, след изпълнението на return оператора, ще бъде изпълнен finally блокът.
Ако при работата с файла възникне изключение, ще се изпълни първо finally блокът и методът няма да върне стойност, а ще завърши с изключение, което ще бъде обработено след това.
Ако при търсенето възникне изключение, но то не бъде прихванато, то ще се обработи от CLR и finally блокът ще бъде изпълнен едва след това. Методът няма да върне стойност и програмата ще приключи аварийно.
Изключенията са много мощен механизъм за обработка на грешки, но ако се използват неправилно, могат да доведат до много трудни за откриване проблеми. Затова ще посочим някои препоръчвани практики при работата с изключения:
- catch блоковете трябва да са подредени така, че да започват от изключенията най-ниско в йерархията и да продължават с по-общите. Така ще бъдат обработени първо по-специфичните изключения и след това по общите. В противен случай кодът за по-специфичните никога няма да се изпълни.
- Всеки catch блок трябва да прихваща само изключенията, които очаква (и знае как да обработва), а не всички. Лоша практика е да се прихващат всички изключения тъй като различните видове изключения изискват различна обработка и специфични действия за справяне с възникналата проблемна ситуация. Избягвайте конструкциите catch (Exception) {…} или просто catch {…}.
- При дефиниране на собствени изключения трябва да се наследява System.ApplicationException, а не директно System.Exception. По този начин може да се направи разграничение на това дали изключението е от .NET Framework или е от приложението.
- Имената на класовете на всички изключения трябва завършват на Exception, например OrderException, InvalidAccountException и т. н. Това прави кода по-разбираем и по-лесен за поддръжка.
- При създаване на инстанция на изключение винаги трябва да й се подава в конструктора подходящо съобщение. Това съобщение ще бъде достъпно по-късно чрез свойството Message на изключението и ще помогне на програмиста, който използва дадения клас, по-лесно да идентифицира проблема.
- Изключенията могат да намалят значително производителността на приложението, понеже всяко хвърлено изключение инстанцира клас (това отнема време), инициализира членовете му (това също отнема време), извършва търсене в стека за подходящ catch блок (и това отнема време) и накрая след като инстанцията стане неизползваема, тя се унищожава от garbage collector (и това също отнема време). Затова, когато е възможно се препоръчва да се прави проверка дали е възможно дадено действие, а не да се разчита на обработката на възникналото изключение. Прекомерното използване на изключенията се отразява на производителността.
- Някои изключения могат да възникват по всяко време без да ги очакваме (например System.OutOfMemoryException). Добра практика е да се централизира прихващането на този тип изключения на най-високо ниво например в Main() метода на програма и да се направи елегантно прекратяване на изпълнението на програмата.
- Изключенията трябва да бъдат хвърляни само при ситуации, които наистина са изключителни и трябва да се обработят. В нормалния ход на програмата (когато не възникват проблеми) не трябва да се хвърлят изключения.
1. Обяснете какво представлява изключенията, кои са силните им страни и кога се препоръчва да се използват.
2. Реализирайте структура от данни Student, която съдържа информация за студент - име, адрес, курс, специалност, изучавани предмети, оценки и т. н. Добавете подходящи конструктори и свойства за достъп до данните от класа. Сложете проверка за валидност на данните за студента в конструкторите и в свойствата за достъп. При невалидни данни хвърляйте изключение. Дефинирайте подходящи собствени класове за изключенията, свързани с класа Student.
1. Светлин Наков, Обектно-ориентирано програмиране в .NET – http://www.nakov.com/dotnet/lectures/Lecture-3-Object-Oriented-Concepts-v1.0.ppt
2. Jeffrey Richter, Applied Microsoft .NET Framework Programming, Microsoft Press, 2002, ISBN 0735614229
3. Suprotim Agarwal, Getting Started with Exception Handling in C# - http://www.c-sharpcorner.com/Code/2004/July/ GettingStartedWithExceptionHandling.asp
4. Steve McConnell, Code Complete, 2nd Edition, Microsoft Press, 2004, ISBN 0735619670
5. MSDN Library – http://msdn.microsoft.com
|
Българска асоциация на разработчиците на софтуер (БАРС) е нестопанска организация, която подпомага професионалното развитие на българските софтуерни специалисти чрез образователни и други инициативи. БАРС работи за насърчаване обмяната на опит между разработчиците и за усъвършенстване на техните знания и умения в областта на проектирането и разработката на софтуер. Асоциацията организира специализирани конференции, семинари и курсове за обучение по разработка на софтуер и софтуерни технологии. БАРС организира създаването на Национална академия по разработка на софтуер – учебен център за професионална подготовка на софтуерни специалисти.
|
- Базови познания за архитектурата на .NET Framework
- Базови познания за езика C#
- Какво е CTS?
- Йерархията на типовете в .NET
- Стойностни и референтни типове
- Типът System.Object
- Предефиниране на стандартните методи на System.Object
- Операторите is и as
- Клониране на обекти
- Опаковане и разопаковане на обекти
- Интерфейсите IComparable, IEnumerable и IEnumerator
В настоящата тема ще разгледаме общата система от типове в .NET Framework. Ще обясним разликата между стойностни и референтни типове, ще разгледаме основополагащия тип System.Object и йерархията на типовете, произлизаща от него. Ще се запознаем накратко и с някои операции при работа с типове – преобразуване към друг тип, проверка на тип, клониране, опаковане, разопаковане и др.
CLR поддържа много езици за програмиране. За да се осигури съвместимост на данните между различните езици е разработена общата система от типове (Common Type System – CTS). CTS дефинира поддържаните от CLR типове данни и операциите над тях.
Всички .NET езици използват типовете от CTS. За всеки тип в даден .NET език има някакво съответствие в CTS, макар че понякога това съответствие не е директно. Обратното не е вярно – съществуват CTS типове, които не се поддържат от някои .NET езици.
По идея всички езици в .NET Framework са обектно-ориентирани. Common Type System също се придържа към идеите на обектно-ориентираното програмиране (ООП) и по тази причина описва освен стандартните типове (числа, символи, низове, структури, масиви) и някои типове данни свързани с ООП (например класове и интерфейси).
Типовете данни в CTS биват най-разнообразни:
- примитивни типове (primitive types – int, float, bool, char, …)
- изброени типове (enums)
- класове (classes)
- структури (structs)
- интерфейси (interfaces)
- делегати (delegates)
- масиви (arrays)
- указатели (pointers)
Всички тези типове повече или по-малко вече са ни познати от езика C#, но всъщност те са част от CTS. Езикът C# и другите .NET езици използват CTS типовете и им съпоставят запазени думи съгласно своя синтаксис. Например типът System.Int32 от CTS съответства на типа int в C#, а типът System.String – на типа string.
В CTS се поддържат две основни категории типове: стойностни типове (value types) и референтни типове (reference types). Стойностните типове съдържат директно стойността си в стека за изпълнение на програмата, докато референтните типове съдържат строго типизиран указател (референция) към стойността, която се намира в динамичната памет. По-нататък ще разгледаме подробно разликите между стойностните и референтните типове и особеностите при тяхното използване.
По принцип в .NET има класически указатели, но те не се използват масово, както при езиците C и C++. Указателите в .NET се поддържат най-вече заради съвместимост с Win32 платформата и се използват в много специални случаи. В силно типизираните езици като C# и VB.NET за достъп до обекти в динамичната памет се използват т. нар. референции (references), които са строго типизирани указатели, подобни на псевдонимите в C++.
С въвеждането на референтните типове в .NET отпада нуждата от класически указатели. На практика реферетните типове са типово-обезопасени указатели, защитени от неправилно преобразуване към друг тип, а сочената от тях динамична памет се управлява автоматично.
CTS дефинира строга йерархия на типовете данни, които се поддържат в .NET Framework:

В основата на йерархията стои системният тип System.Object. Той е общ предшественик (базов тип) за всички останали типове в CTS. Неговите преки наследници са стойностните и референтните типове (които ще дискутираме в детайли по-късно в тази тема).
Стойностните типове биват примитивни (int, float, bool и др.), структури (struct в C#) и изброени типове (enum в C#).
Референтните типове са всички останали – указателите, класовете, интерфейсите, делегатите, масивите и опакованите стойностни типове.
В предходните теми вече се запознахме с някои от CTS типовете. В тази и в следващите теми ще се запознаем и с останалите (опаковани стойностни типове, масиви, делегати).
В CTS всички типове наследяват системния тип System.Object. Не правят изключение дори примитивните типове (int, float, char, ...) и масивите. Всеки тип е наследник на System.Object и имплементира методите, включени в него. Като резултат значително се улеснява работата с типове, защото променлива от произволен тип може да се присвои на променлива от базовия тип System.Object (object в C#). Самият System.Object е референтен тип.
Стойностни типове (типове по стойност) са повечето примитивни типове (int, float, bool, char и др.), структурите (struct в C#) и изброените типове (enum в C#).
Стойностните типове директно съдържат стойността си и се съхраняват физически в работния стек за изпълнение на програмата. Tе не могат да приемат стойност null, защото реално не са указатели.
Стойностните типове заемат необходимата им памет в стека в момента на декларирането им и я освобождават в момента на излизане от обхват (при достигане на края на програмния блок, в който са декларирани). Заделянето и освобождаване на памет за стойностен тип реално се извършва чрез единично преместване на указателя на стека и следователно става много бързо.
Горното обяснение е малко опростено. Всъщност ако стойностен тип има за член-данни само стойностни типове, при инстанциране целият тип ще се задели в стека. Ако, обаче, стойностен тип (например структура) съдържа като член-данни референтни типове, стойностите им ще се запишат в динамичната памет.
CLR се грижи всички стойностни типове да наследяват системния тип System.ValueType. Всички типове, които не наследяват ValueType са референтни типове, т.е. реално са указатели към динамичната памет (адреси в паметта).
При извикване на метод стойностните типове се подават по стойност, т.е. предава се копие от тях. При подготовка на извикването на метод CLR копира подаваните като параметри стойностни типове от оригиналното им местоположение в стека на ново място в стека и подава на извиквания метод направените копия. Ако извикваният метод промени стойността на подадения му по стойност параметър, при връщане от извикването промяната се губи. Това поведение важи, разбира се, само ако параметрите се подават по подразбиране, без да се използват ключовите думи в C# ref и out, които ще разгледаме по-нататък в следващите теми.
Референтни типове (типове по референция) са указателите, класовете, интерфейсите, делегатите, масивите и опакованите стойностни типове. Физически референтните типове представляват указател към стойност в динамичната памет, но за CLR те не са обикновени указатели, а специални типово-обезопасени указатели. Това означава, че CLR не допуска на един референтен тип да се присвои стойност от друг референтен тип, който не е съвместим с него (т.е. не е същия тип или негов наследник). В резултат на това в .NET езиците грешките от неправилна работа с типове са силно намалени.
Всички референтни типове се съхраняват в динамичната памет (т. нар. managed heap), която се контролира от системата за почистване на паметта (garbage collector). Динамичната памет е специално място от паметта, заделено от CLR за съхранение на данни, които се създават динамично по време на изпълнението на програмата. Такива данни са инстанциите на всички референтни типове.
Когато инстанция на референтен тип престане да бъде необходима на програмата, тя се унищожава от системата за почистване на паметта (т. нар. garbage collector).
Когато инстанцираме референтен тип с оператора new, CLR заделя място в динамичната памет, където ще стоят данните и един указател в стека, който съдържа адреса на заделеното място. Веднага след това заделената памет се занулява (освен ако програмистът не инициализира заделената променлива, например чрез извикване на подходящ конструктор).
Ако референтен тип (например клас) съдържа член-данни от стойностен тип, те се съхраняват в динамичната памет. Ако референтен тип съдържа член-данни от референтен тип, в динамичната памет се заделят указатели (референции) за тях, а техните стойности (ако не са null) също се заделят също в динамичната памет, но като отделни обекти.
Понякога се приема, че заделянето на динамична памет е бърза операция, защото в текущата реализация (.NET Framework 1.1) физически се имплементира чрез преместване на един указател. Освобождаването на памет, обаче, е сложна и времеотнемаща операция, която се извършва от време на време от системата за почистване на паметта (garbage collector).
Ако изчислим средното време, необходимо за заделяне и освобождаване на динамична памет, се оказва, че заделянето и освобождаване на стойностните типове е значително по-бързо от референтните типове. Когато производителността е важна за нашата система, трябва да се съобразяваме с особеностите на стойностните и референтните типове и начина, по който те заделят и освобождават памет.
Глобално погледнато, нещата около управлението на динамичната памет в .NET Framework са доста комплексни, но в тази тема няма да се спираме на тях. По-нататък, в темата за управление на паметта и ресурсите, ще им обърнем специално внимание.
Стойностните и референтните типове в .NET Framework се различават съществено. Стойностните типове се разполагат в стека за изпълнение на програмата, докато референтните типове са строго типизирани указатели към динамичната памет, където се съдържа самата им стойност.
Следват някои по-съществени разлики между тях:
- Стойностните типове наследяват системния тип System.ValueType, а референтните наследяват директно System.Object.
- При създаване на променлива от стойностен тип тя се заделя в стека, а при референтните типове – в динамичната памет.
- При присвояване на стойностни типове се копира самата им стойност, а при референтни типове – само референцията (указателя).
- При предаване на променлива от стойностен тип като параметър на метод, се предава копие на стойността й, а при референтните типове се предава копие на референцията, т.е. самата стойност не се копира. В резултат, ако даден метод променя стойностен входен параметър, промените се губят при излизане от метода, а ако входният параметър е референтен, те се запазват.
- Стойностните типове не могат да приемат стойност null, защото не са указатели, докато референтните могат.
- Стойностните типове се унищожават при излизане от обхват, докато референтните се унищожават от системата за почистване на паметта (garbage collector) в някой момент, в който се установи, че вече не са необходими за работата на програмата.
- Променливи от стойностен тип могат да се съхраняват в променливи от референтен тип чрез т.нар. "опаковане" (boxing), което ще разгледаме след малко.
В настоящия пример се демонстрира използването на стойностни и референтни типове и се илюстрира разликата между тях:
|
using System;
// SomeClass is reference type class SomeClass { public int mValue; }
// SomeStruct is value type struct SomeStruct { public int mValue; }
class TestValueAndReferenceTypes { static void Main() { SomeClass class1 = new SomeClass(); class1.mValue = 100; SomeClass class2 = class1; // Копира се референцията class2.mValue = 200; // Променя се и class1.mValue Console.WriteLine(class1.mValue); // Отпечатва се 200
SomeStruct struct1 = new SomeStruct(); struct1.mValue = 100; SomeStruct struct2 = struct1; // Копира се стойността struct2.mValue = 200; // Променя се копираната стойност Console.WriteLine(struct1.mValue); // Отпечатва се 100 } } |
След като се изпълни примерът, се получава следния резултат:

В началото на примера се създава инстанция на класа SomeClass, в нея се записва числото 100 и след това тя се присвоява на две променливи. Аналогично се създава инстанция на структурата SomeStruct, в нея също се записва 100 и след това тя се присвоява на две променливи.
При присвояването на инстанциите на класа, понеже той е референтен тип, се присвоява само референцията и стойността реално не се копира, а остава обща. При присвояването на инстанцията на структурата, понеже тя е стойностен тип, се присвоява самата стойност (нейно копие). Поради тази причина в резултат от изпълнението на програмата на конзолата се отпечатват различни стойности.
По-долу са показани схематично стекът за изпълнение на програмата и динамичната памет в момента преди приключване на програмата. Данните са взети от дебъгера на Visual Studio .NET поради което са много близки до истинското разположение на паметта по време на изпълнение на примерната програма:

Стекът расте отгоре надолу (от големите адреси към адрес 0), защото програмата е изпълнена върху Intel-съвместима архитектура, при която това поведение е нормално.
За да проследим как се изпълнява горният пример стъпка по стъпка, можем да използваме проекта Demo-1-Value-And-Reference-Types от демонстрациите:
1. Отваряме с VS.NET проекта Demo-1-Value-And-Reference-Types.sln.
2. Слагаме точка на прекъсване на последния ред от Main() метода на основния клас.
3. Стартираме приложението с [F5].
4. След като дебъгерът спре в точката на прекъсване, показваме Disassembly и Registers прозорците. От менюто на VS.NET избираме Debug | Windows | Disassembly и Debug | Windows | Registers. Ето как изглежда VS.NET в този момент:

5. Можем да разгледаме асемблерния код, получен след компилиране на програмата и след превръщането на MSIL кода в чист Win32 код за процесор Intel x86.
Повечето компилатори за Intel-базирани процесори генерират код, който използва в тялото на методите регистър EBP като указател към върха на стека. Адресиране от типа на dword ptr [ebp-14h] най-често реферира стойност в стека – локална променлива или параметър.
Спомнете си за разликите между класове и структури (референтни и стойностни типове). Стойностните типове съхраняват стойността си директно в стека. Референтните типове съхраняват в стека само 4-байтов адрес, който указва мястото на променливата в динамичната памет.
Често пъти, с цел оптимизация на производителността, компилаторът вместо някаква област от стека използва регистри за съхранение на локални променливи. В случая в EBX се съхранява референцията class2, а в EDI – референцията class1.
6. Да разгледаме асемблерния код, генериран за операцията присвояване class2=class1. В него се присвоява на регистър EBX стойността на регистър EDI, т.е. на референцията class2 се присвоява референцията class1. Обърнете внимание, че се копира референцията, а не самата стойност.
7. Да разгледаме асемблерния код, генериран за операцията присвояване struct2=struct1. В него се присвоява на регистър EAX стойността от стека, съответстваща на struct1 и след това стойността от EAX се записва обратно в стека, в променливата struct2. На практика се копира самата стойност на структурата, като се използва за работна променлива регистърът EAX.
Когато декларираме променлива в кода, C# компилаторът ни задължава да й зададем стойност преди първото й използване. Ако се опитаме да използване неинициализирана променлива (независимо дали е от стойностен или референтен тип), C# компилаторът дава грешка и отказва да компилира кода. Ето един пример:
|
int someVariable; Console.WriteLine(someVariable); |
При опит за компилация ще възникне грешката "Use of unassigned local variable someVariable".
При създаване на обект от даден тип с оператора new CLR автоматично инициализира декларираната променлива с неутрална (нулева) стойност. Ето един пример:
|
int i = new int(); Console.WriteLine(i); |
Горният код се компилира успешно и отпечатва като резултат 0. Това се дължи на автоматичната инициализация, която операторът new извършва.
Когато заделяме структура или клас, се изпълнява и съответният конструктор и всички член-променливи на новия обект се инициализират с нулеви стойности. Това предпазва разработчиците от проблеми свързани с неинициализирани член-данни, които могат да бъдат много досадни, защото се проявяват само от време на време.
Ако само дефинираме променлива, без да създадем инстанция за нея с оператора new, ще получим грешка по време на компилация, защото променливата ще остане неинициализирана. Ето пример:
|
int i; Console.WriteLine(i); // Use of unassigned local variable 'i' |
Типът System.Object е базов за всички типове в .NET Framework. Както референтните, така и стойностните типове произлизат от System.Object или от негов наследник. Това улеснява програмиста и в много ситуации му спестява писане на излишен код.
В .NET Framework можем да напишем следния код:
|
string s = 5.ToString(); |
Този код извиква виртуалния метод ToString() от класа System.Object. Това е възможно, защото числото 5 е инстанция на типа System.Int32, който е наследник на System.Object.
Понеже всички типове са съвместими със System.Object (object в C#), защото са негови наследници, можем на инстанция на System.Object да присвояваме както референтни, така и стойностни типове:
|
object obj = 5; object obj2 = new SomeClass(); |
Забележка: Ако не е указано друго, в C# целите числа по подразбиране са инстанции на типа System.Int32.
Ако си спомним, че System.Object е референтен тип, изглежда малко странно че стойностните типове също го наследяват. Сякаш има някакво противоречие: Как така стойностните типове, които не са указатели, произлизат от тип, който е указател?
Всъщност противоречие няма, защото архитектите на .NET Framework по изкуствен начин са направили съвместими всички стойностни типове със System.Object. За удобство в CLR всички стойностни типове могат да се преобразуват към референтни чрез операцията "опаковане". Опаковането и обратната му операция "разопаковане" преобразуват стойностни типове в опаковани стойностни типове и обратното. При опаковане стойностните типове се копират в динамичната памет и се получава указател (референция) към тях. При разопаковане стойността от динамичната памет, сочена от съответната референция, се копира в стека.
По късно в настоящата тема ще дискутираме в детайли опаковането и разопаковането на стойностни типове.
При дефиниране на какъвто и да е тип, скрито от нас се наследява System.Object. Например структурата:
|
struct Point { int x, y; } |
е наследник на System.Object, макар това да не се вижда непосредствено от декларацията й.
Като базов тип за всички .NET типове System.Object дефинира обща за всички тях функционалност. Тази функционалност се реализира в няколко метода, някои от които са виртуални и могат да бъдат припокрити:
- bool Equals(object) – виртуален метод, който сравнява текущия обект с друг обект. Методът има и статична версия Equals(object, object), която сравнява два обекта, подадени като параметри. Обектите се сравняват не по адрес, а по съдържание. Методът често пъти се припокрива, за да се даде възможност за сравнение на потребителски обекти.
- string ToString() – виртуален метод, който представя обекта във вид на символен низ. Имплементацията по подразбиране на ToString() отпечатва името самия тип.
- int GetHashCode() – виртуален метод за изчисляване на хеш-код. Използва се при реализацията на някои структури от данни, например хеш-таблици. По-нататък, в темата за масиви и колекции, ще разгледаме този метод по-детайлно.
- Finalize() – виртуален метод за имплементиране на почистващи операции при унищожаване на обект. В C# не може да се дефинира директно, а се имплементира чрез деструктора на типа. Ще разгледаме подробности за т. нар. "финализация на обекти" в темата за управление на паметта и ресурсите.
- Type GetType() – връща метаданни за типа на обекта във вид на инстанция на System.Type. Имплементиран е вътрешно от CLR.
- object MemberwiseClone() – копира двоичното представяне на обекта в нов обект, т. е. извършва плитко копиране. При референтни типове създава нова референция към същия обект. При стойностни типове копира стойността на подадения обект.
- bool ReferenceEquals() – сравнява два обекта по референция. При референтни типове се сравнява дали обектите сочат на едно и също място в динамичната памет. При стойностни типове връща false.
Когато дефинираме собствен тип, често пъти се налага да се имплементира функционалност за сравнение на негови инстанции. В .NET Framework се препоръчва такава функционалност да се реализира чрез имплементиране на предвидените за целта методи в System.Object.
Препоръчва се методите Equals(object), operator ==, operator != и GetHashCode() да се имплементират заедно в комплект. Тази практика спестява някои доста досадни проблеми. Например ако Equals(object) е имплементиран, а операторът == не е имплементиран, потребителите на типа могат да се подведат и да извършват некоректно сравнение с ==, което по подразбиране връща резултата от метода ReferenceEquals().
В настоящия пример се дефинира клас Student, който съдържа 2 информационни полета (име и възраст), след което се дефинират методите за сравнение на студенти. Счита се, че два студента са един и същ, ако имат еднакви имена и възраст. Предефинират се виртуалните методи Equals(object), operator ==, operator !=, GetHashCode() и ToString() от System.Object. С цел илюстриране как се използва предефинираното сравнение в края на примера се създават няколко инстанции на Student и се сравняват една с друга.
|
using System;
public class Student { public string mName; public int mAge;
public override bool Equals(object aObject) { // If the cast is invalid, the result will be null Student student = aObject as Student;
// Check if we have valid not null Student object if (student == null) { return false; }
// Compare the reference type member fields if (! Object.Equals(this.mName, student.mName)) { return false; }
// Compare the value type member fields if (this.mAge != student.mAge) { return false; }
return true; }
public static bool operator == (Student aStudent1, Student aStudent2) { return Student.Equals(aStudent1, aStudent2); }
public static bool operator != (Student aStudent1, Student aStudent2) { return ! (Student.Equals(aStudent1, aStudent2)); }
public override int GetHashCode() { // Return the hash code of the mName field return mName.GetHashCode(); }
public override string ToString() { return String.Format( "Student(Name: {0}, Age: {1})", mName, mAge); }
static void Main() { Student st1 = new Student(); st1.mName = "Бай Иван"; st1.mAge = 68; Console.WriteLine(st1); // Student.ToString() is called
Student st2 = new Student(); if (st1 != st2) // it is true { Console.WriteLine("{0} != {1}", st1, st2); }
st2.mName = "Бай Иван"; st2.mAge = 68; if (st1 == st2) // it is true { Console.WriteLine("{0} == {1}", st1, st2); }
st2.mAge = 70; if (st1 != st2) // it is true { Console.WriteLine("{0} != {1}", st1, st2); }
if (st1 != null) // it is true { Console.WriteLine("{0} is not null", st1); } } } |
След като се изпълни примерът, се получава следния резултат:

Методът Equals(object) е реализиран на няколко стъпки. Първо се проверява дали е подаден обект от тип Student, който не е null. Това е необходимо условие, за да е възможно равенството на подадения студент с текущия студент. След това се сравняват имената на студентите и ако съвпаднат се сравняват и годините им. Истина се връща, само ако и двете сравнения установят равенство.
Операторите == и != се имплементират чрез извикване на Equals( object).
Методът GetHashCode() връща хеш-кода на името на студента, което ще върши работа в повечето случаи. По-подробно на този метод ще се спрем в темата "Масиви и колекции".
Методът ToString() връща символен низ, съдържащ името и възрастта на студента в лесно четим формат.
В главната програма (Main() метода) се извършват серия сравнения, които демонстрират правилната работа на имплементираните методи.
В C# има няколко служебни оператора за работа с типове – is и as и typeof.
Операторът is проверява дали зададеният обект е инстанция на даден тип. Пример:
|
int value = 5; if (value is System.Object) // it is true { Console.WriteLine("{0} is instance of System.Object.", value); } |
Операторът as преобразува даден референтен тип в друг, като при неуспех не предизвиква изключение, а връща стойност null. При стандартно преобразуване на типове, ако има несъвместимост на обекта с резултатния тип, се получава изключение. Например:
|
int i = 5; object obj = i; string str = (string) obj; // System.InvalidCastException |
Операторът as преобразува типове, без да предизвиква изключение:
|
int i = 5; object obj = i; string str = obj as string; // str == null |
Операторът typeof извлича отражението на даден тип във вид на инстанция на System.Type. Пример:
|
Type intType = typeof(int); |
В темата "Отражение на типовете" ще обърнем повече внимание на типа System.Type и на оператора typeof.
В следващия пример се илюстрира използването на операторите is и as:
|
using System;
class Base {
}
class Derived : Base {
}
class TestOperatorsIsAndAs { static void Main() { Object objBase = new Base(); if (objBase is Base) { Console.WriteLine("objBase is Base"); } // Result: objBase is Base
if (! (objBase is Derived)) { Console.WriteLine("objBase is not Derived"); } // Result: objBase is not Derived
if (objBase is System.Object) { Console.WriteLine("objBase is System.Object"); } // Result: objBase is System.Object
Base b = objBase as Base; Console.WriteLine("b = {0}", b); // Result: b = Base
Derived d = objBase as Derived; if (d == null) { Console.WriteLine("d is null"); } // Result: d is null
Object o = objBase as Object; Console.WriteLine("o = {0}", o); // Result: o = Base
Derived der = new Derived(); Base bas = der as Base; Console.WriteLine("bas = {0}", bas); // Result: bas = Derived } } |
Примерът декларира два класа – Base и негов наследник Derived, след което създава няколко инстанции от тези класове и ги преобразува една към друга. Работата на операторите is и as се илюстрира чрез няколко преобразувания и проверки на типовете. Резултатът от изпълнението на примерната програма е следния:

Клонирането (копирането) на обекти е операция, която създава идентично копие на даден обект. При клонирането се създават копия на всички информационни полета (член-променливи) на типа. Съществуват 2 типа клониране – плитко и дълбоко.
При плитко клониране всички стойностни типове се копират, а всички референции се дублицират (копират се адресите). На практика се създава копие на обекта, което може да има общи (споделени) части с оригиналния обект (това са всички полета на оригиналния обект, които са от референтен тип). Плитко клониране се извършва от метода MemberwiseClone() на типа System.Object.
При дълбоко (пълно) клониране се правят копия на всички полета на оригиналния обект и се създава съвсем нов обект, който е идентичен с оригиналния, но не съдържа споделени с него общи данни. На практика се дублицират рекурсивно в дълбочина всички полета на оригиналния обект и съответно техните полета.
В програмирането използването на плитки копия на обектите често води до проблеми и затова не е препоръчвана практика. Когато трябва да се клонира даден обект, обикновено е необходимо да се създаде негово пълно копие, а не само нова референция, сочеща към оригиналния обект.
В някои редки случаи, от съображения за производителност и пестене на ресурси, се налага да се ползват плитки или частични копия на обектите. Ако се прилагат такива техники, това трябва да се прави много внимателно, за да не се получават странни проблеми, като синдромът "ама това вчера работеше".
В .NET Framework под клониране се подразбира "дълбоко клониране". Всички типове, които позволяват клониране, трябва да имплементират интерфейса System.ICloneable.
ICloneable дефинира метод Clone() който връща идентично копие на обекта. Clone() методът трябва да връща дълбоко копие на оригиналния обект. Ако даден обект съдържа като член-данни други обекти, тези обекти трябва също да имплементират ICloneable и да бъдат клонирани посредством Clone() метода им. Ако това не бъде изпълнено, има вероятност клонирането да не работи правилно и да се получат споделени данни между оригиналния обект и копието.
Клонирането като цяло е проблем, при който често възникват грешки, но за щастие рядко се налага да бъде имплементирано ръчно.
Голяма част от често използваните стандартни типове в .NET Framework имат имплементация на ICloneable – масивите, колекциите, символните низове и др. Примитивните стойностни типове (int, float, double, byte, char и т. н.) могат да бъдат клонирани чрез просто присвояване, защото не съдържат вложени членове от референтен тип. При тях на практика всяко клониране е дълбоко.
В следващия пример ще илюстрираме как може да се клонира нетривиална структура от данни, а именно динамично реализиран свързан списък. При него всеки елемент съдържа някаква стойност и референция към следващ елемент. Последният елемент съдържа за следващ елемент стойност null.
При клонирането на свързан списък трябва да се клонират всичките му елементи и връзките между тях. В резултат трябва да се построи нов списък, който съдържа елементите от първия в реда, в който са били в него. На практика клонирането на свързан списък се свежда до обхождането му и построяването на копие на всеки негов елемент и на всяка връзка между два елемента. Следва примерна реализация:
|
using System; using System.Text;
class LinkedList : ICloneable { public string mValue; protected LinkedList mNextNode;
public LinkedList(string aValue, LinkedList aNextNode) { mValue = aValue; mNextNode = aNextNode; }
public LinkedList(string aValue) : this(aValue, null) { }
// Explicit implementation of ICloneable.Clone() object ICloneable.Clone() { return this.Clone(); }
// This method is not ICloneable.Clone() public LinkedList Clone() { // Clone the first element LinkedList original = this; string value = original.mValue; LinkedList result = new LinkedList(value); LinkedList copy = result; original = original.mNextNode;
// Clone the rest of the list while (original != null) { value = original.mValue; copy.mNextNode = new LinkedList(value); original = original.mNextNode; copy = copy.mNextNode; }
return result; }
public override string ToString() { LinkedList currentNode = this; StringBuilder sb = new StringBuilder("("); while (currentNode != null) { sb.Append(currentNode.mValue); currentNode = currentNode.mNextNode; if (currentNode != null) { sb.Append(", "); } } sb.Append(")");
return sb.ToString(); } }
class TestClone { static void Main() { LinkedList list1 = new LinkedList("Бай Иван", new LinkedList("Баба Яга", new LinkedList("Цар Киро")));
Console.WriteLine("list1 = {0}", list1); // Result: list1 = (Бай Иван, Баба Яга, Цар Киро)
LinkedList list2 = list1.Clone(); list2.mValue = "1st changed";
Console.WriteLine("list2 = {0}", list2); // Result: list2 = (1st changed, Баба Яга, Цар Киро)
Console.WriteLine("list1 = {0}", list1); // Result: list1 = (Бай Иван, Баба Яга, Цар Киро) } } |
В примерната реализация е дефиниран свързан списък от символни низове. Методът ICloneable.Clone() е реализиран експлицитно (явно). Допълнително за удобство е дефиниран метод Clone(). Разликата между двата метода е във връщания тип. Имплементацията на интерфейса ICloneable (методът ICloneable.Clone()) връща object и ако се използва, трябва да се извършва преобразуване. Методът Clone() връща директно правилния тип и ни спестява преобразуването.
Методът ToString() използва специалния клас StringBuilder за по-ефективно сглобяване на резултатния низ. Класът StringBuilder и причините за използването му ще бъдат разгледани подробно в темата за работа със символни низове.
Главната програма създава списък list1, съдържащ 3 елемента, и го отпечатва. След това го клонира в променливата list2 и променя първия му елемент. Тъй като оригиналният списък и неговото копие не съдържат споделени данни, оригиналният списък не се променя и това ясно личи от изведения резултат:

Вече обяснихме, че стойностните типове се съхраняват в стека на приложението и не могат да приемат стойност null, докато референтните типове съдържат указател (референция) към стойност в динамичната памет и могат да бъдат null.
Понякога се налага на референтен тип да се присвои обект от стойностен тип. Например може да се наложи в System.Object инстанция да се запише System.Int32 стойност. CLR позволява това благодарение на т. нар. "опаковане" на стойностните типове (boxing).
В .NET Framework стойностните типове могат да се използват без преобразуване навсякъде, където се изискват референтни типове. При нужда CLR опакова и разопакова стойностните типове автоматично. Това спестява дефинирането на обвиващи (wrapper) класове за примитивните типове, структурите и изброените типове, но разбира се, може да доведе и до някои проблеми, които ще дискутираме по-късно.
Опаковането (boxing) е действие, което преобразува стойностен тип в референция към опакована стойност. То се извършва, когато е необходимо да се преобразува стойностен тип към референтен тип, например при преобразуване на Int32 към Object:
|
int i = 5; object obj = i; // i се опакова |
Всяка инстанция на стойностен тип може да бъде опакована чрез просто преобразуване до System.Object. Ако един тип е вече опакован, той не може да бъде опакован втори път и при преобразуване към System.Object си остава опакован само веднъж.
CLR извършва опаковането по следния начин:
1. Заделя динамична памет за създаване на копие на обекта от стойностния тип.
2. Копира съдържанието на стойностната променливата от стека в заделената динамична памет.
3. Връща референция към създадения обект в динамичната памет.
При опаковането в динамичната памет се записва информация, че референцията съдържа опакован обект и се запазва името на оригиналния стойностен тип.
Разопаковането (unboxing) е процесът на извличане на опакована стойност от динамичната памет. Разопаковане се извършва при преобразуване на опакована стойност обратно към инстанция на стойностен тип, например при преобразуване на Object към Int32:
|
object obj = 5; // 5 се опакова int value = (int) obj; // стойността на obj се разопакова |
CLR извършва разопаковането по следния начин:
1. Ако референцията е null се предизвиква NullReferenceException.
2. Ако референцията не сочи към валидна опакована стойност от съответния тип, се предизвиква изключение InvalidCastException.
3. Ако референцията е валидна опакована стойност от правилния тип, стойността се извлича от динамичната памет и се записва в стека.
За разлика от опаковането, разопаковането невинаги е успешна операция (и това трябва да се съобразява, когато се работи с опаковани стойности).
При използване на автоматично опаковане и разопаковане на стойности трябва да се имат предвид някои особености:
- Опаковането и разопаковането намаляват производителността. За оптимална производителност трябва да се намали броят на опакованите и разопакованите обекти.
- Опакованите типове са копия на оригиналните стойности, поради което, ако променяме оригиналния неопакован тип, опакованото копие не се променя.
Нека имаме следния код:
|
int i = 5; object obj = i; // boxing
int i2; i2 = (int) obj; // unboxing |
На картинката по-долу схематично е показано как работят опаковането и разопаковането на стойностни типове в .NET Framework:

При опаковане стойността от стека се копира в динамичната памет, а при разопаковане стойността от динамичната памет се копира в обратно в стека.
Опакованите стойности се държат като останалите референтни типове – разполагат се в динамичната памет, унищожават се от garbage collector, когато не са необходими на програмата, и при подаване като параметър при извикване на метод се пренасят по адрес.
В следващия пример се илюстрира опаковането и разопаковането на стойностни типове, като се обръща внимание на някои особености при тези операции:
|
using System;
class TestBoxingUnboxing { static void Main() { int value1 = 1; object obj = value1; // извършва се опаковане
value1 = 12345; // променя се само стойността в стека
int value2 = (int)obj; // извършва се разопаковане Console.WriteLine(value2); // отпечатва се 1
long value3 = (long) (int) obj; // разопаковане
long value4 = (long) obj; // InvalidCastException } } |
От примера се вижда, че разопаковане на Int32 стойност не може да се извърши чрез директно преобразуване към Int64. Необходимо е първо да се извлече Int32 стойността от опакования обект и след това да се извърши преобразуване до Int64.
При работа с опаковани обекти трябва да се внимава, защото ако не бъдат съобразени някои особености, може да се наблюдава странно поведение на програмата. Ето един такъв пример:
|
using System;
interface IMovable { void Move(int aX, int aY); }
/// <summary> /// Много лоша практика! Структурите не бива /// да съдържат логика, а само данни! /// </summary> struct Point : IMovable { public int mX, mY;
public void Move(int aX, int aY) { mX += aX; mY += aY; }
public override string ToString() { return String.Format("({0},{1})", mX, mY); } }
class TestPoint { static void Main() { Point p1 = new Point(); Console.WriteLine("p1={0}", p1); // p1=(0,0)
IMovable p1mov = (IMovable) p1; // p1 се опакова IMovable p2mov = // p1mov не се опакова втори (IMovable) p1mov; // път, защото е вече опакован Point p2 = (Point) p2mov; // p2mov се разопакова
p1.Move(-100,-100); p2mov.Move(5,5); p2.Move(100,100);
Console.WriteLine("p1={0}", p1); // p1=(-100,-100) Console.WriteLine("p1mov={0}", p1mov); // p1mov=(5,5) Console.WriteLine("p2mov={0}", p2mov); // p2mov=(5,5) Console.WriteLine("p2={0}", p2); // p2=(100,100) } } |
Резултатът от изпълнение на примера е следният:

Основната причина за този резултат е фактът, че при преобразуване към интерфейс структурите се опаковат и съответно се създава копие на данните, намиращи се в тях. Опаковането е съвсем в реда на нещата, като се има предвид, че структурите са стойностни типове, а интерфейсите са референтни типове.
|
|
Препоръчва се, когато се използват структури в C#, те да съдържат само данни. Лоша практика е в структура да се дефинират методи с логика, както и структура да имплементира интерфейс. |
Да разгледаме как работи примерът. Ако съобразим разположението на стойностните и референтните променливи в паметта, можем да си обясним какво се случва:

Променливите p1 и p2 са от стойностен тип и се разполагат директно в стека (и заемат по 8 байта от него).
Променливите p1mov и p2mov са от референтен тип и се разполагат в динамичната памет. В стека за тях се пазят по 4 байта, които съдържат адреса на стойността им.
С помощта на дебъгера на VS.NET можем да проследим точното разположение и стойностите на тези променливи. В горната таблица е показано състоянието им точно преди завършване на програмата.
Напомняме, че при Intel архитектурата стекът расте надолу и свършва на адрес 0x00000000.
Често пъти освен за равенство е необходимо обектите да се сравняват спрямо някаква подредба (например лексикографска за низове или по големина за числови типове). В .NET Framework типовете, които могат да бъдат сравнявани един с друг, трябва да имплементират интерфейса System.IComparable.
Интерфейсът дефинира един-единствен метод – CompareTo(object). Този метод трябва да реализира сравняването и да връща:
- число < 0 – ако подаденият обект е по-голям от this инстанцията
- 0 – ако подаденият обект е равен на this инстанцията
- число > 0 – ако подаденият обект е по-малък от this инстанцията
IComparable се използва от .NET Framework при сортиране на масиви и колекции и при някои други операции, изискващи сравнение по големина.
IComparable е имплементиран от много системни .NET типове, като например от примитивните стойностни типове System.Char, System.Int32, System.Single, System.Double, от символните низове (System.String) и от изброените типове (System.Enum). Това улеснява разработчиците при всекидневната им работа и често пъти им спестява излишни усилия.
В следващия пример е илюстрирано как можем да имплементираме IComparable за потребителски дефинирани типове:
|
using System;
class Student : IComparable { private string mFirstName; private string mLastName;
public Student(string aFirstName, string aLastName) { mFirstName = aFirstName; mLastName = aLastName; }
public int CompareTo(object aObject) { if (! (aObject is Student)) { throw new ArgumentException( "The object is not Student."); }
Student student = (Student) aObject; int firstNameCompareResult = String.Compare(this.mFirstName, student.mFirstName); if (firstNameCompareResult != 0) { return firstNameCompareResult; } else { int lastNameCompareResult = String.Compare(this.mLastName, student.mLastName); return lastNameCompareResult; } } }
class TestIComparable { static void Main() { Student st1 = new Student("Бате", "Киро"); Student st2 = new Student("Кака", "Мара");
Console.WriteLine( "st1.CompareTo(st2) = {0}", st1.CompareTo(st2)); // Result: -1
Console.WriteLine( "st1.CompareTo(st1) = {0}", st1.CompareTo(st1)); // Result: 0
Console.WriteLine( "st1.CompareTo(42) = {0}", st1.CompareTo(42)); // Result: System.ArgumentException } } |
В примера се дефинира клас Student, който съдържа две информационни полета – име и фамилия. Имплементацията на CompareTo() извършва лексикографско сравнение на студенти – първо по име, а след това по фамилия при еднакви имена. Ето как изглежда изходът от примера:

В програмирането се срещат типове, които съдържат много на брой инстанции на други типове. Такива типове се наричат контейнери или още колекции. Колекции например са масивите, защото съдържат много на брой еднакви елементи.
Често пъти се налага да се обходят всички елементи на даденa колекция. За да става това по стандартен начин, в .NET Framework са дефинирани интерфейсите IEnumerable и IEnumerator.
Интерфейсът System.IEnumerable се имплементира от колекции и други типове, които поддържат операцията "обхождане на елементите им в някакъв ред". Този интерфейс дефинира само един метод – методът GetEnumerator(). Той връща итератор (инстанция на IEnumerator) за обхождане на елементите на дадения обект.
Обектите, поддържащи IEnumerable интерфейса, могат да се използват от конструкцията foreach в C# за обхождане на всичките им елементи.
Интерфейсът IEnumerable е реализиран от много системни .NET типове, като System.Array, System.String, ArrayList, Hashtable, Stack, Queue, SortedList и др. с цел да се улесни работата с тях.
Интерфейсът System.IEnumerator имплементира обхождане на всички елементи на колекции и други типове. Той реализира прост итератор чрез следните методи и свойства:
- Свойство Current – връща текущия елемент.
- Метод bool MoveNext() – преминава към следващия елемент и връща true, ако той е валиден.
- Метод Reset() – премества итератора непосредствено преди първия елемент (установява го в начално състояние).
Следващият пример илюстрира как могат да бъдат имплементирани интерфейсите IEnumerable и IEnumerator, след което да бъдат използвани във foreach конструкция в C#:
|
using System; using System.Collections;
class BitSet32 : IEnumerable { private uint mBits = 0;
public void Set(int aIndex, bool aValue) { if (aIndex<0 || aIndex>31) { throw new ArgumentException("Invalid index!"); }
uint bitMask = (uint) 1 << aIndex;
// Set bit aIndex to 0 mBits = mBits & (~bitMask);
if (aValue) { // Set bit aIndex to 1 mBits = mBits | bitMask; } }
public bool Get(int aIndex) { if (aIndex<0 || aIndex>31) { throw new ArgumentException("Invalid index!"); }
uint bitMask = (uint) 1 << aIndex; bool value = ((mBits & bitMask) != 0); return value; }
public IEnumerator GetEnumerator() { return new BitSet32Enumerator(this); }
class BitSet32Enumerator : IEnumerator { private BitSet32 mBitSet32; private int mCurrentIndex = -1;
public BitSet32Enumerator(BitSet32 aBitSet32) { mBitSet32 = aBitSet32; }
public bool MoveNext() { mCurrentIndex++; bool valid = (mCurrentIndex < 32); return valid; }
public void Reset() { mCurrentIndex = -1; }
public object Current { get { return mBitSet32.Get(mCurrentIndex); } } } }
class TestBitSet32 { static void Main() { BitSet32 set = new BitSet32(); set.Set(0, true); set.Set(31, true); set.Set(5, true); set.Set(5, false); set.Set(10, true);
int index = 0; foreach (bool value in set) { Console.WriteLine("set[{0}] = {1}", index, value); index++; } } } |
Резултатът от изпълнение на примера е следният:

Класът BitSet32 представлява множество от 32 булеви стойности. Той съхранява стойностите си в UInt32 поле като комбинация от битове – по 1 бит за всяка от тях. Методът Set(index, value) изчислява битова маска за зададения индекс, нулира съответния бит и ако е зададена стойност true, го установява след това в единица. Методът Get(index) изчислява битова маска за зададения индекс и връща стойността на съответния бит.
Класът BitSet32 имплементира интерфейса IEnumerable като в метода му GetEnumerator() създава и връща инстанция на специален вътрешен клас BitSet32Enumerator, инициализирана по текущия BitSet32 обект.
Класът BitSet32Enumerator е имплементация на интерфейса IEnumerator. Той съхранява текущия индекс от обхождането на BitSet32 обекта във вътрешна променлива mCurrentIndex. Методът MoveNext() увеличава текущия индекс и ако не е достигнат краят, връща true. Методът Reset() задава стойност -1 за текущия индекс (това е елементът преди първия). Свойството Current връща елемента от текущата позиция.
Главната програма демонстрира правилната работа на класа BitSet32 и имплементацията на интерфейсите IEnumerable и IEnumerator. Тя създава инстанция на BitSet32, променя някои от битовете и отпечатва всички стойности с цикъл foreach.
1. Избройте основните разлики между стойностните и референтните типове. Кои от следните типове са стойностни и кои референтни?
- int, char, string, float, изброени типове, класове, структури, интерфейси, делегати, масиви, указатели, опаковани стойностни типове
2. Дефинирайте клас Student, който съдържа данните за един студент: трите му имена, ЕГН, местоживеене (постоянен и временен адрес), телефон (стационарен и мобилен), e-mail, курс, специалност, ВУЗ, факултет и т.н. Използвайте изброен тип (enumeration) за специалностите, ВУЗ-овете и факултетите. Реализирайте стандартните методи, наследени от System.Object: Equals(object), ToString(), GetHashCode() и операторите == и !=.
3. Добавете имплементация на интерфейса ICloneable за класа Student. Методът Clone() трябва да копира в нов обект всяко от полетата на класа Student.
4. Дефинирайте структурата от данни двоично наредено дърво за претърсване (binary search tree) с операции "добавяне на елемент", "търсене на елемент" и "изтриване на елемент". Не е необходимо да поддържате дървото балансирано (това ще ви спести много усилия). Имплементирайте виртуалните методи ToString(), Equals(object), GetHashCode() от System.Object и операторите за сравнение == и !=. Добавете и реализация на интерфейса ICloneable за дълбоко копиране на дървото.
Упътване: За да улесните работата си, използвайте два типа – клас BinarySearchTree (за самото дърво) и клас TreeNode (за елементите на дървото).
5. Дефинирайте клас ComplexNumber, който съдържа комплексно число. Имплементирайте за него интерфейса IComparable.
6. Дефинирайте клас BitSet256, който представлява масив от 256 булеви стойности и се съхранява вътрешно като 4 на брой 64-битови полета (UInt64). Реализирайте методи Get(int index), Set(int index, bool value) и индексатор за достъп. Имплементирайте и интерфейса IEnumerable, като за целта използвате вътрешен клас, който имплементира IEnumerator.
1. Светлин Наков, Обща система от типове (Common Type System) – http://www.nakov.com/dotnet/lectures/Lecture-4-Common-Type-System-v1.0.ppt
2. Jeffrey Richter, Applied Microsoft .NET Framework Programming, Microsoft Press, 2002, ISBN 0735614229
3. Tom Archer, Andrew Whitechapel, Inside C#, 2-nd Edition, Microsoft Press, 2002, ISBN 0735616485
4. MSDN Training, Programming with the MSicrosoft® .NET Framework (MOC 2349B), Module 5: Common Type System
5. Svetlin Nakov, .NET Framework Overview – http://www.nakov.com/ publications/Nakov-DotNET-Framework-Overview-english.ppt
6. MSDN Library – http://msdn.microsoft.com
|
Национална академия по разработка на софтуер |
|
|
Лекторите » Светлин Наков е автор на десетки технически публикации и няколко книги, свързани с разработката на софтуер, заради което е търсен лектор и консултант. Той е разработчик с дългогодишен опит, работил по разнообразни проекти, реализирани с различни технологии (.NET, Java, Oracle, PKI и др.) и преподавател по съвременни софтуерни технологии в СУ "Св. Климент Охридски". През 2004 г. е носител на наградата "Джон Атанасов" на президента на България Георги Първанов. Светлин Наков ръководи обучението по Java технологии в Академията.
» Мартин Кулов е софтуерен инженер и консултант с дългогодишен опит в изграждането на решения с платформите на Microsoft. Мартин е опитен инструктор и сертифициран от Майкрософт разработчик по програмите MCSD, MCSD.NET, MCPD и MVP и международен лектор в световната организация на .NET потребителските групи INETA. Мартин Кулов ръководи обучението по .NET технологии в Академията. |
Академията » Национална академия по разработка на софтуер (НАРС) е център за професионално обучение на софтуерни специалисти.
» НАРС провежда БЕЗПЛАТНО курсове по разработка на софтуер и съвременни софтуерни технологии в София и други градове.
» Предлагани специалности: § Въведение в програмирането (с езиците C# и Java) § Core .NET Developer § Core Java Developer
» Качествено обучение с много практически проекти и индивидуално внимание за всеки.
» Гарантирана работа! Трудов договор при постъпване в Академията.
» БЕЗПЛАТНО! Учите безплатно във въведителните курсове и по стипендии от работодателите в следващите нива. |
- Базови познания за архитектурата на .NET Framework
- Базови познания за общата система от типове в .NET (Common Type System)
- Базови познания по обектно-ориентирано програмиране с .NET Framework и C#
- Делегати (delegates). Дефиниране, инстанциране, извикване
- Single-cast и multicast делегати
- Събития (events)
- Разлика между събитие и инстанция на делегат
- Утвърдени конвенции при дефиниране и използване на събития в .NET Framework
- Кога да използваме интерфейси, събития и делегати?
В настоящата тема ще разгледаме референтния тип делегат. Ще се запознаем с начините на неговото използване, различните видове делегати, както и негови характерни приложения. Ще представим понятието събитие и ще обясним връзката му с делегатите. Ще сравним делегатите и интерфейсите и ще видим в кои случаи е добре да се използват едните и в кои – другите.
Делегатите са референтни типове, които описват сигнатурата на даден метод (броя, типа и последователността на параметрите му) и връщания от него тип. Могат да се разглеждат като "обвивки" на методи - те представляват структури от данни, които приемат като стойност методи, отговарящи на описаната от делегата сигнатура и връщан тип.
Делегатът се инстанцира като клас и методът се подава като параметър на конструктора. Възможно е делегатът да сочи към повече от един метод, но на това ще се спрем подробно малко по-нататък.
Съществува известна прилика между делегатите и указателите към функции в други езици, например Pascal, C, C++, тъй като последните представляват типизиран указател към функция. Делегатите също съдържат силно типизиран указател към функция, но те са и нещо повече – те са напълно обектно-ориентирани. На практика делегатите представляват класове. Инстанцията на един делегат може да съдържа в себе си както инстанция на обект, така и метод.
Едно от основните приложения на делегатите е реализацията на "обратни извиквания", т.нар. callbacks. Идеята е да се предаде референция към метод, който да бъде извикан по-късно. Така може да се осъществи например асинхронна обработка – от даден код извикваме метод, като му подаваме callback метод и продължаваме работа, а извиканият метод извиква callback метода когато е необходимо. Със средствата на делегатите е възможно даден клас да позволи на потребителите си да предоставят метод, извършващ специфична обработка, като по този начин обработката не се фиксира предварително.
Делегатите могат да сочат както към методи на инстанция на класа, в който са декларирани, така и към статични методи. Това представлява удобство, защото можем да използваме делегат, без да сме създали инстанция на съдържащия го клас. Така се спестява създаването на допълнителна инстанция на клас. Друга възможност е да се отложи създаването на инстанция на делегат докато тя стане необходима. За целта можем да дефинираме свойство на класа, който ползва делегата, и в get метода на свойството да създадем делегата.
Следващият пример демонстрира деклариране на делегат, инстанциране на делегат и извикване на метод, сочен от него.
|
// Declaration of a delegate public delegate void SimpleDelegate(string aParam);
class TestDelegate { public static void TestFunction(string aParam) { Console.WriteLine("I was called by a delegate."); Console.WriteLine("I got parameter {0}.", aParam); }
public static void Main() { // Instantiation of а delegate SimpleDelegate simpleDelegate = new SimpleDelegate(TestFunction); // Invocation of the method, pointed by a delegate simpleDelegate("test"); } } |
След изпълнение на примера се получава следният резултат:

В първия ред от кода се декларира делегат. За целта се използва ключовата дума delegate. След това в класа се дефинира функция, която има сигнатура и връщан тип като тези, декларирани от делегата. В главния метод на класа се инстанцира делегата, като дефинираният метод се подава като параметър и след това той се извиква чрез делегата.
Делегатите в .NET Framework са специални класове, които наследяват System.Delegate или System.MulticastDelegate. От тези класове обаче явно могат да наследяват само CLR и компилаторът. Всъщност, те не са от тип делегат – тези класове се използват, за да се наследяват от тях типове делегат.
Всеки делегат има "списък на извикване" (invocation list), който представлява наредено множество делегати, като всеки елемент от него съдържа конкретен метод, рефериран от делегата. Делегатите могат да бъдат единични и множествени.
Единичните делегати наследяват класа System.Delegate. Тези делегати извикват точно един метод. В списъка си на извикване имат единствен елемент, съдържащ референция към метод.
Множествените делегати наследяват класа System.MulticastDelegate, който от своя страна е наследник на класа на System.Delegate. Те могат да викат един или повече метода. Техните списъци на извикване съдържат множество елементи, всеки рефериращ метод. В тях може един и същ метод да се среща повече от веднъж. При извикване делегатът активира всички реферирани методи. Множествените делегати могат да участват в комбиниращи операции.
Езикът C# съдържа запазената дума delegate, чрез която се декларира делегат. При тази декларация компилаторът автоматично наследява типа MulticastDelegate., т.е. създава множествен делегат. Затова ще обърнем по-голямо внимание именно на този вид делегати.
|
|
На практика singlecast делегатите почти не се използват и под делегат обикновено се има предвид multicast делегат. |
При извикване на multicast делегат се изпълняват всички методи от неговия списък на извикване. Методите се викат в реда, в който се намират в списъка, като дублиращите се методи (ако има такива) се викат толкова пъти, колкото се срещат в списъка.
Ако сигнатурата на методите, викани от делегата включва връщана стойност, връща се стойността, получена при изпълнението на последния елемент от списъка на делегата. Когато сигнатурата включва out или ref параметър, то всички извикани методи променят неговата стойност последователно в реда си на извикване и крайният резултат е резултата от последния извикан метод.
Възможно е при извикване на multicast делегат някой от методите от списъка му на извикване да хвърли изключение. В този случай методът спира изпълнение и управлението се връща в кода, извикал делегата. Останалите методи от списъка не се извикват. Дори извикващият метода да хване изключението, останалите методи от списъка не се изпълняват.
Класът System.MulticastDelegat е наследник на System.Delegate. Той е базов клас за всички делегати в C#, но самият той не е тип делегат – при срещане на ключовата дума delegate компилаторът създава клас, наследник на System.MulticastDelegat. Всички делегати наследяват от него няколко важни метода, които сега ще разгледаме.
Multicast делегатите могат да участват в комбиниращи операции. Това се реализира с метода Combine() на класа. Той слива списъците от методи на няколко делегата от еднакъв тип. Този метод е предефиниран и може да приема като параметри както два multicast делегата от еднакъв тип, така и масив от multicast делегати от еднакъв тип. В резултат връща нов multicast делегат, чийто списък от методи съдържа списъците на подадените като параметри делегати.
|
|
Делегатът е неизменяем обект – веднъж създаден, повече не може да се променя. |
Операцията "сливане" не променя съществуващите делегати, а създава нов делегат. Ако делегатът, получен в резултат на комбиниране не реферира нито един метод, Combine() връща стойност null, а не делегат с празен списък от методи. В C# е предефиниран операторът += за комбиниране на делегати.
Освен, че списъците от методи на няколко делегата могат да бъдат обединявани в един, възможно е също от списъка на един делегат да се извади списъкът на друг. Това се извършва чрез метода Remove(). Той приема като параметри два делегата и в резултат връща нов делегат, чийто списък е получен като от списъка на първия аргумент е премахнато последното срещане на списъка на втория аргумент. Ако двата списъка са еднакви, или ако списъкът на втория аргумент не се среща в списъка на първия, резултатът е null. В езика C# е предефиниран операторът -= за изваждане на списъци на делегати.
С метода GetInvocationList() може да се получат методите, викани от делегата. По-точно, методът връща масив от делегати от типа на делегата, за който се вика методът. Всеки делегат от масива съдържа в списъка си от методи единствен елемент – някой от методите, викани от делегата. В масива има по един делегат за всеки метод и делегатите са подредени така, както се извикват от multicast делегата. Извикването на делегатите последователно в реда, в който се срещат в масива, ще има същия ефект като от извикване на самия делегат.
При декларирането на делегат компилаторът създава няколко служебни метода, които не могат да се извикват явно. Методът Invoke() извиква метода (методите), сочен (сочени) от делегата. Следователно при обръщение към делегата всъщност се извиква методът Invoke(), а той вика методите от списъка на делегата. Други методи са BeginInvoke() и EndInvoke(), чрез които се реализира асинхронно извикване. Освен това компилаторът декларира и конструктор на делегата.
Освен описаните методи, класът System.MulticastDelegate има и едно важно свойство – Method. То връща първия метод от списъка на делегата.
В настоящия пример се демонстрира работата с multicast делегати и се илюстрира хода на изпълнение на програмния код:
|
using System;
public delegate void StringDelegate(string aValue);
public class TestDelegateClass { void PrintString(string aValue) { Console.WriteLine(aValue); }
void PrintStringLength(string aValue) { Console.WriteLine("Length = {0}", aValue.Length); }
static void PrintStringWithDate(string aValue) { Console.WriteLine("{0}: {1}", DateTime.Now, aValue); }
static void PrintInvocationList(Delegate aDelegate) { Console.Write("("); Delegate[] list = aDelegate.GetInvocationList(); foreach (Delegate d in list) { Console.Write(" {0}", d.Method.Name); } Console.WriteLine(" )"); }
public static void Main() { TestDelegateClass tdc = new TestDelegateClass(); StringDelegate printDelegate = new StringDelegate(tdc.PrintString); StringDelegate printLengthDelegate = new StringDelegate(tdc.PrintStringLength); StringDelegate printWithDateDelegate = new StringDelegate(PrintStringWithDate);
PrintInvocationList(printDelegate); // Prints: ( PrintString )
StringDelegate combinedDelegate = (StringDelegate) Delegate.Combine(printDelegate, printLengthDelegate);
PrintInvocationList(combinedDelegate); // Prints: ( PrintString PrintStringLength )
combinedDelegate = (StringDelegate) Delegate.Combine(combinedDelegate, printWithDateDelegate);
PrintInvocationList(combinedDelegate); // Prints: ( PrintString PrintStringLength // PrintStringWithDate )
// Invoke the delegate combinedDelegate("test"); } } |
След изпълнение на примера се получава следният резултат:

Най-напред се декларира делегат, след което в класа се дефинират три метода, чиито сигнатури съответстват на декларираната от делегата. В главния метод на класа се създават три инстанции на делегата, всяка съдържаща референция към някой от трите метода и се извежда списъкът с методи на първата инстанция (той съдържа само един метод). След това се създава делегат, обединяващ списъците на първите два делегата, отпечатва се неговия списък с методи (този път списъкът съдържа два метода) и накрая към комбинирания делегат се добавя и третата инстанция на делегат. Отново се извежда списъкът от методи, който сега съдържа и трите метода, реферирани от делегатите, и трите метода се извикват посредством комбинирания делегат.
За проследяване изпълнението на примера стъпка по стъпка ще използваме проекта Demo-1-Multicast-Delegates от демонстрациите, който съдържа кода от горния пример. Изпълняваме следните стъпки:

На картинката по-горе е показан изглед от VS.NET, в момент на постъпково проследяване на изпълнението на примера.
Инструментът .NET Reflector е декомпилатор за .NET асемблита. Използва се за генериране на програмен код от изпълним код. Той има удобен потребителски интерфейс и за разлика от вградения в .NET Framework SDK инструмент ILDASM .NET Reflector може да декомпилира до код на C# и VB.NET, а не само до MSIL код. Инструментът е безплатен и може да бъде изтеглен от адрес http://www.aisto.com/roeder/dotnet/.
Настоящият пример илюстрира използването на инструмента .NET Reflector за разглеждане на кода, който компилаторът на C# генерира при деклариране на делегат.

За да декомпилираме асемблито от предишния пример и да разгледаме кода му, трябва да изпълним следните стъпки:
Събитията могат да се разглеждат като съобщения за настъпване на някакво действие. В компонентно-ориентираното програмиране компонентите изпращат събития (events) към своя притежател за да го уведомят за настъпването на интересна за него ситуация. Този модел е много характерен например за графичните потребителски интерфейси, където контролите уведомяват чрез събития други класове от програмата за действия от страна на потребителя. Например, когато потребителят натисне бутон, бутонът предизвиква събитие, с което известява, че е бил натиснат. Разбира се, събития могат да се предизвикват не само при реализиране на потребителски интерфейси. Нека вземем за пример програма, в която като част от функционалността влиза трансфер на файлове. Приключването на трансфера на файл може да се съобщава чрез събитие.
Механизмът на събитията реализира шаблона "Наблюдател" (Observer) или, както още се нарича, Публикуващ/Абонати (Publisher/Subscriber), при който един клас публикува събитие, а произволен брой други класове могат да се абонират за това събитие. По този начин се реализира връзка на зависимост от тип един към много, при която когато един обект промени състоянието си, зависещите от него обекти биват информирани за промяната и автоматично се обновяват.
Обектът, който предизвиква дадено събитие се нарича изпращач на събитието (event sender). Обектът, който получава дадено събитие се нарича получател на събитието (event receiver). За да могат да получават дадено събитие, получателите му трябва преди това да се абонират за него (subscribe for event).
За едно събитие могат да се абонират произволен брой получатели. Изпращачът на събитието не знае кои ще са получателите на събитието, което той предизвиква. Затова чрез механизма на събитията се постига по-ниска степен на свързаност (coupling) между отделните компоненти на програмата.
В компонентния модел на .NET Framework абонирането, изпращането и получаването на събития се поддържа чрез делегати и събития. Реализацията на механизма на събитията е едно от главните приложения на делегатите. Класът, който публикува събитието, дефинира делегат, който абонатите на събитието трябва да имплементират. Когато събитието бъде предизвикано, методите на абонатите се извикват посредством делегата. Тези методи обикновено се наричат обработчици на събитието. Делегатът е multicast делегат, за да могат чрез него да се извикват много обработващи методи (на всички абонати).
В C# събитията представляват специални инстанции на делегати. Те се декларират с ключовата дума event, която може да се предшества от модификатори, като например модификатори за достъп. Обикновено събитията са с модификатор public. След ключовата дума event се записва името на делегата, с който се свързва съответното събитие. За тази цел делегатът трябва да бъде дефиниран предварително. Той може да бъде дефиниран от потребителя, но може да се използва и вграден делегат. Тези делегати трябва да бъдат от тип void.
Единствените операции, които са позволени върху събития са операциите за абониране и премахване на абонамент. За целта при деклариране на събитие компилаторът автоматично дефинира операторите += за абониране за събитие и -= за премахване на абонамент. Тези оператори могат да бъдат извиквани от класове, външни за класа, в който е дефинирано събитието, така че външен код може да добавя и премахва обработчици на събитието, но не може по никакъв друг начин да манипулира списъка с обработчици. Възможно е подразбиращото се поведение на операторите += и -= да бъде предефинирано.
Събитията и делегатите са много тясно свързани. Въпреки това член-променлива от тип делегат не е еквивалентна на събитие, декларирано с ключовата дума event, т.е. public MyDelegate m не е същото като public event MyDelegate m. Първото е декларация на променлива m, която е от тип MyDelegate, докато второто декларира събитие, което ще се обработва от делегат от тип MyDelegate.
Между делегатите и събитията има и други разлики, освен в декларирането. Например, делегатите не могат да бъдат членове на интерфейси, докато събитията могат. В такъв случай класът, който имплементира интерфейса, трябва да предостави подходящо събитие.
Извикването на събитие може да стане само в класа, в който то е дефинирано. Това означава, че само класът, в който се дефинира събитие, може да предизвика това събитие. Това е наложително, за да се спази шаблонът на Публикуващ/Абонати – абонираните класове се информират при промяна на състоянието на публикуващия и именно публикуващият е отговорен за разпращане на съобщенията за промяната, настъпила у него.
Друга важна подробност за събитията е, че достъпът до тях е синхронизиран. Това има значение при създаването на многонишкови приложения, с които ще се запознаем подробно в темата "Многонишково програмиране и синхронизация".
В .NET Framework се използва утвърдена конвенция за събитията. Тя определя именуването на събитията и свързаните с тях методи и делегати, връщаните типове и приеманите аргументи от делегатите.
Делегатите, използвани за събития по конвенция имат имена, които се състоят от глагол и EventHandler (SomeVerbEventHandler). Така те се различават лесно от декларациите на други делегати в приложението.
|
|
Делегатите, използвани за събития, не трябва да връщат стойност. Връщаният тип от делегата трябва да бъде void. |
Конвенцията налага делегатите, които ще се използват от събития, да приемат два аргумента. Единият представлява обектът-изпращач на събитието, т. е. това е източникът на събитието, или публикуващият от шаблона Публикуващ/Абонати. Той трябва да е от тип System.Object. Другият аргумент представя информация за изпращаното събитие. Той е от тип, наследник на System.EventArgs.
Ето пример за деклариране на делегат, който ще бъде използван от събитие:
|
public delegate void ItemChangedEventHandler( object aSender, ItemChangedEventArgs aEventArgs); |
Събитията обикновено се обявяват като public, въпреки че са възможни и останалите модификатори за достъп. Имената им започват с главна буква, а последната дума от името е глагол. Следва пример за декларация на събитие:
|
public event ItemChangedEventHandler ItemChanged; |
За предизвикване на събитие се създава protected void метод. Прието е името му да започва с On, следвано от името на събитието (например OnEventName). Този метод предизвиква събитието като извиква делегата.
Методът трябва да е protected, защото това позволява при наследяване на класа, в който е декларирано събитието, наследниците да могат да предизвикват събитието. Ако методът не е protected, наследниците няма да могат да предизвикат събитието, защото не могат да се обърнат директно към него, тъй като то е достъпно единствено в класа, в който е декларирано. За още по-голяма гъвкавост е възможно освен protected, методът да бъде обявен virtual, което би позволило на наследниците да го предефинират. Така те биха могли да прихващат извикването на събитията от базовия клас и евентуално да извършват собствена обработка. Следващият пример показва как се декларира метод за предизвикване на събитие:
|
protected void OnItemChanged() { … } |
Обикновено името на метода-получател (обработчикът) на събитието има вида Обект_Събитие, както се илюстрира в следния пример:
|
private void OrderList_ItemChanged () { … } |
В настоящия пример се разглежда дефинирането и използването на събития като се спазва утвърдената конвенция в .NET Framework. Демонстрира се изпращане и получаване на събития.
|
// A delegate type for hooking up change notifications public delegate void TimeChangedEventHandler( object aSender, TimeChangedEventArgs aEventArgs);
// A class that inherits System.EventArgs and adds // information for the current time public class TimeChangedEventArgs : EventArgs { private int mTicksLeft;
public TimeChangedEventArgs(int aTicksLeft) { mTicksLeft = aTicksLeft; }
public int TicksLeft { get { return mTicksLeft; } } }
public class Timer { private int mTickCount; private int mInterval;
// The event that will be raised when the time changes public event TimeChangedEventHandler TimeChanged;
public Timer(int aTickCount, int aInterval) { mTickCount = aTickCount; mInterval = aInterval; }
public int TickCount { get { return mTickCount; } }
public int Interval { get { return mInterval; } }
// The method that invokes the event protected void OnTimeChanged(int aTick) { if (TimeChanged != null) { TimeChangedEventArgs args = new TimeChangedEventArgs(aTick); TimeChanged(this, args); } }
public void Run() { int tick = mTickCount; while (tick > 0) { System.Threading.Thread.Sleep(mInterval); tick--; OnTimeChanged(tick); } } }
public class TimerDemo { // The event handler method private static void Timer_TimeChanged(object aSender, TimeChangedEventArgs aEventArgs) { Console.WriteLine("Timer! Ticks left = {0}", aEventArgs.TicksLeft); }
public static void Main() { Timer timer = new Timer(10, 1000); timer.TimeChanged += new TimeChangedEventHandler(Timer_TimeChanged); Console.WriteLine( "Timer started for 10 ticks at interval 1000 ms."); timer.Run(); } } |
При изпълнение на примера се получава следният резултат:

Класът Timer от примера служи за предизвикване на дадено събитие през определен интервал от време.
Най-напред се декларира делегатът TimeChangedEventHandler, който ще бъде типа на предизвикваното събитие. Според обяснените конвенции той приема два аргумента – един от тип Object, и един от тип, наследник на EventArgs. Този наследник е класът TimeChangedEventArgs. Той добавя член, който съдържа информация за оставащия брой извиквания.
В класа Timer се декларира събитието TimeChanged от тип TimeChangedEventHandler. Методът OnTimeChanged на класа Timer проверява дали има абонати за събитието (за целта проверява дали събитието няма стойност null) и в случай, че има абонати предизвиква събитието. В метода Run() на класа Timer периодично се предизвиква събитието TimeChanged чрез обръщение към метода OnTimeChanged.
В класа TimerDemo се декларира обработчик на събитието TimeChanged и в главната функция TimerDemo се абонира за събитието.
За проследяване на примера стъпка по стъпка можем да използваме проекта Demo-3-Events от демонстрациите. За целта изпълняваме следните стъпки:
На картинката е показан изглед от VS.NET в момент на постъпково проследяване на изпълнението на примера:

Не винаги има нужда събитието да генерира някакви данни, които да изпрати на абонатите. В такъв случай може да се използва вградения делегат System.EventHandler. Той дефинира референция към callback метод, който обработва именно такива събития. Съответно методите-обработчици на такива събития трябва да съответстват на декларираните от делегата System.EventHandler връщан тип и сигнатура. Следва декларацията на делегата:
|
public delegate void EventHandler(Object sender, EventArgs e); |
Този делегат се използва на много места вътрешно от .NET Framework. Например, типът на събитието, което възниква при натискане на бутон, е точно EventHandler, тъй като при това събитие не се генерира информация за предаване.
Както се вижда от горната дефиниция, вграденият делегат EventHandler има като втори аргумент обект от тип EventArgs. За този тип вече стана дума – всеки делегат, който се използва за тип на събитие трябва да приема аргумент от тип, който е наследник на EventArgs.
Класът EventArgs наследява всичките си членове от System.Object, като добавя конструктор и публично статично поле Empty, което представя събитие без данни. По този начин се улеснява използването на събития, които не носят данни.
Ако събитието трябва да съдържа информация се използва клас, който наследява System.EventArgs и добавя необходимите членове за нейното представяне. Ако все пак няма нужда от подобна информация, може директно да се използва инстанция на System.EventArgs при предизвикване на събитието. Случаят с EventHandler е точно такъв, тъй като той се използва за обработка на събития, които не носят информация. Затова той приема като аргумент директно EventArgs.
В настоящия пример ще се илюстрира употребата на вградения делегат System.EventHandler:
|
public class Button { public event EventHandler Click; public event EventHandler GotFocus; public event EventHandler TextChanged; ... }
public class ButtonTest { private static void Button_Click(object aSender, EventArgs aEventArgs) { Console.WriteLine("Button_Click() event called."); }
public static void Main() { Button button = new Button(); button.Click += new EventHandler(Button_Click); button.DoClick(); } } |
В горния пример се дефинира клас Button с набор събития. Класът ButtonTest дефинира функция Button_Click(…), съответстваща на делегата за обработване на събитието Click на класа Button и в главния си метод се абонира за това събитие и го предизвиква с обръщение към метода DoClick(), който за краткост е изпуснат в примера.
Както беше вече споменато, за разлика от делегатите, събитията могат да бъдат членове на интерфейси. Следващият код илюстрира това:
|
public interface IClickable { event ClickEventHandler Click; } |
При имплементация на интерфейса, имплементиращият клас трябва да предизвиква събитието, което е декларирано в интерфейса. Допустимо е освен това при конкретната имплементация класът да реализира специфични add и remove методи, с което да промени тяхното поведение по подразбиране и да добави нетривиална логика.
Когато в интерфейс се декларират свойства, имплементиращият клас е длъжен да реализира техните методи (set, get или и двата, в зависимост от декларацията). За разлика от свойствата, при събитията не е задължително да се имплементират специфични add и remove методи – ако специфична реализация липсва, те получават поведение по подразбиране, съответно да добавят и премахват обработчици на събитието.
В настоящия пример ще разгледаме съвместната употреба на събития и интерфейси:
|
public delegate void ClickEventHandler(object aSender, EventArgs aEventArgs);
public interface IClickable { event ClickEventHandler Click; }
public class Button : IClickable { private ClickEventHandler mClick;
// Implement the event from the interface IClickable public event ClickEventHandler Click { add { mClick += value; Console.WriteLine("Subscribed to Button.Clicked event."); } remove { mClick -= value; Console.WriteLine( "Unsubscribed from Button.Clicked event."); } }
protected void OnClick() { if (mClick != null) { mClick(this, EventArgs.Empty); } }
public void FireClick() { Console.WriteLine("Button.FireClick() called."); OnClick(); } }
public class ButtonTest { private static void Button_Click(object aSender, EventArgs aEventArgs) { Console.WriteLine("Button_Click() event called."); }
public static void Main() { Button button = new Button(); button.Click += new ClickEventHandler(Button_Click); button.FireClick(); button.Click -= new ClickEventHandler(Button_Click); } } |
След изпълнение на примера се получава следният резултат:

В началото се декларира делегат, който ще бъде тип на събитието, дефинирано в интерфейса. След това се дефинира интерфейс IClickable с единствен член, който е събитието Click. Класът Button имплементира интерфейса IClickable, като добавя частна член-променлива от типа на делегата и реализира специфични add и remove методи за събитието, чрез които се извършва манипулация на списъка с методи на променливата-делегат. Освен това в класа се декларират методи за предизвикване на събитието и извикване на обработчиците му. Класът ButtonTest дефинира метод-обработчик на събитието Click и в главния си метод създава инстанция на класа Button, абонира се за събитието Click, предизвиква го и след това премахва абонамента.
В .NET Framework поведението "обратно извикване" може да се реализира чрез три механизма: делегати, събития и интерфейси. Досега разгледахме използването на делегатите и събитията.
Чрез интерфейси "обратно извикване" може да се реализира като методът, който трябва да се използва за "обратно извикване" се декларира като член на интерфейс. След това в различните класове, които имплементират интерфейса, методът може да бъде имплементиран по различни начини и така той не се обвързва с конкретна реализация. В класа, който ще извършва обръщение към callback метода се декларира променлива от типа на интерфейса, който съдържа декларацията на съответния метод. На тази променлива могат да се присвояват референции към обекти от различни класове, имплементиращи съответния интерфейс. По този начин могат да бъдат викани методи с различно поведение, в зависимост от необходимостта.
Следващият пример показва как можем да използваме интерфейс, за да реализираме "обратно извикване":
|
public interface IClickListener { void ClickPerformed(); }
public class Button { private IClickListener mClickListener;
public Button(IClickListener aClickListener) { mClickListener = aClickListener; }
public void DoClick() { if (mClickListener != null) { mClickListener.ClickPerformed(); } } }
public class ButtonTest : IClickListener { public static void Main() { ButtonTest buttonTest = new ButtonTest(); Button button = new Button(buttonTest); button.DoClick(); }
void IClickListener.ClickPerformed() { Console.WriteLine("Click performed."); } } |
След изпълнения на примера се получава следният резултат:

Интерфейсът IClickListener съдържа метода ClickPerformed(), чрез който ще се реализира "обратно извикване". В класа Button са дефинирани член mClickListener от типа на интерфейса и метод DoClick(), който извиква интерфейсния метод ClickPerformed(). Класът ButtonTest имплементира интерфейса IClickListener и в главната си функция създава обект от тип Button и извиква метода му DoClick(). При това се извиква имплементацията на ClickPerformed(), която е предоставена от ButtonTest.
Въпреки че "обратно извикване" може да се реализира чрез интерфейси, това не е типично приложение на интерфейс и е добре да се използва по-рядко. Докато делегатите дават възможност за извикване на множество методи, то всички те трябва да имат еднакъв връщан тип и сигнатура. При интерфейсите няма такова ограничение, и затова те трябва да се ползват именно когато е нужно даден обект да предоставя съвкупност от много различни callback методи.
Събитията се използват, когато разработваме компоненти, които трябва да известяват своя притежател за нещо, обикновено за промяна в текущото състояние или за извършване на някакво действие. Освен това чрез използване на събитията се поддържа съвместимост с компонентния модел на .NET.
Основните приложения на делегатите са за асинхронна обработка посредством callback методи и за даване на възможност на потребителите на клас да предоставят метод, извършващ специфична обработка, която не е предварително фиксирана. В този случай извикването на callback метода не е свързано с настъпването на някаква промяна или събитие, както е при събитията. Делегатите се използват, когато имаме единичен callback метод, който не е свързан с компонентния модел на .NET.
1. Обяснете какво представляват делегатите в .NET Framework.
2. Обяснете какво представляват събитията (events) в .NET Framework.
3. Какво се препоръчва от утвърдената конвенция за събитията в .NET Framework? Опишете програмния код за дефиниране и използване на събития.
4. Чрез средствата на делегатите реализирайте универсален статичен метод за изчисляване с някаква точност на безкрайни сходящи редове по зададена функция за общия им член. Чрез подходящи функции за общия член изчислете с точност два десетични знака безкрайните редове:
- 1 + 1/2 + 1/4 + 1/8 + 1/16 + …
- 1 + 1/4 + 1/9 + 1/16 + 1/25 + …
- 1 + 1/2! + 1/3! + 1/4! + 1/5! + …
5. Напишете клас Person, който описва един човек и съдържа свойствата: име, презиме, фамилия, пол, рождена дата, ЕГН, адрес, e-mail и телефон. Добавете към класа Person за всяко от неговите свойства по едно събитие от системния делегат System.EventHandler, което се активира при промяна на съответното свойство.
6. Създайте клас PropertyChangedEventArgs, наследник на класа System.EventArgs и дефинирайте в него три свойства – име на променено свойство (string), стара стойност (object) и нова стойност (object) заедно с подходящ конструктор. Създайте делегат PropertyChangedEventHandler за обработка на събития, който да приема два параметъра – обект-изпращач и инстанция на PropertyChangedEventArgs.
7. Напишете нов вариант на класа Person, който има само едно събитие с име PropertyChanged от тип PropertyChangedEventHandler, което се активира при промяна на някое от свойствата на класа (и съответно се извиква с подходящи параметри).
8. Изнесете дефиницията на събитието PropertyChanged в отделен интерфейс и променете класа така, че да имплементира интерфейса.
1. Светлин Наков, Делегати и събития – http://www.nakov.com/dotnet/ lectures/Lecture-5-Delegates-and-Events-v1.0.ppt
2. Jeffrey Richter, Applied Microsoft .NET Framework Programming, Microsoft Press, 2002, ISBN 0735614229
3. Jesse Liberty, Programming C#, O’Reilly, 2001, ISBN 0-596-00117-7
4. Andrew Whitechapel, Tom Archer, Inside C#, Microsoft Press, 2002, ISBN 0-7356-1648-5
5. MSDN Training, Programming with the Microsoft® .NET Framework (MOC 2349B), Module 8: Delegates and Events
6. Julien Couvreur, Curiosity is bliss – http://blog.monstuff.com/archives/ 000040.html
7. MSDN Library – http://msdn.microsoft.com
- Базови познания за архитектурата на .NET Framework
- Базови познания за общата система от типове в .NET (Common Type System)
- Базови познания за езика C#
- Какво представляват атрибутите?
- Прилагане на атрибути. Атрибути с параметри. Задаване на цел при прилагане на атрибут
- Къде се използват атрибутите?
- Дефиниране на собствени атрибути
- Извличане на атрибути от метаданните
- Мета-атрибутът AttributeUsage
В настоящата тема ще разгледаме какво представляват атрибутите в .NET Framework, как се прилагат и къде се използват. Ще обясним как могат да се дефинират собствени атрибути и да се извличат атрибути от метаданните на асемблитата.
В повечето езици за програмиране съществуват ключови думи. Такива например са спецификаторите за достъп, които определят областта на видимост на член-променливите на класовете (public, private, protected, …). Най-често компилаторите разпознават само ограничен набор ключови думи и програмистите нямат възможност да дефинират свои собствени.
За компенсиране на тази слабост в .NET Framework се дава възможност програмно да се добавят т. нар. атрибути. Те представляват описателни декларации към типове, полета, методи, свойства и други елементи на кода, подобни на ключовите думи от езиците за програмиране.
Атрибутите позволяват да се добавят собствени описателни елементи (анотации) към кода, написан на C# или на някой от другите езици от .NET платформата, без да се налага промяна в компилатора. По време на компилация те се записват в метаданните на асемблито и при изпълнение на кода могат да бъдат извличани и да влияят на поведението му.
Атрибутите са описателни тагове, които могат да се прилагат към различни елементи от кода, наричани цели. Целите могат да бъдат най-разнообразни: асемблита, типове, свойства, полета, методи, параметри и други елементи от кода.
Декларативна информация се асоциира с програмния код (към типовете, методите, свойствата и т.н.) чрез атрибути. Други приложения могат да извличат тази информация, за да определят как да бъдат използвани елементите, свързани с атрибутите.
Атрибутите реално представляват класове, които се инстанцират по време на компилация на сорс кода и се записват в метаданните на асемблито, от където могат да бъдат извличани по време на работа на приложението.
Атрибутите се делят на две групи – вградени в .NET Framework (които са част от CLR) и дефинирани от програмистите за целите на отделните приложения. Последните се наричат собствени (потребителски) атрибути и най-често се използват в комбинация с reflection (отражение на типовете).
По-долу ще разгледаме начините, по които можем да приложим атрибут към дадена цел.
За да се приложи атрибут, името му се огражда в квадратни скоби и се поставя непосредствено преди декларацията, за която се отнася. Ето един пример:
|
// Apply attribute System.FlagsAttribute to FileAccess enum [Flags] public enum FileAccess { Read = 1, Write = 2, ReadWrite = Read | Write } |
В посочения пример системният атрибут Flags (реално това е типът System.FlagsAttribute) е приложен към дефиницията на изброения тип FileAccess и указва, че този изброен тип може да се третира като битово поле, т.е. като множество от битови флагове.
За да бъде приложен атрибут към дадена дефиниция в кода, трябва да се изпълнят следните стъпки:
1. Да се дефинира нов атрибут или да се използва съществуващ, като неговото пространство от имена (namespace) се импортира в началото на текущия файл от сорс кода.
2. Да се изпише името на атрибута в квадратни скоби точно преди целта, към която се прилага. По желание могат да му бъдат предадени някакви параметри (инициализиращи данни).
Атрибутите за дадена цел могат да се прилагат и в комбинация. Това става по два начина: като се приложат един след друг или като се изброят със запетаи:
|
[MyFirstAttribute] [MySecondAttribute] public void SomeMethod() { … }
[MyFirstAttribute, MySecondAttribute] public void SomeMethod() { … } |
Двете декларации в горния пример са напълно еквивалентни. Те дефинират публичен метод SomeMethod() и прилагат към него атрибутите MyFirstAttribute и MySecondAttribute.
При прилагането на атрибути суфиксът Attribute може да бъде пропуснат и се подразбира от компилатора. Така следните декларации са еквивалентни на горните две:
|
[MyFirst] [MySecond] public void SomeMethod() { … }
[MyFirst, MySecond] public void SomeMethod() { … } |
Тъй като атрибутите са класове, при тяхното прилагане може да бъде извикван конструкторът на съответния клас. Ако атрибутът предлага конструктор без параметри, той може да бъде извикан като се добави () към декларацията. Следователно следващите две декларации са валидни и еквивалентни на предходните две:
|
[MyFirst()] [MySecondAttribute()] public void SomeMethod() { … }
[MyFirstAttribute(), MySecond()] public void SomeMethod() { … } |
От примерите виждаме, че има много синтактично валидни начини за прилагане на един и същ атрибут към дадена цел. За компилатора няма значение кой от тези варианти е употребен, но препоръката е да се използва този без суфикс Attribute, без изброяване със запетаи и без скоби (). За нашия пример препоръчителен е следният запис:
|
[MyFirst] [MySecond] public void SomeMethod() { … } |
Атрибутите в .NET Framework реално представляват .NET обекти (инстанции на клас, наследник на системния клас System.Attribute). Като такива те могат да имат един или няколко конструктора (вкл. конструктор по подразбиране), публични и частни полета, свойства и др. членове. Най-често атрибутите дефинират конструктори, публични полета и свойства, които използват за съхраняване на данните, подавани им като параметри по време на инициализация.
Всички атрибути в .NET Framework задължително наследяват класа System.Attribute (или негов наследник). Както ще видим по-долу, при дефиниране на собствени (потребителски атрибути) ние също трябва да наследяваме този клас.
Някои атрибути могат да приемат параметри. Параметрите биват два вида: позиционни и именувани. Позиционните параметри се подават с определена последователност и се инициализират от конструктора на атрибута, докато именуваните се подават в произволен ред и задават стойност на свойство или публично поле. Ето един пример:
|
[DllImport("user32.dll", EntryPoint="MessageBox")] public static extern int ShowMessageBox(int hWnd, string text, string caption, int type); ... ShowMessageBox(0, "Some text", "Some caption", 0); |
В примера е използвана комбинация между позиционни и непозиционни параметри. За да бъде приложен към метаданните в асемблито, атрибутът [DllImport] (System.Runtime.InteropServices.DllImportAttribute) по време на компилация се инстанцира и инициализира от компилатора по следния начин:
1. Създава се обект от класа System.Runtime.InteropServices. DllImportAttribute.
2. В конструктора му се подава като позиционен параметър стойност "user32.dll".
3. В публичното му поле EntryPoint се записва стойност "MessageBox".
Преди да бъдат записани в метаданните на съответното асембли, атрибутите се инициализират посредством подадените им параметри, с които се задават стойности за техните полета и свойства. При съхраняване в асемблито атрибутите запазват състоянието си.
На по-късен етап, когато атрибутите бъдат извлечени от метаданните на асемблито, стойностите на техните полета и свойства се извличат заедно с тях и могат да бъдат използвани от програмиста.
Атрибутите в .NET Framework могат да се прилагат към различни цели, например асембли, клас, интерфейс, член-променлива на тип и др. Възможните цели на атрибутите се дефинират от изброения тип AttributeTargets както следва:
|
Име на целта |
Употреба (прилага се към) |
|
Assembly |
самото асембли |
|
Module |
текущия модул |
|
Class |
клас |
|
Struct |
структура |
|
Interface |
интерфейс |
|
Enum |
изброен тип |
|
Delegate |
делегат |
|
Constructor |
конструктор |
|
Method |
метод |
|
Parameter |
параметър на метод |
|
ReturnValue |
връщаната стойност от метод |
|
Property |
свойство |
|
Field |
поле (член-променлива) |
|
Event |
събитие |
|
All |
всички възможни цели |
При прилагане на атрибут целта обикновено се подразбира. Например, ако поставим атрибут преди декларацията на даден метод, той ще се отнася за съответния метод.
Понякога не може да се използва целта по подразбиране, например ако искаме да приложим атрибут към асемблито. В такива случаи целта може да се зададе преди името на атрибута, отделена от него с двоеточие:
|
// The following attributes are applied to the target "assembly" [assembly: AssemblyTitle("Attributes Demo")] [assembly: AssemblyCompany("DemoSoft")] [assembly: AssemblyProduct("Entreprise Demo Suite")] [assembly: AssemblyCopyright("(c) 1963-1964 DemoSoft")] [assembly: AssemblyVersion("2.0.1.37")]
[Serializable] // The compiler assumes [type: Serializable] class TestClass { [NonSerialized] // The compiler assumes [field: NonSerialized] private int mStatus; ... } |
Както се вижда от коментарите в кода, за някои от атрибутите целта се задава експлицитно, а за други тя се подразбира.
Атрибутите се използват вътрешно от .NET Framework за различни цели – при сериализация на данни, за описание на различни характеристики, свързани със сигурността на кода, за задаване на ограничения за оптимизациите от JIT компилатора, така че кодът да може да се дебъгва, за взаимодействие с дизайнера на средата за разработка при създаване на .NET компоненти, при взаимодействие с неуправляван код, при работа с уеб услуги, в ASP.NET потребителски контроли и на много други места.
Ще разгледаме някои от най-важните приложения на атрибутите вътрешно в .NET Framework.
.NET Framework дава възможност на разработчиците да поставят ограничения върху сигурността в два стила. Императивният стил е свързан със създаването на Permission обекти по време на изпълнение и извършване на обръщения към техните методи. Декларативният се осъществява чрез атрибути. Следва пример за декларативно управление на сигурността чрез атрибути:
|
[PrincipalPermissionAttribute(SecurityAction.Demand, Name = "SomeUser", Role = "Administrator")] public void DeleteCustomer(string aCustomerId) { // Delete the customer } |
В горния пример чрез атрибута PrincipalPermissionAttribute се указва на CLR, че за изпълнението на метода DeleteCustomer(…) е необходимо текущата нишка да се изпълнява от потребител SomeUser, който е в роля Administrator.
В темата "Сигурност в .NET Framework" ще се спрем в детайли върху императивното и декларативното управление на сигурността.
Сериализацията е процес на конвертиране на обект или свързан граф от обекти в поток от байтове. Десериализацията представлява обратния процес. В .NET Framework сериализацията и десериализацията се извършват автоматично от CLR, но за да се укаже, че даден обект подлежи на сериализация, се използват атрибути. Ето един пример:
|
[Serializable] public struct User { private string mLogin; private string mPassword; private string mRealName; // ... } |
В примера чрез атрибута SerializableAttribute се указва на CLR, че структурата User може да се сериализира по стандартния за CLR начин.
Сериализацията в .NET Framework ще разгледаме по-детайлно в темата за сериализация на данни.
При проектирането на .NET платформата е залегнал принципът за компонентно-ориентираното програмиране. .NET Framework дефинира компонентен модел, който установява стандарти за разработване и използване на компоненти. При разработване на .NET компоненти чрез атрибути към тях може да се дефинират метаданни, които се използват от Visual Studio .NET по време на дизайн. Ето един пример, в който чрез атрибут се указва категорията, в която да се появи свойството BorderColor в панела за настройка на свойствата за даден компонент:
|
public class SomeComponent : Control { // ...
[Category("Appearance")] public Color BorderColor { get { … } set { … } }
// ... } |
На компонентния модел на .NET Framework ще обърнем по-специално внимание в темата за Windows Forms.
.NET Framework има вградена поддръжка на уеб услуги. Уеб услугите служат за обмяна на информация между отдалечени приложения посредством стандартни протоколи. За управление на поведението на уеб услугите в ASP.NET се използват атрибути. Ето един пример, в който чрез атрибута [WebMethod] се указва, че даден метод е публично достъпен като част от дадена уеб услуга:
|
public class AddService : WebService { [WebMethod] public int Add(int a, int b) { return a + b; } } |
На уеб услугите в ASP.NET ще обърнем специално внимание в темата за уеб услуги.
.NET Framework може да взаимодейства с неуправляван Win32 код: да извиква Win32 функции, да използва COM компоненти и да публикува COM компоненти. За настройка на различни параметри на взаимодействието с неуправляван код се използват атрибути. Един от тях е системният атрибут DllImport. Нека разгледаме следния пример на декларация на външна Win32 функция:
|
[DllImport("user32.dll", EntryPoint="MessageBox")] public static extern int ShowMessageBox(int hWnd, string text, string caption, int type); |
В примерния код чрез DllImport се указва, че статичният метод ShowMessageBox(…) е външна неуправлявана функция с име MessageBox от Win32 библиотеката user32.dll.
Ще обърнем специално внимание на атрибутите за връзка с неуправляван код в темата "Взаимодействие с неуправляван код".
.NET Framework използва вътрешно някои атрибути, за да осигури синхронизация при конкурентен достъп до даден метод от няколко нишки. Ето един пример, при който чрез специален атрибут (System.Runtime. CompilerServices.MethodImplAttribute) се указва, че методът SomeMethod() може да бъде изпълняван от най-много една нишка в даден момент:
|
[MethodImplAttribute (MethodImplOptions.Synchronized)] public void SomeMethod() { ... } |
До момента разгледахме какво представляват атрибутите и как се ползват атрибути, дефинирани стандартно в .NET Framework или дефинирани от други разработчици. Сега ще разгледаме как можем да дефинираме собствени атрибути, които да използваме за свои специфични цели: например, когато разработваме сървърни приложения или компоненти.
За да бъде създаден потребителски атрибут, той задължително трябва да наследява класа System.Attribute и на компилатора трябва да се укаже към какъв вид елементи от кода може да се прилага атрибутът, т.е. какви са неговите цели. Това става с помощта на мета-атрибута AttributeUsage.
Да разгледаме следния пример: в даден проект има изискване всеки клас да съдържа в себе си информация за своя автор. Една възможност да се реализира това е във всеки един от класовете да се сложи коментар, подобен на този:
|
// This class is written by Person X. |
За четящия кода ще бъде ясно кой е авторът, но няма да е възможно тази информация да се извлича по време на изпълнение на програмата, след като сорс кодът е бил вече компилиран.
За да решим този проблем, можем вместо горния коментар да ползваме специален атрибут:
|
[Author("Person X")] |
Въпреки че има само функциите на коментар, този атрибут може да бъде извличан от метаданните програмно чрез специално създадени за това инструменти. Ако използваме подобен атрибут, ще бъде възможно, при настъпване на изключение в нашата програма, да изведем информация не само в кой метод и кой клас е настъпило то, но също и кой е авторът на кода, в който е възникнал проблемът. Това би могло да бъде полезно при неговото решаване.
Нека сега дефинираме нашия атрибут за автор. Както вече отбелязахме, всички атрибути са инстанции на класове, а всеки клас, дефиниращ собствен атрибут, наследява класа System.Attribute. В нашия случай, когато създаваме атрибут, съдържащ името на автора на класа, можем да използваме следната дефиниция:
|
public class AuthorAttribute: System.Attribute { … } |
Както имената на вградените атрибути, така и имената на потребителските атрибути трябва да завършват с окончанието Attribute (по възприетата в .NET Framework конвенция за имената).
Следващото нещо, което е необходимо, за да стане нашият клас AuthorAttribute потребителски атрибут, е да му приложим атрибута AttributeUsage. Чрез него указваме кои са целите, към които може да се прилага, и дали се допуска многократно прилагане към една и съща цел.
За да е възможно подаването на параметър при създаването на атрибута, за него трябва да се дефинира и подходящ конструктор.
Следва примерна реализация на атрибута AuthorAttribute:
|
using System;
[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class | AttributeTargets.Interface)] public class AuthorAttribute: System.Attribute { private string mName;
public AuthorAttribute(string aName) { mName = aName; }
public string Name { get { return mName; } } } |
Както се вижда от декларацията, нашият атрибут може да се прилага само към структури, класове и интерфейси, като към дадена цел не може да се прилага повече от веднъж (това се подразбира ако не е указано друго).
Понеже нашият атрибут е клас, който има само един конструктор, единственият начин да го инстанцираме е, като извикаме този конструктор. Следователно при използване на нашия атрибут винаги трябва да подаваме позиционния параметър за име на автор.
И така, веднъж деклариран, нашият атрибут вече може да бъде прилаган като всички останали атрибути:
|
[Author("Светлин Наков")] class CustomAttributesDemo { … } |
В примера към класа CustomAttributesDemo е приложен AuthorAttribute, който задава автор Светлин Наков.
Ако се опитаме да приложим AuthorAttribute няколко пъти към една и съща цел или да го приложим без параметри, ще получим грешка по време на компилация.
До момента дефинирахме и приложихме свой собствен атрибут, но защо ни беше това? Печелим възможността да добавяме допълнителна информация към елементи от кода и да я извличаме на по-късен етап от вече компилирания код. Възниква въпросът как точно става това извличане.
По време на изпълнение на програмата може да се използва следният код, за да се извлече атрибутът, приложен върху класа CustomAttributesDemo от горния пример:
|
string className = "CustomAttributesDemo"; Assembly ass = Assembly.GetExecutingAssembly(); Type type = ass.GetType(className); object[] allAttributes = type.GetCustomAttributes(false); AuthorAttribute author = allAttributes[0] as AuthorAttribute; Console.WriteLine("Class {0} is written by {1}. ", className, author.Name); |
В примера първо се взема текущото асембли, от него се изваждат метаданните за класа CustomAttributesDemo, след което се извличат всички атрибути, приложени към този клас. Накрая се взема първият атрибут от списъка и се преобразува до тип AuthorAttribute. Както се вижда, извлеченият атрибут е най-обикновена инстанция на класа AuthorAttribute и тя може да се използва така, сякаш е създадена в момента, а не е извлечена от асемблито.
Горният пример използва технологията "reflection" (отражение на типовете), на която ще обърнем по-голямо внимание в съответната тема. Засега е достатъчно да знаем, че има лесен начин атрибутите да бъдат извличани по време на изпълнение на приложението.
Вече се сблъскахме с атрибута AttributeUsage в предходния пример. Нека сега си изясним по-детайлно за какво служи той и кога се използва.
AttributeUsage е системен атрибут, който се прилага към декларациите на други атрибути. В този смисъл той е мета-атрибут, т.е. предоставя метаданни за атрибутите (мета-метаданни).
Когато се използва AttributeUsage, на конструктора се подават два аргумента. Първият от тях е набор от флагове, указващи допустимите цели, (например класове и структури), а вторият (AllowMultiple) е булев флаг, указващ дали е допустимо към дадена цел да се приложи повече от една инстанция на дефинирания атрибут.
Следва пример, в който се дефинира атрибут, който служи за добавяне на коментари в кода, които, за разлика от обикновените коментари, при компилация не се губят, а се запазват в компилираните асемблита:
|
[AttributeUsage(AttributeTargets.All, AllowMultiple=true)] public class CommentAttribute: System.Attribute { private string mCommentText;
public CommentAttribute(string aCommentText) { mCommentText = aCommentText; }
public string CommentText { get { return mCommentText; } } } |
Коментарите, дефинирани чрез горния атрибут, могат да се прикрепят към всякакви цели: асембли, тип, метод, поле, свойство и др., като към всяка цел може да се добавя повече от един коментар. Следва пример за използването на CommentAttribute:
|
using System;
[assembly: Comment("This is a test assembly!")]
[Comment("This class is for test purposes only.")] [Comment("(C) Svetlin Nakov, 2005. All rights reserved!")] class TestCommentAttribute { [Comment("The name of the configuration file.")] public static string CONFIG_FILE_NAME = "config.xml";
[Comment("This is the program entry point.")] static void Main() { ... } } |
Коментарите, прикрепени към кода по този начин, могат да се извличат от компилирания код и да се използват от приложението по време на изпълнение или от различни инструменти за работа с кода, като дебъгери, оптимизатори (execution profilers) и други.
Вече знаем, че атрибутите са инстанции на някакъв клас, наследник на System.Attribute. Те се съхраняват като метаданни в асемблито и могат да бъдат извличани по време на изпълнение на програмата. Сега ще разгледаме как точно се съхраняват.
По време на компилация приложените към дадена цел атрибути се обработват по следния начин:
1. Компилаторът намира типа, който съответства на приложения атрибут. В нашия пример с добавянето на коментари към кода на атрибута Comment съответства класа CommentAttribute.
2. Компилаторът създава инстанция на приложения атрибут. В нашия пример се инстанцира класът CommentAttribute (който е дефиниран в нашия сорс код и е компилиран преди това).
3. Компилаторът инициализира полетата на приложения атрибут чрез параметрите, подадени в конструктора му и чрез установяване на свойствата, за които е зададена стойност. В нашия случай полето mCommentText се инициализира от конструктора на CommentAttribute с подадената за него стойност.
4. Инстанцията на атрибута, която е получена, се сериализира (представя се като последователност от байтове).
5. Сериализираната инстанция се записва в таблицата с метаданните за целта, към която е приложена.
По време на изпълнение, когато са необходими, атрибутите се десериализират от метаданните на асемблито и се предоставят на приложението. За тях се създават най-обикновени обекти от съответните им класове и състоянието им се извлича от метаданните.
Повече за сериализацията и десериализацията ще научим в темата за сериализация в .NET Framework, но за момента можем да считаме, че чрез тези техники можем да запазваме обекти от паметта във файл или друг носител и да ги възстановяваме след време обратно в паметта.
1. Обяснете какво представляват атрибутите в .NET Framework. Как се прилагат атрибути? Как се прилагат атрибути с параметри? Как се задава цел при прилагане на атрибут?
2. Дефинирайте собствен атрибутен тип VersionAttribute, който може да се прилага само към типове или методи и служи за задаване на версията на даден тип или метод. Версията трябва да се състои от символен низ за самата версия и незадължителен текстов коментар. Дефинирайте подходящи конструктори и свойства за класа.
3. Създайте клас VersionsDemo с няколко метода и им приложете атрибута VersionAttribute с някакви примерни версии, на места придружени от коментари.
4. Създайте малка програма, която зарежда класа VersionsDemo и отпечатва неговата версия, както и версията на всеки негов метод заедно с текстовия коментар към нея (ако има такъв). За целта използвайте методите GetMethods() и GetCustomAttributes() на класа System.Type.
1. Светлин Наков, Атрибути – http://www.nakov.com/dotnet/lectures/ Lecture-6-Attributes-v1.0.ppt
2. Jeffrey Richter, Applied Microsoft .NET Framework Programming, Microsoft Press, 2002, ISBN 0735614229
3. MSDN Training, Programming with the Microsoft® .NET Framework (MOC 2349B), Module 17: Attributes
4. MSDN Library – http://msdn.microsoft.com
|
Национална академия по разработка на софтуер |
|
|
Лекторите » Светлин Наков е автор на десетки технически публикации и няколко книги, свързани с разработката на софтуер, заради което е търсен лектор и консултант. Той е разработчик с дългогодишен опит, работил по разнообразни проекти, реализирани с различни технологии (.NET, Java, Oracle, PKI и др.) и преподавател по съвременни софтуерни технологии в СУ "Св. Климент Охридски". През 2004 г. е носител на наградата "Джон Атанасов" на президента на България Георги Първанов. Светлин Наков ръководи обучението по Java технологии в Академията.
» Мартин Кулов е софтуерен инженер и консултант с дългогодишен опит в изграждането на решения с платформите на Microsoft. Мартин е опитен инструктор и сертифициран от Майкрософт разработчик по програмите MCSD, MCSD.NET, MCPD и MVP и международен лектор в световната организация на .NET потребителските групи INETA. Мартин Кулов ръководи обучението по .NET технологии в Академията. |
Академията » Национална академия по разработка на софтуер (НАРС) е център за професионално обучение на софтуерни специалисти.
» НАРС провежда БЕЗПЛАТНО курсове по разработка на софтуер и съвременни софтуерни технологии в София и други градове.
» Предлагани специалности: § Въведение в програмирането (с езиците C# и Java) § Core .NET Developer § Core Java Developer
» Качествено обучение с много практически проекти и индивидуално внимание за всеки.
» Гарантирана работа! Трудов договор при постъпване в Академията.
» БЕЗПЛАТНО! Учите безплатно във въведителните курсове и по стипендии от работодателите в следващите нива. |
- Базови познания по структури от данни
- Базови познания за общата система от типове в .NET (Common Type System)
- Базови познания за езика C#
- Масиви в .NET Framework
- Многомерни масиви. Масиви от масиви
- Типът System.Array
- Сортиране на масиви и двоично търсене
- Колекции в .NET Framework
- IList, ArrayList, Queue и Stack
- IDictionary и Hashtable. Собствени хеш-функции
- Класът SortedList
В настоящата тема ще се спрем на масивите и колекциите в .NET Framework. Ще разгледаме видовете масиви: едномерни, многомерни и масиви от масиви (т. нар. назъбени масиви), както и базовия тип за всички масиви System.Array. Ще се запознаем с начините за сортиране на масиви и търсене в тях. Ще разгледаме с колекциите и тяхната реализация в .NET Framework: класовете ArrayList, Queue, Stack, Hashtable и SortedList, както и интерфейсите, които те имплементират.
Масивите са наредени последователности от еднакви по тип елементи. Те представляват механизми, който ни позволяват да третираме тези последователности като едно цяло.
Масиви в C# декларираме по следния начин:
|
int[] myArray; |
В случая сме декларирали масив с име myArray от целочислен тип (System.Int32). В началото myArray има стойност null, тъй като не е заделена памет за елементите на масива.
Със следния код заделяме (алокираме) масив в C#:
|
myArray = new int[5]; |
В примера се заделя масив с размер 5 елемента от целочислен тип.
При заделяне на масив CLR автоматично инициализира всички негови елементи с неутрална стойност (0 или null). В нашия пример всеки от тези 5 елемента е със стойност 0, за разлика от други неуправлявани среди, където стойностите ще са произволни (C, C++). Адресът на блока памет, заделен за този масив се записва в променливата myArray.

Всички масиви в .NET Framework наследяват типа System.Array, което означава, че те винаги са референтни типове и се разполагат в блокове от динамичната памет (т. нар. managed heap).
От своят страна типът System.Array имплементира следните интерфейси: ICloneable, IList, IEnumerable и ICollection, които позволят масивите да се използват лесно в различни ситуации. Ще разгледаме тези интерфейси малко по-късно в настоящата тема.
Тъй като са референтни типове масивите винаги се предават по референция (т.е. по адрес, а не по стойност). Ако искаме да подадем даден масив като параметър, но да защитим от промяна стойностите на неговите елементи, трябва да подадем негово копие. Копие на масив можем да направим чрез статичния метод Array.Copy(…). Обърнете внимание, че този статичен метод прави плитки копия на елементите на масива.
Достъпът до елементите на масивите е пряк, по индекс (пореден номер на елемента). Масивите обикновено са нулево-базирани, т.е. номерацията на елементите започва от 0. В .NET Framework обаче могат да се създадат и масиви с ненулева долна граница. Елементите на масивите са достъпни както за четене така и за писане.
Достъпът до елементите на масивите е проверен, т.е не се допуска излизане извън границите и размерностите на масив и при всеки достъп CLR проверява дали индексът е валиден и ако не е, се подава изключение System.IndexOutOfRangeException. Естествено тази проверка си има и своята цена и това е производителността. CLR обаче ни предоставя възможността да я изключим, като използваме ключовата дума unsafe. Тя се използва винаги, когато искаме да извършваме операции свързани с указатели. С unsafe трябва да обозначим метода, който ще извършва тези операции. Ето пример:
|
unsafe static void FastArrayAccess(int[] myArray) { // Acess the array elements here with no checks } |
При компилация на код, който използва unsafe трябва да укажем опцията на компилатора, че кодът има unsafe блокове. Ето как става това:
|
csc.exe /unsafe UnsafeArrayAccess.cs |
Ако за нашето приложение бързодействието е от най-голямо значение можем да използваме unsafe код. Не трябва да забравяме обаче, че кодът, който пишем, вече няма да е управляван (managed) и CLR няма да се грижи за неговата обезопасеност.
В .NET Framework се подържат едномерни, многомерни и масиви от масиви ("назъбени" масиви). Всеки от тези видове пази в себе си информация за броя на размерностите си (т. нар. ранг), както и границите на всяка от тях. CLR е оптимизиран за работа с едномерни, нулево-базирани масиви, затова се препоръчва тяхното използване, когато е възможно. Масивите могат да се инициализират при деклариране.
Ще илюстрираме работата с масиви със следния пример:
|
int[] primes = {2, 3, 5, 7, 11, 13, 17, 19}; foreach (int p in primes) { Console.Write("{0} ", p); } Console.WriteLine(); // Output: 2 3 5 7 11 13 17 19
for (int i = 0; i < primes.Length; i++) { primes[i] = primes[i] * primes[i]; }
foreach (int p in primes) { Console.Write("{0} ",p); }
Console.WriteLine(); // Output: 4 9 25 49 121 169 289 361 |
В горния пример първоначално създаваме едномерен масив от тип System.Int32, инициализираме го с първите 8 прости числа, след което го извеждаме в конзолата.
След това на всеки елемент му се присвоява за стойност неговия квадрат. Забележете, че се използва свойството Length, което връща броя на елементите на масива. В .NET Framework всеки масив знае своята дължина.
Най-накрая новополучените стойности отново извеждаме на екрана чрез цикъл, реализиран с конструкцията foreach. Използването на foreach е възможно, защото масивите реализират интерфейса IEnumerable.
В следващия пример ще използваме масиви за да реализираме един от най-старите алгоритми – решето на Ератостен:
|
using System;
class PrimeNumbersDemo { static void Main() { const int COUNT = 100;
bool[] prime = new bool[COUNT+1]; // array [0..100] for (int i = 2; i <= COUNT; i++) { prime[i] = true; }
for (int p = 2; p <= COUNT; p++) { if (prime[p]) { Console.Write("{0} ", p); for (int i = 2*p; i <= COUNT; i += p) { prime[i] = false; } } }
Console.WriteLine(); } } |
Резултатът от изпълнението на програмата е следният:

Алгоритъмът на Ератостен работи по следния начин: записваме числата от 2 до n (в нашия случай 100) в редица. Първоначално всички числа са незачертани. Намираме първото незачертано число – в началото това е 2, маркираме го като просто и зачертаваме всяко кратно на 2 число в редицата. Продължаваме по същия начин със следващото незачертано число – 3. Процесът продължава докато не остане нито едно незачертано число. Тогава всички маркирани числа са прости.
Сега да се спрем по-подробно на конкретната реализация на C#. За маркирането и зачертаването на елементите ще използваме масив от тип System.Boolean. Понеже CLR инициализира по подразбиране булевите стойности с false, първият цикъл в кода им задава стойност true, като по този начин маркираме всички числа като прости. След това започваме цикъл по дължината на масива (COUNT) и за всяка негова итерация проверяваме дали текущото число е маркирано, ако е извеждаме го и зачертаваме неговите кратни. В крайна сметка ако елемент на масива prime има стойност true, неговият индекс е просто число.
Забележка: първият цикъл започва от 2, защото както знаем 1 не е просто число.
Със следващия пример ще илюстрираме използването на масив, чиито елементи са референтни типове, в частност – инстанции на класове дефинирани от нас.
|
using System;
class Animal { public virtual void Eat() { Console.WriteLine("Animals eat food"); } }
class Tiger : Animal { public override void Eat() { Console.WriteLine("Tigers eat meat"); } }
class Cow : Animal { public override void Eat() { Console.WriteLine("Cows eat grass"); } }
class ArrayTest { static void Main() { Animal[] animals = new Animal[3]; animals[0] = new Animal(); animals[1] = new Tiger(); animals[2] = new Cow();
foreach (Animal animal in animals) { animal.Eat(); } } } |
В примера се дефинира клас Animal, който има виртуален метод Eat(). Класът Animal се наследява от други два класа – Tiger и Cow, които от своя страна припокриват (override) този виртуален метод. В Main() метода създаваме масив от обекти от тип Animal. След създаването на масива animals, всеки един от неговите елементи е инициализиран със стойност null, защото Animal е референтен тип и се създават само нулеви референции към него, а действителните обекти се създават на следващите 3 реда. Обърнете внимание, че в масива можем да записваме инстанции не само към Animal, а и към всички класове, които са негови наследници. Накрая за всеки елемент на масива извикваме метода Eat(). Благодарение на полиморфизма резултатът от изпълнението на програмата е следният:

Освен вече разгледаните едномерни масиви, .NET Framework поддържа и многомерни такива (масиви с няколко размерности). Декларирането на многомерен масив е почти същото, както при едномерните, но само с една разлика – трябва да поставим запетая между размерностите му:
|
int[,] matrix = new int[3,3]; char[,,] box = new char[2,5,10]; |
Ако искаме да инициализираме многомерен масив още при декларация трябва да спазим следното правило, а именно че трябва да поставим всяко измерение в отделни "къдрави скоби". Ето и пример:
|
int[,] matrix = { {1, 2, 3} , {4, 5, 6} , {7, 8, 9} }; |
Достъпът до елементите отново е по индекс, само че в многомерния вариант отново трябва да поставим запетая между индексите на отделните размерности:
|
int elem = matrix[3,3]; box[1,2,3] = 'a'; |
Многомерните масиви разполагат елементите си последователно – един след друг в линейни блокове от динамичната памет. Ето как би изглеждало това за вече декларирания и инициализиран с естествените числа от 1 до 9 двумерен масив matrix:

В следващия пример ще използваме двумерни масиви за да реализираме умножение на матрици:
|
using System;
class MatrixMultiplicationDemo { static void PrintMatrix(int[,] aMatrix) { for (int row = 0; row < aMatrix.GetLength(0); row++) { for (int col = 0; col < aMatrix.GetLength(1); col++) { Console.Write("{0} ", aMatrix[row, col]); } Console.WriteLine(); } Console.WriteLine(); }
static int[,] Mult(int[,] aMatrix1, int[,] aMatrix2) { int width1 = aMatrix1.GetLength(1); int height1 = aMatrix1.GetLength(0); int width2 = aMatrix2.GetLength(1); int height2 = aMatrix2.GetLength(0);
if (width1 != height2) { throw new ArgumentException("Invalid dimensions!"); }
int[,] resultMatrix = new int[height1, width2]; for (int row = 0; row < height1; row++) { for (int col = 0; col < width2; col++) { resultMatrix[row, col] = 0; for (int i = 0; i < width1; i++) { resultMatrix[row, col] += aMatrix1[row, i] * aMatrix2[i, col]; } } }
return resultMatrix; }
static void Main() { int[,] m1 = new int[4,2] { {1,2}, {3,4}, {5,6}, {7,8} }; PrintMatrix(m1);
int[,] m2 = new int[2,3] { {1,2,3}, {4,5,6} }; PrintMatrix(m2);
int[,] m3 = Mult(m1, m2); PrintMatrix(m3); } } |
Условието, на което трябва да отговарят две матрици за да можем да ги умножим е: броят на стълбовете на първата матрица да е равен на броя на редовете на втората матрица. Ако това не е изпълнено подаваме ArgumentException. След умножението новополучената матрица ще има следните размери: брой редове – броят на редовете на първата матрица, брой стълбове – броят на стълбовете на втората.
Самото умножение става така: всеки ред на първата матрица се умножава с всеки стълб на втората, т. е. първият елемент от реда с първия от стълба, вторият с втория и т. н. Получените произведения сумираме и това е стойността на елемента, който ще запишем в новополучената матрица на ред текущия ред от първата матрица и стълб – текущият от втората.
Ето и изхода от примера:

В .NET Framework могат да се използват още и масиви от масиви или т. нар. назъбени (jagged) масиви. Може би се чудите от къде идва това име? След следващите редове ще ви се изясни.
Назъбеният масив представлява масив от масиви, т. е. всеки негов ред на практика е масив, който може да има различна дължина от останалите в назъбения масив, но не може да има различна размерност. Със следващия код декларираме масив от масиви:
|
int[][] jaggedArray; |
Единственото по-особено е, че нямаме само една двойка скоби, както при обикновените масиви, а имаме вече две двойки такива. По следния начин заделяме назъбен масив:
|
jaggedArray = new int[2][]; jaggedArray[0] = new int[5]; jaggedArray[1] = new int[3]; |
Възможно е и декларирането, заделянето и инициализацията на един масив от масиви да се извършва в един израз. Ето пример:
|
int[][] myJaggedArray = { new int[] {1,3,5,7,9}, new int[] {17,23}, new int[] {0,2,4,6} }; |
Достъпът до елементите на масивите, който са част от назъбения, отново е по индекс. Ето пример за достъп до елемента с индекс 3 от масива, който има индекс 0 в по-горе дефинирания назъбен масив jaggedArray:
|
jaggedArray[0][3] = 12345; |
Както споменахме, елементите на назъбения масив може и да са не само едномерни масиви, но и многомерни такива. В следващия код създаваме назъбен масив от двумерни масиви:
|
int[][,] jaggedOfMulti = new int [3][,] ; jaggedOfMulti[0] = new int[,] { {9,27}, {10,20} }; |
Ако все още не ви си е изяснило защо наричаме масивите от масиви – назъбени, може би тази следващата картинка ще ви помогне. На нея може да видим вече дефинирания назъбен масив myJaggedArray и по точно неговото разположение в паметта. Както се вижда, самият назъбен масив съдържа само референции към масивите, а не самите тях. Тъй като не знае каква ще е размерността на всеки от масивите, CLR заделя само референцията за тях. Чак след като се задели памет за някой от масивите елементи на назъбения, тогава се насочва указателя към новосъздадения блок динамична памет.

В следващия пример ще използваме назъбен масив за да генерираме и визуализираме триъгълника на Паскал. Както знаем от математиката, всяко число от триъгълника се образува като се съберат горните две над него. Естествено, това не важи за първото число в триъгълника – 1. Триъгълникът на Паскал има широко приложение в комбинаториката.
|
using System;
class PascalTriangle { static void Main() { const int HEIGHT = 12;
// Allocate the array in a triangle form long [][] triangle = new long[HEIGHT+1][]; for (int row = 0; row <= HEIGHT; row++) { triangle[row] = new long[row+1]; }
// Calculate the Pascal's triangle triangle[0][0] = 1; for (int row = 0; row < HEIGHT; row++) { for (int col = 0; col <= row; col++) { triangle[row+1][col] += triangle[row][col]; triangle[row+1][col+1] += triangle[row][col]; } }
// Print the Pascal's triangle for (int row = 0; row <= HEIGHT; row++) { Console.Write("".PadLeft((HEIGHT-row)*2)); for (int col = 0; col <= row; col++) { Console.Write("{0,3} ", triangle[row][col]); } Console.WriteLine(); } |