Задача этой книги — кратко, доступно и строго изложить основы С#, одного из самых перспективных современных языков программирования. Книга содержит описание версии С# 2.0 (2005) и предназначена для студентов, изучающих язык «с нуля», но будет полезна и опытным программистам, желающим освоить новый язык, не тратя времени на увесистые переводные фолианты.
Помимо конструкций языка в книге рассматриваются основные структуры данных, используемые при написании программ, классы библиотеки, а также рекомендации по стилю и технологии программирования. По ключевым темам приводятся задания для лабораторных работ, каждая из которых содержит до двадцати однотипных вариантов в расчете на учебную группу студентов.
Язык С# как средство обучения программированию обладает рядом несомненных достоинств. Он хорошо организован, строг, большинство его конструкций логичны и удобны. Развитые средства диагностики и редактирования кода делают процесс программирования приятным и эффективным. Мощная библиотека классов платформы .NET берет на себя массу рутинных операций, что дает возможность решать более сложные задачи, используя готовые «строительные блоки». Все это позволяет расценивать С# как перспективную замену языков Паскаль, BASIC и C++ при обучении программированию.
Немаловажно, что С# является не учебным, а профессиональным языком, предназначенным для решения широкого спектра задач, и в первую очередь — в быстро развивающейся области создания распределенных приложений. Поэтому базовый курс программирования, построенный на основе языка С#, позволит студентам быстрее стать востребованными специалистами-профессионалами.
Мощь языка С# имеет и оборотную сторону: во-первых, он достаточно требователен к ресурсам компьютера1, во-вторых, для осмысленного написания простейшей программы, вычисляющей, «сколько будет дважды два», требуется изучить достаточно много материала, но многочисленные достоинства языка и платформы .NET перевешивают все недостатки.
Дополнительным достоинством является то, что компания Microsoft распространяет версию С# Express 2005, по которой можно познакомиться с языком бесплатно (http://msdn.microsoft.com/vstudio/express/visualCsharp/), что дает нам долгожданную возможность обойтись без пиратских копий программного обеспечения и почувствовать себя законопослушными гражданами.
ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------
Название языка вообще-то следовало бы произносить как «Си-диез». Компания Microsoft неоднократно подчеркивала, что, несмотря на некоторое различие в начертании и коде символов «решетки» и «диеза», в названии языка имеется в виду именно музыкальный диез и обыгрывается тот факт, что символом С в музыке обозначается нота «до». Таким образом, создатели языка вложили в его название смысл «на полтона выше С». Однако в нашей стране укоренилась калька с английского — «Си-шарп». Поклонники этого произношения должны иметь в виду, что язык C++ они должны называть тогда исключительно «Си-плас-плас», потому что программист прежде всего обязан следовать логике!
Все возможности языка С# описаны в соответствии с его спецификацией (http:// www.ecma-international.org/publications/standards/Ecma-334.htm) и сопровождаются простыми примерами, к которым даны исчерпывающие пояснения. Большинство примеров представляют собой консольные приложения, однако в главе 14 дается введение в программирование под Windows, а в главе 15 — введение в создание веб-форм и веб-служб. Возможности, появившиеся во второй версии языка, рассматриваются не в отдельной главе, а по логике изложения материала, при этом они всегда снабжены соответствующей ремаркой для тех, кто пользуется платформой .NET предыдущих версий.
В книге приведено краткое введение в интересные и полезные темы, которые обычно не освещаются в базовом курсе С#, например, многопоточные приложения, асинхронные делегаты, асинхронный ввод-вывод и регулярные выражения. Определения синтаксических элементов языка выделены в тексте книги полужирным шрифтом. Все ключевые слова, типы, классы и большинство методов, описанных в книге, можно найти по алфавитному указателю, что позволяет использовать ее и в качестве справочника. Планируется интернет-поддержка книги на сайте http://ips.ifmo.ru в виде конспекта лекций, тестовых вопросов и проверки выполнения лабораторных работ.
Традиционного раздела «Благодарности» в этом предисловии не будет: при написании этой книги мне никто не помогал, и вся ответственность за ошибки лежит исключительно на мне. У меня нет собаки или жены, чтобы поблагодарить их, как это принято у зарубежных авторов, а мой попугай постоянно мешал мне своими громкими неорганизованными выкриками.
Ваши замечания, пожелания, дополнения, а также замеченные ошибки и опечатки не ленитесь присылать по адресу mux@tp2055.spb.edu — и тогда благодаря вам кто-то сможет получить более правильный вариант этой книги.
Ваши замечания, предложения и вопросы отправляйте также по адресу электронной почты comp@piter.com (издательство «Питер»). Мы будем рады узнать ваше мнение! Подробную информацию о наших книгах вы найдете на веб-сайте издательства www.piter.com.
Первый взгляд на платформу .NET
Программист пишет программу, компьютер ее выполняет. Программа создается на языке, понятном человеку, а компьютер умеет исполнять только программы, написанные на его языке — в машинных кодах. Совокупность средств, с помощью которых программы пишут, корректируют, преобразуют в машинные коды, отлаживают и запускают, называют средой разработки, или оболочкой.
Среда разработки обычно содержит:
□ текстовый редактор, предназначенный для ввода и корректировки текста программы;
□ компилятор, с помощью которого программа переводится с языка, на котором она написана, в машинные коды;
□ средства отладки и запуска программ;
□ общие библиотеки, содержащие многократно используемые элементы программ;
□ справочную систему и другие элементы.
Под платформой понимается нечто большее, чем среда разработки для одного языка. Платформа .NET (произносится «дотнет») включает не только среду разработки для нескольких языков программирования, называемую Visual Studio.NET, но и множество других средств, например, механизмы поддержки баз данных, электронной почты и коммерции.
В эпоху стремительного развития Интернета — глобальной информационной сети, объединяющей компьютеры разных архитектур, важнейшими задачами при создании программ становятся:
□ переносимость — возможность выполнения на различных типах компьютеров;
□ безопасность — невозможность несанкционированных действий;
□ надежность — способность выполнять необходимые функции в предопределенных условиях; средний интервал между отказами;
□ использование готовых компонентов — для ускорения разработки;
□ межъязыковое взаимодействие — возможность применять одновременно несколько языков программирования.
Платформа .NET позволяет успешно решать все эти задачи. Для обеспечения переносимости компиляторы, входящие в состав платформы, переводят программу не в машинные коды, а в промежуточный язык (Microsoft Intermediate Language, MSIL, или просто IL), который не содержит команд, зависящих от языка, операционной системы и типа компьютера. Программа на этом языке выполняется не самостоятельно, а под управлением системы, которая называется общеязыковой средой выполнения (Common Language Runtime, CLR).
Среда CLR может быть реализована для любой операционной системы. При выполнении программы CLR вызывает так называемый JIT-компилятор, переводящий код с языка IL в машинные команды конкретного процессора, которые немедленно выполняются. JIT означает «just in time», что можно перевести как «вовремя», то есть компилируются только те части программы, которые требуется выполнить в данный момент. Каждая часть программы компилируется один раз и сохраняется в кэше для дальнейшего использования. Схема выполнения программы при использовании платформы .NET приведена на рис. 1.1.
Компилятор в качестве результата своего выполнения создает так называемую сборку — файл с расширением ехе или dll, который содержит код на языке IL и метаданные. Метаданные представляют собой сведения об объектах, используемых в программе, а также сведения о самой сборке. Они позволяют организовать межъязыковое взаимодействие, обеспечивают безопасность и облегчают развертывание приложений, то есть установку программ на компьютеры пользователей.
ПРИМЕЧАНИЕ -----------------—-------------------------------------------------------------------------
Сборка может состоять из нескольких модулей. В любом случае она представляет собой программу, готовую для установки и не требующую для этого ни дополнительной информации, ни сложной последовательности действий. Каждая сборка имеет уникальное имя.
---------------------------------------------------------------------------------------------------------------------
Во время работы программы среда CLR следит за тем, чтобы выполнялись только разрешенные операции, осуществляет распределение и очистку памяти и обрабатывает возникающие ошибки. Это многократно повышает безопасность и надежность программ.
Платформа .NET содержит огромную библибтеку классов, которые можно использовать при программировании на любом языке .NET. Общая структура библиотеки приведена на рис. 1.2. Библиотека имеет несколько уровней. На самом нижнем находятся базовые классы среды, которые используются при создании любой программы: классы ввода-вывода, обработки строк, управления безопасностью, графического интерфейса пользователя, хранения данных и пр.
Над этим слоем находится набор классов, позволяющий работать с базами данных и XML (с XML вы познакомитесь в последней главе этой книги). Классы самого верхнего уровня поддерживают разработку распределенных приложений, а также веб- и Windows-приложений. Программа может использовать классы любого уровня.
Подробное изучение библиотеки классов .NET — необходимая, но и наиболее трудоемкая задача программиста при освоении этой платформы. Библиотека
классов вместе с CLR образуют каркас (framework), то есть основу платформы. Назначение остальных частей платформы мы рассмотрим по мере изучения материала.
ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------
Термин «приложение» можно для начала воспринимать как синоним слова «программа». Например, вместо фразы «программа, работающая под управлением Windows», Говорят «Windows-приложение» или просто «приложение».
------------------------------------------------------------------------------------------------------------------
Платформа .NET рассчитана на объектно-ориентированную технологию создания программ, поэтому прежде чем начинать изучение языка С#, необходимо познакомиться с основными понятиями объектно-ориентированного программирования (ООП).
Объектно-ориентированное программирование
Принципы ООП проще всего понять на примере программ моделирования. В реальном мире каждый предмет или процесс обладает набором статических и динамических характеристик, иными словами, свойствами и поведением. Поведение объекта зависит от его состояния и внешних воздействий. Например, объект «автомобиль» никуда не поедет, если в баке нет бензина, а если повернуть руль, изменится положение колес.
Понятие объекта в программе совпадает с обыденным смыслом этого слова: объект представляется как совокупность данных, характеризующих его состояние, и функций их обработки, моделирующих его поведение. Вызов функции на выполнение часто называют посылкой сообщения объекту.
При создании объектно-ориентированной программы предметная область представляется в виде совокупности объектов. Выполнение программы состоит в том, что объекты обмениваются сообщениями. Это позволяет использовать при программировании понятия, более адекватно отражающие предметную область.
При представлении реального объекта с помощью программного необходимо выделить в первом его существенные особенности. Их список зависит от цели моделирования. Например, объект «крыса» с точки зрения биолога, изучающего миграции, ветеринара или, скажем, повара будет иметь совершенно разные характеристики. Выделение существенных с той или иной точки зрения свойств называется абстрагированием. Таким образом, программный объект — это абстракция.
Важным свойством объекта является его обособленность. Детали реализации объекта, то есть внутренние структуры данных и алгоритмы их обработки, скрыты
от пользователя объекта и недоступны для непреднамеренных изменений. Объект используется через его интерфейс — совокупность правил доступа.
Скрытие деталей реализации называется инкапсуляцией (от слова «капсула»). Ничего сложного в этом понятии нет: ведь и в обычной жизни мы пользуемся объектами через их интерфейсы. Сколько информации пришлось бы держать в голове, если бы для просмотра новостей надо было знать устройство телевизора!
Таким образом, объект является «черним ящиком», замкнутым по отношению к внешнему миру. Это позволяет представить программу в укрупненном виде — на уровне объектов и их взаимосвязей, а следовательно, управлять большим объемом информации и успешно отлаживать сложные программы.
Сказанное можно сформулировать более кратко и строго: объект — это инкапсулированная абстракция с четко определенным интерфейсом.
Инкапсуляция позволяет изменить реализацию объекта без модификации основной части программы, если его интерфейс остался прежним. Простота модификации является очень важным критерием качества программы, ведь любой программный продукт в течение своего жизненного цикла претерпевает множество изменений и дополнений.
Кроме того, инкапсуляция позволяет использовать объект в другом окружении и быть уверенным, что он не испортит не принадлежащие ему области памяти, а также создавать библиотеки объектов для применения во многих программах.
Каждый год в мире пишется огромное количество новых программ, и важнейшее значение приобретает возможность многократного использования кода. Преимущество объектно-ориентированного программирования состоит в том, что для объекта можно определить наследников, корректирующих или дополняющих его поведение. При этом нет необходимости не только повторять исходный код родительского объекта, но даже иметь к нему доступ.
Наследование является мощнейшим инструментом ООП и применяется для следующих взаимосвязанных целей:
□ исключения из программы повторяющихся фрагментов кода;
□ упрощения модификации программы;
□ упрощения создания новых программ на основе существующих.
Кроме того, только благодаря наследованию появляется возможность использовать объекты, исходный код которых недоступен, но в которые требуется внести изменения.
Наследование позволяет создавать иерархии объектов. Иерархия представляется в виде дерева, в котором более общие объекты располагаются ближе к корню, а более специализированные — на ветвях и листьях. Наследование облегчает использование библиотек объектов, поскольку программист может взять за основу объекты, разработанные кем-то другим, и создать наследников с требуемыми свойствами.
Объект, на основании которого строится новый объект, называется родительским объектом, объектом-предком, базовым классом, или суперклассом, а унаследованный от него объект — потомком, подклассом, или производным классом.
ООП позволяет писать гибкие, расширяемые и читабельные программы. Во многом это обеспечивается благодаря полиморфизму, под которым понимается возможность во время выполнения программы с помощью одного и того же имени выполнять разные действия или обращаться к объектам разного типа. Чаще всего понятие полиморфизма связывают с механизмом виртуальных методов, который мы рассмотрим в главе 8.
Подведя итог сказанному, сформулирую достоинства ООП:
□ использование при программировании понятий, близких к предметной области;
□ возможность успешно управлять большими объемами исходного кода благодаря инкапсуляции, то есть скрытию деталей реализации объектов и упрощению структуры программы;
□ возможность многократного использования кода за счет наследования;
□ сравнительно простая возможность модификации программ;
□ возможность создания и использования библиотек объектов.
Эти преимущества особенно явно проявляются при разработке программ большого объема и классов программ. Однако ничто не дается даром: создание объектно-ориентированной программы представляет собой весьма непростую задачу, поскольку требует разработки иерархии объектов, а плохо спроектированная иерархия может свести к нулю все преимущества объектно-ориентированного подхода.
Кроме того, идеи ООП не просты для понимания и в особенности для практического применения. Чтобы эффективно использовать готовые объекты из библиотек, необходимо освоить большой объем достаточно сложной информации. Неграмотное же применение ООП способно привести к созданию излишне сложных программ, которые невозможно отлаживать и усовершенствовать.
Для представления объектов в языках С#, Java, C++, Delphi и т. п. используется понятие класс, аналогичное обыденному смыслу этого слова в контексте «класс членистоногих», «класс млекопитающих», «класс задач» и т. п. Класс является обобщенным понятием, определяющим характеристики и поведение некоторого множества конкретных объектов этого класса, называемых экземплярами класса. «Классический» класс содержит данные, задающие свойства объектов класса, и функции, определяющие их поведение. В последнее время в класс часто добавляется третья составляющая — события, на которые может реагировать объект класса. Все классы библиотеки .NET, а также все классы, которые создает программист в среде .NET, имеют одного общего предка — класс object и организованы в единую иерархическую структуру. Внутри нее классы логически сгруппированы в так называемые пространства имен, которые служат для упорядочивания имен классов и предотвращения конфликтов имен: в разных пространствах имена могут совпадать. Пространства имен могут быть вложенными, их идея аналогична знакомой вам иерархической структуре каталогов на компьютере.
Любая программа, создаваемая в NET, использует пространство имен System. В нем определены классы, которые обеспечивают базовую функциональность, например, поддерживают выполнение математических операций, управление памятью и ввод-вывод.
Обычно в одно пространство имен объединяют взаимосвязанные классы. Например, пространство System. Net содержит классы, относящиеся к передаче данных по сети, System.Windows.Forms — элементы графического интерфейса пользователя, такие как формы, кнопки и т. д. Имя каждого пространства имен представляет собой неделимую сущность, однозначно его определяющую.
Последнее, о чем необходимо поговорить, прежде чем начать последовательное изучение языка С#, — среда разработки Visual Studio.NET.
Среда Visual Studio.NET
Среда разработки Visual Studio.NET предоставляет мощные и удобные средства написания, корректировки, компиляции, отладки и запуска приложений, использующих NET-совместимые языки. Корпорация Microsoft включила в платформу средства разработки для четырех языков: С#, VB.NET, C++ и J#.
Платформа .NET является открытой средой. Это значит, что компиляторы для нее могут поставляться и сторонними разработчиками. К настоящему времени разработаны десятки компиляторов для .NET, например, Ada, COBOL, Delphi, Eiffel, Fortran, Lisp, Oberon, Perl и Python.
Bee .NET-совместимые языки должны отвечать требованиям общеязыковой спецификации (Common Language Specification, CLS), в которой описывается набор общих для всех языков характеристик. Это позволяет использовать для разработки приложения несколько языков программирования и вести полноценную межъязыковую отладку. Все программы независимо от языка используют одни и те же базовые классы библиотеки NET.
Приложение в процессе разработки называется проектом. Проект объединяет все необходимое для создания приложения: файлы, папки, ссылки и прочие ресурсы. Среда Visual Studio.NET позволяет создавать проекты различных типов, например:
□ Windows-приложение использует элементы интерфейса Windows, включая формы, кнопки, флажки и пр.;
□ консольное приложение выполняет вывод «на консоль», то есть в окно командного процессора;
□ библиотека классов объединяет классы, которые предназначены для использования в других приложениях;
□ веб-приложение — это приложение, доступ к которому выполняется через браузер (например, Internet Explorer) и которое по запросу формирует веб-страницу и отправляет ее клиенту по сети;
□ веб-сервис — компонент, методы которого могут вызываться через Интернет.
Несколько проектов можно объединить в решение (solution). Это облегчает совместную разработку проектов.
Консольные приложения
Среда Visual Studio.NET работает на платформе Windows и ориентирована на создание Windows- и веб-приложений, однако разработчики предусмотрели работу и с консольными приложениями. При запуске консольного приложения операционная система создает так называемое консольное окно, через которое идет весь ввод-вывод программы. Внешне это напоминает работу в операционной системе в режиме командной строки, когда ввод-вывод представляет собой поток символов.
Консольные приложения наилучшим образом подходят для изучения языка, так как в них не используется множество стандартных объектов, необходимых для создания графического интерфейса. В первой части курса мы будем создавать только консольные приложения, чтобы сосредоточить внимание на базовых свойствах языка С#. В следующем разделе рассмотрены самые простые действия в среде: создание и запуск на выполнение консольного приложения на С#. Более полные сведения, необходимые для работы в Visual Studio.NET, можно получить из документации или книг [8], [16].
ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------
Большинство примеров, приведенных в книге, иллюстрируют базовые возможности С# и разрабатывались в интегрированной среде версии 7.1 (библиотека NET Framework 1.1), однако вы можете работать и в более новых версиях. Программы, которые используют новые средства языка, появившиеся в спецификации версии 2.0, проверялись в Visual C# 2005 Express Edition (библиотека .NET Framework 2.0).
------------------------------------------------------------------------------------------------------------------
Создание проекта. Основные окна среды
Для создания проекта следует после запуска Visual Studio.NET в главном меню выбрать команду File ► New ► Project.... В левой части открывшегося диалогового окна нужно выбрать пункт Visual C# Projects, в правой — пункт Console Application. В поле Name можно ввести имя проекта, а в поле Location — место его сохранения на диске, если заданные по умолчанию значения вас не устраивают. После щелчка на кнопке ОК среда создаст решение и проект с указанным именем. Примерный вид экрана приведен на рис. 1.3.
В верхней части экрана располагается главное меню (с разделами File, Edit, View и т. д.) и панели инструментов (toolbars). Панелей инструментов в среде великое множество, и если включить их все (View ► Toolbars...), они займут половину экрана.
В верхней левой части экрана располагается окно управления проектом Solution Explorer (если оно не отображается, следует воспользоваться командой View ► Solution Explorer главного меню). В окне перечислены все ресурсы, входящие в проект: ссылки на библиотеку (System, System.Data, System.XML), файл ярлыка (App.ico), файл с исходным текстом класса (Class1.cs) и информация о сборке (Assemblylnfo.cs).
В этом же окне можно увидеть и другую информацию, если перейти на вкладку Class View, ярлычок которой находится в нижней части окна. На вкладке Class View представлен список всех классов, входящих в приложение, их элементов и предков.
ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------
Небезынтересно полюбопытствовать, какие файлы создала среда для поддержки проекта. С помощью проводника Windows можно увидеть, что на заданном диске появилась папка с указанным именем, содержащая несколько других файлов и вложенных папок. Среди них — файл проекта (с расширением csproj), файл решения , (с расширением sin) и файл с кодом класса (Class 1 .cs).
----------------------------------------------------------------------------------------------------------------
В нижней левой части экрана расположено окно свойств Properties (если окна не видно, воспользуйтесь командой View ► Properties главного меню). В окне свойств
отображаются важнейшие характеристики выделенного элемента. Например, чтобы изменить имя файла, в котором хранится класс Classl, надо выделить этот файл в окне управления проектом и задать в окне свойств новое значение свойству FileName (ввод заканчивается нажатием клавиши Enter). Основное пространство экрана занимает окно редактора, в котором располагается текст программы, созданный средой автоматически. Текст представляет собой каркас, в который программист добавляет код по мере необходимости. Ключевые (зарезервированные) слова1 отображаются синим цветом, комментарии различных типов — серым и темно-зеленым, остальной текст — черным. Слева от текста находятся символы структуры: щелкнув на любом квадратике с минусом, можно скрыть соответствующий блок кода. При этом минус превращается в плюс, щелкнув на котором, можно опять вывести блок на экран. Это средство хорошо визуально структурирует код и позволяет сфокусировать внимание на нужных фрагментах.
Заготовка консольной программы
Рассмотрим каждую строку заготовки программы (листинг 1.1). Не пытайтесь сразу понять абсолютно все, что в ней написано. Пока что ваша цель — изучить принципы работы в оболочке, а досконально разбираться в программе мы будем позже.
Листинг 1.1. Заготовка консольной программы
using System;
namespace ConsoleApplicationl
{
/// <summary>
/// Summary description for Classl.
/// </summary> class Classl
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
//
// TODO: Add code to start application here
//
}
}
}
Директива using System разрешает использовать имена стандартных классов из пространства имен System непосредственно (без указания имени пространства).
Ключевое слово namespace создает для проекта собственное пространство имен, названное по умолчанию ConsoleApplicationl. Это сделано для того, чтобы можно было давать программным объектам имена, не заботясь о том, что они могут совпасть с именами в других пространствах имен.
Строки, начинающиеся с двух или трех косых черт, являются комментариями и предназначены для документирования текста программы.
С# — объектно-ориентированный язык, поэтому написанная на нем программа представляет собой совокупность взаимодействующих между собой классов. В нашей заготовке программы всего один класс, которому по умолчанию задано имя Classl. Описание класса начинается с ключевого слова class, за которым следуют его имя и далее в фигурных скобках — список элементов класса (его данных и функций, называемых также методами).
ВНИМАНИЕ -------------------------------------------------------------------------------------------------
Фигурные скобки являются важным элементом синтаксиса. Каждой открывающей скобке соответствует своя закрывающая, которая обычно располагается ниже по тексту с тем же отступом. Эти скобки ограничивают блок, внутри которого могут располагаться другие блоки, вложенные в него, как матрешки. Блок может применяться в любом месте, где допускается отдельный оператор.
---------------------------------------------------------------------------------------------------------------------
В данном случае внутри класса только один элемент — метод Main. Каждое приложение должно содержать метод Main — с него начинается выполнение программы. Все методы описываются по единым правилам.
Упрощенный синтаксис метода:
[ спецификаторы ] тип имя_метода ( [ параметры ] )
{
тело метода: действия, выполняемые методом
}
ВНИМАНИЕ--------------------------------------------------------------------------------------------------
Для описания языка программирования в документации часто используется некоторый формальный метаязык, например, формулы Бэкуса—Наура или синтаксические диаграммы. Для наглядности и простоты изложения в этой книге используется широко распространенный неформальный способ описания, когда необязательные части синтаксических конструкций заключаются в квадратные скобки, текст, который необходимо заменить конкретным значением, пишется по-русски, а возможность выбора одного из нескольких элементов обозначается вертикальной чертой. Например:
[ void | int ] имя_метода();
Эта запись означает, что вместо конструкции имя метода необходимо указать конкретное имя в соответствии с правилами языка, а перед ним может находиться либо слово void, либо слово int, либо ничего. Символ подчеркивания используется для связи слов вместо пробела, показывая, что на этом месте должен стоять один синтаксический элемент, а не два. В тех случаях, когда квадратные скобки являются элементом синтаксиса, это оговаривается особо.
------------------------------------------------------------------------------------------------------------------
Таким образом, любой метод должен иметь тип, имя и тело, остальные части описания являются необязательными, поэтому мы их пока проигнорируем. Методы подробно рассматриваются в разделе «Методы» (см. с. 106).
ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------Наряду с понятием «метод» часто используется другое — функция-член класса. Метод является частным случаем функции — законченного фрагмента кода, который можно вызвать по имени. Далее в книге используются оба эти понятия.
--------------------------------------------------------------------------------------------------------------------
Среда заботливо поместила внутрь метода Main комментарий:
// TODO: Add code to start application here
Это означает: «Добавьте сюда код, выполняемый при запуске приложения». Последуем совету и добавим после строк комментария (но не в той же строке!) строку
Console.WriteLineC"Ур-ра! Зар-работало!(с)Кот Матроскин");
Здесь Console — это имя стандартного класса из пространства имен System. Его метод WriteLine выводит на экран заданный в кавычках текст. Как видите, для обращения к методу класса используется конструкция
имя_класса.имя_метода
Если вы не сделали ошибку в первом же слове, то сразу после ввода с клавиатуры следом за словом Console символа точки среда выведет подсказку, содержащую список всех доступных элементов класса Console. Выбор нужного имени выполняется либо мышью, либо клавишами управления курсором, либо вводом одного или нескольких начальных символов имени. При нажатии клавиши Enter выбранное имя появляется в тексте программы.
СОВЕТ --------------------------------------------------------------------------------------------------------
Не пренебрегайте возможностью автоматического ввода — это убережет вас от опечаток и сэкономит время. Если подсказка не появляется, это свидетельствует об ошибке в имени или в месте расположения в программе вводимого текста.
------------------------------------------------------------------------------------------------------------------
Программа должна приобрести вид, приведенный в листинге 1.2 (для того чтобы вы могли сосредоточиться на структуре программы, из нее убраны все комментарии и другие пока лишние для нас детали). Ничего ужасного в этом листинге нет, не правда ли?
Обратите внимание на то, что после внесения изменений около имени файла на ярлычке в верхней части окна редактора появился символ * — это означает, что текст, сохраненный на диске, и текст, представленный в окне редактора, не совпадают. Для сохранения файла воспользуйтесь командой File ► Save главного меню или кнопкой Save на панели инструментов (текстовый курсор должен при этом находиться в окне редактора). Впрочем, при запуске программы среда сохранит исходный текст самостоятельно.
На рис. 1.4 приведен экран после создания консольного приложения в Visual С# 2005 Express Edition. Как видите, текст заготовки приложения более лаконичен, чем в предыдущей версии, и практически совпадает с листингом 1.1 без учета комментариев.
Листинг 1.2. Первая программа на С#
using System;
namespace ConsoleApplicationl
{
class Classl
{
static void Main()
{
Console.WriteLine( "Ур-ра! Зар-работало! (с) Кот Матроскин"
}
}
Запуск программы
Самый простой способ запустить программу — нажать клавишу F5 (или выбрать в меню команду Debug ► Start). Если программа написана без ошибок, то фраза Ур-ра! Зар-работало! (с) Кот Матроскин промелькнет перед вашими глазами в консольном окне, которое незамедлительно закроется. Это хороший результат, но для того чтобы пронаблюдать его спокойно, следует воспользоваться клавишами Ctrl+F5 (или выбрать в меню команду Debug ► Start Without Debugging).
После внесения изменений компилятор может обнаружить в тексте программы синтаксические ошибки. Он сообщает об этом в окне, расположенном в нижней части экрана.
Давайте внесем в программу синтаксическую ошибку, чтобы впервые увидеть то, что впоследствии вам придется наблюдать многие тысячи раз. Уберите точку с запятой после оператора Console.WriteLine(...) и запустите программу заново. Вы увидите диалоговое окно с сообщением о том, что при построении приложения обнаружены ошибки, и вопросом, продолжать ли дальше (There were build errors. Continue?). Сурово ответив No, обратимся к окну вывода ошибок за разъяснениями.
ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------
Если сообщения об ошибке не видно, просмотрите содержимое окна с помощью полосы прокрутки, пытаясь найти в нем слово «error».
---------------------------------------------------------------------------------------------------------------------
Двойной щелчок на строке с сообщением об ошибке error CS1002 ; expected (означающее, что здесь ожидалась точка с запятой) подсвечивает неверную строку в программе. Для получения дополнительных пояснений можно нажать клавишу F1, при этом в окне редактора появится новая вкладка с соответствующей страницей из справочной системы. Для возвращения к коду класса следует щелкнуть на ярлычке с именем файла в верхней строке редактора.
Другой способ получения справки — использовать окно Dynamic Help, расположенное на том же месте, что и окно свойств Properties (его ярлычок находится в нижней строке окна). Содержимое этого окна динамически меняется в зависимости от того, какой элемент выделен. Для получения справки надо щелкнуть на соответствующей ссылке, и страница справочной системы будет отображена на отдельной вкладке окна редактора (в Visual C# 2005 Express Edition справочная информация появляется в отдельном окне).
Теперь, когда вы получили общее представление о платформе .NET, можно, наконец, приступить к планомерному изучению языка С#.
Рекомендации по программированию
В этой главе дается очень краткое введение в интересную и обширную тему — платформу .NET. Для более глубокого понимания механизмов ее функционирования настоятельно рекомендуется изучить дополнительную литературу (например, [5], [19], [26], [27]) и публикации в Интернете.
Среда разработки Visual Studio.NET предоставляет программисту мощные и удобные средства написания, корректировки, компиляции, отладки и запуска приложений, описанию которых посвящены целые книги. В процессе изучения языка С# желательно постепенно изучать эти возможности, ведь чем лучше вы будете владеть инструментом, тем эффективнее и приятнее будет процесс программирования.
Основные понятия языка
В этой главе рассматриваются элементарные «строительные блоки» языка С#, вводится понятие типа данных и приводится несколько классификаций типов.
Язык программирования можно уподобить очень примитивному иностранному языку с жесткими правилами без исключений. Изучение иностранного языка обычно начинают с алфавита, затем переходят к словам и законам построения фраз, и только в результате длительной практики и накопления словарного запаса появляется возможность свободно выражать на этом языке свои мысли. Примерно так же поступим и мы при изучении языка С#.
Алфавит и лексемы
Все тексты на языке пишутся с помощью его алфавита. Например, в русском языке один алфавит (набор символов), а в албанском — другой. В С# используется кодировка символов Unicode. Давайте разберемся, что это такое. Компьютер умеет работать только с числами, и для того чтобы можно было хранить в его памяти текст, требуется определить, каким числом будет представляться (кодироваться) каждый символ. Соответствие между символами и кодирующими их числами называется кодировкой, или кодовой таблицей (character set). Существует множество различных кодировок символов. Например, в Windows часто используется кодировка ANSI, а конкретно — СР1251. Каждый символ представляется в ней одним байтом (8 бит), поэтому в этой кодировке можно одновременно задать только 256 символов. В первой половине кодовой таблицы находятся латинские буквы, цифры, знаки арифметических операций и другие распространенные символы. Вторую половину занимают символы русского алфавита. Если требуется представлять символы другого национального алфавита (например, албанского), необходимо использовать другую кодовую таблицу.
Кодировка Unicode позволяет представить символы всех существующих алфавитов одновременно, что коренным образом улучшает переносимость текстов. Каждому символу соответствует свой уникальный код. Естественно, что при этом для хранения каждого символа требуется больше памяти. Первые 128 Unicode-символов в соответствуют первой части кодовой таблицы ANSI.
Алфавит С# включает:
□ буквы (латинские и национальных алфавитов) и символ подчеркивания (_), который употребляется наряду с буквами;
□ цифры;
□ специальные символы, например +, *, { и &;
□ пробельные символы (пробел и символы табуляции);
□ символы перевода строки.
Из символов составляются более крупные строительные блоки: лексемы, директивы препроцессора и комментарии.
Лексема (token) —это минимальная единица языка, имеющая самостоятельный смысл. Существуют следующие виды лексем:
□ имена (идентификаторы);
□ ключевые слова;
□ знаки операций;
□ разделители;
□ литералы (константы).
Лексемы языка программирования аналогичны словам естественного языка. Например, лексемами являются число 128 (но не его часть 12), имя Vasia, ключевое слово goto и знак операции сложения +. Далее мы рассмотрим лексемы подробнее.
Директивы препроцессора пришли в С# из его предшественника — языка C++. Препроцессором называется предварительная стадия компиляции, на которой формируется окончательный вид исходного текста программы. Например, с помощью директив (инструкций, команд) препроцессора можно включить или выключить из процесса компиляции фрагменты кода. Директивы препроцессора не играют в С# такой важной роли, как в C++. Мы рассмотрим их в свое время—в разделе «Директивы препроцессора» (см. с. 287).
Комментарии предназначены для записи пояснений к программе и формирования документации. Правила записи комментариев описаны далее в этом разделе.
Из лексем составляются выражения и операторы. Выражение задает правило вычисления некоторого значения. Например, выражение а + b задает правило вычисления суммы двух величин.
ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------
Компилятор выполняет перевод программы в исполняемый файл на языке IL за две последовательные фазы: лексического и синтаксического анализа. При лексическом анализе из потока символов, составляющих исходный текст программы, выделяются лексемы (по другим лексемам, разделителям, пробельным символам и переводам строки). На этапе синтаксического анализа программа переводится в исполняемый код. Кроме того, компилятор формирует сообщения о синтаксических ошибках.
---------------------------------------------------------------------------------------------------------------------
Оператор задает законченное описание некоторого действия, данных или элемента программы. Например:
int а;
Это — оператор описания целочисленной переменной а;
Идентификаторы
Имена в программах служат той же цели, что и имена в мире людей, — чтобы обращаться к программным объектам и различать их, то есть идентифицировать. Поэтому имена также называют идентификаторами. В идентификаторе могут использоваться буквы, цифры и символ подчеркивания. Прописные и строчные буквы различаются, например, sysop, SySoP и SYSOP — три разных имени.
Первым символом идентификатора может быть буква или знак подчеркивания, но не цифра. Длина идентификатора не ограничена. Пробелы внутри имен не допускаются.
В идентификаторах С# разрешается использовать помимо латинских букв буквы национальных алфавитов. Например, Пёсик или çç являются правильными идентификаторами. Более того, в идентификаторах можно применять даже так называемые escape-последовательности Unicode, то есть представлять символ с помощью его кода в шестнадцатеричном виде с префиксом \и, например, \u00F2.
ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------
Примеры неправильных имен: 21 ate, Big gig, Б#г; первое начинается с цифры, второе и третье содержат недопустимые символы (пробел и #).
---------------------------------------------------------------------------------------------------------------------
Имена даются элементам программы, к которым требуется обращаться: переменным, типам, константам, методам, меткам и т. д. Идентификатор создается на этапе объявления переменной (метода, типа и т. п.), после этого его можно использовать в последующих операторах программы. При выборе идентификатора необходимо иметь в виду следующее:
□ идентификатор не должен совпадать с ключевыми словами (см. следующий раздел);
□ не рекомендуется начинать идентификаторы с двух символов подчеркивания,
поскольку такие имена зарезервированы для служебного использования. Для улучшения читабельности программы следует давать объектам осмысленные имена, составленные в соответствии с определенными правилами. Понятные и согласованные между собой имена — основа хорошего стиля программирования. Существует несколько видов так называемых нотаций — соглашений о правилах cоздания имен.
В нотации Паскаля каждое слово, составляющее идентификатор, начинается с прописной буквы, например, MaxLength, MyFuzzyShooshpanchik. Венгерская нотация (ее предложил венгр по национальности, сотрудник компании Microsoft) отличается от предыдущей наличием префикса, соответствующего типу величины, например, MaxLength, TpfnMyFuzzyShooshpanchik. Согласно нотации Camel, с прописной буквы начинается каждое слово, составляющее идентификатор, кроме первого, например, maxLength, myFuzzyShooshpanchik. Человеку с богатой фантазией абрис имени может напоминать верблюда, откуда и произошло название этой нотации.
Еще одна традиция — разделять слова, составляющие имя, знаками подчеркивания: max_length, my_fuzzy_shooshpanchik, при этом все составные части начинаются со строчной буквы.
В С# для именования различных видов программных объектов чаще всего используются две нотации: Паскаля и Camel. Многобуквенные идентификаторы в примерах этой книги соответствуют рекомендациям, приведенным в спецификации языка. Кроме того, в примерах для краткости часто используются одно-буквенные имена. В реальных программах такие имена можно применять только в ограниченном наборе случаев.
Ключевые слова
Ключевые слова — это зарезервированные идентификаторы, которые имеют специальное значение для компилятора. Их можно использовать только в том смысле, в котором они определены. Список ключевых слов С# приведен в табл. 2.1.
Знаки операций и разделители
Знак операции — это один или более символов, определяющих действие над операндами. Внутри знака операции пробелы не допускаются. Например, в выражении а += b знак += является знаком операции, а а и b — операндами. Символы, составляющие знак операций, могут быть как специальными, например, &&, | и <, так и буквенными, такими как as или new.
Операции делятся на унарные, бинарные и тернарную по количеству участвующих в них операндов. Один и тот же знак может интерпретироваться по-разному в зависимости от контекста. Все знаки операций, за исключением [ ], ( ).и ? :, представляют собой отдельные лексемы.
ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------
Знаки операций С# описаны в разделе «Операции и выражения» (см. с. 42). Большинство стандартных операций может быть переопределено (перегружено). Перегрузка операций рассматривается в разделе «Операции класса» (см. с. 161).
--------------------------------------------------------------------------------------------------------------------
Разделители используются для разделения или, наоборот, группирования элементов. Примеры разделителей: скобки, точка, запятая. Ниже перечислены все знаки операций и разделители, использующиеся в С#:
{} [] (). . :: + - * / * % & | ˆ ! ˜ =
< > ? ++ -- && || « » == != <=>= += - = .*=
/= %=
&= |= ˆ= <<= >>= ->
Литералы
Литералами, или константами, называют неизменяемые величины. В С# есть логические, целые, вещественные, символьные и строковые константы, а также константа null. Компилятор, выделив константу в качестве лексемы, относит ее к одному из типов данных по ее внешнему виду. Программист может задать тип константы и самостоятельно.
Описание и примеры констант каждого типа приведены в табл. 2.2. Примеры, иллюстрирующие наиболее часто употребляемые формы констант, выделены полужирным шрифтом (при первом чтении можно обратить внимание только на них).
Рассмотрим табл. 2.2 более подробно. Логических литералов всего два. Они широко используются в качестве признаков наличия или отсутствия чего-либо. Целые литералы могут быть представлены либо в десятичной, либо в шестнадцатеричной системе счисления, а вещественные — только в десятичной системе, но в двух формах: с фиксированной точкой и с порядком. Вещественная константа с порядком представляется в виде мантиссы и порядка. Мантисса записывается слева от знака экспоненты (Е или е), порядок — справа от знака. Значение константы определяется как произведение мантиссы и возведенного в указанную в порядке степень числа 10 (например, 1.3е2 = 1,3 • 102 = 130). При записи вещественного числа могут быть опущены либо целая часть, либо дробная, но, конечно, не обе сразу.
ВНИМАНИЕ -------------------------------------------------------------------------------------------------
Пробелы внутри числа не допускаются. Для отделения целой части от дробной используется не запятая, а точка. Символ Е не представляет собой знакомое всем из математики число е, а указывает, что далее располагается степень, в которую нужно возвести число 10.
_____________________________________________________________________________
Если требуется сформировать отрицательную целую или вещественную константу, то перед ней ставится знак унарной операции изменения знака (-), например:
-218, -022, -0х3С, -4.8, -0.1е4.
Когда компилятор распознает константу, он отводит ей место в памяти в соответствии с ее видом и значением. Если по каким-либо причинам требуется явным образом задать, сколько памяти следует отвести под константу, используются суффиксы, описания которых приведены в табл. 2.3. Поскольку такая необходимость возникает нечасто, эту информацию можно при первом чтении пропустить.
Допустимые диапазоны значений целых и вещественных констант в зависимости от префикса мы рассмотрим немного позже в этой главе. Символьная константа представляет собой любой символ в кодировке Unicode. Символьные константы записываются в одной из четырех форм, представленных в табл. 2.2:
□ «обычный» символ, имеющий графическое представление (кроме апострофа и символа перевода строки), — 'А','ю','*';
□ управляющая последовательность — '\0', '\n';
□ символ в виде шестнадцатеричного кода — '\xF', ' \х74';
□ символ в виде escape-последовательности Unicode — '\uA81B'.
Управляющей последовательностью, или простой escape-последовательностью, называют определенный символ, предваряемый обратной косой чертой. Управляющая последовательность интерпретируется как одиночный символ и используется для представления:
□ кодов, не имеющих графического изображения (например, \n — переход в начало следующей строки);
□ символов, имеющих специальное значение в строковых и символьных литералах, например, апострофа .
В табл. 2.4 приведены допустимые значения последовательностей. Если непосредственно за обратной косой чертой следует символ, не предусмотренный таблицей, возникает ошибка компиляции.
Символ, представленный в виде шестнадцатеричного кода, начинается с префикса \0х, за которым следует код символа. Числовое значение должно находиться в диапазоне от 0 до 216 - 1, иначе возникает ошибка компиляции. Escape-последовательности Unicode служат для представления символа в кодировке Unicode с помощью его кода в шестнадцатеричном виде с префиксом \и или \U, например, \u00F2, \U00010011. Коды в диапазоне от \U10000 до \U1OFFFF представляются в виде двух последовательных символов; коды, превышающие \U10FFFF, не поддерживаются.
Управляющие последовательности обоих видов могут использоваться и в строковых константах, называемых иначе строковыми литералами. Например, если требуется вывести несколько строк, можно объединить их в один литерал, отделив одну строку от другой символами \n:
"Никто не доволен своей\пвнешностью, но каждый доволен\псвоим умом"
Этот литерал при выводе будет выглядеть так:
Никто не доволен своей внешностью, но каждый доволен своим умом
Другой пример: если внутри строки требуется использовать кавычку, ее предваряют косой чертой, по которой компилятор отличает ее от кавычки, ограничивающей строку:
"Издательский дом \"Питер\""
Как видите, строковые литералы с управляющими символами несколько теряют в читабельности, поэтому в С# введен второй вид литералов — дословные литералы (verbatim strings). Эти литералы предваряются символом @, который отключает обработку управляющих последовательностей и позволяет получать строки в том виде, в котором они записаны. Например, два приведенных выше литерала в дословном виде выглядят так:
@"Никто не доволен своей
внешностью, но каждый
доволен своим умом"
@"Издательский дом "Питер""
Чаще всего дословные литералы применяются в регулярных выражениях и при задании полного пути файла, поскольку в нем присутствуют символы обратной косой черты, которые в обычном литерале пришлось бы представлять с помощью управляющей последовательности. Сравните два варианта записи одного и того же пути:
"С: \\app\\bin\\debugWa.exe"
@"С:\app\bin\debug\a.ехе"
Строка может быть пустой (записывается парой смежных двойных кавычек ""), пустая символьная константа недопустима.
Константа null представляет собой значение, задаваемое по умолчанию для величин так называемых ссылочных типов, которые мы рассмотрим далее в этой главе.
Комментарии
Комментарии предназначены для записи пояснений к программе и формирования документации. Компилятор комментарии игнорирует. Внутри комментария можно использовать любые символы. В С# есть два вида комментариев: однострочные и многострочные.
Однострочный комментарий начинается с двух символов прямой косой черты (//) и заканчивается символом перехода на новую строку, многострочный заключается между символами-скобками /* и */ и может занимать часть строки, целую строку или несколько строк. Комментарии не вкладываются друг в друга: символы // и /* не обладают никаким специальным значением внутри комментария.
Кроме того, в языке есть еще одна разновидность комментариев, которые начинаются с трех подряд идущих символов косой черты (///). Они предназначены для формирования документации к программе в формате XML. Компилятор извлекает эти комментарии из программы, проверяет их соответствие правилам и записывает их в отдельный файл. Правила задания комментариев этого вида мы рассмотрим в главе 15.
Данные, с которыми работает программа, хранятся в оперативной памяти. Естественно, что компилятору необходимо точно знать, сколько места они занимают, как именно закодированы и какие действия с ними можно выполнять. Все это задается при описании данных с помощью типа. Тип данных однозначно определяет:
□ внутреннее представление данных, а следовательно, и множество их возможных значений;
□ допустимые действия над данными (операции и функции).
Например, целые и вещественные числа, даже если они занимают одинаковый объем памяти, имеют совершенно разные диапазоны возможных значений; целые числа можно умножать друг на друга, а, например, символы — нельзя. Каждое выражение в программе имеет определенный тип. Величин, не имеющих никакого типа, не существует. Компилятор использует информацию о типе при проверке допустимости описанных в программе действий. Память, в которой хранятся данные во время выполнения программы, делится на две области: стек (stack) и динамическая область, или хип (heap). Стек используется для хранения величин, память под которые выделяет компилятор, а в динамической области память резервируется и освобождается во время выполнения программы с помощью специальных команд. Основным местом для хранения данных в С# является хип.
Классификация типов
Любая информация легче усваивается, если она «разложена по полочкам». Поэтому, прежде чем перейти к изучению конкретных типов языка С#, рассмотрим их классификацию. Типы можно классифицировать по разным признакам. Если принять за основу строение элемента, все типы можно разделить на простые (не имеют внутренней структуры) и структурированные (состоят из элементов других типов). По своему «создателю» типы можно разделить на встроенные
(стандартные) и определяемые программистом (рис. 2.1). Для данных статического типа память выделяется в момент объявления, при этом ее требуемый объем известен. Для данных динамического типа размер данных в момент объявления может быть неизвестен, и память под них выделяется по запросу в процессе выполнения программы.
Встроенные типы
Встроенные типы не требуют предварительного определения. Для каждого типа существует ключевое слово, которое используется при описании переменных, констант и т. д. Если же программист определяет собственный тип данных, он описывает его характеристики и сам дает ему имя, которое затем применяется точно так же, как имена стандартных типов. Описание собственного типа данных должно включать всю информацию, необходимую для его использования, а именно внутреннее представление и допустимые действия.
Встроенные типы С# приведены в табл. 2.5. Они однозначно соответствуют стандартным классам библиотеки .NET, определенным в пространстве имен System. Как видно из таблицы, существуют несколько вариантов представления целых и вещественных величин. Программист выбирает тип каждой величины, используемой в программе, с учетом необходимого ему диапазона и точности представления данных.
Целые типы, а также символьный, вещественные и финансовый типы можно объединить под названием арифметических типов.
Внутреннее представление величины целого типа — целое число в двоичном коде. В знаковых типах старший бит числа интерпретируется как знаковый (0 — положительное число, 1 — отрицательное). Отрицательные числа чаще всего представляются в так называемом дополнительном коде. Для преобразования числа в дополнительный код все разряды числа, за исключением знакового, инвертируются, затем к числу прибавляется единица, и знаковому биту тоже присваивается единица. Беззнаковые типы позволяют представлять только положительные числа, поскольку старший разряд рассматривается как часть кода числа.
ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------
Если под величину отведено и двоичных разрядов, то в ней можно представить 2n различных сочетаний нулей и единиц. Если старший бит отведен под знак, то диапазон возможных значений величины — [-2n-1, 2n-1 - 1], а если все разряды используются для представления значения, диапазон смещается в область положительных чисел и равен [0, 2n - 1] (см. табл. 2.5).
---------------------------------------------------------------------------------------------------------------------
Вещественные типы, или типы данных с плавающей точкой, хранятся в памяти компьютера иначе, чем целочисленные. Внутреннее представление вещественного числа состоит из двух частей — мантиссы и порядка, каждая часть имеет знак. Длина мантиссы определяет точность числа, а длина порядка — его диапазон. В первом приближении это можно представить себе так: например, для числа 0,381 • 104
хранятся цифры мантиссы 381 и порядок 4, для числа 560,3 • 102 — мантисса 5603 и порядок 5 (мантисса нормализуется), а число 0,012 представлено как 12 и 1. Конечно, в этом примере не учтены система счисления и другие особенности.
Все вещественные типы могут представлять как положительные, так и отрицательные числа. Чаще всего в программах используется тип double, поскольку его диапазон и точность покрывают большинство потребностей. Этот тип имеют вещественные литералы и многие стандартные математические функции.
ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------
Обратите внимание на то, что при одинаковом количестве байтов, отводимых под величины типа float и int, диапазоны их допустимых значений сильно различаются из-за внутренней формы представления. То же самое относится к Long и double.
________________________________________________________________
Тип decimal предназначен для денежных вычислений, в которых критичны ошибки округления. Как видно из табл. 2.5, тип float позволяет хранить одновременно всего 7 значащих десятичных цифр, тип double — 15-16. При вычислениях ошибки округления накапливаются, и при определенном сочетании значений это даже может привести к результату, в котором не будет ни одной верной значащей цифры! Величины типа decimal позволяют хранить 28-29 десятичных разрядов.
Тип decimal не относится к вещественным типам, у них различное внутреннее представление. Величины денежного типа даже нельзя использовать в одном выражении с вещественными без явного преобразования типа. Использование величин финансового типа в одном выражении с целыми допускается.
Любой встроенный тип С# соответствует стандартному классу библиотеки .NET, определенному в пространстве имен System. Везде, где используется имя встроенного типа, его можно заменить именем класса библиотеки. Это значит, что у встроенных типов данных С# есть методы и поля. С их помощью можно, например, получить минимальные и максимальные значения для целых, символьных, финансовых и вещественных чисел:
□ double. MaxValue (или System. Double. MaxValue) — максимальное число типа double;
□ uint.MinValue (или System.UInt32.MinValue) — минимальное число типа uint.
ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------
Интересно, что в вещественных классах есть элементы, представляющие положительную и отрицательную бесконечности, а также значение «не число» — это Positivelnfinity, Negativelnfinity и NaN соответственно. При выводе на экран, например, первого из них получится слово «бесконечность». Все доступные элементы класса можно посмотреть в окне редактора кода, введя символ точки сразу после имени типа.
_____________________________________________________________________________
Типы литералов
Как уже говорилось, величин, не имеющих типа, не существует. Поэтому литералы (константы) тоже имеют тип. Если значение целого литерала находится внутри диапазона допустимых значений типа int, литерал рассматривается как int, иначе он относится к наименьшему из типов uint, long или ulong, в диапазон значений которого он входит. Вещественные литералы по умолчанию относятся к типу double.
Например, константа 10 относится к типу int (хотя для ее хранения достаточно и байта), а константа 2147483648 будет определена как uint. Для явного задания типа литерала служит суффикс, например, l. lf, 1UL, 1000m (все суффиксы перечислены в табл. 2.3). Явное задание применяется в основном для уменьшения количества неявных преобразований типа, выполняемых компилятором.
Типы-значения и ссылочные типы
Чаще всего типы С# разделяют по способу хранения элементов на типы-значения и ссылочные типы (рис. 2.2). Элементы типов-значений , или значимых типов (value types), представляют собой просто последовательность битов в памяти, необходимый объем которой выделяет компилятор. Иными словами, величины значимых типов хранят свои значения непосредственно. Величина ссылочного типа хранит не сами данные, а ссылку на них (адрес, по которому расположены данные). Сами данные хранятся в хипе.
ВНИМАНИЕ ---------------------------------------------------------------------------------
Несмотря на различия в способе хранения, и типы-значения, и ссылочные типы являются потомками общего базового класса object.
Рисунок 2.3 иллюстрирует разницу между величинами значимого и ссылочного типов. Одни и те же действия над ними выполняются по-разному. Рассмотрим в качестве примера проверку на равенство. Величины значимого типа равны, если равны их значения. Величины ссылочного типа равны, если они ссылаются на
одни и те же данные (на рисунке b и с равны, но а не равно b даже при одинаковых значениях). Из этого следует, что если изменить значение одной величины ссылочного типа, это может отразиться на другой.
Обратите внимание на то, что не все значимые типы являются простыми. По другой классификации структуры и перечисления относятся к структурированным типам, определяемым программистом. Мы рассмотрим эти типы в главе 9. Ссылочные типы мы будем изучать в главе 5 и последующих главах, после основных операторов С#, когда вы освоите синтаксис языка, а до этого ограничимся использованием встроенных типов-значений. Типы nullable введены в версию С# 2.0 и рассматриваются в главе 13.
Упаковка и распаковка
Для того чтобы величины ссылочного и значимого типов могли использоваться совместно, необходимо иметь возможность преобразования из одного типа в другой. Язык С# обеспечивает такую возможность. Преобразование из типа-значения в ссылочный тип называется упаковкой (boxing), обратное преобразование — распаковкой (unboxing).
Если величина значимого типа используется в том месте, где требуется ссылочный тип, автоматически выполняется создание промежуточной величины ссылочного типа: создается ссылка, в хипе выделяется соответствующий объем памяти и туда копируется значение величины, то есть значение как бы упаковывается в объект. При необходимости обратного преобразования с величины ссылочного типа «снимается упаковка», и в дальнейших действиях участвует только ее значение.
Рекомендации по программированию
Понятия, введенные в этой главе, являются базой для всего дальнейшего материала. На первый взгляд, изучение видов лексем может показаться излишним (пусть их различает компилятор!), однако это совершенно не так. Для того чтобы
читать программы, необходимо понимать, из каких элементов языка они состоят. Это помогает и при поиске ошибок, и при обращении к справочной системе, и при изучении новых версий языка. Более того, изучение любого нового языка рекомендуется начинать именно с лексем, которые в нем поддерживаются. Понятие типа данных лежит в основе большинства языковых средств. При изучении любого типа необходимо рассмотреть две вещи: его внутреннее представление а следовательно, множество возможных значений величин этого типа), а также что можно делать с этими величинами. Множество типов данных, реализуемых в языке, является одной из его важнейших характеристик. Выбор наиболее подходящего типа для представления данных — одно из необходимых условий создания эффективных программ.
Новые языки и средства программирования появляются непрерывно, поэтому программист вынужден учиться всю жизнь. Следовательно, очень важно сразу научиться учиться быстро и эффективно. Для этого надо подходить к освоению каждого языка системно: выделить составные части, понять их организацию и взаимосвязь, найти сходства и отличия от средств, изученных ранее, — короче говоря, за минимальное время разложить все в мозгу «по полочкам» так, чтобы новые знания гармонично дополнили имеющиеся. Только в этом случае ими будет легко и приятно пользоваться. Программист-профессионал должен уметь:
□ грамотно поставить задачу;
□ выбрать соответствующие языковые средства;
□ выбрать наиболее подходящие для представления данных структуры;
□ разработать эффективный алгоритм;
□ написать и документировать надежную и легко модифицируемую программу;
□ обеспечить ее исчерпывающее тестирование.
Кроме того, все это необходимо выполнять в заранее заданные сроки. Надеюсь, что эта книга даст вам первоначальный импульс в нужном направлении, а также ключ к дальнейшему совершенствованию в программировании как на С#, так и на других языках.
Переменные, операции и выражения
В этой главе рассказывается о переменных, основных операциях С#, простейших средствах ввода-вывода и наиболее употребительных математических функциях. Приведенные сведения позволят вам создавать простые линейные программы и являются базовыми для всего дальнейшего изучения материала.
Переменная — это именованная область памяти, предназначенная для хранения данных определенного типа. Во время выполнения программы значение переменной можно изменять. Все переменные, используемые в программе, должны быть описаны явным образом. При описании для каждой переменной задаются ее имя и тип.
Пример описания целой переменной с именем а и вещественной переменной х:
int a; float x;
Имя переменной служит для обращения к области памяти, в которой хранится значение переменной. Имя дает программист. Оно должно соответствовать правилам именования идентификаторов С#, отражать смысл хранимой величины и быть легко распознаваемым. Например, если в программе вычисляется количество каких-либо предметов, лучше назвать соответствующую переменную quantity или, на худой конец, kolich, но не, скажем, A, tl7_xz или prikol.
СОВЕТ --------------------------------------------------------------------------------------------------------
Желательно, чтобы имя не содержало символов, которые можно перепутать друг с другом, например 1 (строчная буква L) и I (прописная буква i).
Тип переменной выбирается, исходя из диапазона и требуемой точности представления данных. Например, нет необходимости заводить переменную вещественного типа для хранения величины, которая может принимать только целые значения, — хотя бы потому, что целочисленные операции выполняются гораздо быстрее.
При объявлении можно присвоить переменной некоторое начальное значение, то ест инициализировать ее, например:
int a, b = 1;
float x - 0.1, у = 0.lf;
Здесь описаны:
□ переменная а типа Int, начальное значение которой не присваивается;
□ переменная b типа int, ее начальное значение равно 1;
□ переменные х и у типа f I oat, которым присвоены одинаковые начальные значения 0.1. Разница между ними состоит в том, что для инициализации переменной х сначала формируется константа типа double (это тип, присваиваемый по умолчанию литералам с дробной частью), а затем она преобразуется к типу float; переменной у значение 0.1 присваивается без промежуточного преобразования.
При инициализации можно использовать не только константу, но и выражение — главное, чтобы на момент описания оно было вычисляемым, например:
int b = 1. а = 100;
int x = b * а. + 25:
Инициализировать переменную прямо при объявлении не обязательно, но перед тем, как ее использовать в вычислениях, это сделать все равно придется, иначе компилятор сообщит об ошибке.
ВНИМАНИЕ -------------------------------------------------------------------------------------------------Рекомендуется всегда инициализировать переменные при описании.
--------------------------------------------------------------------------------------------------------------------
Впрочем, иногда эту работу делает за программиста компилятор, это зависит от местонахождения описания переменной. Как вы помните из главы 1, программа на С# состоит из классов, внутри которых описывают методы и данные. Переменные, описанные непосредственно внутри класса, называются полями класса. Им автоматически присваивается так называемое «значение по умолчанию» — как правило, это 0 соответствующего типа.
Переменные, описанные внутри метода класса, называются локальными переменными. Их инициализация возлагается на программиста.
ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------
В этой главе рассматриваются только локальные переменные простых встроенных типов данных.
Так называемая область действия переменной, то есть область программы, где можно использовать переменную, начинается в точке ее описания и длится до конца блока, внутри которого она описана. Блок — это код, заключенный в фигурные скобки/ Основное назначение блока — группировка операторов. В С# любая переменная описана внутри какого-либо блока: класса, метода или блока внутри метода.
Имя переменной должно быть уникальным в области ее действия. Область действия распространяется на вложенные в метод блоки, из этого следует, что во вложенном блоке нельзя объявить переменную с таким же именем, что и в охватывающем его, например:
Давайте разберемся в этом примере. В нем описан класс X, содержащий три элемента: поле А, поле В и метод Y. Непосредственно внутри метода Y заданы две локальные переменные — С и А.
Внутри метода класса можно описывать переменную с именем, совпадающим с полем класса, потому что существует способ доступа к полю класса с помощью ключевого слова this (это иллюстрирует строка, отмеченная символами ***). Таким образом, локальная переменная А не «закрывает» поле А класса X, а вот попытка описать во вложенном блоке другую локальную переменную с тем же именем окончится неудачей (эти строки закомментированы).
Если внутри метода нет локальных переменных, совпадающих с полями класса, к этим полям можно обратиться в методе непосредственно (см. строку, помеченную
символами **). Две переменные с именем 0 не конфликтуют между собой, поскольку блоки, в которых они описаны, не вложены один в другой.
СОВЕТ -------------------------------------------------------------------------------------------------------
Как правило, переменным с большой областью действия даются более длинные имена, а для переменных, вся «жизнь» которых — несколько строк исходного текста, хватит и одной буквы с комментарием при объявлении.
-------------------------------------------------------------------------------------------------------------------
В листинге 3.1 приведен пример программы, в которой описываются и выводятся на экран локальные переменные.
Как вы догадались, метод Write делает то же самое, что и WriteLine, но не переводит строку. Более удобные способы вывода рассмотрены в конце этой главы в разделе «Простейший ввод-вывод» (см. с. 59).
ВНИМАНИЕ -------------------------------------------------------------------------------------------------
Переменные создаются при входе в их область действия (блок) и уничтожаются при выходе. Это означает, что после выхода из блока значение переменной не сохраняется. При повторном входе в этот же блок переменная создается заново.
---------------------------------------------------------------------------------------------------------------------
Можно запретить изменять значение переменной, задав при ее описании ключевое слово const, например:
const int b = 1;
const float x = 0.1, у = O.lf; // const распространяется на обе переменные
Такие величины называют именованными константами, или просто константами. Они применяются для того, чтобы вместо значений констант можно было использовать в программе их имена. Это делает программу более понятной и облегчает внесение в нее изменений, поскольку изменить значение достаточно только в одном месте программы.
ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------Улучшение читабельности происходит только при осмысленном выборе имен констант. В хорошо написанной программе вообще не должно встречаться иных чисел, кроме 0 и 1, все остальные числа должны задаваться именованными константами с именами, отражающими их назначение.
---------------------------------------------------------------------------------------------------------------------
Именованные константы должны обязательно инициализироваться при описании. При инициализации можно использовать не только константу, но и выражение — главное, чтобы оно было вычисляемым на этапе компиляции, например:
const int b = 1, а = 100;
const int x = b * а + 25;
Выражение — это правило вычисления значения. В выражении участвуют операнды, объединенные знаками операций. Операндами простейшего выражения могут быть константы, переменные и вызовы функций.
Например, а + 2 — это выражение, в котором + является знаком операции, а а и 2 — операндами. Пробелы внутри знака операции, состоящей из нескольких символов, не допускаются. По количеству участвующих в одной операции операндов операции делятся на унарные, бинарные и тернарную. Операции С# приведены в табл. 3.1.
ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------
В версию С# 2.0 введена операция объединения ??, которая рассматривается в главе 13 (см. раздел «Обнуляемые типы», с. 309).
--------------------------------------------------------------------------------------------------------------------
Операции в выражении выполняются в определенном порядке в соответствии с приоритетами, как и в математике. В табл. 3.1 операции расположены по убыванию приоритетов, уровни приоритетов разделены в таблице горизонтальными линиями.
Результат вычисления выражения характеризуется значением и типом. Например, пусть а и b — переменные целого типа и описаны так:
int а = 2. b = 5;
Тогда выражение а + b имеет значение 7 и тип int, а выражение а = b имеет значение, равное помещенному в переменную а (в данному случае — 5), и тип, совпадающий с типом этой переменной.
Если в одном выражении соседствуют несколько операций одинакового приоритета, операции присваивания и условная операция выполняются справа налево, остальные — слева направо. Для изменения порядка выполнения операций используются круглые скобки, уровень их вложенности практически не ограничен.
Например, а + b + с означает (a + b)+c, a a = b = c означает а = (b = с). То есть сначала вычисляется выражение b = с, а затем его результат становится правым f операндом для операции присваивания переменной а.
ПРИМЕЧАНИЕ-----------------------------------------------------------------------------------------
Часто перед выполнением операции требуется вычислить значения операндов.
Например, в выражении F (i) + G (i ++) * Н (i) сначала вызываются функции F, G и Н, а затем выполняются умножение и сложение. Операнды всегда вычисляются слева направо независимо от приоритетов операций, в которых они участвуют. Кстати, в приведенном примере метод Н вызывается с новым значением i (увеличенным на 1).
-------------------------------------------------------------------------------------------------------------------
Тип результата выражения в общем случае формируется по правилам, которые описаны в следующем разделе.
Преобразования встроенных арифметических типов-значений
При вычислении выражений может возникнуть необходимость в преобразовании типов. Если операнды, входящие в выражение, одного типа и операция для этого типа определена, то результат выражения будет иметь тот же тип. Если Операнды разного типа и/или операция для этого типа не определена, перед вычислениями автоматически выполняется преобразование типа по правилам, обеспечивающим приведение более коротких типов к более длинным для сохранения значимости и точности. Автоматическое (неявное) преобразование возможно не всегда, а только если при этом не может случиться потеря значимости. Если неявного преобразования из одного типа в другой не существует, программист может задать явное преобразование типа с помощью операции (тип)х. Его результат остается на совести программиста. Явное преобразование рассматривается в этой главе немного позже.
ВНИМАНИЕ--------------------------------------------------------------------------------------------------
Арифметические операции не определены для более коротких, чем int, типов. Это означает, что если в выражении участвуют только величины типов sbyte, byte, short и ushort, перед выполнением операции они будут преобразованы в int. Таким образом, результат любой арифметической операции имеет тип не менее int.
--------------------------------------------------------------------------------------------------------------------
Правила неявного преобразования иллюстрирует рис. 3.1. Если один из операндов имеет тип, изображенный на более низком уровне, чем другой, то он приводится к типу второго операнда при наличии пути между ними. Если пути нет, возникает ошибка компиляции. Если путей несколько, выбирается наиболее короткий, не содержащий пунктирных линий. Преобразование выполняется не последовательно, а непосредственно из исходного типа в результирующий.
Преобразование более коротких, чем int, типов выполняется при присваивании. Обратите внимание на то, что неявного преобразования из float и double в decimal не существует.
ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------
Преобразование из типов int, uint и long в тип float и из типа long в тип double может вызвать потерю точности, но не потерю значимости. В процессе других вариантов неявного преобразования никакая информация не теряется.
---------------------------------------------------------------------------------------------------------------------
Введение в исключения
При вычислении выражений могут возникнуть ошибки, например, переполнение, исчезновение порядка или деление на ноль. В С# есть механизм, который позволяет обрабатывать подобные ошибки и таким образом избегать аварийного завершения программы. Он так и называется: механизм обработки исключительных ситуаций {исключений).
Если в процессе вычислений возникла ошибка, система сигнализирует об этом с помощью специального действия, называемого выбрасыванием (генерированием) исключения. Каждому типу ошибки соответствует свое исключение. Поскольку С# — язык объектно-ориентированный, исключения являются классами, которые имеют общего предка — класс Exception, определенный в пространстве имен System.
Например, при делении на ноль будет выброшено (сгенерировано) исключение с длинным, но понятным именем DivideByZeroException, при недостатке памяти — исключение OutOfMemoryException, при переполнении — исключение OverflowException.
ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------
Стандартных исключений очень много, тем не менее программист может создавать и собственные исключения на основе класса Exception.
_____________________________________________________________________________
Программист может задать способ обработки исключения в специальном блоке кода, начинающемся с ключевого слова catch («перехватить»), который будет автоматически выполнен при возникновении соответствующей исключительной ситуации. Внутри блока можно, например, вывести предупреждающее сообщение или скорректировать значения величин и продолжить выполнение программы. Если этот блок не задан, система выполнит действия по умолчанию, которые обычно заключаются в выводе диагностического сообщения и нормальном завершении программы.
Процессом выбрасывания исключений, возникающих при переполнении, можно управлять. Для этого служат ключевые слова checked и unchecked. Слово checked включает проверку переполнения, слово unchecked выключает. При выключенной проверке исключения, связанные с переполнением, не генерируются, а результат операции усекается. Проверку переполнения можно реализовать для отдельного выражения или для целого блока операторов, например:
Проверка не распространяется на функции, вызванные в блоке. Если проверка переполнения включена, говорят, что вычисления выполняются в проверяемом контексте, если выключена — в непроверяемом. Проверку переполнения выключают в случаях, когда усечение результата операции необходимо в соответствии с алгоритмом.
Можно задать проверку переполнения во всей программе с помощью ключа компилятора /checked, это полезно при отладке программы. Поскольку подобная проверка несколько замедляет работу, в готовой программе этот режим обычно не используется.
Мы подробно рассмотрим исключения и их обработку в разделе «Обработка исключительных ситуаций» (см. с. 89).
Основные операции С#
В этом разделе кратко описаны синтаксис и применение всех операций С#, кроме некоторых первичных, которые рассматриваются в последующих главах при изучении соответствующего материала.
Инкремент и декремент
Операции инкремента (++) и декремента (--), называемые также операциями увеличения и уменьшения на единицу, имеют две формы записи — префиксную, когда знак операции записывается перед операндом, и постфиксную. В префиксной форме сначала изменяется операнд, а затем его значение становится результирующим значением выражения, а в постфиксной форме значением выражения является исходное значение операнда, после чего он изменяется. Листинг 3.2 иллюстрирует эти операции.
Результат работы программы:
Значение префиксного выражения: 4
Значение х после приращения: 4
Значение постфиксного выражения: 3
Значение у после приращения: 4
Стандартные операции инкремента существуют для целых, символьных, вещественных и финансовых величин, а также для перечислений. Операндом может быть переменная, свойство или индексатор (мы рассмотрим свойства и индексаторы в свое время, в главах 5 и 7).
Операция new
Операция new служит для создания нового объекта. Формат операции:
new тип ( [ аргументы ] )
С помощью этой операции можно создавать объекты как ссылочных, так и значимых типов, например:
object z = new object ();
int i = new int(); // то же самое, что int i = 0;
Объекты ссылочного типа обычно формируют именно этим способом, а переменные значимого типа чаще создаются так, как описано ранее в разделе «Переменные».
При выполнении операции new сначала выделяется необходимый объем памяти (для ссылочных типов в хипе, для значимых — в стеке), а затем вызывается так называемый конструктор по умолчанию, то есть метод, с помощью которого инициализируется объект. Переменной значимого типа присваивается значение по умолчанию, которое равно нулю соответствующего типа. Для ссылочных типов стандартный конструктор инициализирует значениями по умолчанию все поля объекта.
Если необходимый для хранения объекта объем памяти выделить не удалось, генерируется исключение OutOfMemoryException.
Операции отрицания
Арифметическое отрицание (унарный минус -) меняет знак операнда на противоположный. Стандартная операция отрицания определена для типов int, long, float, double и decimal. К величинам других типов ее можно применять, если для них возможно неявное преобразование к этим типам (см. рис. 3.1). Тип результата соответствует типу операции.
ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------
Для значений целого и финансового типов результат достигается вычитанием исходного значения из нуля. При этом может возникнуть переполнение. Будет ли при этом выброшено исключение, зависит от контекста.
Логическое отрицание (!) определено для типа boo!. Результат операции —
значение false, если операнд равен true, и значение true, если операнд равен
false.
Поразрядное отрицание (~), часто называемое побитовым, инвертирует каждый
разряд в двоичном представлении операнда типа int, uint, long или ulong.
Операции отрицания представлены в листинге 3.3.
Явное преобразование типа
Операция используется, как и следует из ее названия, для явного преобразования величины из одного типа в другой. Это требуется в том случае, когда неявного преобразования не существует. При преобразовании из более длинного типа в более короткий возможна потеря информации, если исходное значение выходит за пределы диапазона результирующего типа.
Формат операции:
( тип ) выражение
Здесь тип — это имя того типа, в который осуществляется преобразование, а выражение чаще всего представляет собой имя переменной, например:
long b = 300;
int a = (int) b; // данные не теряются
byte d = (byte) a: // данные теряются
Преобразование типа часто применяется для ссылочных типов при работе с иерархиями объектов.
Умножение, деление и остаток от деления
Операция умножения (.*) возвращает результат перемножения двух операндов. Стандартная операция умножения определена для типов int, uint, long, ulong, float, double и decimal: К величинам других типов ее можно применять, если для них возможно неявное преобразование к этим типам (см. рис. 3.1). Тип результата операции равен «наибольшему» из типов операндов, но не менее int.
Если оба операнда целочисленные или типа decimal и результат операции слишком велик для представления с помощью заданного типа, генерируется исключение System. OverflowExcepti on.
Все возможные значения для вещественных операндов приведены в табл. 3.2. Символами х и у обозначены конечные положительные значения, символом z — результат операции вещественного умножения. Если результат слишком велик для представления с помощью заданного типа, он принимается равным значению «бесконечность», если слишком мал, он принимается за 0. NaN (not a number) означает, что результат не является числом.
Операция деления (/) вычисляет частное от деления первого операнда на второй. Стандартная операция деления определена для типов int, uint, long, ulong, float, double и decimal. К величинам других типов ее можно применять, если для них существует неявное преобразование к этим типам. Тип результата определяется правилами преобразования (см. рис. 3.1), но не меньше int.
Если оба операнда целочисленные, результат операции округляется вниз до ближайшего целого числа. Если делитель равен нулю, генерируется исключение
System.Di videByZeroExcepti on.
Если хотя бы один из операндов вещественный, дробная часть результата деления не отбрасывается, а все возможные значения приведены в табл. 3.3. Символами х и у обозначены конечные положительные значения, символом z — результат
операции вещественного деления. Если результат слишком велик для представления с помощью заданного типа, он принимается равным значению «бесконечность», если слишком мал, он принимается за 0.
Для финансовых величин (тип decimal) при делении на 0 и переполнении генерируются соответствующие исключения, при исчезновении порядка результат равен 0.
Операция остатка от деления (%) также интерпретируется по-разному для целых, вещественных и финансовых величин. Если оба операнда целочисленные, результат операции вычисляется по формуле х-(х /у) * у. Если делитель равен нулю, генерируется исключение System.DivideByZeroException. Тип результата операции равен «наибольшему» из типов операндов, но не менее int (см. рис. 3.1).
Если хотя бы один из операндов вещественный, результат операции вычисляется по формуле х - n * у, где n — наибольшее целое, меньшее или равное результату деления х на у. Все возможные комбинации значений операндов приведены в табл. 3.4. Символами х и у обозначены конечные положительные значения, символом z — результат операции остатка от деления.
Для финансовых величин (тип decimal) при получении остатка от деления на 0 и при переполнении генерируются соответствующие исключения, при исчезновении порядка результат равен 0. Знак результата равен знаку первого операнда.
Пример применения операциий умножения, деления и получения остатка представлен в листинге 3.4.
Еще раз обращаю ваше внимание на то, что несколько операций одного приоритета выполняются слева направо. Для примера рассмотрим выражение 2 / х∙ у. Деление и умножение имеют один и тот же приоритет, поэтому сначала 2 делится на х, а затем результат этих вычислений умножается на у. Иными словами, это выражение эквивалентно формуле
Если же мы хотим, чтобы выражение х∙ у было в знаменателе, следует заключить его в круглые скобки или сначала поделить числитель на х, а потом на у, то есть записать как
2 / {х∙ у) или 2 / х / у.
Сложение и вычитание
Операция сложения (+) возвращает сумму двух операндов. Стандартная операция сложения определена для типов int, uint, long, ulong, float, double и decimal. К величинам других типов ее можно применять, если для них существует неявное преобразование к этим типам (см. рис. 3.1). Тип результата операции равен «наибольшему» из типов операндов, но не менее int.
Если оба операнда целочисленные или типа decimal и результат операции слишком велик для представления с помощью заданного типа, генерируется исключение System. OverflowExcepti on.
Все возможные значения для вещественных операндов приведены в табл. 3.5. Символами х и у обозначены конечные положительные значения, символом z — результат операции вещественного сложения. Если результат слишком велик для представления с помощью заданного типа, он принимается равным значению «бесконечность», если слишком мал, он принимается за 0.
Операция вычитания (-) возвращает разность двух операндов. Стандартная операция вычитания определена для типов int, uint, long, ulong, float, double и decimal. К величинам других типов ее можно применять, если для них существует неявное преобразование к этим типам (см. рис. 3.1). Тип результата операции равен «наибольшему» из типов операндов, но не менее int.
Если оба операнда целочисленные или типа decimal и результат операции слишком велик для представления с помощью заданного типа, генерируется исключение System. OverfiowException.
Все возможные значения результата вычитания для вещественных операндов приведены в табл. 3.6. Символами х и у обозначены конечные положительные значения, символом z — результат операции вещественного вычитания. Если х и у равны, результат равен положительному нулю. Если результат слишком велик для представления с помощью заданного типа, он принимается равным значению «бесконечность» с тем же знаком, что и х - у, если слишком мал, он принимается за 0 с тем же знаком, что и х - у.
Операции сдвига
Операции сдвига (« и ») применяются к целочисленным операндам. Они сдвигают двоичное представление первого операнда влево или вправо на количество двоичных разрядов, заданное вторым операндом.
При сдвиге влево («) освободившиеся разряды обнуляются. При сдвиге вправо (») освободившиеся биты заполняются нулями, если первый операнд беззнакового типа (то есть выполняется логический сдвиг), и знаковым разрядом — в противном случае (выполняется арифметический сдвиг). Операции сдвига никогда не приводят к переполнению и потере значимости. Стандартные операции сдвига определены для типов int, uint, long и ulong.
Пример применения операций сдвига представлен в листинге 3.5.
Операции отношения и проверки на равенство
Операции отношения (<, <=, >, >=, ==, ! =) сравнивают первый операнд со вторым. Операнды должны быть арифметического типа. Результат операции — логического типа, равен true или false. Правила вычисления результатов приведены в табл. 3.7.
ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------
Обратите внимание на то, что операции сравнения на равенство и неравенство имеют меньший приоритет, чем остальные операции сравнения.
---------------------------------------------------------------------------------------------------------------------
Очень интересно формируется результат операций отношения для особых случаев вещественных значений. Например, если один из операндов равен NaN, результатом для всех операций, кроме !=, будет false (для операции != результат равен true).
Очевиден факт, что для любых операндов результат операции х ! = у всегда равен результату операции ! (х == у), однако если один или оба операнда равны NaN, для операций <, >, <= и >= этот факт не подтверждается. Например, если х или у равны
NaN, то х < у даст false, а !(х >= у) — true.
Другие особые случаи рассматриваются следующим образом:
□ значения +0 и -0 равны;
□ значение - ∞ меньше любого конечного значения и равно другому значению - ∞;
□ значение + ∞ больше любого конечного значения и равно другому значению + ∞.
Поразрядные логические операции
Поразрядные логические операции (&, |, ˆ ) применяются к целочисленным операндам и работают с их двоичными представлениями. При выполнении операций операнды сопоставляются побитно (первый бит первого операнда с первым битом второго, второй бит первого операнда со вторым битом второго и т. д.). Стандартные операции определены для типов int, uint, long и ulong.
При поразрядной конъюнкции, или поразрядном И (операция обозначается &), бит результата равен 1 только тогда, когда соответствующие биты обоих операндов равны 1.
При поразрядной дизъюнкции, или поразрядном ИЛИ (операция обозначается |), бит результата равен 1 тогда, когда соответствующий бит хотя бы одного из операндов равен 1.
При поразрядном исключающем ИЛИ (операция обозначается &) бит результата равен 1 только тогда, когда соответствующий бит только одного из операндов равен 1.
Пример применения поразрядных логических операций представлен в листинге 3.6.
Условные логические операции
Условные логические операции И (&&) и ИЛИ (׀ ׀ ) чаще всего используются с операндами логического типа. Результатом логической операции является true или false. Операции вычисляются по сокращенной схеме.
Результат операции логическое И имеет значение true, только если оба операнда имеют значение true. Результат операции логическое ИЛИ имеет значение true, если хотя бы один из операндов имел значение true.
ВНИМАНИЕ -------------------------------------------------------------------------------------------------
Если значения первого операнда достаточно, чтобы определить результат операции, второй операнд не вычисляется. Например, если первый операнд операции И равен false, результатом операции будет false независимо от значения второго операнда, поэтому он не вычисляется.
---------------------------------------------------------------------------------------------------------------------
Пример применения условных логических операций представлен в листинге 3.7.
Условная операция
Условная операция (? :) — тернарная, то есть имеет три операнда. Ее формат:
операнд_1 операнд_2 : операнд_3
Первый операнд — выражение, для которого существует неявное преобразование к логическому типу. Если результат вычисления первого операнда равен true, то результатом условной операции будет значение второго операнда, иначе — третьего операнда. Вычисляется всегда либо второй операнд, либо третий. Их тип может различаться.
Тип результата операции зависит от типа второго и третьего операндов:
□ если операнды одного типа, он и становится типом результата операции;
□ иначе, если существует неявное преобразование типа от операнда 2 к операнду 3, но не наоборот, то типом результата операции становится тип операнда 3;
□ иначе, если существует неявное преобразование типа от операнда 3 к операнду 2, но не наоборот, то типом результата операции становится тип операнда 2;
□ иначе возникает ошибка компиляции.
Условную операцию часто используют вместо условного оператора if (он рассматривается в следующей главе) для сокращения текста программы.
Пример применения условной операции представлен в листинге 3.8.
Другой пример применения условной операции: требуется, чтобы некоторая целая величина увеличивалась на 1, если ее значение не превышает n, а иначе принимала значение 1. Это удобно реализовать следующим образом:
i = (1 < n) ? i + 1: 1;
Условная операция правоассоциативна, то есть выполняется справа налево. Например, выражение а ? b : с ? d : е вычисляется как а ? b : (.c?d : е).
Операции присваивания
Операции присваивания (=, +=, -=, *= и т. д.) задают новое значение переменной. Эти операции могут использоваться в программе как законченные операторы.
Формат операции простого присваивания (=):
переменная = выражение
Механизм выполнения операции присваивания такой: вычисляется выражение и его результат заносится в память по адресу, который определяется именем переменной, находящейся слева от знака операции. То, что ранее хранилось в этой области памяти, естественно, теряется. Схематично это полезно представить себе так:
Переменная ← Выражение
Напомню, что константа и переменная являются частными случаями выражения.
Примеры операторов присваивания:
а = b + с / 2;
b = a;
а - b;
х = 1:
х = х + 0.5;
Обратите внимание: b = а и а = b — это совершенно разные действия!
ПРИМЕЧАНИЕ---------------------------------------------------------------------------------------------
Чтобы не перепутать, что чему присваивается, запомните мнемоническое правило: присваивание — это передача данных «налево».
Начинающие часто делают ошибку, воспринимая присваивание как аналог равенства в математике. Чтобы избежать этой ошибки, надо понимать механизм работы оператора присваивания. Рассмотрим для этого последний пример (х = х + 0.5). Сначала из ячейки памяти, в которой хранится значение переменной х, выбирается это значение. Затем к нему прибавляется 0.5, после чего получившийся результат записывается в ту же самую ячейку, а то, что хранилось там ранее, теряется безвозвратно. Операторы такого вида применяются в программировании очень широко.
Для правого операнда операции присваивания должно существовать неявное преобразование к типу левого операнда. Например, выражение целого типа можно присвоить вещественной переменной, потому что целые числа являются подмножеством вещественных, и информация при таком присваивании не теряется:
вещественная_переменная := целоевыражение:
Правила преобразований перечислены в разделе «Преобразования встроенных арифметических типов-значений» (см. с. 45).
Результатом операции присваивания является значение, записанное в левый операнд. Тип результата совпадает с типом левого операнда.
В сложных операциях присваивания ( + =, * =, /= и т. п.) при вычислении выражения, стоящего в правой части, используется значение из левой части. Например, при сложении с присваиванием ко второму операнду прибавляется первый, и результат записывается в первый операнд, то есть выражение а + = b является более компактной записью выражения а = а + b.
Результатом операции сложного присваивания является значение, записанное в левый операнд.
ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------
В документации написано, что тип результата совпадает с типом левого операнда, если он не менее int. Это означает, что если, например, переменные а и b имеют тип byte, присваивание а += b недопустимо и требуется преобразовать тип явным образом: а += (byte)b. Однако на практике компилятор ошибку не выдает.
Напомню, что операции присваивания правоассоциативны, то есть выполняются справа налево, в отличие от большинства других операций (а = b = с означает а = (b = с)).
Не рассмотренные в этом разделе операции будут описаны позже.
Линейной называется программа, все операторы которой выполняются последовательно в том порядке, в котором они записаны. Простейшим примером линейной программы является программа расчета по заданной формуле. Она состоит из трех этапов: ввод исходных данных, вычисление по формуле и вывод результатов. Для того чтобы написать подобную программу, нам пока не хватает знаний о том, как организовать ввод и вывод на языке С#. Подробно этот вопрос рассматривается в главе И, а здесь приводятся только минимально необходимые сведения.
Простейший ввод-вывод
Любая программа при вводе исходных данных и выводе результатов взаимодействует с внешними устройствами. Совокупность стандартных устройств ввода и вывода, то есть клавиатуры и экрана, называется консолью. Обмен данными с консолью является частным случаем обмена с внешними устройствами, который подробно рассмотрен в главе 11.
В языке С#, как и во многих других, нет операторов ввода и вывода. Вместо них для обмена с внешними устройствами применяются стандартные объекты. Для работы с консолью в С# применяется класс Console, определенный в пространстве имен System. Методы этого класса Write и WriteLine уже использовались в наших программах. Поговорим о них подробнее, для чего внесем некоторые изменения в листинг 3.1. Результаты этих изменений представлены в листинге 3.9.
Результат работы программы:
i=3
y = 4.12
d = 600 -
s = Вася
До сих пор мы использовали метод WriteLine для выхода значений переменных и литералов различных встроенных типов. Это возможно благодаря тому, что в классе Console существует несколько вариантов методов с именами Write и WriteLine, предназначенных для вывода значений различных типов.
Методы с одинаковыми именами, но разными параметрами называются перегруженными. Компилятор определяет, какой из методов вызван, по типу передаваемых в него величин. Методы вывода в классе Console перегружены для всех встроенных типов данных, кроме того, предусмотрены варианты форматного вывода.
Листинг 3.9 содержит два наиболее употребительных варианта вызова методов вывода. Сначала обратите внимание на способ вывода пояснений к значениям переменных в строках 1 и 3. Пояснения представляют собой строковые литералы. Если метод WriteLine вызван с одним параметром, он может быть любого встроенного типа, например, числом, символом или строкой. Нам же требуется вывести в каждой строке не одну, а две величины: текстовое пояснение и значение переменной, — поэтому прежде чем передавать их для вывода, их требуется «склеить» в одну строку с помощью операции +.
Перед объединением строки с числом надо преобразовать число из его внутренней формы представления в последовательность символов, то есть в строку. Преобразование в строку определено во всех стандартных классах С# — для этого служит метод ToString( ). В данном случае он выполняется неявно, но можно вызвать его и явным образом:
Console.WriteLine( "i = " + i.ToString () ):
Оператор 2 иллюстрирует форматный вывод. В этом случае используется другой вариант метода WriteLine, который содержит более одного параметра. Первым параметром методу передается строковый литерал, содержащий помимо обычных символов, предназначенных для вывода на консоль, параметры в фигурных скобках, а также управляющие последовательности (они были рассмотрены в главе 2).
Параметры нумеруются с нуля, перед выводом они заменяются значениями соответствующих переменных в списке вывода: нулевой параметр заменяется значением первой переменной (в данном примере — у), первый параметр — второй переменной (в данном примере — d) и т. д.
ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------
Для каждого параметра можно задать ширину поля вывода и формат вывода. Мы рассмотрим эти возможности в разделе «Форматирование строк» (см. с. 146).
-------------------------------------------------------------------------------------------------------------------
Из управляющих последовательностей чаще всего используются символы перевода строки (\n) и горизонтальной табуляции (\t).
Рассмотрим простейшие способы ввода с клавиатуры. В классе Console определены методы ввода строки и отдельного символа, но нет методов, которые позволяют непосредственно считывать с клавиатуры числа. Ввод числовых данных выполняется в два этапа:
1. Символы, представляющие собой число, вводятся с клавиатуры в строковую переменную.
2. Выполняется преобразование из строки в переменную соответствующего типа.
Преобразование можно выполнить либо с помощью специального класса Convert, определенного в пространстве имен System, либо с помощью метода Parse, имеющегося в каждом стандартном арифметическом классе. В листинге 3.10 используются оба способа.
К этому примеру необходимо сделать несколько пояснений. Ввод строки выполняется в операторе 1. Длина строки не ограничена, ввод выполняется до символа перевода строки.
Ввод символа выполняется с помощью метода Read, который считывает один символ из входного потока (оператор 2). Метод возвращает значение типа int, представляющее собой код символа, или — 1, если символов во входном потоке нет (например, пользователь нажал клавишу Enter). Поскольку нам требуется не int, a char, а неявного преобразования от int к char не существует, приходится применить операцию явного преобразования типа, которая описана в разделе «Явное преобразование типа» (см. с. 49).
За оператором 2 записан оператор 3, который считывает остаток строки и никуда его не передает. Это необходимо потому, что ввод данных выполняется через буфер — специальную область оперативной памяти. Фактически, данные сначала заносятся в буфер, а затем считываются оттуда процедурами ввода. Занесение в буфер выполняется по нажатию клавиши Enter вместе с ее кодом. Метод Read, в отличие от ReadLine, не очищает буфер, поэтому следующий после него ввод будет выполняться с того места, на котором закончился предыдущий.
В операторах 4 и 5 используются методы класса Convert, в операторах 6 и 7 — методы Parse классов Double и Decimal библиотеки .NET, которые используются здесь через имена типов С# double и decimal.
ВНИМАНИЕ -------------------------------------------------------------------------------------------------
При вводе вещественных чисел дробная часть отделяется от целой с помощью запятой, а не точки. Иными словами, при вводе используются правила операционной системы, а не языка программирования. Допускается задавать числа с порядком, например, 1,95е-8.
Если вводимые с клавиатуры символы нельзя интерпретировать как вещественное число, генерируется исключение.
Ввод-вывод в файлы
При отладке даже небольших программ может потребоваться их выполнить не раз, не два и даже не десять. При этом ввод исходных данных может стать утомительным и испортить все удовольствие от процесса. Удобно заранее подготовить исходные данные в текстовом файле и считывать их в программе. Кроме того, это дает возможность не торопясь продумать, какие исходные данные требуется ввести для полной проверки программы, и заранее рассчитать, что должно получиться в результате.
Вывод из программы тоже бывает полезно выполнить не на экран, а в текстовый файл для последующего неспешного анализа и распечатки. Работа с файлами подробно рассматривается в главе 11, а здесь приводятся лишь образцы для использования в программах. В листинге 3.11 приведена версия программы из листинга 3.9, выполняющая вывод не на экран, а в текстовый файл с именем output.txt. Файл создается в том же каталоге, что и исполняемый файл программы, по умолчанию — ...\ConsoleApplication1\bin\Debug.
Для того чтобы использовать в программе файлы, необходимо:
1. Подключить пространство имен, в котором описываются стандартные классы для работы с файлами (оператор 1).
2. Объявить файловую переменную и связать ее с файлом на диске (оператор 2). 3: Выполнить операции ввода-вывода (операторы 3-5).
4. Закрыть файл (оператор 6).
СОВЕТ ---------------------------------------------------------------------------------------------------------
При отладке программы бывает удобно выводить одну и ту же информацию и на экран, и в текстовый файл. Для этого соответствующие операторы дублируют.
--------------------------------------------------------------------------------------------------------------------
Ввод данных из файла выполняется аналогично. В листинге 3.12 приведена программа, аналогичная листингу 3.10, но ввод выполняется из файла с именем input.txt, расположенного в каталоге D:\C#. Естественно, из программы убраны все приглашения к вводу.
Текстовый файл можно создать с помощью любого текстового редактора, но удобнее использовать Visual Studio.NET. Для этого следует выбрать в меню команду File ► New ► File... и в появившемся диалоговом окне выбрать тип файла Text File.
Математические функции — класс Math
В выражениях часто используются математические функции, например синус или возведение в степень. Они реализованы в классе Math, определенном в пространстве имен System. С помощью методов этого класса можно вычислить:
□ тригонометрические функции: Sin, Cos, Tan;
□ обратные тригонометрические функции: ASin, ACos, ATan, ATan2;
□ гиперболические функции: Tanh, Sinh, Cosh;
□ экспоненту и логарифмические функции: Exp, Log, Log10;
□ модуль (абсолютную величину), квадратный корень, знак: Abs, Sqrt, Sign;
□ округление: Ceiling, Floor, Round;
□ минимум, максимум: Mi n, Max;
□ степень, остаток: Pow, IEEEReminder;
□ полное произведение двух целых величин: BigMul;
□ деление и остаток от деления: DivRem.
Кроме того, у класса есть два полезных поля: число п и число е. Описание методов и полей приведено в табл. 3.8.
В листинге 3.13 приведен пример применения двух методов класса Math. Остальные методы используются аналогично.
В качестве примера рассмотрим программу расчета по заданной формуле
Из формулы видно, что исходными данными для программы являются две величины — х и а. Поскольку их тип и точность представления в условии не оговорены, выберем для них тип double. Программа приведена в листинге 3.14.
Итак, к настоящему моменту у вас накопилось достаточно сведений, чтобы писать на С# простейшие линейные программы, выполняющие вычисления по формулам. В следующей главе мы займемся изучением операторов, позволяющих реализовывать более сложные алгоритмы.
Рекомендации по программированию
Приступая к написанию программы, четко определите, что является ее исходными данными и что требуется получить в результате. Выбирайте тип переменных с учетом диапазона и требуемой точности представления данных.
Давайте переменным имена, отражающие их назначение. Правильно выбранные имена могут сделать программу в некоторой степени самодокументированной. Неудачные имена, наоборот, служат источником проблем. В именах следует избегать сокращений. Они делают программу менее понятной, к тому же часто легко забыть, как именно было сокращено то или иное слово.
Общая тенденция такая: чем больше область действия переменной, тем более длинное у нее имя. Перед таким именем можно поставить префикс типа (одну или несколько букв, по которым можно определить тип переменной). Напротив, для переменных, вся «жизнь» которых проходит на протяжении нескольких строк кода, лучше обойтись однобуквенными именами типа i или к.
Имена переменных логического типа, используемые в качестве флагов, должны быть такими, чтобы по ним можно было судить о том, что означают значения true и false. Например, признак «пусто» лучше описать не как bool flag, а как bool empty.
ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------
В С# принято называть классы, методы и константы в соответствии с нотацией Паскаля, а локальные переменные — в соответствии с нотацией Camel (см. раздел «Идентификаторы», с. 24).
-------------------------------------------------------------------------------------------------------------------
Переменные желательно инициализировать при их объявлении, а объявлять как можно ближе к месту их непосредственного использования. С другой стороны, удобно все объявления локальных переменных метода располагать в начале блока так, чтобы их было просто найти. При небольших размерах методов оба эти пожелания довольно легко совместить.
Избегайте использования в программе чисел в явном виде. Константы должны иметь осмысленные имена, заданные с помощью ключевого слова const. Символическое имя делает программу более понятной, а кроме того, при необходимости изменить значение константы потребуется изменить программу только в одном месте. Конечно, этот совет не относится к константам 0 и 1.
Ввод с клавиатуры предваряйте приглашением, а выводимые значения — пояснениями. Для контроля сразу же после ввода выводите исходные данные на дисплей (по крайней мере, в процессе отладки).
До запуска программы подготовьте тестовые примеры, содержание исходные данные и ожидаемые результаты. Отдельно проверьте реакцию программы на неверные исходные данные.
При записи выражений обращайте внимание на приоритет операций. Если в одном выражении соседствует несколько операций одинакового приоритета, операции присваивания и условная операция выполняются справа налево, остальные — слева направо. Для изменения порядка выполнения операций используйте круглые скобки.
Тщательно форматируйте текст программы так, чтобы его было удобно читать. Ставьте пробелы после знаков препинания, отделяйте пробелами знаки операций, не пишите много операторов в одной строке, используйте комментарии и пустые строки для разделения логически законченных фрагментов программы.