Учебник по С#

  1. Обзор .NET

  2. Введение в C#
    • Общее описание языка
    • Сравнение С# с другими языками
    • Использование C#
    • Пример программы на C#

  3. Основы C#
    • Типы данных
    • Переменные
    • Массивы
    • Константы
    • Управляющие констукции
    • Структура программы
    • Комментарии
    • Вспомогательный материал - работа с консолью

  4. ООП и C#
    • Различия между объектными типами и типами значения
    • Классы
    • Интерфейсы
    • Наследование и реализация
    • Переопределение операторов

  5. Продвинутые темы
    • Исключения
    • Пользовательские преобразования типов
    • Атрибуты
    • Делегаты
    • События
    • Директивы препроцессора
    • Небезопасный код
Источник: www.c-sharp.ru


Краткий обзор.NET

Историческая справка

Прошло уже больше полутора лет с тех пор, как на конференции в Лос-Анжелосе компания Microsoft представила первую версию новой платформы для разработки приложений. Изначально технология назвалась NGWS, что расшифровывается как Next Generation Web Services и Next Generation Windows Services. Сперва данная платформа рекламировалась как базис для разработки веб-приложений и веб-служб. Именно в то время термин "веб-сервис" (web service) вошел в широкое употребление. Примерно тогда же широкая общественность получила драфт нового языка C#, а также узнала о развитии ASP и ADO в ASP+ и ADO+. Но это была лишь вершина айсберга и взгляда на Preview версию новой платформы было достаточно, чтобы понять - это переворот. Уже позже стало понятно, что это не революция, а результат двухлетней эволюции технологии COM+ и развития Windows DNA. Разработка NGWS шла настолько секретно, что сообщество разработчиков и не подозревало какой подарок для них готовится. Чуть позже новая технология получила название .NET (произносится "дотнет"). ASP+ плавно перерос в ASP.NET, а ADO+ стало ADO.NET.

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

Предназначение .NET

К моменту выхода .NET у Java разработчиков был красивый, структурированный, простой в изучении и при этом позволяющий воспользоваться мощью ООП язык. У Java была технология JSP, во многом превосходящая ASP. У Java была удобная в использовании, общая для всех, структурированная библиотека типов. У разработчиков для Windows платформы были DLL Hell, , MFC, VB, Delphi, интерпретируемый VBScript для создания корпоративных веб-приложений. Совместимость компонент на бинарном не позволяла пользоваться технологией объектно-ориентированной разработки в кроссязыковых разработках. Да и сложно предположить нечто иное. Windows API - далеко не нов и развивался до повсеместного введения ООП. Visual Basic вырос из простейшего языка и имел RAD (rapid application development) высшей целью. При этом его возможностей откровенно не хватало для создания красивого кода и для производства сложных систем. Что бы там не говорили про COM+ и возможности бейсика в Windows DNA. С++ - язык, явно не предназначенный для быстрого создания приложений. В Delphi явно были многие прогрессивные начинания, но не было мощи Microsoft. Ну а историю с J++ думаю все знают.

Всем руководила необходимость кардинально менять сам принцип разработки ПО. Начиная с создания нового языка и кончая созданием базиса для возможности прозрачной интеграции приложений, написанных на разных языках. Более того, эта интеграция должна была отлично работать с объектно-ориентированными языками, которые стали стандартом де факто для разработки большинства современного ПО. Вдобавок подобная интеграция позволяла бы предоставить разработчиком новый, структурированный по принципам ООП программный интерфейс самой системы. Для того, чтобы правильно собрать эту мозаику Microsoft привлекло к созданию новой платформы лучших инженеров. Скажем, над C# работал Хайлсберг - до этого прославившийся своим вкладом в Delphi.

Общие возможности .NET

Так с чем же Вас придется столкнуться при разработке новых приложений, ориентированных на Windows, веб или веб-сервисы? .NET имеет в себе общеязыковую среду времени выполнения(CLR, common language runtime). Ее можно сравнивать с Java Runtime Environment, хотя есть и отличия. Любой программный код, написанный под новую платформу называется управляемым (managed code) и компилируется в бинарный вид, понятный .NET runtime. Данная среда выполнения сейчас имеется для многих версий Windows и доступна для скачиваня.

Именно эта прослойка между вашим кодом и системой позволяет использовать большинство возможностей .NET. Так как код компилируется в промежуточный бинарный формат, он может быть использован в любом языке, поддерживающем CLR. Этот формат называется Microsoft Intermediate Language (MSIL, IL). Этот формат поддерживает ООП, хранит полную информацию о типах, их связях. Более того, этот формат для любого компонента хранит мета-информацию, которая определяет параметры выполнения, среды безопасности и многое другое. Трудно переоценить значение переходи на новый бинарный формат. Именно этот шаг позволил создать новую базовую библиотеку классов (Base Class Library), которая предоставляет объектно-ориентированный интерфейс к системным и вспомогательным возможностям. Уже сейчас можно сказать, что разработчики пишут не на конкретном языке, а на .NET, так как вся системная библиотека у них общая. MSIL и BCL позволил включить несколько языков, работающих с .NET уже в Preview версию. Сейчас базовыми считаются C#, Visual Basic.NET, Managed C++, JScript, но количество совеместимых с .NET языков (CLS compliant) непрерывно растет. Здесь и J#, и Pascal под .NET, и COBOL.NET, Perl.NET и даже такие экзотические языки, как Mixal.NET

Но кроссязыковые возможности - не единственное достижение новой среды. Для упрощения разработки в среду включен автоматический подсчет ссылок и сборщик мусора. Среда предоставляет расширенные возможности по управлению безопасностью кода, имеет стандартные возможности по работе с различными версиями одних и тех же компонент (versioning).

Части .NET

Мы узнали какие возможности предоставляет общеязыковая среда выполнения, поднимемся же уровнем выше и узнаем, что несет нам базовая библиотека типов и какие приложения мы можем писать на новой платформе. Без преувеличения - любые Windows приложения, за исключением низкоуровневых приложений, систем реального времени, а также критичных на время выполнения частей систем. Библиотека классов имеет в себе части, посвященные работе с данными (ADO.NET), с веб приложениями(ASP.NET), с windows интерфейсом (Windows Froms), с XML (DOM, XSD, XSLT, сериализация/десериализация, интеграция с ADO.NET), с сетевыми приложениями (System.Net), с распределенными приложениями (remoting), с COM+ (Enterprise Services, Serviced components), с шифрованием данных (System.Security.Cryptography).

.NET вобрала в себя лучшее из имеющегося на рынке создания ПО. Она полностью использует возможности XML и веб. Именно это позволяет ее назвать самой лучшей платформой для разработки веб-сервисов. В .NET полностью пересмотрен взгляд на веб разработку и нашему вниманию предоставлена мощная и удобная в использовании технология ASP.NET, которая заметно обходит разработки конкурентов. Писать код на ASP.NET проще, быстрее, и решение получается отличное по производительности (чего не скажешь о старом ASP). Новая платформа охватывает также и новый рынок приложений для мобильных устройств. Cейчас первенство в области мобильных технологий и сейчас держит J2ME, весьма вероятно, что вскоре ситуация изменится.

Интеграция с существующими решениями

У многих возникает вопрос о том, что делать с существующими приложениями, написанными с использованием DLL, COM, ASP, ADO, Visual Basic, MFC. В данной ситуации у Вас есть три пути - поддерживать старое решение, интегрировать старое решение с новым и создавать полностью новое решение. Каждый из путей имеет право на существование и пред его выбором следует провести детальный анализ.

Мы же можем Вам сказать, что будущее именно за .NET. Для облегчения перехода новая платформа имеет такие возможности, как Windows Interop (технология взаимодействия с DLL и Windows API), COM Interop (технология использования .NET объектов из COM и vice versa), поддержку COM+, возможность совместного существования ASP и ASP.NET приложений.


Общее описание языка

Последнее время С и С++ являются наиболее используемыми языками для разработки коммерческих и бизнес приложений. Эти языки устраивают многих разработчиков, но в действительности не обеспечивают должной продуктивности разработки. К примеру, процесс написания приложения на С++ зачастую занимает значительно больше времени, чем разработка эквивалентного приложения, скажем, на Visual Basic. Сейчас существуют языки, увеличивающие продуктивность разработки за счет потери в гибкости, которая так привычна и необходима программистам на С/С++. Подобные решения являются весьма неудобными для разработчиков и зачастую предлагают значительно меньшие возможности. Эти языки также не ориентированы на взаимодействие с появляющимися сегодня системами и очень часто они не соответствуют существующей практике программирования для Web. Многие разработчики хотели бы использовать современный язык, который позволял бы писать, читать и сопровождать программы с простотой Visual Basic и в то же время давал мощь и гибкость C++, обеспечивал доступ ко всем функциональным возможностям системы, взаимодействовал бы с существующими программами и легко работал с возникающими Web стандартами.

Web интеграция

Ввиду очень удобного объектно-ориентированного дизайна, C# является хорошим выбором для быстрого конструирования различных компонентов - от высокоуровневой бизнес логики до системных приложений, использующих низкоуровневый код. Также следует отметить, что C# является и Web ориентированным - используя простые встроенные конструкции языка ваши компоненты могут быть легко превращены в Web сервисы, к которым можно будет обращаться из Internet посредством любого языка на любой операционной системе. Дополнительные возможности и преимущества перед другими языками приносит в C# использование передовых Web технологий, таких как: XML (Extensible Markup Language) и SOAP (Simple Object Access Protocol). Среда разработки Web сервисов позволяет программисту смотреть на существующие сегодня Web приложения, как на родные C# объекты, что дает возможность разработчикам соотнести имеющиеся Web сервисы с их познаниями в объектно-ориентированном программировании.

Исключение ошибок

Очень часто можно проследить такую связь - чем более язык защищен и устойчив к ошибкам, тем меньше производительность программ, написанных на нем. К примеру рассмотрим две крайности - очевидно это Assembler и Java. В первом случае вы можете добиться фантастической быстроты своей программы, но вам придется очень долго заставлять ее работать правильно не на вашем компьютере. В случае же с Java - вы получаете защищенность, независимость от платформы, но, к сожалению, скорость вашей программы вряд ли совместима со сложившимся представлением о скорости, например, какого-либо отдельного клиентского приложения (конечно существуют оговорки - JIT компиляция и прочее). Рассмотрим C++ с этой точки зрения - на мой взгляд соотношение в скорости и защищенности близко к желаемому результату, но на основе собственного опыта программирования я могу с уверенностью сказать, что практически всегда лучше понести незначительную потерю в производительности программы и приобрести такую удобную особенность, как "сборка мусора", которая не только освобождает вас от утомительной обязанности управлять памятью вручную, но и помогает избежать вам многих потенциальных ошибок в вашем приложении. В действительности скоро "сборка мусора", да и любые доругие шаги к устранению потенциальных ошибок стану отличительными чертами современного языка. В C#, как в несомненно современном языке, также существуют характерные особенности для обхода возможных ошибок. Например, помимо упомянутой выше "сборки мусора", там все переменные автоматически инициализируются средой и обладают типовой защищенностью, что позволяет избежать неопределенных ситуаций в случае, если программист забудет инициализировать переменную в объекте или попытается произвести недопустимое преобразование типов. Также в C# были предприняты меры для исключения ошибок при обновлении программного обеспечения. Изменение кода, в такой ситуации, может непредсказуемо изменить суть самой программы. Чтобы помочь разработчикам бороться с этой проблемой C# включает в себя поддержку совместимости версий (vesioning). В частности, в отличии от C++ и Java, если метод класса был изменен, это должно быть специально оговорено. Это позволяет обойти ошибки в коде и обеспечить гибкую совместимость версий. Также новой особенностью является native поддержка интерфейсов и наследования интерфейсов. Данные возможности позволяют разрабатывать сложные системы и развивать их со временем.

В C# была унифицирована система типов, теперь вы можете рассматривать каждый тип как объект. Несмотря на то, используете вы класс, структуру, массив или встроенный тип, вы можете обращаться к нему как к объекту. Объекты собраны в пространства имен (namespaces), которые позволяют программно обращаться к чему-либо. Это значит что вместо списка включаемых файлов заголовков в своей программе вы должны написать какие пространства имен, для доступа к объектам и классам внутри них, вы хотите использовать. В C# выражение using позволяет вам не писать каждый раз название пространства имен, когда вы используете класс из него. Например, пространство имен System содержит несколько классов, в том числе и Console. И вы можете писать либо название пространства имен перед каждым обращением к классу, либо использовать using как это было показано в примере выше.

Важной и отличительной от С++ особенностью C# является его простота. К примеру, всегда ли вы помните, когда пишите на С++, где нужно использовать "->", где "::", а где "."? Даже если нет, то компилятор всегда поправляет вас в случае ошибки. Это говорит лишь о том, что в действительности можно обойтись только одним оператором, а компилятор сам будет распознавать его значение. Так в C#, оператор"->" используется очень ограничено (в unsafe блоках, о которых речь пойдет ниже), оператор "::" вообще не существует. Практически всегда вы используете только оператор "." и вам больше не нужно стоять перед выбором. Еще один пример. При написании программ на C/С++ вам приходилось думать не только о типах данных, но и о их размере в конкретной реализации. В C# все упрощено - теперь символ Unicode называется просто char (а не wchar_t, как в С++) и 64-битное целое теперь - long (а не __int64). Также в C# нет знаковых и беззнаковых символьных типов. В C#, также как и в Visual Basic после каждого выражения case в блоке switch подразумевается break. И более не будет происходить странных вещей если вы забыли поставить этот break. Однако если вы действительно хотите чтобы после одного выражения case программа перешла к следующему вы можете переписать свою программу с использованием, например, оператора goto. Многим программистам (на тот момент, наверное, будущим программистам) было не так легко во время изучения C++ полностью освоиться с механизмом ссылок и указателей. В C# (кто-то сейчас вспомнит о Java) нет указателей. В действительности нетривиальность указателей соответствовала их полезности. Например, порой, трудно себе представить программирование без указателей на функции. В соответствии с этим в C# присутствуют Delegates - как прямой аналог указателя на функцию, но их отличает типовая защищенность, безопасность и полное соответствие концепциям объектно-ориентированного программирования.

Хотелось бы подчеркнуть современное удобство C#. Когда вы начнете работу с C#, а, надеюсь, это произойдет как можно скорее, вы увидите, что довольно большое значение в нем имеют пространства имен. Уже сейчас, на основе первого примера, вы можете судить об этом - ведь все файлы заголовков заменены именно пространством имен. Так в C#, помимо просто выражения using, предоставляется еще одна очень удобная возможность - использование дополнительного имени (alias) пространства имен или класса.

Современность C# проявляется и в новых шагах к облегчению процесса отладки программы. Традиционым средством для отладки программ на стадии разработки в C++ является маркировка обширных частей кода директивами #ifdef и т.д. В C#, используя атрибуты, ориентированные на условные слова, вы можете куда быстрее писать отлаживаемый код.

В наше время, когда усиливается связь между миром коммерции и миром разработки программного обеспечения, и корпорации тратят много усилий на планирование бизнеса, ощущается необходимость в соответствии абстрактных бизнес процессов их программным реализациям. К сожалению, большинство языков реально не имеют прямого пути для связи бизнес логики и кода. Например, сегодня многие программисты комментируют свои программы для объяснения того, какие классы реализуют какой-либо абстрактный бизнес объект. C# позволяет использовать типизированные, расширяемые метаданные, которые могут быть прикреплены к объекту. Архитектурой проекта могут определяться локальные атрибуты, которые будут связанны с любыми элементами языка - классами, интерфейсами и т.д. Разработчик может программно проверить атрибуты какого-либо элемента. Это существенно упрощает работу, к примеру, вместо того чтобы писать автоматизированный инструмент, который будет проверять каждый класс или интерфейс, на то, является ли он действительно частью абстрактного бизнесс объекта, можно просто воспользоваться сообщениями основанными на определенных в объекте локальных аттрибутах.


Сравнение C# с другими языками программирования

C#, являясь последним из широко распространенных языков программирования, должен впитать в себя весь имеющийся опыт и вобрать лучшие стороны существующих языков программирования, при этом являясь специально созданным для работы в .NET. Сама архитектура .NET продиктовала ему (как и многим другим языкам, на которых можно писать под .NET) объектно-ориентированную направленность. Конечно, это не является правилом, возможно создание компиляторов даже функциональных языков по .NET, на эту тему существуют специальные работы.

Свой синтаксис C# во многом унаследовал от C++ и Java. Разработчики, имеющие опыт написания приложений на этих языках, найдут в C# много знакомых черт. Но вместе с тем он является во многом новаторским - аттрибуты, делегаты и события, прекрасно вписанные в общую идеологию языка, прочно заняли место в сердцах .NET - разработчиков. Их введение позволило применять принципиально новые приемы программирования.

Конечно, излюбленным объектом для сравнения с C# у мировой коммьюнити является Java. Также разработанный для работы в виртуальной среде выполнения, имеющей объектно-ориентированную архитектуру и сборщик мусора, осноыванный на механизме ссылок. При сравнении с этим языком сразу выделаются такие особенности, как возможность объявлять несколько классов в одном файле, из чего следует синтаксическая поддержка иерархической системы пространств имен. Из реализации ООП-концепций сходство в механизме наследования и реализации (и в Java и в C# возможно единичное наследование, но множественная реализация интерфейсов, в отличие от C++). Но в Java отсутствуют свойства и индексаторы (а также делегаты и события, но они отсутствуют еще много где). Также есть возможность перечисления контейнеров.

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

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


Использование C#

В этой части будут рассмотрены более практические аспекты использования C#. Компиляция, отладка и тому подобные вопросы. Начнем с компиляции:

Переходя к более подробному знакомству с C#, традиционно рассмотрим программу "Hello, world":

        using System;
        class Hello
        {
            static void Main()
            {
                Console.WriteLine("hello, world");
            }
        }

Поместите эту программу в файл hello.cs и скомпилируйте ее командой

csc hello.cs

Так выглядит вызов компилятора в простейшем случае. Далее мы приведем полный список параметров компилятора. При вызове компилятора убедитесь, что у вас прописан путь csc.exe - C Sharp Compiler

В результате вы получите файл hello.exe, запустив который, вы увидите надпись "hello, world".

Метод Main - это точка входа в программу. С него начинается выполнение (те, кто знаком с C++ или Java) понимают, о чем идет речь. Этот метод обязательно должен быть статическим. Далее в нашем примере используется метод WriteLine из класса System.Console. Этот метод отправляет строку на стандартный вывод.

Вы можете также создавать библиотеки классов - dll-сборки, которые могут использоваться другими приложениями. Для этого нужно указать /target:library в опциях компилятора

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

Вообще, компилятор C# имеет множество возможностей, узнать о которых можно из слеующей таблицы. В ней описаны все опции командной строки компилятора CSC:

Выходные файлы

/out:<file> Имя выходного файла (если не указано - производное от имени первого исходного файла)

/target:exe Скомпилировать консольный запускаемый файл (по умолчанию) (Краткая форма: /t:exe)

/target:winexe Скомпилировать запускаемый файл Windows (Краткая форма: /t:winexe)

/target:library Скомпилировать в библиотеку DLL (Краткая форма: /t:library)

/target:module Скомпилировать модуль, который может быть добавлен в другую конструкцию (Краткая форма: /t:module)

* Примечание: Модуль и библиотека имеют одинаковые расширения (DLL), но несмотря на это, они представляют из себя разные вещи. Библиотека - это классическая библиотека DLL. Модуль - это файл откомпилированный без создания конструкции (assembly). В дальнейшем вы можете добавить модуль в любую другую конструкцию при компиляции (с помощью опции /addmodule).

/nooutput[+|-] Только проверять ошибки в коде; не создавать запускаемый файл /define:<список символов> Определяет директивы препроцессора (Краткая форма: /d)

Написание csc /define:mark1 MyApp.cs равносильно наличию в файле строки #define mark1 /doc:<file> Создать XML файл документации

Входные файлы

/recurse:<маска> Включить все файлы, удовлетворяющие маске.

/main:<тип> Обозначить класс, содержащий точку входа в программу (все остальные будут игнорироваться) (Краткая форма: /m)

/reference:<список файлов> Включить метаданные из указанных файлов конструкций (Краткая форма: /r)

* Примечание:

все данные, помеченные как public, из указаных файлов конструкций будут включены в данную конструкцию. Если файл не содержит объявления конструкции, то метаданные из него могут быть включены с помощью /addmodule.

/addmodule:<список файлов> Включить указанные модули в сборку

Ресурсы

/win32res:<file> Добавляет файл ресурсов Win32(.res) в выходной файл

/win32icon:<file> Добавляет иконку в выходной файл

/resource:<resinfo> Включает .NET ресурс (Краткая форма: /res)

* Примечание: resinfo являет собой следующую последовательность: filename[,identifier[,mimetype]], где filename - имя файла ресурса, identifier - логическое имя ресурса (используется для его загрузки), mimetype - медиа тип ресурса (обычно отсутствует).

/linkresource:<resinfo> Создает ссылку на ресурсный файл в данной сборке (не включая его в выходной файл)(Краткая форма: /linkres)

Создание кода

/debug[+|-] Создавать информацию отладки.

/debug:{full|pdbonly} Указание типа отладки ('full' - по умолчанию, позволяет прикреплять отладчик к запускаемой программе)

/optimize[+|-] Запускать оптимизацию (Краткая форма: /o)

/incremental[+|-] Запускать компиляцию только измененных частей кода(Краткая форма: /incr)

Ошибки и предупреждения

/warnaserror[+|-] Относиться к предупреждениям, как к ошибкам

/warn: Установить уровень предупреждений (0-4) (Краткая форма: /w)

/nowarn:<список предупреждений> Исключить указанные предупреждения

* Примечание: список предупреждений - последовательность их номеров num1[,num2[...]]

Язык

/checked[+|-] Выполнять проверки на переполнения и опустошения

/unsafe[+|-] Допускать unsafe код (unsafe код - небезопасный код, он будет обсужден в дополнительных главах)

Разное

@<file> Прочтение команд и опций компилятора из файла

/help Показывает информацию о использовании (Краткая форма: /?)

/nologo Не показывать копирайт при компиляции

Дополнительно

/baseaddress:<address> Базовый адрес для создаваемой библиотеки

/bugreport:<file> Создавать файл отчета об ошибках

/codepage:<n> Указать таблицу символов, для использования во время открытия исходного текста

/fullpaths Компилятор будет указывать полные имена файлов

/nostdlib[+|-] Не допускать включения стандартной библиотеки (mscorlib.dll). В этой библиотеке хранятся все основные классы .NET Framework.


Пример программы на C#

Теперь пришло время "пощупать" новый язык - посмотреть как он на вкус. Для этого мы разберем небольшую программу на C#.
            // Используем основные классы .NET Framework
            using System;

            // Объявляем, что мы работаем в пространстве имен SampleProgram
            namespace SampleProgram
            {
              ///
              /// Класс Client содержит информацию о кленте - его имя и баланс счета
              ///
              class Client
              {
                ///
                /// Эта переменная хранит имя клиента
                ///
                private string _name;

                ///
                /// Эта переменная хранит баланс счета клиента
                ///
                private int _balance;

                ///
                /// Это свойство используется для доступа к имени клиента.
                /// Принято все переменные класса закрывать для доступа извне
                /// и предоставлять доступ к ним через свойства, чтобы контролировать
                /// доступ.
                ///
                public string Name
                {
                  // Здесь мы определяем код, который будет использован для
                  // получения значения свойства
                  get
                  {
                    return _name;
                  }

                  // Этот код служит для присвоения свойству нового значения
                  // Параметр value здесь представляет новое значения свойства
                  set
                  {
                    _name = value;
                  }
                }

                ///
                /// Это свойство служит для доступа к счету клиента. Доступ на
                /// запись для него закрыт, чтобы нельзя было извне класса
                /// в обход его методов изменить сумму на счету клиента.
                ///
                public int Balance
                {
                  get
                  {
                    return _balance;
                  }
                }

                ///
                /// Здесь мы определяем констуктор - метод, используемый
                /// для создания нового объекта данного класса.
                ///
                /// Параметр name задает имя для нового клиента
                /// Параметр initialCash задает начальную сумму, которая будет на его счету
                public Client( string name, int initialCash )
                {
                  // Задаем имя клиента через свойство
                  Name = name;

                  // Задаем начальный счет через непосредственный доступ к переменной
                  _balance = initialCash;
                }

                ///
                /// Этот метод перечисляет деньги на счет клиента
                ///
                /// Этот параметр задает сумму, которая будет перечислена
                /// Этот параметр задает идентификатор транзакции для проверки
                /// подлинности перечисления
                public void AddMoney( int amount, int transactionID )
                {
                  // Если транзакция верна
                  if (Transaction.CheckID(transactionID))
                  // Перечисляем деньги
                    _balance += amount;
                  else
                  // Иначе генерируем исключение
                    throw new Exception("Wrong transaction ID");
                }

                ///
                /// Этот метод переопределяет стандартный метод ToString, определенный в классе
                /// System.Object. По умолчанию этот метод выводит класс объекта.
                ///
                /// Метод возвращает стороковое представление объекта
                public override string ToString()
                {
                  return Name + " (" + Balance + ")";
                }
              }

              ///
              /// Класс транзакции. В нашем примере он будет лишь проверять
              /// правильность идентификатора транзакции
              ///
              public class Transaction
              {
                ///
                /// Этот метод проверяет, верный ли идентификатор ему передан
                ///
                /// Идентификатор для проверки
                /// Возвращает true, если идентификатор верен и false если нет
                public static bool CheckID( int id )
                {
                  // В нашем примере идентификатор считается правильным,
                  // если он не делится на 2
                  return id % 2 != 0;
                }
              }

              ///
              /// Класс приложения - класс, предстваляющий основное направление
              /// выполнения программы.
              /// В нем определена точка входа
              ///
              public class Application
              {
                ///
                /// Точка входа в программу
                ///
                public static void Main()
                {
                  // Создаем нового клиента по имени Johny Smith и двумястами долларами на счету
                  Client client = new Client("Johnny Smith", 200);

                  // Выводим начальное состояние клиента
                  // Благодаря перегрузке метода ToString, мы получим информацию о клиенте
                  Console.WriteLine("Начальное состояние:  " + client);

                  // Следующий код мы заключаем в try...catch блок, так как в нем могут
                  // возникнуть исключения.
                  try
                  {
                    client.AddMoney(100, 0);
                    client.AddMoney(140, 1);
                  }
                  // Этот блок будет вызван, если возникнет исключение
                  catch (Exception)
                  {
                    Console.WriteLine("Перечисление денег было отклонено");
                  }
                  // Этот код будет вызван в любом случае либо по отработке блока catch,
                  // либо блока try
                  finally
                  {
                    Console.WriteLine("Конечное состояние клиента: " + client);
                  }
                }
              }
            }

Приведенная программа демонстрирует различные аспекты C#. Что касается логики, она просто создает объект класса Client и пытается изменить состояние его счета. Первый раз удачно, второй - нет. Программа выводит на экран следющий текст:

            Начальное состояние:  Johnny Smith (200)
            Перечисление денег было отклонено
            Конечное состояние клиента: Johnny Smith (200)


Типы данных в C#

Прежде чем разговаривать о том как работать с данными на C#, нужно поговорить о том, какие это могут быть данные.

Типы значения предназначены, являются обычными типами данных, представляющими значения, например, целые числа для типа int. Но значения могут быть и более сложными - типы значений делятся на несколько групп. Это могут быть структуры, перечисления, числовые типы (которые, в свою очередь делятся на целые, вещественные и тип decimal) и булевый тип.

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

public struct Point
{
   public int x, y;

   public Point(int p1, int p2)
   {
      x = p1;
      y = p2;
   }
}

Эта структура описывает точку на плоскости.

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

Например, определим перечисление, описывающее время года:

public enum Season {Winter, Spring, Summer, Fall};

Для описания простых типов данных в C# сначала приведем сводную таблицу, а затем обсудим некоторые ее детали.

Целые типы

Название типа | Диапазон значений | Размер
sbyte | -128..127 | Знаковое 8-битное целое
byte | 0..255 | Беззнаковое 8-битное целое
char | U+0000..U+FFFF | 16-битный Unicode-символ
short | -32.768..32.767 | Знаковое 16-битное целое
ushort | 0.65535 | Беззнаковое 16-битное целое
int | -2.147.483.648..2.147.483.647 | Знаковое 32-битное целое
uint | 0..4.294.967.295 | Беззнаковое 32-битное целое
long | -9,223,372,036,854,775,808..9,223,372,036,854,775,807 | Знаковое 64-битное целое
ulong | 0..18,446,744,073,709,551,615 | Беззнаковое 64-битное целое

Из особенностей отметим наличие 64-битных чисел и представление всех символов как Unicode (соответственно отсутствют знковые/беззнаковые символьные типы).

Типы с плавающей точкой

Название типа | Примерный диапазон значений | Точность
float | +1.5E-45..+3.4E38 | 7 знаков
double | +5.0E-324..+1.7E308 | 15-16 знаков

Тип decimal

Decimal описывает 128-битный числовой тип. Он может быть как целым, так и вещественным с большой точностью, что делает его популярным для хранения денежных сумм.

Название типа | Примерный Диапазон значений | Точность
decimal | 1.0E-28 to 7.9E28 | 28-39 значащих цифр

Все типы значений представлены соответствующими типами .NET Framework из пространства имен System. Они наследуются от класса ValueType. Для каждого типа значения поддерживается соответствующий "упакованный" (boxed) тип, который является классом, реализующим то же поведение и содержащим те же данные. Если требуется передать тип значения по ссылке, он автоматически упаковывается (box) в соответствующий упакованный тип, а по прибытии при необходимости распаковывается (unbox) снова в тип значения. Находясь в упакованном виде, тип может использовать все методы класса System.ValueType. Например, допустима следующая конструкция:
string s = 54.ToString();

При этом значение 54 упаковывается в класс System.Int32, который наследует от класса System.ValueType метод ToString(), который и вызывается.

Ссылочные типы представлены классами, делегатами и интерфейсами. Важнейшими представителями являются классы object и string (им соответствуют классы System.Object и System.String .NET Framework)

Указатели доступны только в unsafe блоках и будут рассмотрены в соответствующей части.


Переменные

Данные в C# хранятся в переменных соответствующего типа. C# является языком со строгой типизацией - значит, компилятор гарантирует, что переменная всегда имеет значение своего типа.

В C# определено 7 типов переменных: статические, экземплярные, элементы массивов. пераметры передаваемые по значению, выходные параметры и локальные переменные:

Статическими переменными являются поля класса, объявленные с ключевым словом static. Их значение является общим для всех экземпляров класса. Поясним сказанное на примере:
using System;

namespace StaticVariables
{
  ///
  /// Класс Singleton реализует шаблон Singleton (одиночка).
  /// Этот шаблон позволяет ограничивать количество
  /// экземпляров класса одним и обеспечивает к нему доступ.
  ///
  ///
  class Singleton
  {
    ///
    /// Хранит единственный экземпляр класса
    ///
    private static Singleton _instance;

    // Обеспечивает доступ к этому экземпляру
    public static Singleton Instance
    {
      get
      {
        // Если экземпляр не был создан, создаем его
        if (_instance == null)
          _instance = new Singleton();

        return _instance;
      }
    }

    ///
    /// Объявляем конструктор закрытым, чтобы не допустить создание
    /// дополнительных экзепляров
    ///
    private Singleton()
    {
    }

    ///
    /// Стандартный hello, world!
    ///
    public void HelloWorld()
    {
      Console.WriteLine("Hello, world");
    }
  }

  public class Application
  {
    public static void Main()
    {
      Singleton singleton = Singleton.Instance;

      singleton.HelloWorld();
    }
  }
}

В этом примере для доступа к статическому члену не нужно создавать экземпляра класса.

Экземплярные переменные - это поля класса, значения которых для каждого экземпляра индивидуальны.
using System;

namespace InstanceVariables
{
  ///
  /// Этот класс хранит имя мужчины
  ///
  class Man
  {
    ///
    /// Хранит обращение - общее для всех экземапляров
    ///
    private static string _prefix = "Mr.";

    ///
    /// Хранит имя каждого отдкльного мужчины
    ///
    public string _name;

    ///
    /// Создает мужчину с заданным именем
    ///
    /// Имя
    public Man( string name )
    {
      _name = name;
    }

    ///
    /// Меняет обращение для всех мужчин
    ///
    /// Новое обращение
    public void ChangePrefix( string prefix )
    {
      _prefix = prefix;
    }

    ///
    /// Выводит строковое представление экземпляра
    ///
    /// Возвращает строковое представление
    public override string ToString()
    {
      return _prefix + " " + _name;
    }
  }

  public class Application
  {
    public static void Main()
    {
      // Создаем двух мужчин с разными именами
      Man smith = new Man("Smith");
      Man pitt = new Man("Pitt");

      // Выводим их начальные состояния
      Console.WriteLine("До изменения: " + smith + ", " + pitt);

      // Изменяем имя одного из них
      pitt._name = "Pumpkin";

      Console.WriteLine("После изменения имени одного из них: " + smith + ", " + pitt);

      // Меняем обращение
      pitt.ChangePrefix("Dr.");

      Console.WriteLine("После изменения обращения: " + smith + ", " + pitt);
    }
  }
}
Вот вывод программы:
                        До изменения: Mr. Smith, Mr. Pitt
                        После изменения имени одного из них: Mr. Smith, Mr. Pumpkin
                        После изменения обращения: Dr. Smith, Dr. Pumpkin

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

Элементы массивов, а также различные параметры будут обсуждены в соответствующих главах. Здесь мы разберем лишь локальные переменные:

Локальные переменные - это переменные, объявленные в теле метода. Они не инициализируются автоматически и время их жизни зависит от компилятора, но оно не больше, чем время исполнения метода. Локальные переменные могут быть определены и в управляющих блоках for, while, foreach, if.
using System;

namespace LocalVariables
{
  public class Application
  {
    static int i = 117;

    public static void Main()
    {
      for (int i = 0; i < 10; i++)
      {
        Console.WriteLine("i = {0}", i);
      }

      Console.WriteLine("i = {0}", Application.i);
    }
  }
}
Вывод программы следующий:
i = 0
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 117


Массивы

Массивы в C# очень просты и похожи на массивы в C++. В C# имеется три основных типа массивов: одномерные, многомерные и неровные(jagged):

С одномерными массивами все как обычно:

Декларация

Декларируются они также как в C++:
int[] array = new int[10];
int[] array = {1, 2, 3, 4, 5};

Доступ

Доступ к элементам производится с помощью оператора []:
int element = array[0];
Элементы нумеруются индексами от 0 до N - 1, где N - размер массива.

Многомерные массивы представляют собой многомерные кубы значений. Элементы таких массивов идентифицируются набором индексов - "координат" в многомерном пространстве. Каждое измерение имеет свлю размерность, не зависящую от других. Отметим, что многомерные массивы являются важным отличием от других подобных языков (Java), ибо по сравнению с неровными массивами, обеспечивают гораздо большую производительность.

Декларация

При декларации размерности измерений указыватся через запятую:
int[,] array = new int[10, 20];
int[,] array = {{1, 2}, {3, 4}};

Доступ

Доступ к элементам производится с помощью оператора [], в котором индексы также указываются через запятую:
int element = array[0, 2];
Элементы нумеруются наборами индексов, каждый из которых может принимать значение от 0 до N - 1, где N - размер соответствующего измерения массива.

Неровные (jagged) массивы - это, по сути, массивы массивов. Собственно, формы декларации и доступа выьекают из этого:

Декларация

int array[][] = new int[2][];
array[0] = new int[4];
array[1] = new int[20];

Доступ

int element = array[0][1];

Неровные массивы похожи на многмерные, но их размеры могут различаться даже в одном измерении. В приведенно примере существует элеиент array[1][15], но не существует элемента array[0][15].


Константы

Константы в C# - это члены класса, значения которых определены в течении всего выполнения программы и определяются еще на этапе компиляции. Определение константы производится посредством ключевого слова const:
const int a = 2;
Константы не могут определяться со словом static, но в то же время являются членами класса, а не экземпляра. Константы могут быть инициализированы как прямо конкретными значениями, так и через другие константы:
const int a = 3;
const int b = a + 7;


Управляющие конструкции

Управляющие конструкции - это то средство, которое позволяет вам создавать алгоритмы. Стандартными управляющими конструкциями являются if, for, while goto. К тому же в C# есть много дополнительных конструкций: switch, do и foreach.

if..else

Блок if..else, как очевидствует из названия, позволяет выполнять код при выполнении определенного условия.
if (a > b)
{
    Console.WriteLine("a > b");
}
else
{
    Console.WriteLine("a <= b");
}
Логическое выражение в скобках должно иметь тип bool (в отличие от C/C++, где выражение было целым). Если оно истинно, выполнится первый блок команд, если ложно - второй. При этом если какой-либо блок команд состоит лишь из одной команды, обрамляющие фигурные скобки можно опустить:
if (serverObject != null)
    serverObject.Initialize();
Покажем несколько примеров:
// Идет проверка, чтобы избежать деления на 0
if (x != 0)
{
    d /= x;
}
else
    return;

// Проверяем, вернула ли процедура значение и, если нет, возвращаем null
if (command.Parameters["RETURN_VALUE"].Value != Convert.DBNull)
    return command.Parameters["RETURN_VALUE"].Value;
else
    return null;

Цикл for

Цикл for представляет цикл с инициализирующей командой, условием окончания и командой перехода.
    ArrayList items = new ArrayList(); // Инициализируем список некоторым образом
    CreateArray(items); // Выводим все его элементы по очереди
    for (int i = 0; i < items.Count; i++)
    {
        Console.WriteLine("i[{0}] = {1}", i, items[i]);
    }
Логика выполнения цикла следующая: сначала выполняется инициализирующая команда, далее идет шаг цикла: проверяется условие окончания, если оно ложно, то цикл прекращается и управление переходит на следующую за циклом команду, иначе выполняется тело цикла, а после него - команда перехода.
В простейшем случае цикл for оформлен следующим образом:
// Складываем все элементы массива
for (int i = 0; i < 10; i++)
    sum += array[i];
Такая форма цикла используется если вам нужно проитерироваться с целым параметром, указывающим номер итерации или если просто нужно выполнить действие некоторое количество (в нашем примере 10) раз.

while

Цикл while является циклом с предусловием: сначала проверяется определенное условие перехода, а затем выполняется итерация цикла. Синтаксис цикла while в общем случае следующий:
// Задаем число
int num = 678;

// Число разрядов - 0
int len = 0;

// считаем сколько разрядов в числе
while (num > 0)
{
    len++;
    num /= 10;
}

Console.WriteLine(len);
Семантика очень проста: проверяем условие перехода. Если оно истинно, то переходим к телу цикла и затем снова к проверке условия, иначе - прекращаем выполнение цикла.
В качестве дополнительного примера опять сошлемся на ADO.NET - чтение данных посредством SqlReader:
// Есть ли еще данные в result set? Если есть, читаем их
while (reader.Read())
{
    // Обрабатываем данные
    users.Add(new User((int)reader["ID"]);
}
Если тело цикла не совсем понятно - ничего страшного. Просто такая структура действительно часто используется (по крайней мере мной). goto goto - это просто оператор безусловного перехода. Он применяется в двух ситуациях: когда нужно перейти на обработку определенного случая в блоке switch (он будет рассмотрен позднее) или выполнить переход в определенное место программы. Во втором случае нужно указать метку в этом месте. Метка указывается просто - в нужном месте программы пишется имя метки с двоеточием на конце:
    Exit:  return 0;
Чтобы перейти на эту метку, используется следующий синтаксис оператора goto:
    goto Exit;
Еще goto применяется если нужно перейти к определенной ветви в блоке switch:
int level = GetLevel();
switch (level)
{
    case 0:
        Console.WriteLine("Level is less than 1");
        break;
    case 1:
        goto case 2;
    case 2:
        Console.WriteLine("level is greater than or equal to 1");
        goto default;
    default:
        Console.WriteLine("bye");
        break;
}

switch

Оператор switch является расширенным оператором ветвления, который позволяет в зависимости от значения выражения перейти к выполнению определенного кода. По сути он эквивалентен набору блоков if, но гораздо более оптимален, так как в нем для вычисления адреса перехода используется хеш.
Наиболее распространенным синтаксисом switch является следующий:
int a  = 1;

switch (a)
{
    case 0:
        //Операторы, выполняющиеся если a = 0
        Console.WriteLine("A = 0");
        break;
    case 1:
        //Операторы, выполняющиеся если a = 1
        Console.WriteLine("A = 1");
        break;
    default:
        //Операторы, выполняющиеся, если ни один из явных случаев не подошел
        Console.WriteLine("A <> 0 и A <> 1");
        break;
}
Блок case x выполняется, если выражение, указанное в скобках после switch равно x. Блок default - если ни одно из case-выражений не выполнилось. Удобно switch использовать для обработки выражений перечислимых типов.
Для переходов между блоками можно использовать команды goto case и goto default. Для подробной информации смотрите описание оператора goto.

do

Оператор do реализует цикл с постусловием. Суть этого цикла аналогична while, но условие выхода проверяется не до итерации, а после. Это иногда бывает удобным.
string s = "A, B, C, D";

do
{
    s = s.Substring(s.IndexOf(",") + 1);
} while (s.Length > 0);
Обычно его используют, когда проверку нужно выполнить после того, как прошла хотя бы одна итерация.

foreach

foreach - это очень удобный оператор, значительно облегчающий работу с коллекциями в C#. Он позволяет перебрать все элементы некоторой коллекции, даже написанной вами, для этого класс коллекции должен лишь реализовать интерфейс IEnumerable.

Наиболее общей формой использования foreach является следующая:
// Получаем некоторый DataSet
DataSet data = GetData();

foreach (Row row in data.Tables[0].Rows)
{
    Console.WriteLine("ID = {0}", row["ID"]);
}
Оператор перебирает все элементы коллекции и выполняет для каждого тело перечислителя.


Стркутура программы

В этом разделе мы обсудим общую структуру программ на C# - как организовать код, какие элементы служат для этого.

Логическая организация программы

Логическая организация кода помогает лучше в нем ориентироваться, делает код более понятным и облегчает его повторное использование. C# отражает иерархию пространств имен, принятую в .NET. Все классы могут быть распределены по пространствам имен, которые образуют иерархическую структуру. Например, пространство имен System включает в себя пространства имен Collections, Data и другие, а Data включает в себя SqlClient, OleDbClient, SqlTypes. Полные имена последних - System.Data.SqlClient, System.Data.OleDbClient, System.Data.SqlTypes. Видно, что в полных именах пропичываетася весь "путь" к namespace от корня, при этом уровни разделяются точкой.

Удобно все классы проекта разделить в иерархическую структуру, более того, все проекты, которые вы выполняются, могут иметь общую иерархию, начинающуюся с имени вашей компании. Например в компании с именем HT Electronics, которая выполняет работу над проектами с именами Web Admin и Processor Manager, классы, относящиеся к этим проектам могут быть расположены в пространствах имен com.HTElectronics.WebAdmin и com.HTElectronics.ProcessorManager. Также может быть общая библиотека классов в пространстве имен com.HTElectronics.Common.

Создание модели классов выходит за рамки данной публикации - созданию хорошей архитектуры посвящены целые книги, которые мы вам очень рекомендуем прочитать. В первую очередь это R. Gamma et al. "Design Patterns. Elements Of Reuseable Object-Oriented Software" и J. Larman "Applying UML and Design Patterns". Обе книги переведены на русский язык.

Последнее, о чем следует напомнить здесь - это о методе Main - точке входа в программу. Мы уже описывали этот метод, так что здесь мы просто напомним о том, что нужно решить, где у вас будет начинаться выполение программы.

Физическая организация программы

Под физической организацией кода мы понимаем его размещение по файлам и каталогам. В Java существуют достаточно жесткие ограничения - соответствие имен файлов именам классов, имен каталогов именам пакетов, размещение не более одного public-класса в файле и т.д.

В C# подобных ограничений нет. Так что организация файлов и каталогов лежит на программисте. В общем случае невозможно отразить структуру пространств имен структурой каталогов - в одном файле могут быть объявлены классы из разных пространств. Причем отсутствие такого ограничения не уменьшает логичности структуры, зато часто это удобно - например, в Web-приложениях.


Комментарии

В этом разделе мы опишем приемы комментирования исходного кода. В C# используется три типа комментирования: комментирование блока текста, комментирование строки и XML-комментирование.

Комментирование блока текста

Чтобы вставить блочный (многострочный комментарий), текст комментария обрамляется /* */:
/* Эта переменная хранит количество элементов */
int count;

Комментирование строки текста

Также можно закомментировать строку текста (всю или часть). Для этого начало комментария отмечается текстом //:
int count;  // Эта переменная хранит количество элементов

XML документирование кода C#

Как уже говорили, третим способом комментирования является XML документирование вашего C# кода. Если вы уже ознакомились с опциями компилятора C#, то наверняка отметили наличие параметра "/doc". Используя его, вы можете получить из исходного файла C#, XML файл, описывающий его. Полученный файл вы можете обрабатывать как хотите, например, преобразовав в HTML-документацию с помощью специальной утилиты. Конечно же, сущeствуют правила документирования кода. О них то мы и будем говорить далее...

Тэги документирования

<c>

Тэг <c> указывает на то, что текст, заключенный в него, является кодом. <code>

Тэг <code> имеет тоже самое значение, что и тэг <c>, но используется для маркировки нескольких строк.

<example>

Тэг <example> используется для указания примера использования какого-либо метода или иного элемента вашего модуля. <exception> Тэг

<exception>

Используется для комментирования исключений в вашем коде. Для этого параметр тэга cref="..." должен содержать System.Exception. Компилятор проверит существует ли исключение, и в случае отсутствия выдаст ошибку. В следующем примере демонстрируется использование этого тэга:
/// <exception cref="System.Exception"> Это исключение </exception>
class SampleClass : Exception {     }

Результат обработки тэга <exception> таков: В XML файле, в тэге
<member name="T:SampleClass">
, соответствующем элементу вашего кода, будет находиться тэг <exception>, с параметром cref, указывающим каноническое имя элемента. Внутри этого тэга будет находиться ваш комментарий. Например таким будет результат компилирования последнего примера:
<?xml version="1.0"?>
<doc>
    <members>
        <member name="T:SampleClass">
            <exception cref="T:System.Exception"> Это исключение </exception>
        </member>
    </members>
</doc>

<list>

Тэг <list> используется для создания таблицы, ненумерованного, нумерованного списка или списка определений. У него есть параметр type, который может принимать три значения: "bullet" - для ненумерованного списка, "number" - для нумерованного списка и "table" - для таблицы.

Внутри <list> могут находиться два вида тэгов: <listheader> и <item>. Каждый из них может содержать внутри себя два тэга - <term> и <description>.

<listheader>

используется для описания заголовка таблицы или списка, тэг <item> для указания элемента. В случае таблицы, нумерованных или ненумерованных списков внутри тэгов <listheader> и <item> используется только тэг <term>, в случае списка определений <term> и <description>.

Следующий пример демонстрирует использование <term> для создания списка определений:
public class SampleClass
{
    /// <list type="bullet">
    /// <listheader>
    /// <term> Фрукты </term>
    /// </listheader>
    /// <item>
    /// <term> Яблоко </term>
    /// <description> растет на яблоне </description>
    /// </item>
    /// <item>
    /// <term> Банан </term>
    /// <description> растет на пальме </description>
    /// </item>
    /// <item>
    /// <term> Груша </term>
    /// <description> растет на груше </description>
    /// </item>
    /// </list>
    public static void Main ()
    {
    }
}

<para>

Тэг <para>

используется для обозначения параграфа. Его следует использовать внутри таких тэгов, как <return> и <remark>.

<param>

Тэг <param> используется для описания агрументов методов. Его параметр name содержит имя аргуметна метода, в сам же тэг заключено описание агрумента.
public class SampleClass {
/// <param name="input"> Содержит строку ввода </param>
public static void SampleMethod (String input) {   }

<paramref>

Тэг <paramref> показывает, что слово является аргументом метода. Его параметр name содержит имя аргуметна метода. Его использование показано в следующем примере:
public class SampleClass {
/// <paramref name="input"/> - это строка ввода.
public static void SampleMethod (String input) {   }

<permission>

Тэг <permission> используется для комментирования прав доступа к элементам вашего кода. Для этого параметр тэга cref="..." должен содержать System.Security.PermissionSet. В следующем примере демонстрируется использование этого тэга:
using System;
class SampleClass{

/// <exception cref="System.Security.PermissionSet">
/// Свободный доступ к этому методу </exception>
public static void SampleMethod() {   }
public static void Main() {    }

Результат обработки этого примера показан ниже:
<?xml version="1.0"?>
<doc>
    <members>
        <member name="M:SampleClass.SampleMethod">
            <permission cref="T:System.Security.PermissionSet">
            Свободный доступ к этому методу </permission>
        </member>
    </members>
</doc>

<remarks>

Тэг <remarks> служит для вставки комментариев для вашего класса или другого типа.

<returns>

Тэг <returns> используется для описания возвращаемого значения метода. Следующий пример демонстрирует его использование:
using System;
class SampleClass{

/// <returns> Возвращает число Пи </returns>
public static double PiSample() { return 3.1415926; }
public static void Main() {    }

<see>

Тэг <see> позволяет вам ссылаться на любой элемент, доступный в процессе компиляции, например элемент вашего кода. Его параметр cref="..." - ссылка на этот элемент. Пример:
using System;
class SampleClass{

/// <para> Метод Main использует System.Console.
/// <see cref="System.Console"/> for console operations. </para>
public static void Main() {
System.Console.WriteLine("SampleClass Console Output");
}

<seealso>

Тэг <seealso> ссылается на элементы, которые следует поместить в раздел "смотри также". Парамент этого тэга cref="..." - ссылка на любой элемент доступный в процессе компиляции.

<summary>

Тэг <summary> следует использовать для описания элемента вашего класса. Для описания самого класса следует использовать <remarks>. Пример:
using System;

/// <remarks> Это мой класс </remarks>
class SampleClass{
/// <summary> Main это метод класса SampleClass </summary>
public static void Main() {       }

<value>

Тэг <value> используется для описания property.

Правила документирования

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

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

Особенности получаемого xml файла

В получаемом нами xml файле, все элементы кода имеют некоторые правила представления. Во-первых, при упоминании элемента кода, используется полное иерархическое представление пространств имен. Например, если вы использовали using System и тип String, то его представление в xml будет иметь вид System.String. Точнее T:System.String. Перед каждым элементом кода в xml, ставится модификатор, говорящий о том, чем является этот элемент. Ниже приведена таблица модификаторов:


Работа с консолью

Хотя время консольных приложений уходит, многие программы, не требующие взаимодействия с пользователем, остаются консольными. Также на таких программах удобно делать первые шаги. Эти аргументы и заставили нас кратко описать основные приемы работы с консолью. Это не относится непосредственно к C#, но поможет читать приведенный в учебнике код и создавать свои приложения.

Для работы с консолью в .NET используется класс Console. Удобство использование этого класса кроется в двух аспектах: все его методы являются статическими, так что не нужно создавать его экземпляр для использования. Во-вторых, он объединяет в себе ввод, вывод и вывод ошибок. По умолчанию ввод/вывод производится на стандартную консоль (если ее нет, например, в оконных приложениях, вывод просто не производится), но устройства ввода и вывода можно изменить.

Для работы с консолью обычно используется 4 метода: Read, ReadLine, Write и WriteLine. Первые два используются для ввода, последние - для вывода.

Метод Read

Метод Read читает символ из потока ввода. Он возваращает значение типа int, равное коду прочитанного символа, либо -1, если ничего прочитано не было. Приведем пример программы:
do
{
    int i = Console.Read();

    if (i != -1)
        Console.WriteLine("{0} ({1})", (char)i, i);
    else
        break;
} while (true);

Эта программа выводит на экран введенные символы и их коды.

Метод ReadLine

Метод ReadLine читает из потока ввода строку текста (она завершается символом перевода строки или возврата каретки). Метод возвращает объект типа string или null, если ввод осуществить не удалось.
do
{
    string s = Console.ReadLine();

    if (s != null)
        Console.WriteLine("Введенная строка: " + s);
    else
        break;
} while (true);

Методы Write и WriteLine

Метод Write выводит на экран значение переданной ему переменной. Он определен для всех базовых типов и поддерживает форматированные строки. Таким образом, можно либо вызвать Write с указанным значением в качестве параметра:
Console.Write(1);
Console.Write(0.754);
Console.Write("Hello!");
Либо передать строку форматирования и список значений. В строке форматирования применяется множество модификаторов, здесь мы отметим лишь, что вмето "{n}" подставляется n-й входной параметр (нумерация начинается с 0):
Console.Write("Привет, {0}", Name);
Метод WriteLine отличается от Write только тем, что выводит символ перевода строки в конце.


Различия между объектными типами и типами значения

Как уже упоминалось, в C# есть, помимо прочих, два класса типов: объектные типы (reference types) и типы значения (value types). Их отличие заключается в том, что переменные типа значения содержат сами по себе все данные, в то время как переменные ссылочных типов лишь ссылаются на некоторое значение.

То есть, с типами значения все просто - переменные таких типов хранят сами данные, все как обычно. А ссылочные типы ведут себя иначе - при создании нового объекта такого типа сам объект создается в специальном пуле, а переменная представляет всего лишь ссылку на него:
class MyClass
{
    ...
}

MyClass a = new MyClass();

Если мы далее присваиваем другой переменной значение только что созданной, для нее не создается нового объекта, она просто ссылается на существующий:
MyClass b = a;

При этом все изменения объекта, которые производятся через b отражаются и на a. Также значения ссылок a и b можно изменять - при этом они перестают ссылаться на текущий объект и ссылаются на новый (или не ссылаются ни на что - имеют значение null). Если возникает ситуация, что все ссылки на объект изменили свои значения и на объект больше никто не ссылается, то он становится "бесхозным" - и удаляется.
a = null;
b = null;

Поясним еще одним примером:
class Sample
{
    public int field;
}

...

int i, j;

i = 3;
j = i;

// Выведет: i = 3, j = 3
Console.WriteLine("i = {0}, j = {1}", i, j);

i = 2;

// Выведет: i = 2, j = 3
Console.WriteLine("i = {0}, j = {1}", i, j);

Sample a = new Sample();
a.field = 1;

Sample b = a;

// Выведет a.field = 1, b.field = 1
Console.WriteLine("a.field = {0}, b.field = {1}", a.field, b.field);

a.field = 2;

// Выведет a.field = 2, b.field = 2
Console.WriteLine("a.field = {0}, b.field = {1}", a.field, b.field);

Впрочем, при вызове метода можно передать значение типа значения по ссылке - для этого используется модификатор ref.


Классы

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

Здесь же мы лишь опишем класс как структуру данных, содержащую члены-данные (такие как константы и поля), функциональные члены (методы, свойства, события, индексаторы, операторы, конструкторы и деструкторы) и вложенные типы. Классы также поддерживают наследование.

Простейшее объявление класса выглядит так:
// Класс объявленный как public, доступен отовсюду, включая другие сборки
// Можно также написать internal - класс будет доступен только из текущей сборки
public class HelloWorldClass
{
    public void HelloWorld()
    {
        Console.WriteLine("Hello, world");
    }
}

Можно также сделать класс абстрактным:
public abstract class AgentFactory
{
    public void WriteInfo()
    {
        Console.WriteLine("I'm agent factory");
    }

    public abstract Agent CreateAgent();
}

Такие классы могут содержать абстрактные методы (методы, которые не реализованы в текущем классе, но должны быть реализованы в неабстрактных потомках). Фбстрактные классы не могут быть инстанцированы. То есть для использования этого класса, должен быть написан потомок:
public class FootballAgentFactory : AgentFactory
{
    public Agent CreateAgent()
    {
        return new FootballAgent();
    }
}

Если же вы наоборот не хотите, чтобы от вашего класса кто-то наследовался, объявите его как sealed:
public sealed class IAmTheCoolest
{
    public void WhoAmI()
    {
        Console.WriteLine("I am cool and I don't want any inheritance");
    }
}

// Это приведет к ошибке компиляции
public class IAmMoreCoolThanTheCoolest : IAmTheCoolest
{
    public void IAmTheCoolest()
    {
        Console.WriteLine("I'm even cooler that the coolest");
    }
}

Как вы уже поняли, после двоеточия указывается класс, от которого наследуется данный. Наследоваться в C# можно только от одного класса, но можно реализовывать несколько интерфейсов:
public class Base1
{
}

public class Base2
{
}

public interface Interface1
{
}

public interface Interface2
{
}

// Попытка наследования нескольких классов приведет к ошибке
public class MyClass : Base1, Base2
{
}

// Зато можно реализовывать несколько интерфейсов
public class MyClass2 : Base1, Interface1, Interface2
{
}

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

В теле класса объявляются его члены. Далее мы опишем все виды членов класса.

Поля

Поле представляет переменную, связанную с данным классом или его экземпляром.
public class MyClass
{
    // Указывается что это поле доступно из любого класса, даже из другой сборки
    // Также можно указать protected - поле будет доступно только из классов-наследников
    // и private - доступно только из текущего класса.
    public int a;

    // Присваиваем начальное значение в конструкторе
    public MyClass()
    {
        a = 3;
    }
}

public class Application
{
    public static void Main()
    {
        MyClass myClass = new MyClass();

        // a = 5
        Console.WriteLine("a = {0}", myClass.a);

        myClass.a += 5;

        // a = 10
        Console.WriteLine("a = {0}", myClass.a);
    }
}

Также можно обявить поле как readonly - запрещается запись в поле, кроме как при инициалищации посредством инийиализатора или конструктора.
public class MyClass
{
    // Присваиваем начальное значение в инициализаторе
    readonly int a = 1;

    // Присваиваем начальное значение в конструкторе
    public MyClass()
    {
        a = 3;
    }

    public MyMethod()
    {
        a = 5; // Ошибка компиляции
    }
}

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

Модификатор static декларирует принадлежность члена класса всему классу, а не конкретному члену. Доступ к таким членам осуществляется через имя класса, а не его экземпляра.
public class MyStaticMemberClass
{
    public static int a = 10;

    public MyMethod()
    {
        a = 5;
    }
}

public class Application
{
    public static void Main()
    {
        // a = 10
        Console.WriteLine("a = {0}", MyStaticMemberClass.a);

        MyStaticMemberClass.a = 2;

        // a = 2
        Console.WriteLine("a = {0}", MyStaticMemberClass.a);
    }
}

Методы

Методы представляют собой действия, которые можно произвести в связи с данным классом.
public class MyHelper
{
    private int a, b;

    public MyHelper( int a, int b )
    {
        this.a = a;
        this.b = b;
    }

    public int Max()
    {
        return a > b ? a : b;
    }
}

public class Application
{
    public static void Main()
    {
        MyHelper helper = new Helper(2, 10);

        // MAX(2, 10) = 10
        Console.WriteLine("MAX(2, 10) = {0}", helper.Max());
    }
}

Возможные модификаторы - уже описанные public, static, protected, abstract и еще virtual, override, new и extern. static - то же самое, что и у полей. Укзазывает. что метод принадлежит классу и а не конкретному экземпляру, работать такой класс может только со статическими членами: public class SampleClass

{
    public static int a;
    public int b;

    public static void DoubleA()
    {
        a *= 2;
    }

    public void SetB()
    {
        b = a;
    }
}

public class Application
{
    public static void Main()
    {
        SampleClass sc1 = new SampleClass(), sc2 = new SampleClass();

        SampleClass.a = 1;

        sc1.SetB();

        SampleClass.a = 2;

        sc2.SetB();

        Console.WriteLine("sc1.b = {0}, sc2.b = {1}", sc1.b, sc2.b);
    }
}

virtual - указывает, что метод может быть переопределен (override) в потомках. Для перегрузки методов в потомках используются модификаторы new и override. При использовании модификатора new, метод потомка вызывается лишь в значениях класса потомка, а при использовании override - в любом случае - метод предка замещается. Модификатор override может быть использован только для виртуальных методов: class Base
{
    public void MyMethodA()
    {
        Console.WriteLine("Base.MyMethodA");
    }

    public virtual void MyMethodB()
    {
        Console.WriteLine("Base.MyMethodB");
    }
}

class Child : Base
{
    public new void MyMethodA()
    {
        Console.WriteLine("Child.MyMethodA");
    }

    public override void MyMethodB()
    {
        Console.WriteLine("Child.MyMethodB");
    }
}

class Class1
{
    static void Main(string[] args)
    {
          Base childClass = (Base)new Child();

          // Base.MyMethodA
          childClass.MyMethodA();

          // ChildClass.MyMethodB
          childClass.MyMethodB();
    }
}

Методам также можно передавать параметры, которые параметризуют его действия.

Формат списка параметров такой: модификатор параметра тип параметра имя параметра

Единственной необычной частью является модификатор параметра. Существуют следующие модификаторы параметров:

out - указывает, что значение параметра не передается в метод, а возвращается из него. Реально это означает, что можно передавать только переменную, она может быть не инициализирована и ее значение может быть изменено в методе. ref - указывает, что параметр передается по ссылке. То есть, если в теле метода будет изменено его значение, изменится и значение передаваемой переменной. С модификатором ref передаваться могут только переменные.

Также существует возможность декларировать метод как принимающий произвольное число аргументов. Для этого используется следующий синтаксис:
    param тип элемента[] имя списка

Для пояснения приведем пример:
class Application
{
    static void SampleMethod( string text, params string[] args )
    {
        Console.WriteLine(text);

        foreach (string s in args)
        {
            Console.WriteLine(s);
        }
    }

    public static void Main()
    {
        SampleMethod("Parameters list:", "Param1", "Param2", "Param3");
    }
}

Свойства

Свойства расширяют возможности полей. Они представляют собой типизированные объекты, у которых можно получить значение и (опционально) записать в них новое значение. При выполнении этих действий выполняется определяемый программистом код. Синтаксис определения полей следующий:
    модификаторы тип имя
    {
        get Тело get
        get Тело set
    }

В теле get пишется код, который выполняется при получении значения свойства. В теле set пишется код, который выполняется при установке нового значения. При этом в set доступно присваеваемое значение через ключевое слово value:
public class SampleClass
{
    private static int a;

    public static int A
    {
        get
        {
            return a;
        }
        set
        {
            a = value;
        }
    }

    public static void Main()
    {
        A = 2;

        Console.WriteLine("A = {0}", A);
    }
}

Этот пример демонстрирует распространенный прием инкапсуляции полей.

Индексаторы

Одним из важных направлений развития языков программирования является удобство и наглядность кода. Именно стремление упростить структуру кода привело в свое время к перегрузке операторов С++ и свойствам в Object Pascal. Предлагая новый язык специально для своей новой платформы, Microsoft, разумеется, не могла не учесть это сторону вопроса. Поэтому язык включает в себя свойства (properties) и индексаторы (indexers).

При написании приложений часто возникает необходимость создавать объекты, представляющие коллекцию других объектов (они не обязательно явно хранят в себе все эти объекты, но предоставляют доступ к ним). В языках вроде C или Java для этого использовались специальные функции (методы) вроде GetItem( int index ). Конечно, это возможный подход, но часто, когда нужно часто обращаться к элементам коллекции, неудобно каждый раз писать громоздкие конструкции.

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

Для того, чтобы объявить в классе индексатор, нужно использовать следующий синтаксис:
    тип this [список_параметров]

Где тип - тип возвращаемого значения (тип элемента коллекции), а список параметров - типы и имена параметров, используемых для идентификации элемента коллекции.

Индексатор может иметь любые модификаторы, кроме static, а к параметрам нельзя применять модификаторы out и ref.

Для определения кода индексатора, нужно прописать ему метода доступа - get и, возможно, set (если индексатор у вас позволяет менять элементы). При этом в методе get доступны все параметры индексатора как переменные, а в методе set дополнительно к ним параметр value, представляющий новое значение элемента. Таким образом, индексатор примет вид, аналогичный следующему:
public object this [int index]
{
    get
    {
        return GetItem(index);
    }
    set
    {
        SetItem(index, value);
    }
}
Для полной ясности картины, приведем небольшой пример:
using System;

public CustomIndexerClass
{
    // Объявляем внутренний массив, где будут храниться действительные значения
    private int internalArray = new int[10];

    // Объявляем индексатор
    public int this [int index]
    {
        // Доступ на чтение
        get
        {

            // Проверка границ
            if (index < internalArray.GetLowerBound(0) || index > internalArray.GetUpperBound(0))
                return 0;

            // Врщвращаем значение соответствующего элемента массива
            return internalArray[index];
        }
        set
        {
            // Проверка границ и запись нового значения
            if (index >= internalArray.GetLowerBound(0) && index <= internalArray.GetUpperBound(0))
                internalArray[index] = value;
        }
    }

    public class ApplicationClass
    {
        public static void Main()
        {
            CustomIndexerClass myIndexer = new CustomIndexerClass();

            myIndexer[1] = 10;
            myIndexer[5] = 7;

            for (int i = 0; i < 10; i++)
                Console.WriteLine("myIndexer[{0}] = {1}\n", i, myIndexer[i]);
        }
    }
}
Эта программа создает класс, имеющий индексатор, который просто читает или записывает значения во внутреннем массиве. Главная программа сначала изменяет несколько ячеек массива, пользуясь индексатором, а затем читает все его ячейки последовательно.

Констукторы

Конструкторы экземпляров - предназначены для инициализации внось созданных экземпляров класса. Это специальный тип метода, который присваивает начальные значение членам класса, производит другие инициализирующие действия. Важным моментом здесь является инициализация базового класса. Для выхова конструктора базового класса используется ключевое слово base. Вместо долгого формального описания, просто покажем пример:
class BaseClass
{
    int _a;

    public int A
    {
        get
        {
            return _a;
        }
    }

    public BaseClass( int a )
    {
        _a = a;
    }
}

class InheritedClass : BaseClass
{
    public InheritedClass() : base(1)
    {
    }
}

Статические конструкторы - инициализируют класс в целом. Никто не знает когда происходит его вызов - гарантирется лишь то, что он будет вызван до создания первого экземпляра класса. В качестве примера покажем интереснейшую реализацию паттерна Singleton. В разделе "Переменные" был показана стандартная реализация, которую легко перенести на большинство языков. Здесь же мы приведем вариант, использующий особенности C#.
public class Singleton
{
    private static Singleton _instance;

    public static Singleton Instance
    {
        get
        {
            return _instance;
        }
    }

    public static Singleton
    {
        _instance = new Singleton();
    }
}

Деструкторы

Деструктор - это метод, который вызывается при уничтожении объекта. Так как в .NET CLR используется сборщик мусора, нельзя явно удалить определенный экземпляр. Удаление происходит когда никакой код уже не может воспользоваться этим экземпляром. Деструкторы определяются как методы с именем, совпадающим с именем класса, предваренным знаком ~ (тильда): public class A
{
    ~A
    {
        Console.WriteLine("I'm being descructed");
    }
}

public class Application
{
    public static void Main()
    {
        A a = new A();
        a = null;
    }
}

Программа выведет "I'm being destructed"


Интерфейсы

Интерфейсы предоставляют средство обобщения классов, от которых требуются реакция на определенные вызовы. Список вызовов, на которые должен реагировать класс (эти вызовы могут быть представлены в форме вызовов методов, изменении значений свойств и индексаторов, генерации событий) собираются в интерфейсе. Если класс реализует интерфейс, мы смело можем делать с ним то, что описано в этом интерфейсе.
interface IEnumGenerator
{
    string GenerateEnumeration( ArrayList objects );
}

class ColonEnumGenerator : IEnumGenerator
{
    public string GenerateEnumeration( ArrayList objects )
    {
      string result = "";

      foreach (object obj in objects)
        result += (result == "" ? "" : ", ") + obj.ToString();

      return result;
    }
}

class CREnumGenerator : IEnumGenerator
{
    public string GenerateEnumeration( ArrayList objects )
    {
      string result = "";

      for (int i = objects.Count - 1; i >= 0; i--)
        result += (result == "" ? "" : "\n") + objects[i].ToString();

      return result;
    }
}

class Class1
{
    [STAThread]
    static void Main(string[] args)
    {
        ArrayList list = new ArrayList();
        list.Add("Matt");
        list.Add("Julia");
        list.Add("Robert");

        IEnumGenerator cgen = new ColonEnumGenerator();

        Console.WriteLine(cgen.GenerateEnumeration(list));

        cgen = new CREnumGenerator();

        Console.WriteLine(cgen.GenerateEnumeration(list));
    }
}

Важно отметить, что интерфейсы могут наследоваться от других интерфейсов.
interface A
{
    void MethodA();
}

interface B : A
{
    void MethodB();
}

class C : A, B
{
    public void MethodA()
    {
    }

    public void MethodB()
    {
    }
}

Также в ряде случаев (например, при реализации двух интерфейсов с одинаковыми членами) полезно использовать явную реализацию. Если член интерфейса реализован явно, доступ к нему может быть получен только через значение типа интерфейса:
interface IGameObject
{
    string GetName();
}

interface IPlayer
{
    string GetName();
}

class Smith : IGameObject, IPlayer
{
    public IGameObject.GetName()
    {
        return "Player";
    }

    public IPlayer.GetName()
    {
        return "Smith";
    }
}

class Application
{
    public static void Main()
    {
        Smith smith = new Smith();

        // Ошибка
        // smith.GetName();

        IGameObject gosmith = (IGameObject)smith;
        IPlayer psmith = (IPlayer)smith;

        // Player
        Console.WriteLine(gosmith.GetName());

        // Smith
        Console.WriteLine(psmith.GetName());
    }
}


Наследование и реализация

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

При наследовании наследник получает от базового класса все члены (то есть объект типа наследника является также в полной мере объектом типа базового класса).
class A
{
    public int a;

    public void AddA()
    {
        a++;
    }
}

class B : A
{
    public void SubA()
    {
        a--;
    }
}

class Application
{
    public static void Main()
    {
        B b = new B();

        A a = (A)b;

        a.a = 0;

        // A = 0
        Console.WriteLine("A = {0}", b.a);

        a.AddA();

        // A = 1
        Console.WriteLine("A = {0}", b.a);

        // Ошибка
        // a.SubA();
        b.SubA();

        // A = 0
        Console.WriteLine("A = {0}", b.a);
    }
}
Интересные вещи при наследовании происходят с методами - их можно перекрывать и добавлять. Подробнее смотрите раздел классы.


Перегрузка операторов

В дополнение к стандартным интерпретациям унарных и бинарных операторов в C# есть возможность для классов и структур переопределить их значение. Для этого используется ключевое слово operator.

Можно перегружать следующие опрераторы: унарные +, -, !, ~, ++, --, true, false и бинарные +, -, *, /, %, &, |, ^, <<, >>, ==, !=, >, <, >=, <=, к тому же при перегрузке бинарных операторов перегружается и соответствующие оператор присваивания: например для + автоматически перегружается +=.

При перегрузке операторов используется синтаксис
        operator символ оператора ( x )
для унарных и
        operator символ оператора ( x, y )
для бинарных операторов.

Приведем простой пример:
        public struct Vector
        {
            private double x, y;

            public Vector( double x, double y )
            {
                this.x = x;
                this.y = y;
            }

            public static Vector operator + ( Vector a, Vector b )
            {
                return new Vector(a.x + b.x, a.y + b.y);
            }

            public override string ToString()
            {
                return "[" + x + ", " + y + "]";
            }
        }

        public class Application
        {
            public static void Main()
            {
                Vector a(1, 2);
                Vector b(2, 3);

                Vector c = a + b;

                Console.WriteLine("{0} + {1} = {2}", a, b, c);
            }
        }

Программа выведет следующее:
        [1, 2] + [2, 3] = [3, 5]


Исключения

Мы не будем объяснять, зачем исключения в C#. Это не новое веяние. Это - стандарт. Искючения были придуманы уже больше 15 лет назад, стандартный синтаксис в них берет свое начало от C++.

Исключения служат для обработки ошибок. То есть, допустим, в некотором методе у вас случается ошибка. Что делать? Можно, конечно, прервать процесс без объяснений, но это не дело. К тому же метод, в котором возникла ошибка, ничего не знает об окружении , в котором он выполняется и, стаоло быть, не вполне компетентен в выборе стратегии в нештатной ситуации. Правильно было бы сообщить об этом "наверх", вызывающему методу. В языках без поддержки исключений использовались коды ошибок - метод (функция) завершался, возвращая "особенное" значение, которое трактовалось как ошибочное. Понятно, что такой подход являлся очень неудобным (сложно передать всю информацию об ошиюке в одном числе, сложность в выделении специальных значений).

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

Синтаксис довольно прост: в общем случае он выглядит следующим образом:
        try
        {
            блок команд, в котором может возникнуть ошибка
        }
        [catch [(тип_исключения имя_исключения)]
        {
            блок обработки исключения
        }]
        [finally
        {
            команды, которые выполняются в любом случае
        }]

Схема работы такой конструкции очень проста - если в блоке try происходит исключение, то идет переход на блок catch и ему это исключение передается в качестве параметра. В любом случае (произошло исключение или нет) в конце вызывается блок команд finally.

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

Приведем небольшой пример:
class ReverseFunction
{
    public static double Calculate( double f )
    {
        if (d == 0)
            throw new Exception("Function is undefined in 0");

        return 1 / d;
    }

    public static void Main()
    {
        try
        {
            Console.WriteLine("1 / {0} = {1}", 2, Calculate(2));
            Console.WriteLine("1 / {0} = {1}", 0, Calculate(0));
            Console.WriteLine("1 / {0} = {1}", 1, Calculate(1));
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: {0}", e);
        }
    }
}

Можно также производить различные действия при различных типах исключения. Для этого используется несколько блоков catch подряд. При этом вызывается первый блок, чей тип исключения соответствует типу пришедшего исключения:
try
{
    CalculateSpline();
}
catch (DivisionByZeroException e)
{
    Console.WriteLine("Divizion by 0");
}
catch (OverflowException e)
{
    Console.WriteLine("Overflow");
}
catch (Exception e) // Обработка остальных типов исключения
{
    Console.WriteLine("Exception: {0}", e);
}


Пользовательские преобразования типов

При разработке собственных типов часто бывает удобно преобразоывать их в другие. Например, если вы разрабатывается свой класс числа, использующий неограниченную точность, было бы удобно если бы при необходимости в выражениях он преобразовывался в double, float или int и, наоборот, float и компания преобразовывались в наш VeryCoolNumber.

В C# есть все средства для этого. Декларация подобных преобразований осуществляется с помощью оператора, у которого имя совпадает с именем типа, в который нужно преобразовывать, а в качестве параметра он берет значение преобразуемого типа. Также преобразования могут быть явными и неявными - то есть, нужно ли явно использовать опреатор преобразования () или преобразоване будет выполнено автоматически. Для указания типа используются ключевые слова explicit и implicit соответственно в декларации оператора.

В качестве примера приведем класс комплексного числа, который явно может быть преобразован к числу, являющемуся его вещественной частью и также из вещественного числа может быть преобразован в комплексное
class Complex
{
    public double Real;
    public double Imag;

    public Vector( double real, double imag )
    {
        Real = real;
        Imag = imag;
    }

    public static explicit operator Vector( double real )
    {
        return new Vector(real, 0);
    }

    public static explicit operator double( Vector vector )
    {
        return vector.real;
    }

    public static void Main()
    {
        Console.WriteLine("{0}", (double)(Vector)(((double)new Vector(3, 2)) + 4));
    }
}


Аттрибуты

Что такое атрибут

Все существующие компиляторы сохраняют в исполняемом файле некоторую информацию об исходном коде. Эта информация жестко определяется создателями компилятора и не может быть расширена пользователем. Так было до настоящего момента. Но с выходом Microsoft.NET это стало возможным благодаря атрибутам (attributes). Хотя атрибуты и не являются частью какого либо конкретного языка, их может поддерживать любой язык входящий в Microsoft.NET, в данный момент они присутствуют только в C#. Атрибуты позволяют вам добавлять любую дополнительную текстовую информацию о ваших классах, методах, полях, свойствах и т.д. Атрибуты сохраняются вместе с метаданными (metadata) и могут быть достаточно просто получены во время выполнения программы при помощи процесса называемого reflection. Атрибуты могут быть полезны как программистам так и создателям средств разработки, таких как UML средства, ClassBrowser'ы и т.д. В C# достаточно много предопределенных атрибутов и большинство программистов скорее будут использовать стандартные атрибуты, чем вводить новые.

К примеру в C# вы можете использовать атрибут DllImport чтобы указать, что метод класса импортируется из Dll.
using System.Runtime.InteropServices;

public class AppMain {

  [DllImport("user32.dll")]
  public static extern int MessageBoxA(int handle,string message,string caption,int flags);

  public static void Main(){
    MessageBoxA(0,"Hello World","Native Message Box",0);
  }
}

Создание и использование собственных атрибутов

В качестве примера создадим свой атрибут MethodComments, позволяющий добавлять некоторые комментарии к методам нашего класса. Как вы уже наверно заметили добавление атрибута к члену класса очень похоже на создание экземпляра некоего класса. Так оно и есть. Чтобы создать свой атрибут необходимо написать класс наследник класса System.Attribute. Microsoft рекомендует добавлять слово Attribute к названию класса атрибут задающего. Хотя это и не обязательно, но позволяет отличить такие классы от всех остальных. При добавлении атрибута к коду вы можете опустить слово Attribute и компилятор добавит его автоматически. Вот код создающий MethodComments.
using System;

[AttributeUsage(AttributeTargets.Method)]
public class MethodCommentsAttribute : System.Attribute {
  private string comments;

  public string Comments {

    get {
        return comments;
    }
    set {
      comments = value;
    }
  }

  public MethodCommentsAttribute(string acomments) {
    comments = acomments;
  }

  public override string ToString() {
    return comments;
  }
}

Некоторые комментарии к коду

При создании атрибута в C# вам понадобится предопределенный атрибут - AttributeUsage. Он задает элементы кода к которым применим атрибут (в данном случае методы),а также некоторые свойства атрибута. Например, может ли быть атрибут примененным несколько раз к одному элементу. Метод ToString() был переопределен для простоты распечатки данного атрибута.

Теперь, после того как атрибут создан, покажем как узнать его значение во время выполнения программы. Это делается при помощи технологии называемой reflection.
using System;
using System.Reflection;

public class SampleClass {

  [MethodComments("Some method that returns a void")]
  public void SomeVoidMethod() {   }

  [MethodComments("Some method that returns an int")]
  public int SomeIntMethod() {
    return 0;
  }
}

public class AppMain {
  public static void Main() {

    Type comments = typeof(MethodCommentsAttribute);
    MethodInfo[] methods = typeof(SampleClass).GetMethods();
    object[] attributes;
    Console.WriteLine("Method Info for SampleClass");

    foreach(MethodInfo method in methods) {
       Console.WriteLine("Method Name: {0}",method.Name);
       if (method.IsDefined(comments)) {
          attributes = method.GetCustomAttributes(comments);
          foreach(object obj in attributes)
          Console.WriteLine("Method comments : {0}",obj);
       }
       else   {
          Console.WriteLine("Method comments are unavailable.");
       }
    }
  }
}


Делегаты

В платформе .NET, и соответственно в C#, реализована концепция делегатов - объектно-ориентированного подобия указателей на функции в C++. Делегаты имеют очень обширную сферу применения, в том числе и для реализации событийной модели. Делегаты - объектно-ориентированная, безопасная концепция, не нарушающая типовой защищенности. При использовании делегата для указания на статический метод - он привязывается только к этому методу, в случае же указания на нестатический метод - он привязывается к экземпляру класса и этому методу.

Объявление делегатов имеет следующий синтаксис:
[атрибуты] [модификаторы] delegate возвращаемый_тип идентифиратор ([параметры]);
где модификаторы - это либо new, либо модификаторы доступа.

Существует два типа делегатов - одиночные (Singlecast) и комбинированные (Multicast). С точки зрения синтаксиса объявления - они различаются только типом возвращаемого значения. Разница видна при просмотре откомпилированного кода с помощью IL Disassembler (ildasm). При описании одиночных делегатов создается класс, наследующий System.Delegate, а при описании комбинированных - наследующий System.MulticastDelegate.

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

Приведем пример использования одиночного делегата:
using System;

public delegate int SingleDelegate(int x);

public class SampleDelegateClass {

    int sampleMethod(int x) {
        //некоторый код
        Console.WriteLine(x);
        return 0;
    }

    public static void Main(String[] args)
    {
        // Создаем экземпляр этого класса
        SampleDelegateClass sdc = new SampleDelegateClass();

        //Создаем экземпляр делегата
        SingleDelegate sDelegate = new SingleDelegate(sdc.sampleMethod);

        Console.WriteLine(sDelegate(255));

    }
}
По своей структуре комбинированные делегаты - это связный список делегатов с возвращаемым типом - void. Вообще любой делегат, с возвращаемым типом void является комбинированным, даже если представляет связный список с одним элементом. Комбинированные делегаты наследуют System.MulticastDelegate. Комбинированные делегаты могут соединяться с помощью статического метода Combine класса System.Delegate:
public static Delegate Combine(Delegate[]);     // комбинирует два делегата
public static Delegate Combine(Delegate, Delegate); // комбинирует массив делегатов

Для удаления делегата из связного списка существует статический метод Remove класса System.Delegate:
public static Delegate Remove(Delegate source, Delegate value);
где source - связный список делегатов, value - удаляемый делегат.

Комбинирование и удаление делегата также можно осуществлять с помощью перегруженных операторов += и -= соответственно.

Приведем пример использования комбинированных делегатов:
using System;

public delegate void sampleDelegate(int x);

public class DelegateClass1 {
    public void sampleMethod(int x) {
        Console.WriteLine(x-2);
    }
}

public class DelegateClass2 {
    public void sampleMethod(int x) {
        Console.WriteLine(x+2);
    }
}

public class UseDelegates{
    public static void Main(String[] args)
    {
        DelegateClass1 dc1 = new DelegateClass1();
        DelegateClass2 dc2 = new DelegateClass2();

        //Создаем экземпляр делегата
        sampleDelegate sDelegate1 = new sampleDelegate(dc1.sampleMethod);
        sampleDelegate sDelegate2 = new sampleDelegate(dc2.sampleMethod);

        sampleDelegate sdCombination;
        sdCombination=(sampleDelegate)Delegate.Combine(sDelegate1,sDelegate2);
        sdCombination(3);
    }
}
Здесь мы комбинируем sDelegate1 и sDelegate2 в sdCombination, а затем вызываем его. Получить массив делегатов, скомбинированных в один делегат sdCombination можно с помощью метода GetInvocationList(), который возвращает массив делегатов.
Delegate[] sDelegates;
sDelegates = sdCombination.GetInvocationList();


События

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

Для объявления события в C# используются делегаты. Когда происходит событие, вызываются все методы делегатов, переданных классу клиентами. Приведем простой пример использования событий:
using System;
public delegate void SampleDelegate();

public class SampleClass{
   public event SampleDelegate SampleEvent;

   public void onEvent()
   {
      if (SampleEvent != null)
         SampleEvent();
   }
}

public class Loader{

   private static void eHandler1()
   {
      Console.WriteLine("Произошло событие: Первый обработчик");
   }

   private static void eHandler2()
   {
      Console.WriteLine("Произошло событие: Второй обработчик");
   }

   static public void Main ()   {
      SampleClass sc = new SampleClass();

      sc.SampleEvent += new SampleDelegate(eHandler1);
      sc.SampleEvent += new SampleDelegate(eHandler2);

      sc.onEvent();
   }
}

С событиями связано несколько особенностей. События могут использоваться только в том классе, в котором они описаны и не могут напрямую использоваться в порожденных классах. События отличаются от переменных-членов тем, что они могут быть помещены в интерфейс. Приведем определения события в интерфейсе:
using System;
public delegate void SampleDelegate();

public interface I {
   event SampleDelegate SampleEvent;
   void onEvent();
}

public class SampleClass: I
{
   public event SampleDelegate SampleEvent;

   public void onEvent()
   {
      if (SampleEvent != null)
         SampleEvent();
   }
}

public class Loader{

   private static void eHandler1() {
      Console.WriteLine("Произошло событие: Первый обработчик");
   }

   private static void eHandler2() {
      Console.WriteLine("Произошло событие: Второй обработчик");
   }

   static public void Main () {
      I i = new SampleClass();

      i.SampleEvent += new SampleDelegate(eHandler1);
      i.SampleEvent += new SampleDelegate(eHandler2);
      i.onEvent();
   }
}

Платформа .NET определяет некоторые правила использования событий, и во время написания компонентов, которые будут использоваться совместно с .NET компонентами, их необходимо придерживаться: Делегаты, используемые с событиями должны принимать два агрумента - объект, говорящий о том, кто вызвал событие; и некоторый экземпляр класса, наследованного от класса EventArgs. Для всех событий, которые не используют дополнительную информацию в .NET уже определен соответствующий комбинированный делегат - System.EventHandler.
public delegate void EventHandler(object sender,EventArgs e);

Приведем пример программы использующей событие нажатия на кнопку:
Команда компиляции:
csc /t:winexe /r:System.DLL;System.WinForms.DLL;Microsoft.Win32.Interop.DLL d.cs

using System;
using System.WinForms;
public class SampleForm : Form
{
    private Button button1 = new Button();
    public static void Main (String[] args)
    {
        Application.Run(new SampleForm());
    }

    public SampleForm()
    {
        this.Text = "Delegates & Events";
        button1.Text = "Event";
        button1.Click += new System.EventHandler(buttonClicked);
        this.Controls.Add(button1);
    }

    private void buttonClicked(object sender, EventArgs evArgs)
    {
        MessageBox.Show("Кнопка была нажата");
    }
}


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

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

Директива #define имеет следующий вид:
        #define имя_определения

После этой директивы определение с данным именем считается определенным. Определение можно снять с помощью директивы #undef:
        #undef имя_определения
Как мы уже говорили, с помощью определений можно контролировать исключение некоторого кода из компиляции. Делается это с помощью блока #if..#else|#elif..#endif. Общий вид такого блока следующий:
        #if выражение1
        ...
        #elif выражение2
        ...
        #elif выражениеk
        ...
        #else
        ...
        #endif

Блоков #elif и #else может и не быть. Выражение - это логическок выражение, в котором термами являются имена определений, а их истинностным значением - их определенность. Таким образом, если выражение верно, то выполняется соответствующий блок.

Еще одной группой директив являются директивы, контролирующие генерацию ошибок. Это директивы #error, #warning и #line. Их синтаксис таков:
        #error текст
        #warning текст
        #line номер_строки ["имя_файла"] | default

Если компилятор встречает директиву #error или #warning, то он генерирует ошибку или предупреждение с соответствующим текстом. Директива #line позволяет переопределить имя файла или номер строки, которые будут выводиться при ошибках. default восстанавливает оригинальную нумерацию.

Последнее, что мы разберем это регионы. С помощью директив #region..#endregion можно выделить регион в исходном тексте, Они удобны, так как редактор Visual Studio позволяет сворачивать их. Ситаксис таков:
        #region имя_региона
        #endregion


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

В C# наравне с выполненением управляемого кода есть возможность напрямую работать с указателями и голой платформой внутри небезопасных блоков. Это может понядобиться для некоторых вещей:
  • Работа с Win32-функциями, требующими работы с указателями
  • Увеличение прозиводительности

Основной возможностью является прямое обращение к памяти (конечно, настолько прямое насколько это позволено процессу), что позволяет выполнять некоторые опреации заметно быстрее. Но в большинстве случаев использование небезопасного кода неоправданы, особенно если учесть дополнительные проверки, совершаемые при выходе из такого блока. В качестве примера раскроем стандартный пример из MSDN - он использует снандартную функцию Win32 API и вынужден использовать указатели, так как эта функция получает указатель в качестве параметра:
using System;
using System.Runtime.InteropServices;
using System.Text;

class FileRead
{
   const uint GENERIC_READ = 0x80000000;
   const uint OPEN_EXISTING = 3;
   int handle;

   public FileRead(string filename)
   {
       // Эта функция открывает существущийй файл на диске для чтения
       handle = CreateFile(filename,
                           GENERIC_READ,
                           0,
                           0,
                           OPEN_EXISTING,
                           0,
                           0);
   }

   // Импортируем Win32-функцию CreateFile
   [DllImport("kernel32", SetLastError=true)]
   static extern unsafe int CreateFile(
      string filename,
      uint desiredAccess,
      uint shareMode,
      uint attributes,   // really SecurityAttributes pointer
      uint creationDisposition,
      uint flagsAndAttributes,
      uint templateFile);

   // Импортируем Win32-функцию ReadFile
   [DllImport("kernel32", SetLastError=true)]
   static extern unsafe bool ReadFile(int hFile,
      void* lpBuffer, int nBytesToRead,
      int* nBytesRead, int overlapped);

   // Импортируем Win32-функцию GetLastError
   [DllImport("kernel32", SetLastError=true)]
   static extern int GetLastError();

   // Этот метод помечен как unsafe, так как он использует указатели.
   // Можно также пометить как unsafe
   // лишь блок кода - нуно заключить его в блок unsafe {...}
   public unsafe int Read(byte[] buffer, int index, int count)
   {
      int n = 0;

      // Блок fixed запрещает передвигать переменную в памяти,
      // чтобы указатель на нее не изменился
      fixed (byte* p = buffer)
      {
         ReadFile(handle, p + index, count, &n, 0);
      }
      return n;
   }
}

class Test
{
   public static void Main(string[] args)
   {
       FileRead fr = new FileRead(args[0]);

       byte[] buffer = new byte[128];
       ASCIIEncoding e = new ASCIIEncoding();

      // Читаем весь файл блоками по 128 байт
      while (fr.Read(buffer, 0, 128) != 0)
      {
          Console.Write("{0}", e.GetString(buffer));
      }
   }
}