This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
в кратком изложении Перевод с английского К.Г.Финогенова
Москва БИНОМ. Лаборатория знаний 2005
УДК 004.65 ББК 32.973 Б59
Б59
Бишоп Дж. С# в кратком изложении / Дж. Бишоп, Н. Хорспул; Пер. с англ. — М.: БИНОМ. Лаборатория знаний, 2005. — 472 с , ил. ISBN 5-94774-211-Х (русск.) ISBN 0-321-15418-5 (англ.) Книга предназначена для обучения основам объектно-ориентированного программирования с использованием языка С# и затрагивает почти все основные средства языка, включая пространства имен, использование коллекций и программирование сетевых задач. Особенное внимание уделяется концепциям полиморфизма и расширяемости. Книга изобилует многочисленными примерами, представляющими собой функционирующие программы, и сводными таблицами с компактным описанием основных языковых средств. Ориентированная прежде всего на студентов первого года обучения, книга в равной степени адресована студентам всех уровней, для которых она будет служить прекрасным пособием, а также всем, кто работает на других языках и желает перейти на С#. УДК 004.65 ББК 32.973
ISBN 5-94774-211-Х (русск.) ISBN 0-321-15418-5 (англ.)
^ д н а р у с с к и й ЯЗЫК) БИНОМ. Лаборатория знаний, 2005
@ п
К читателям
Многие книги, посвященные компьютерам, имеют такой объем, что, поднимая их, вы рискуете заработать грыжу, однако материала в них не больше, чем в бульварной газете. Академические учебники зачастую так скучны, что у вас плавятся мозги, хотя они не учат вас ничему, что могло бы пригодиться в реальной жизни. Но только не эта книга! Этот краткий курс аккумулирует в себе многолетний опыт и знания авторов. В каждую главу включены многочисленные примеры и упражнения, но что мне лично нравится больше всего —так это волшебная россыпь компактных справочных «форм», щедро разбросанная по всей книге. Широкое использование не зависящего от платформы пространства имен Views, специально разработанного для этой книги и позволяющего создавать элементы графического интерфейса пользователя, облегчает изучение материала и способствует приобретению практического опыта. Еще одним достоинством книги является включение специальной главы, посвященной отладке. Даже в невероятном предположении, что вы при создании программы никогда не вносите в нее ошибки, отладка представляет собой идеальное средство для понимания динамики работы программы; ну а для нас, простых людей, это дополнительное мощное средство локализации наших ошибок. Имея на своем столе эту книгу, вы превратите каждый рабочий день в праздничный. Эрик Мейер руководитель группы технической поддержки Microsoft WebData Team 31 июля 2003 г.
Предисловие Уилльяму и Майклу, как и всегда, а также Джанет, без любви и поддержки которых эта книга не была бы написана Дж. Бишоп Микаэле, которая в течение подготовки этой книги щедро помогала и ободряла нас; мы и представить себе не могли, что эта работа займет столько времени Н. Хорспул Написание этой книги заняло у нас целый год. Нам хотелось бы объяснить вам, будь вы преподаватель, студент или просто любознательный читатель, зачем мы взяли на себя этот труд и что вы почерпнете из этой книги.
С# — откуда и куда С# — это новый язык, разработанный Эндерсом Хейлсбергом (Anders Hejlsberg), Скоттом Уилтамутом (Scott Wiltamuth) и Питером Гоулдом (Peter Golde) в корпорации Microsoft в качестве основной среды разработки для .Net Framework и всех будущих продуктов Microsoft. C# берет свое начало в других языках, в основном, в C++, Java, Delphi, Modula-2 и Smalltalk. Про Хейлсберга следует сказать, что он был главным архитектором Turbo Pascal и Borland Delphi, и его огромный опыт способствовал весьма тщательной проработке нового языка. С одной стороны, для С# в еще большей степени, чем для упомянутых выше языков, характерна внутренняя объектная ориентация; с другой стороны, в нем реализована новая концепция упрощения объектов, что существенно облегчает освоение мира объектно-ориентированного программирования. В настоящее время С# утверждается среди языков, используемых как для разработки нового программного обеспечения, так и для обучения. Поскольку эта книга является прежде всего учебником, мы хотели бы подчеркнуть, что, по нашему мнению, С# является идеальным языком для начального обучения программированию. Некоторым препятствием здесь является консервативность: в большинстве организаций твердые позиции занял язык Java и такое его положение поддерживается новыми разработками и всей инфрастуктурой. С# также удобен для более специализированных курсов. Ввиду того что этот язык поддерживает такие современные концепции, как делегирование и перегрузку операторов, и даже делаются предложения о реализации родовых классов, С# представляется превосходной средой для таких, в частности, курсов, как «Современные методы программирования», «Структуры данных», «Сетецентрические модели вычислений» и «Распределенные системы». Использование С# может также оказаться полезным для курсов повышения квалификации с производственным
Предисловие
уклоном, к каковым относятся многие программы обучения Microsoft, поскольку обучающиеся сразу обеспечиваются реальной средой программирования. В отличие от Java, C# можно использовать только при определенных обстоятельствах. Если академическая организация принадлежит объединению Microsoft Academic Alliance (вступить куда можно за минимальную плату), то программное обеспечение будет доступно как сотрудникам, так и студентам. Язык С# (но не все его библиотеки) утвержден в качестве стандарта Европейской ассоциацией ЕСМА (в декабре 2001 г.) в результате чего появились и другие (также бесплатные) компиляторы для Windows и других платформ. Сама корпорация Microsoft выпустила три таких продукта под кодовым именем Rotor для платформ Windows, FreeBSD и Macintosh OSX. Продукты Microsoft обычно отличаются интенсивным потреблением ресурсов и требуют таких компьютеров, которые находятся за пределами возможностей многих студенческих лабораторий. В силу этого бесплатные компиляторы могут оказаться предпочтительнее. Однако в стандарте ЕСМА имеется серьезное упущение — отсутствует прикладной интерфейс Windows.Form, используемый для программирования графических интерфейсов пользователя. Мы работаем с корпорацией Microsoft над восполнением этого пробела с апреля 2002 г., и результатом этой работы явился продукт Views, небольшая экономная система графического интерфейса пользователя (GUI), основанная на языке XML. Разработаны варианты Views как для стандартных Windows-систем, так и для других платформ, воспринимающих систему Rotor. Совокупность системы Views и бесплатных компиляторов С# дает вполне жизнеспособную и технологически современную альтернативу комбинации лицензионных С# и Visual Studio корпорации Microsoft, обеспечивая при этом еще и независимость от платформы. С# несомненно имеет большое будущее как в обучении и исследованиях, так и в системных разработках. Целью этой книги является приближение этого будущего путем максимально широкого распространения языка С#.
Обзор книги Для современных учебников по программированию стала общим местом декларация использования подхода «объекты в первую очередь». Внимательное изучение учебников и докладов на образовательных конференциях по компьютерным направлениям показывает, что смысл выражения «объекты в первую очередь» довольно туманен. Более того, в этих работах редко указывается, что же понимается под второй или третьей очередью. Несомненно, настоящая книга использует подход «объекты в первую очередь», при этом, используя новаторскую методику изложения, нам удалось, надеемся, вложить в это выражение более четкий смысл. В гл. 2 мы вводим объекты действительно в первую очередь путем исполь-
8
Предисловие
зования их, а не определения для них типов. Мы основываем изложение на двух встроенных структурных типах, DateTime (структура) и Random (класс), и развиваем концепции реализации, переменных, методов и присваивания значительно более естественным путем, чем было бы возможно в случае требования первоначального определения типов. Далее в гл. 3 мы рассматриваем вопрос определения типа, возвращаясь назад и обобщая уже знакомые читателю концепции, а также развивая формализм таких понятий, как параметры и свойства. Мы приняли твердое решение оставаться на этой стадии в мире значений переменных, используя для создания объектов структуры и упоминая ссылки лишь как средство передачи параметров. Мы уверены, что лучшим педагогическим принципом является строго последовательное рассмотрение концепций, и, откладывая до гл. 7 детальное рассмотрение аппарата ссылок, мы тем самым устраняем часто возникающие недоумения по поводу использования значений переменных и ссылок на них. Ввиду такого решения обсуждение классов начинается лишь в гл. 8. С# в большей степени, чем другие современные объектно-ориентированные языки, опирается на понятие типа, причем это понятие интерпретируется одинаково, независимо от того, идет ли речь о скалярной переменной типа int, структуре или классе. Мы придерживаемся того же принципа, и откладывание обсуждения классов на более поздний этап не приводит к каким-либо неестественным программным конструкциям. На вопрос «если объекты в первую очередь, то что во вторую?» мы отвечаем в гл. 4: управляющие структуры, строки и массивы. Все они представляют собой базовые строительные блоки, используемые при разработке программ. Без этих блоков нельзя полноценно использовать более мощные объектно-ориентированные средства — коллекции и полиморфизм. Перед тем, однако, как перейти к этим полезным предметам, мы потратим две главы на то, чтобы заложить основы целенаправленного программирования. Глава 5 описывает систему Views, которую мы разработали в качестве дополнения, облегчающего обслуживание графических пользовательских интерфейсов в любой С#-программе. Имея такое инструментальное средство, вы получаете возможность быстрее разрабатывать программы, взаимодействующие с пользователем, причем независимо от платформы. В гл. 7 мы завершаем исследование ввода и вывода, рассмотрев хранение данных в файлах, а также представление файлов в С# в виде потоков данных. Гл. 6, посвященная ошибкам и процедурам отладки, почти уникальна для учебников по программированию. В этой главе описываются как стандартные, так и новые методики отладки, от выводимых на экран сообщений до контрольных вызовов. Большая часть этих методик не требует никакого дополнительного программного обеспечения кроме компилятора С#. В отдельном разделе описывается использование отладчика, а в Приложении Ж приводятся дополнительные сведения об отладочных возможностях Microsoft Visual Studio.
Предисловие
К этому моменту программисты смогут справиться с любой задачей, доступной решению на более старых языках, но в объектно-ориентированном стиле. Главы 8 и 9 завершают тему объектов рассмотрением коллекций (с которыми особенно удобно работать на С#), а также расширяемости и полиморфизма. В гл. 9 мы проводим обсуждение принципов, а затем показываем, как они могут быть реализованы с помощью любой из трех имеющихся методик — наследования, интерфейсов и абстрактных классов. В девяти первых главах книги мы рассмотрели большую часть языка С#, хотя, разумеется, остался еще целый пласт функций прикладного интерфейса пользователя (API). Какие из них особенно важны? Какие следует тщательно изучить? Книга не может считаться завершенной, если в ней на рассказывается, как выполнять рисование различных изображений. Поэтому мы посвятили гл. 10 рассмотрению API пространства имен System.Drawing, показав использование этих возможностей как независимо, так и интегрированными в систему View. Далее, неотъемлемой частью современной жизни является распределенная обработка данных, и мы посвятили несколько страниц способам, с помощью которых компьютеры могут взаимодействовать в С#. Мы показываем, как эти способы позволяют с легкостью реализовывать даже весьма сложные программы.
Наш подход Настоящая книга выполняет свои обучающие функции с помощью смеси равных долей формализма и примеров. Каждый новый структурный компонент или встроенный тип описывается сначала в виде «формы» — таблицы, в которой даются синтаксис и семантика объекта, затем приводятся простые примеры и, наконец, рассматривается полный текст работоспособной программы. Таких программ насчитывается в книге более 50; некоторые из них развиваются по мере перехода от главы к главе, демонстрируя, как новые средства позволяют получить более полное решение исходной задачи. Важными аспектами программирования являются графический интерфейс пользователя (GUI) и тестирование программ. Везде, где можно, для получения тестовых данных используется генератор случайных чисел, что позволяет должным образом испытать программы и получить разумный объем выходных данных. Никакой язык нельзя представить в виде набора изолированных друг от друга средств, и С# не является исключением. При рассмотрении некоторых средств, таких как вывод, строки, циклы и массивы, мы использовали «спиральный» подход, сначала вводя новые понятия в элементарной форме, и лишь в дальнейшем завершая их полное описание. Хороший учебник должен быть максимально интерактивным, и мы предусмотрели ряд дополнительных средств обучения как в самой книге, так и на соответствующем Web-сайте. Эти средства включают в себя:
1_0
Предисловие
• ряд приложений с перечнем ключевых прикладных интерфейсов и средств языка, объединенных в группы для облегчения поиска; • контрольные вопросники с альтернативными ответами в конце каждой главы 1 ; • разнообразные упражнения разного уровня сложности в конце большинства глав; • все примеры, доступные в сети по адресам http://www.cs.up.ac.za/ csharp или http://www.booksites.net/bishop; • полная система Views, со справочником и примерами; • дискуссионный форум для вопросов, касающихся С#, книги и Views; • набор слайдов и ответов для преподавателя.
Система Views Система Views представляет собой специальный класс для создания прикладных GUI независящим от поставщика способом с использованием спецификации XML. Система была разработана авторами в качестве проекта в рамках инициативы Rotor, организованной корпорацией Microsoft в апреле 2002 г. Система тестировалась студентами первого и второго года обучения, которые с энтузиазмом использовали ее в качестве альтернативы обычному программированию посредством API Windows.Form или Visual Studio. Последнюю версию Views можно переписать по сети с Web-сайта этой книги по адресу http://www.cs.up.ac.za/csharp или http:// www.booksites.net/bishop.
Благодарности Мы хотели бы поблагодарить за значительный практический вклад в эту книгу и в систему Views студентов Polelo Group в Университете Претории — Бэзила Уорралла (Basil Worrall), Катрину Берг (Kathrin Berg), Джони Ло (Johny Lo), Дейвида-Джона Миллера (David-John Miller), Мэнди ван Шалквик (Mandy van Schalkwyk) и Сфисо Щабалала (Sfiso Tshabalala). Глубокая благодарность Эдвину Пиру (Edwin Peer), который разработал и обслуживал Web-репозиторий, столь хорошо поддерживающий эту книгу. В Университете Виктории Джонатан Мейсон (Jonathan Mason) помог найти и устранить ошибки в программах View, Раджвиндер Пейнсар-Уэйлаведж (Rajwinder Panesar-Walawedge) отвечал за реализацию варианта View для системы Rotor, а Бернард Шольц (Bernhard Scholz) (в творческом отпуске, предоставленном Венским Техническим Университетом) Издательство сочло целесообразным дополнить русское издание книги списком правильных ответов. — Прим. ред.
Предисловие
1_1_
работал над важными программами клиентской части Views, которые будут выпущены позже. Нам хотелось бы поблагодарить корпорацию Microsoft за их щедрый дар в поддержку этой книги и системы Views, а Дж. Бишоп особо отмечает инициативу и поддержку со стороны Лианн Скотт-Уилльямс (Leanne Scott-Williams) из корпорации Microsoft (Южная Африка). Эта работа также поддерживалась грантом THRIP из южноафриканского фонда National Research Foundation. H. Хорспул также благодарит Памелу Лоз (Pamela Lauz) из Microsoft Canada за поддержку. Выполняя совместную работу при наличии 10-часовой разницы во времени, мы не могли бы выжить без превосходной интернет-поддержки, предоставленной нашими кафедрами в Университетах Претории и Виктории, а также Microsoft Research Laboratory в Кембридже и Венском Техническом Университете, где эта книга была завершена. Дж. Бишоп особо благодарит д-ра Пола Мира (Paul Meyer) и штат Адденбрукского госпиталя в Кембридже за то, что они поставили ее на ноги в тот момент, когда работа над книгой дошла до завершающих стадий. Поскольку значительная часть работы над книгой выполнялась в необычное время суток, Н. Хорспул хотел бы поблагодарить Torrefazione Italia за весьма необходимый и прекрасно приготовленный кофе, a KBSG Seattle за предоставление удачной фоновой музыки. Дж. Бишоп также ценит вклад винной индустрии Кейптауна и Late Late Show от Classic FM. Наконец, наша общая признательность нашим семьям за их терпение в течение многих часов, проведенных нами дома в работе над этим проектом, а также за стимулирующие обсуждения, в которых они, такие же компьютерно-ориентированные, как и мы сами, могли принимать участие. Мы надеемся, что наши семьи будут так же довольны получившимся результатом, как и мы сами. Джудит Бишоп Претория, Южная Африка и Кембридж, Англия Найджел Хорспул Виктория, Канада и Вена, Австрия Август 2003 г.
Введение
Изучение техники программирования — увлекательнейшее занятие. Столько надо понять перед тем, как удастся написать хотя бы простейшую программу! В первой главе мы постараемся дать именно эти начальные знания. Мы обсудим роль языков программирования и их развитие во времени. Общее направление их совершенствования — позволить программисту меньше думать о деталях и сосредоточиться на решении поставленной перед ним задачи. Для ввода программы в компьютер и ее запуска должны быть предусмотрены какие-то средства, и мы рассмотрим две имеющиеся возможности. Некоторое внимание будет также уделено разработке программного обеспечения.
1.1 Преамбула Любая достаточно развитая технология неотличима от магии. Артур К. Кларк Любой пользователь компьютера, наверное, полностью согласится с этой цитатой знаменитого писателя-фантаста. (Приведенное высказывание известно также под именем Третьего Закона Кларка.) С момента появления электронных компьютеров в начале 40-х гг. управляющее их работой программное обеспечение становилось все более сложным и изощренным. На сегодня оно достигло такой степени сложности, что его функционирование представляется недалеким от магии. Программное обеспечение компьютера приобрело такой объем, что никакой человек не может охватить его целиком. Не следует даже пытаться разобраться во всех деталях его работы. Рассмотрим, однако, некоторую аналогию. Допустим, что мы изучаем юриспруденцию. Количество законов, принятых в стране, и сложность этих законов также превышают возможности их понимания конкретным человеком. Студент-юрист и не пытается запомнить все законы. Вместо этого он постарается изучить основные принципы права, построит некоторую систему знаний, касающихся юриспруденции в целом, освоит специфический язык, используемый в текстах законов, научится рассуждать и делать заключения на основе принципов права и, возмож-
1.2 Роль языков программирования
КЗ
но, постарается приобрести глубокие знания законодательства в одной конкретной области юриспруденции (уголовное право, право собственности, налоговое право и т. д.). Изучая программное обеспечение компьютера, мы следуем тому же подходу. Мы рассматриваем базовые принципы организации программ и работы компьютера. Мы знакомимся с языком, на котором составляют программы, учимся применять принципы на практике и, возможно, становимся специалистами в одной узкой области использования программного обеспечения. В этой книге мы концентрируем наше внимание на единственном языке программирования, называемом С# (произносится «си шарп»). Мы научимся использовать язык С# для разработки простых прикладных программ. По ходу дела мы также познакомимся с некоторыми сторонами функционирования компьютеров.
1.2 Роль языков программирования Компьютер Компьютер представляет собой электронное устройство, состоящее из нескольких основных узлов. Одним из таких компонентов является память, которая хранит комбинации единиц и нолей. Эти единицы и ноли могут физически реализовываться в виде крошечных электронных устройств (в которых протекание тока в одном направлении означает единицу, а в другом — ноль, или устройство может содержать конденсатор, заряженное состояние которого означает единицу, а разряженное — ноль) или микроскопических магнитных полей (направление магнитного потока по часовой стрелке может означать единицу, а против часовой стрелки — ноль), или отражающей способности миниатюрных углублений в металлическом слое вращающегося компакт-диска, или различных состояний любого физического объекта. Возможности здесь неисчерпаемы. Компьютер содержит блоки памяти разных видов, работающие по-разному и предназначенные для выполнения различных задач. К счастью, детали функционирования памяти компьютера обычно не важны для программиста. Программа манипулирует с комбинациями единиц и нолей безотносительно к их физической реализации. Программа использует эти комбинации для обозначения чисел в двоичной форме или букв алфавита, или любой другой информации в соответствии с нашей схемой ее представления. Другим стандартным компонентом компьютера является центральный процессорный узел (central processor unit, CPU) или просто процессор. Процессор может выполнять элементарные действия над комбинациями единиц и нолей в памяти компьютера. Например, процессор может заглянуть в две ячейки компьютерной памяти, извлечь из них комбинации, представляющие два числа, сложить эти числа и сохранить результат в виде новой комбинации единиц и нолей в другой ячейке памяти.
14
Глава 1. Введение
Невероятные способности и кажущееся волшебство функционирования компьютера проистекают из способа управления процессором. Процессору необходимо сообщить, из каких ячеек памяти ему надо извлечь числа; ему следует указать, что числа надо сложить (или, возможно, умножить или вычесть, или выполнить какую-либо другую арифметическую операцию); наконец, процессор должен знать, в какую ячейку памяти отправить результат. После того, как процессор выполнит описанные операции, ему следует сообщить, что делать дальше. Последовательность действий, которые мы хотим получить от процессора, определяется длинной последовательностью единиц и нолей в памяти компьютера. На рис. 1.1 изображена реальная комбинация единиц и нолей, которая заставляет компьютер с процессором Intel Pentium извлечь два числа из конкретных ячеек компьютерной памяти, сложить эти числа и сохранить их сумму в третьей ячейке. Приведенная комбинация не очень наглядна, но это именно то, что понимает компьютер. Комбинации на рисунке изображены в виде трех строк, потому что в них заключено описание трех различных команд, которые последовательно, друг за другом, говорят процессору, что ему делать. 10100001 00000011 10100011
01101000 00000101 01110000
10111100 01101100 10111100
01000001 10111100 01000001
00000000 01000001 00000000
00000000
Рис. 1 . 1 . Двоичная программа сложения двух чисел для процессора Intel Pentium
Пропуски между группами из восьмерок единиц и нолей соответствуют организации памяти в компьютере — в компьютерах с процессором Intel Pentium (как, впрочем, и почти во всех остальных) память разделена на группы таких восьмерок, которые называются байтами или, реже, октетами. Индивидуальные единицы и ноли известны под названием битов; этот термин (bit) является сокращением от слов binary digits (двоичные разряды). Комбинация из 16 байт, показанная на рисунке, является, между прочим, маленьким фрагментом значительно более длинной компьютерной программы. Ведь крайне маловероятно, что мы захотим написать программу, которая выполнит всего лишь одно сложение и после этого завершится.
Представление данных Память компьютера организована в виде групп битов. Почти во всех используемых сегодня компьютерах эти группы состоят из 8, 16, 32, часто 64 и иногда 128 бит. С помощью двоичной системы счисления группы битов можно использовать для представления целых чисел. Например, 8-битовая группа 01101011 представляет целое число 0х2 7 + 1х26 + 1х25 + 0х2 4 + 1х23 + 0х2 2 + 1x2* + 1x2°,
1.2 Роль языков программирования
15
которое равно 0+64+32+0+8+0+2+1, или 107. Процессор компьютера с легкостью может манипулировать с числами, записанными в двоичной системе. Однако в действительности этот вопрос оказывается не таким простым, так как компьютеры должны уметь обрабатывать и отрицательные числа. Кроме того, с помощью специальной системы представления, называемой плавающей точкой, компьютеры могут обрабатывать числа, имеющие очень большую величину, например, 6,023x1023. Различные формы представления чисел будут подробнее рассмотрены в гл. 3. Хотя первые компьютеры разрабатывались исключительно для выполнения операций над числами, компьютерные программисты быстро поняли, что компьютеры могут с таким же успехом обрабатывать тексты, написанные, например, на английском языке. Достаточно назначить каждому алфавитному символу некоторое число, и память компьютера сможет хранить фрагменты текста. Обычно для кодирования символов используется таблица ASCII (American Standard Code for Information Interchange). В соответствии с этой таблицей каждому символу назначается код в диапазоне от 0 до 127; этот код удобно размещается в байте, причем остается еще один неиспользуемый бит. Согласно таблице ASCII, буква «А» (латинская) имеет код 65, буква «В» — 66 и т. д. Слово «BANANA» запишется в виде такой последовательности из 6 целых чисел: 66 65 78 65 78 65 и займет в памяти компьютера 6 последовательных байтов. Компьютеру редко приходится выполнять арифметические операции над числами, представляющими текст (пожалуй, разумным примером является прибавление 1 для получения следующей буквы алфавита). Однако символы можно перемещать с одного места на другое, а также сравнивать друг с другом. Например, поскольку компьютер может определить, что 65<66, он может упорядочить список английских слов, правильно поместив APPLE перед BANANA. Язык С# поддерживает кроме ASCII еще и расширенную кодировку символов, называемую Unicode. Согласно этой системе кодирования каждому символу назначается 16-битовое число. При наличии такого количества битов различающиеся коды можно присвоить символам многих разных языков (греческого, русского, японского и др.), а также акцентированным буквам (а, а, а и т. д.). Использование кодов символов и символьных строк будет подробнее рассмотрено в гл. 4.
Языки ассемблеров На заре эпохи компьютеров программисты действительно составляли программные предложения из битов, получая программы наподобие той, что была изображена на рис. 1.1. Эти программы вводились в память компьютера установкой в верхнее или нижнее положение переключателей на передней панели компьютера. Легко представить, какая это была медлен-
_16
Глава 1. Введение
ная и утомительная работа, и как легко было ввести в компьютер ошибочный код. Однако очень скоро компьютерные инженеры сообразили, что компьютер сам может помочь программисту. Действительно, если CPU манипулирует комбинациями единиц и нолей в памяти, а программа и состоит из таких битов, то почему бы не заставить компьютер помогать в создании его собственных программ? Первым шагом в этом направлении было изобретение языка ассемблера. Язык ассемблера представляет собой язык, который позволяет программисту использовать для передачи компьютеру указаний, какие действия ему следует исполнять, не комбинации битов, а слова, или мнемонические обозначения. Три команды языка ассемблера, соответствующие двоичным комбинациям, показанным на рис. 1.1, выглядят следующим образом: mov add mov
eax,dword p t r ds:[0041BC68h] eax,dword p t r ds:[0041BC6Ch] dword p t r ds:[0041BC70h],eax
Хотя для непосвященного эта запись выглядит так же непонятно, как и коллекция битов, однако программист, знающий язык ассемблера, может с легкостью читать и писать такого рода команды. Это просто вопрос практики. Приведенные команды говорят процессору, что он должен: переместить число из ячейки памяти 0041ВС68 в регистр с именем еах (регистры обычно используются для временного хранения чисел); прибавить число из ячейки 0041ВС6С к числу в регистре еах, оставив результат в еах; затем переместить число из еах назад в память, в ячейку ОО41ВС7О. Ячейкам памяти даются номера, и при составлении программы на языке ассемблера эти номера обычно записываются в системе счисления с основанием 16 (шестнадцатеричной системе), вместо системы с основанием 10 (десятичной системы), которую мы используем в повседневной жизни. Последовательность знаков 0041ВС68 представляет собой шестнадцатеричное число. Буква h, которой завершается каждая комбинация шестнадцатеричных цифр, используется в языке ассемблера для того чтобы можно было отличить шестнадцатеричные числа от десятичных. Мы еще столкнемся с шестнадцатеричными числами, когда будем обсуждать кодировку Unicode. Строки программы на языке ассемблера считываются, как входные данные, компьютерной программой, называемой символическим ассемблером (или для краткости просто ассемблером) и преобразуются ею в последовательности битов, которые может понять процессор. Результат этой обработки, представляющий собой длинную последовательность битов (готовую к исполнению программу), обычно сохраняется в виде компьютерного файла на жестком диске. Если мы хотим выполнить эту программу, мы пользуемся другой программой, обычно называемой загрузчиком, который копирует последовательность битов из компьютерного файла в память, а затем заставляет процессор приступить- к исполнению заключенных в программе команд.
1.2 Роль языков программирования
|7
Переход к языкам программирования Приведенный выше пример фрагмента программы на языке ассемблера обычно записывается программистом таким образом: mov
eax,b
add
еах,с
mov
а,еах
//Получить число из ячейки Ь, поместить его в //регистр еах //Прибавить число из ячейки с к содержимому //регистра еах //Сохранить содержимое регистра еах в ячейке а
Здесь для обозначения конкретных ячеек памяти использованы три символических имени (a, b и с). В каком-то другом месте программы на языке ассемблера программист должен определить, какие ячейки памяти соответствуют этим трем именам и сколько байтов должно быть в каждой ячейке. Например, программист может включить в программу следующие три директивы, которые транслятор с языка ассемблера использует для резервирования ячеек памяти: a:
.word 0
b: с:
.word 37 .word 15
#3арезервировать одно слово памяти и занести в #него число О #Аналогично, но с начальным значением 37 #Аналогично, но с начальным значением 15
Не составит большого труда представить себе более совершенный вид языка ассемблера, который позволит программисту вместо трех директив резервирования памяти и трех команд языка ассемблера, описывающих сложение, написать такого рода объявления ячеек памяти вместе с предложением, описывающим требуемые действия: int а int b int с а = b
= = = +
0; 37; 15; с;
//Объявить а, как число из 4 байтов, начально 0 //Аналогично для Ь, инициализировать числом 37 //Аналогично для с, инициализировать числом 15 //Сложить b и с, занести результат в а
Приведенные строки гораздо лаконичнее и, с точки зрения читателя-человека, нагляднее. Они являются примером языка программирования. Последняя строка, называемая в языках программирования предложением, использует для операции сложения знак « + », как и в обычных арифметических выражения. Символ « = », однако, не эквивалентен в этом предложении слову равно. Он означает изменить а, сделав его равным результату операции, указанной справа от знака равенства. Если мы будем читать это предложение вслух, мы, скорее всего, скажем «присвоить ячейке а значение b плюс с». Символ равенства «=» носит название оператора присваивания или назначения. Первые языки программирования были, в сущности, лишь слегка замаскированными языками ассемблера. Программист мог без особого труда представить себе последовательность предложения языка ассемб-
Глава 1. Введение
лера, соответствующюю каждому предложению языка программирования. Преобладала точка зрения, что язык программирования всего лишь обеспечивает программисту некоторые удобства. С течением лет, однако, языки программирования стали значительно более изощренными. Теперь уже только специалист сможет определить, какая последовательность команд языка ассемблера соответствует типичному предложению языка программирования. Но зачем нам выяснять, какую последовательность команд выполняет компьютер, реализуя действия, описанные в программном предложении? Если получается именно тот результат, который мы ожидали, то больше нем ничего и не нужно. Программист, составляя программу, пользуется языком программирования, а не предложениями языка ассемблера (и уж, тем более, не теми комбинациями битов, которые фактически выполняет процессор). По мере развития вычислительной техники были предложены тысячи различных языков программирования. Судя по всему, пока мы еще не приблизились к такому положению, когда все программисты используют один, наиболее продуктивный и естественный язык программирования. Сегодня представляется, что различные предметные области требуют различных языков программирования. Действительно, программа, с помощью которой банк управляет своим финансовым хозяйством, радикально отличается по своей природе от программы, реализующей, например, интерактивную игру. Возможно, две различные предметные области требуют различных языков программирования? Язык С# является самым современным из серии языков программирования, разработка которых началась в 50-х годах, и которые включают в себя Fortran, Algol60, Simula67, PL/1, C, Pascal, C++ и Java (приблизительно в такой временной последовательности). Совершенно ясно, что С# не будет последним изобретенным языком программирования. Однако в качестве средства для разработки программ общего назначения это на сегодня один из лучших языков. Он отличается последовательностью и полнотой, которых не было в предшествующих ему языках. C++, Java и С# заимствуют из языка Simula67 концепцию, называемую классом. Класс является базовой конструкцией объектно-ориентированного программирования (ООП). Хотя доказать это трудно, но многие считают, что программу, написанную в объектно-ориентированном стиле, впоследствии легче модифицировать, добавляя к ней новые возможности. Это качество носит названия расширяемости. Принципы ООП также способствуют повторному использованию программного обеспечения, которое позволяет программистам разрабатывать новые программные продукты, используя при этом фрагменты других программ.
Синтаксис и семантика Программы должны следовать синтаксическим правилам языка программирования. В настоящей книге синтаксические правила языка С# приводятся в виде таблиц, схожих с приведенным ниже примером из гл. 4.
1.3 О компиляции
19
Цикл while while (условие) тело цикла
{
Оценить условие, и, если оно удовлетворяется (результат оценки дает истину, true), выполнить предложения в теле цикла. Далее снова оценить условие, и если оно удовлетворяется, снова выполнить тело цикла. Повторять этот процесс до тех пор, пока условие не перестанет удовлетворяться (результат оценки дает ложь, false).
Мы будем называть такие таблицами формами, потому что они дают форму конструкций языка С#. Имя конструкции дается в заголовке формы, ниже приводится ее синтаксис, а еще ниже — краткое описание. Слова в синтаксической части, выделенные курсивом, представляют другие конструкции С#, синтаксис которых можно найти в соответствующих формах . Описание позволяет понять смысл конструкции — при изучении языков программирования для обозначения смысла языковой конструкции часто используется слово семантика. Полный список форм приведен в Приложении А.
1.3 О компиляции Компиляция программы Программа на языке С# может содержать предложения подобные следующим: int b с
a,
b,
= 23; = 3*4;
с;
//Объявить //Занести //Занести
a,
b и с,
как целочисленные
переменные
число 23 в b 12 в с
а = b + с; //Прибавить b к с и сохранить результат в а Console.WriteLine("Answer = {С}", а ) ; //Вывести результат на экран
Здесь точки обозначают опущенные предложения. Мы можем создать С#-программу, как компьютерный текстовый файл, с помощью любого текстового редактора, например, Notepad или TextEdit. Текстовый файл с исходным текстом программы должен быть преобразован в последовательность битов (машинные команды), которые будут понятны компьютеру. Процесс этого преобразования носит название компиляции, а программа, выполняющая это преобразование, называется компилятором.
А также условные обозначения элементов данной конструкции, которые при ее использовании должны быть заменены на имена конкретных идентификаторов. — Прим. перев.
20
Глава 1. Введение
Предположим, что мы создали С#-программу и сохранили ее на нашем компьютере в файле с именем myprogram.cs. Если наш компьютер управляется системой Windows, мы можем, открыв командное окно (окно MS-DOS), запустить компилятор С#, введя команду esc
myprogram.cs
Если все идет, как должно, и компилятор С# не встретился с какими-либо трудностями, он создаст новый файл с именем myprogram.exe. Этот файл называется выполнимым, потому что он содержит машинные команды, распознаваемые процессором. Если далее в окне команд ввести имя файла myprogram.ехе
(суффикс «.ехе» можно опустить при вводе имени файла в качестве команды), это заставит Windows загрузить файл myprogram.exe в память компьютера и инициировать его выполнение процессором. Командное окно, в котором будут видны команды на компиляцию и запуск программы, а затем и результат ее выполнения, может выглядеть так, как показано на рис. 1.2. • '• Command Pror» Microsoft Windows XP [Uersion 5.1.26003 Copyright 1985-2881 Microsoft Corp. C:\Docuinents and Settings\nigelh>d: E>:\>cd programs D:\prograros>cse nyprogram.es Microsoft Uisual Ctt .NET СоюрНег version 7.00.9372Л for Microsoft .NET Framework version 1.8.3785 Copyright Microsoft Corporation 2001. fill rights reserved. D :\pro grants >myprogram.exe ftnswer ж 3S D:\pro grants >
Рис. 1.2. Компиляция и запуск программы в командном окне
Если при выводе на рабочий стол Windows содержимого папки в ней не указаны расширения имен файлов, следовательно, у вас включен режим «Hide extensions for known file types» [He отображать расширения MS-DOS для файлов зарегистрированных типов]. Мы настоятельно рекомендуем отключить этот режим, чтобы избежать путаницы, когда два файла имеют одинаковое имя, но различные расширения. (В противном случае единственным указанием на то, какое имя к какому файлу относится, будут различные пиктограммы, обозначающие типы файлов.)
1.3 О компиляции
21_
Само командное окно является запускаемой нами программой (ее часто называют Command Prompt, системный запрос). В стандартной Windows-системе, работающей под управлением Windows XP, этой программе соответствует файл C:\WINDOWS\system32\cmd.exe. Command Prompt является весьма специфической программой, которая, среди прочих действий, читает команды, введенные с клавиатуры, отображает эти команды на экране в командном окне, загружает в память программы, соответствующие этим командам, и организует их запуск. Программа Command Prompt сложна, но все-таки это просто программа.
Динамичная компиляция Хотя во всех случаях исходный текст программы, чтобы ее можно было выполнить, должен быть преобразован в последовательность машинных команд, однако нет необходимости выполнять это преобразование заранее. Язык Java получил известность, в частности, использованным в нем подходом, носящим название интерпретации. Исходный текст программы на языке Java преобразуется компилятором в формат, называемый байт-кодом Java. Когда вы сохраняете откомпилированный исходный текст Java-программы, вы сохраняете как раз файл с байт-кодом. Этот файл не содержит битовых последовательностей для Intel-компьютера или компьютера любого другого рода. Файл с байт-кодом содержит команды для воображаемого компьютера, называемого виртуальной Java-машиной, а также дополнительную информацию, используемую при отладке программ и при объединении нескольких файлов с байт-кодами с целью образования более сложных программных единиц. Когда Java-программа запускается на выполнение, программа, называемая виртуальной Java-машиной, эмулирует воображаемый компьютер, читая и анализируя файлы с байт-кодами. Эмуляция заключается в извлечении комбинации битов для очередной команды воображаемого компьютера, определения того, какое действие должен выполнить воображаемый компьютер и затем выполнении этого действия. Ячейки памяти в воображаемом компьютере представляются переменными в программе виртуальной Java-машины. Этот подход, интерпретация, обладает серьезными достоинствами. В то время как команды процессора Intel действуют только на Intelкомпьютерах (и даже, возможно, лишь на определенных моделях таких компьютеров), команды байт-кодов могут исполняться на любом компьютере, для которого имеется программа виртуальной Java-машины. Главная причина широкого распространения языка Java заключается в том, что файлы байт-кодов Java можно пересылать вместе с Web-страницами (в виде апплетов Java) и исполнять их на любых компьютерах: PC, Macintosh, рабочих станциях Sun и т. д. К тому же файлы байт-кодов обладают более высокой надежностью, так как команды выполнения нежелательных действий, например, стирания важных файлов, не могут быть скрыты в такой программе. Виртуальная Java-машина перед тем, как приступить к интерпретации и выполнению любых файлов байт-кодов, проверяет их на нарушение защиты.
22
Глава 1. Введение
Интерпретация, однако, влечет за собой снижение производительности. Анализ каждой команды виртуальной машины должен производиться перед каждым выполнением этой команды, а к некоторым командам программа в одном прогоне может обращаться много миллионов раз. Интерпретация теперь уступает место методике, известной как динамичная, или оперативная компиляция. Java-программы обычно исполняются оперативными компиляторами, иногда для краткости называемыми JIT-компиляторами (от just-in-time, оперативно). Сопрограммы всегда исполняются посредством ЛТ-компиляции. Сопрограмма преобразуется компилятором С# в промежуточную форму, носящую название MSIL (от Microsoft Intermediate Language, промежуточный язык Microsoft). В примере на рис. 1.2 файл, названный myprogram.exe, в действительности не содержит машинных команд процессора Intel; он состоит из команд MSIL. Если нас интересуют эти команды, мы можем увидеть их с помощью программы с именем ildasm, поставляемой вместе с Windows. Ввод с клавиатуры команды ildasm
myprogram.exe
выведет на экран окно, подобное показанному на рис. 1.3.
• MANIFEST
.assembly myprogfarn ( " Рис. 1.3. Окно программы ildasm
Если щелчком мыши по тому или иному имени в окне выбрать интересующую нас часть программы, в окно выводится соответствующий блок MSIL-кодов в достаточно удобочитаемом виде, наподобие языка ассемблера. На рис. 1.4 представлен блок, который прибавляет 25 к произведению 3 на 4 и выводит результат на экран.
1.3 О компиляции
23
f.nethotl private tudebysig instance Moid
Go() cil nanaged
2? (8xii>) // Code size .maxstack 2 .locals init (int:32 W в. int32 U 1, Int82 0~2)
IL 8080: IL 0082: IL~8083: IL 0005: IL 8886: IL 0807: IL 8888: IL 0009: IL 880a: IL B00f: IL 6818: ILJJ81S:
Когда мы вводим в командном окне имя файла, например, myprogram. ехе, содержащего коды MSIL, операционная система прежде всего выясняет, к какому типу принадлежит запускаемый исполняемый файл. Если файл содержит MSIL-коды, а операционной системой является Windows, система прежде всего запускает программу, называемую .NET runtime. Именно эта программа несет ответственность за выполнение программы MSIL; она активизирует ЛТ-компилятор для обработки первого по ходу программы блока MSIL-кодов. ЛТ-компилятор транслирует MSIL-коды в эквивалентную последовательность машинных команд для нашего компьютера, например команд процессора Intel. Эта последовательность сохраняется в памяти компьютера, и программа .NET runtime передает управление на первую команду этой последовательности. Если оттранслированная часть программы должна передать управление на другую часть, MSIL-коды которой еще не оттранслированы, в действие опять вступает программа .NET runtime, которая активизирует ЛТ-компилятор, после чего передает управление на созданные им машинные команды. Каждый блок MSIL-кодов транслируется оперативно (just-in-time), непосредственно перед его выполнением. Такой подход оказывается весьма эффективным, потому что каждый блок транслируется за время выполнения программы только один раз. В то же время интерпретатор, вроде виртуальной Java-машины, будет, возможно, транслировать один и тот же блок много раз. Даже простейшая компьютерная программа содержит много деталей. Программист должен обеспечить их правильное взаимодействие, иначе программа не будет работать. Язык С# и компилятор С# были разработаны таким образом, что компилятор может надежно проверить
24
Глава 1. Введение
программу на ее внутреннюю согласованность. Если обнаруживается несогласованность, компилятор сообщает об ошибке программисту. На первых этапах разработки программы вам, возможно, придется многократно повторять попытки откомпилировать программу, внося в нее после каждой попытки исправления в соответствии с получаемыми сообщениями об ошибках. Однако после того, как программа успешно пройдет компиляцию, будет самонадеянно думать, что она лишена ошибок. Весьма вероятно, что программа содержит ошибки, которые приведут к выполнению ею непредусмотренных действий. Хороший программист обязательно всесторонне испытает программу, наблюдая ее работу в широком диапазоне входных данных.
Разработка программного обеспечения Процесс разработки программы можно представить себе в виде схемы, приведенной на рис. 1.5. Необходимо отдавать себе отчет в том, что время, потраченное еще до начала фактического программирования на всесторонний анализ поставленной задачи, относится к числу полезных затрат. Эти затраты более чем окупят себя экономией времени, которое будет потрачено на исправление ошибок. Обнаружение фатальных упущений —
Анализ задачи Пересмотр проекта Разработка программы на бумаге -—
Переработка логики программы \ Ввод и редактирование текста программы с помощью текстового редактора Исправление ошибок компиляции*) Компиляция программы Создание тестовых данных Завершение проектирования?
Рис.
1.5. Процесс разработки программного обеспечения
Даже после того, как программа будет закончена, полностью оттестирована и передана пользователям, работа по программированию обычно не завершается. Пользователи могут обнаружить дополнительные ошибки, не выявленные в процессе тестирования. Пользователи могут также потребовать усовершенствования программы или (в предположении, что разрабатывается коммерческий программный продукт) руководство компанией пожелает включить в программу дополнительные возможности, чтобы сделать разработанный продукт более конкурентоспособным. Вся эта дополнительная работа носит название программной
1.4 Интерактивные среды разработки
25
поддержки. Общий объем программной поддержки конкретного программного продукта может оказаться неограниченным. В мире все еще используются значительное количество бухгалтерских программ, разработанных во времена языков Cobol и RPG более 30 лет назад, и большинство этих программ требуют частого пересмотра в силу изменения налоговых правил или других факторов реальной жизни.
1.4 Интерактивные среды разработки Еще с самого начала компьютерной эпохи программисты для компиляции программы и ее запуска использовали команды, вводимые с клавиатуры. Для компьютеров, работающих под управлением операционной системы Unix , это и на сегодня остается обычной процедурой. Однако имеется и альтернатива этому способу — интерактивная среда разработки (IDE, от Interactive Development Environment). На компьютерах с системой Microsoft Windows в качестве такой интерактивной среды для разработки С#-программ можно использовать Visual Studio .NET. На рис. 1.6 показан рабочий кадр IDE Visual Studio .NET в процессе разработки крошечной программы. =
•
.
,'.
•
:
. ' . . . . • • •
-
.
•
•
.
.
.... ..'...V.1
• v- :
'i, ?
> !.wbinj
•
;
yj>
Л&Я
•
% ir. I««-J
i*W..._ » X!
w m X m y p t
»j
Si'1
1 u.
£ X ,,x* 1-я
.-J
Л,;' o g
>.-!?
S r a t i
Я.Ю;
fr
void
b
e
i
ii a,
ill
b,
i;
sline
»»J
//
S', r t «
1*4)
//
(tt
//
Ml
•
" b
••• c ;
CO i s o l e . « t i t s L i n e i " A ™
1. . • 1; f
fШ
»< b
lytd
t h e Bllabtt
К
i
i I i ! t
1 «ям» | ji JL
Try
Ш>
«
:^
»n:J
с 23
« O r *
RtoOH (МЦНМХ ou 8
"
rsfS'lit:
Л.
•' I (.«TttfV "
; B SJ
;:,
i
tV,*? |1СС#вЯ
;•
;
J
Orir*}
"
iv.tllm Stv.rt.--i
;:.
"
-::
. - . ,1 ,. - M V ! « - I
i2L
M ndBv|
V
'f> static
void »
:i I
Sain
Tryi)>0e
:)
jjj
Jj
» Я ;;
1*' j i 1' { tM i -i ^
.... о ^
.'.;*„'.
Рис. 1.6. IDE Visual Studio .NET
3
Под именем Unix мы понимаем все варианты этой системы, включая Linux, FreeBSD и Solaris.
26
Глава 1. Введение
Естественно было ожидать, что программисты создадут инструментальные программные средства, облегчающие труд программиста.Visual Studio является лишь одним примером таких средств. Интерактивная среда разработки типично включает в себя следующие элементы: • интерактивный текстовый редактор, обычно содержащий средства форматирования и облагораживания внешнего вида программы; • интерактивный справочник, предоставляющий информацию о языке программирования и сопутствующем программном обеспечении; • средства разработки графического интерфейса пользователя (GUI); • компилятор; • отладчик, позволяющий выполнять пробные прогоны программы с контролем ее выполнения. Среда IDE вроде Visual Studio .Net предназначена для профессиональных программистов. Хотя она сильно способствует сокращению времени, требуемого для разработки программ, сама она является довольно сложным в использовании программным средством. Перед тем, как приступить к использованию этой IDE, пользователь должен достаточно глубоко разобраться в принципах функционирования компьютера и в особенностях использования конкретной IDE. В этой книге мы будем предполагать, что для создания исходного текста программы используется простой текстовый редактор, а для ее компиляции и прогона — командное окно.
1.5 Начинаем работать на С# Как уже отмечалось, в этой книге предполагается, что вы будете компилировать и запускать программы с командной строки. Рассмотрение и использование интерактивной среды разработки является предметом более профессионального учебного курса или уделом особо любознательных и отважных читателей. Повсюду в этом разделе мы полагаем, что для разработки и запуска С#-программ используется компьютер, управляемый операционной системой Windows (имея в виду под этим названием различные варианты операционной системы, включая Windows ME, Windows NT, Windows 2000 и Windows XP).
Создание и редактирование файлов с исходным текстом Разрабатывая С#-программу, мы создаем ее в виде одного или нескольких текстовых файлов. Рассмотрим простейший случай, когда программа состоит из единственного текстового файла. Этот файл можно создать с помощью любого текстового редактора. В системах Windows стандартный текстовый редактор носит имя Notepad (Блокнот). Можно также использовать программу WordPad. Несложный поиск с помощью Web-браузера позволит обнаружить еще несколько альтернативных программ, имеющих более дружественный интерфейс. Одним из таких достойных всяческого одобрения и, к тому же, бесплатных продуктов является программа
1.5
Начинаем работать на С #
27
EditPadLite, которую можно найти по адресу http://www.EditPadLite.com. С ее помощью были созданы многие программы из этой книги. На рис. 1.7 показан кадр EditPadLite с исходным текстом простейшей Сопрограммы. Файлу следует дать имя (любое) с расширением .cs, которое является сокращением от «CSharp».
class Hello { s t a t i c void Hain() { Console.WriteLine("Surprise!
9: 1 Рис. 1.7. Редактор EditPadLite использован для создания Сопрограммы
Компиляция С#-программы При работе в системе Windows можно использовать программу Command Prompt. Обычно ее можно найти в разделе Start/Programs/Accessories (Пуск>Программы>Сеанс MS-DOS) главного меню. Запуск этой программы приводит к выводу на рабочий стол Windows окна DOS; в это окно можно вводить команды. К этим командам относятся: Команды командной строки (выборка) cd cd путь cd . . d: dir path help help команда
//Вывод имени текущего каталога //Смена текущего каталога на каталог путь //Смена текущего каталога на вышележащий каталог //Смена текущего диска на диск d: //Вывод имен файлов текущего каталога //Вывод значения переменной окружения Path //Вывод списка доступных команд //Получение информации об использовании //указанной команды
Окно Command Prompt в системе Windows позволяет пользователю вводить с клавиатуры команды, напоминающие команды Unix. Многие из этих команд устарели (поскольку восходят к старой операционной системе MS-DOS).
28
Глава 1. Введение
Для командного окна существует понятие текущего каталога (текущей папки). По умолчанию имя текущего каталога выводится в начале каждого нового системного запроса в командном окне. Например, текст C:\Documents
and Settings\thomas>
говорит о том, что текущим является каталог C:\Documents and Settings\ thomas. Если подлежащий компиляции файл с исходным текстом С#-программы расположен в другом каталоге, имеет смысл именно его назначить текущим с помощью команды cd. Заметьте, что если каталог, в который вы хотите перейти, принадлежит другой файловой системе (другому логическому диску), то вы должны прежде всего перейти в эту файловую систему. Например, для перехода в каталог D:\My Work\Files надо использовать такую последовательность команд: d: cd "my. work" cd f i l e s Как видно из этого примера, Windows не различает в именах файлов прописные и строчные буквы. (Однако для систем Unix и MacOS они будут различающимися символами.) Если текущим является каталог с исходным текстом С#-программы, она должна откомпилироваться, если ввести такую команду: esc
/debug:full
hello.cs
где hello.cs является именем файла с исходным текстом. Здесь требуется сделать два пояснения. Во-первых, что собой представляет команда esc, и, во-вторых, что обозначает ключ /debug:full? Команда esc. Мнемоническое обозначение esc не является встроенной в программу Command Prompt командой, как, например, команда cd. Наша задача — активизировать компилятор С#, который представляет собой программу с именем csc.exe; именно это и попытается для нас сделать программа Command Prompt. Если имя, введенное в командной строке, не является встроенной командой, программа Command Prompt ищет на диске выполнимый файл с именем (без суффикса) esc. Но где она ищет этот файл? Выполнимые файлы ищутся только в текущем каталоге, а также в тех каталогах, которые перечислены в строке, являющейся значением переменной окружения Path. Если в ответ на нашу команду программа Command Prompt выведет такое сообщение ' e s c ' i s not recognized as an i n t e r n a l or external command, operable program or batch f i l e , ['esc' не распознано, как внутренняя или внешняя команда, выполнимая программа или командный файл]
1.5 Начинаем работать на С#
-
29
это означает, что компилятор С#, который представляет собой программу с именем csc.exe, не был обнаружен ни в одном из каталогов, перечисленных в строке, являющейся значением переменной Path. Если такое случится в компьютере в системе коллективного доступа (например, в компьютерном классе колледжа), придется поискать системного администратора и попросить его устранить возникшее затруднение. Для этого потребуется проверить правильность установки компилятора С# и если с ним все в порядке, проверить значение переменной Path. Текущую установку этой переменной можно получить с помощью команды path
введенной в командном окне (строчными или прописными буквами). Если дело в переменной Path, то обойти возникшую трудность можно с помощью указания полного пути к файлу компилятора С#. Пусть этот путь таков: С:\Program
(всю на одной строке), и программа Command Prompt активизирует компилятор С#, не выполняя предварительный поиск этой программы в каталогах списка переменной Path. Ключ /debug. Вторая особенность использования компилятора С# заключается в том, что процессом компиляции можно в какой-то степени управлять путем указания соответствующих ключей в командной строке. Полный список этих ключей можно получить, введя команду esc
/help
К счастью, значения ключей, действующие по умолчанию, во всех случаях разумны, и мы, реализуя программы, описанные в этой книге, можем спокойно игнорировать все возможности настройки компилятора. Единственной настройкой, для которой можно посоветовать изменить значение, действующее по умолчанию, является режим отладки, определяемый ключом debug. Указав для ключа /debug значение full, мы требуем от компилятора С# сгенерировать дополнительную информацию, которая приводит к выводу на экран более детальных сообщений об ошибках, если они обнаруживаются в программе (что, к сожалению, является самым обычным делом). Имеется и более короткая форма приведенной выше команды:
Этот путь не является стандартным, поэтому на конкретном компьютере, возможно, придется поискать, на каком именно диске находится файл csc.exe.
30
Глава 1. Введение
esc /debug+ hello.cs
Больп1ие по объему программы обычно собираются из нескольких исходных файлов. Если наша программа состоит из файлов one.cs, two.cs и three.cs, то для получения выполнимого файла следует ввести команду esc
/debug:full
one.cs two.cs three.cs
Результирующий выполнимый файл будет назван one.cs, two.cs или three.cs, в зависимости от того, какой исходный файл содержит стартовую точку программы (которой обычно является фрагмент программы с именем Main — как будет детально объяснено в гл. 2). Если для создания программы необходимо компилировать много исходных файлов, лучше использовать IDE Visual Studio. Ввод на командной строке всех этих имен может оказаться утомительным делом, сопряженным с возможными ошибками. Если же программа состоит всего лишь из нескольких файлов, то вполне можно обойтись компиляцией с командной строки.
Запуск и выполнение откомпилированной программы Если компилятор С# не обнаружил в исходном тексте С#-программы каких-либо ошибок, он создаст выполнимый файл. В системах Windows имя по умолчанию для выполнимого файла совпадает с именем исходного файла, но суффикс .cs заменяется на .ехе. Если, например, исходный файл назван hello.cs, то выполнимый файл получит имя hello.exe. Для запуска полученной программы достаточно ввести ее имя на командной строке. Это имя можно ввести как без суффикса .ехе hello
так и с суффиксом hello.exe
Если в состав программы входили несколько исходных файлов, то запустить надо тот, в котором находится метод Main. В следующей главе мы детальнее познакомимся с методом Main и с другими частями С#-программы.
Основные понятия, рассмотренные в гл. 1 В этой вводной главе были затронуты некоторые аспекты истории языков программирования, идея языка ассемблера и собственно языков программирования, а также понятие компилятора, назначением которого является трансляция программы в двоичный код, исполняемый компьютером. Была также рассмотрена процедура подготовки выполнимой программы с помощью программ текстового редактора и компилятора, вызываемых в командном окне.
Контрольные вопросы
31
Понятия, затронутые в гл.1: программирование
язык ассемблера
двоичная система
шестнадцатеричная система
кодирование символов
оператор присваивания
интерактивная среда разработки
запуск программы
Рассмотренные операторы:
Определения и синтаксические формы: языки программирования
определение компьютера
команда esc
команды командной строки (выборка)
разработка программ
цикл while
Контрольные вопросы Контрольные вопросы в этой книге снабжены альтернативными ответами. Приводимые ниже пять вопросов позволят вам проверить усвоение вами основных понятий этой главы. 1.1. Сколько может быть различных комбинаций битов в одном байте? (а) 64
(б) 16
(в) 8
(г) 256
1.2. Если в регистре еах содержится число 7, то что, по вашему мнению, будет содержаться в этом регистре после выполнения приведенной ниже команды Intel Pentium? add
еах,еах
(а) невозможно определить
(б) 7
(в) 14
(г) 8
1.3. Какое десятичное число представляет двоичное число 0011111? (а) 63
(б) 31
(в) 111111
(г) 6
1.4. Если код ASCII буквы «А» равен 65, код «В» — 66 и т. д., какая строка текста представлена приведенной ниже последовательностью чисел:? 72
69
76
76
79
32
• (a) hello (в) OLLEH
Глава 1. Введение (б) GEKKO (г) HELLO
1.5. Если в системе Windows текущим является каталог C:\a\b\c\d, в какой каталог мы попадем после выполнения в командном окне приведенной ниже цепочки команд? cd cd cd cd
. . . . с flub
(a) C:\a\b\c\d\flub
(6) C:\a\b\c\flub
(в) C:\a\c\flub
(г) C:\c\flub
Упражнения Упражнения заключаются в том, что вы должны что-то сделать. В последующих главах упражнения потребуют от вас составления или модификации С#-программ. Поскольку мы пока еще не дошли до практического программирования, предлагаемые ниже упражнения попроще. 1.1. Для каких языков программирования есть компиляторы на вашем компьютере? Перечислите все, которые вам удастся найти. 1.2. Является ли программа Command Prompt на компьютере с системой Windows компилятором? Обоснуйте ваш ответ. 1.3. Каждый символ (буква, цифра, знак препинания и т. д.) требуют в кодировке Unicode двух байтов памяти. Оцените, сколько потребуется байтов для хранения символов, из которых состоит вся эта книга. 1.4. Распознает ли ваш текстовый редактор синтаксис языков программирования с окраской различных элементов языка в разные цвета? Можете ли вы установить такой режим для С#? 1.5. Определите, сколько памяти имеет ваш компьютер и какой объем памяти занимают ваш компилятор С#, редактор и IDE.
Использование объектов 2
Наше путешествие в мир программирования мы начнем с обзора принципов объектной ориентации, после чего перейдем к реализации этих принципов на С # . Для создания объектов мы будем использовать типы, уже существующие в языке и служащие для описания дат, изображений, строк, целых чисел и случайных последовательностей. Типы включают в себя данные и функции, и мы прежде всего остановимся на четырех видах функций — конструкторах, свойствах, операторах и методах. Будут рассмотрены ввод и вывод на уровне строк и изображений, а также базовые принципы форматирования выводимой на экран информации. С помощью нескольких примеров использования объектов в программах мы проиллюстрируем основы объектной ориентации.
2.1 Введение в объекты Что такое объект? Такой вопрос часто задается, и весьма существенно получить на него ответ. Объекты являются краеугольными камнями программ, написанных на современном языке типа С#. Такие языки предназначены для разработки объектов, взаимодействующих друг с другом предопределенным образом. В этом разделе мы рассмотрим объекты в общем виде, вместе с присущими им типами, а в следующем разделе перейдем собственно к языку С#. Неформально объект является представлением чего-то, существующего в реальном мире, с отображением и того, чем он является, и того, что он делает. Рассмотрим рис. 2.1. На этой фотографии мы можем идентифицировать многие объекты реального мира, например, суда, деревья, строения и людей. Сами эти объекты, очевидно, не могут существовать в компьютере. Функцией компьютера является обработка информации. Информация представляет собой абстрактное понятие, в то время как объекты на фотографии являются физическими объектами реального мира. Программа же сохраняет и обрабатывает информацию об объектах реального мира, причем она может это делать весьма аккуратно и чрезвычайно быстро. Для объекта на фотографии, например, судна на переднем плане, компьютер может записать и затем обновлять данные о таких деталях, 2—2047
34
Глава 2. Использование объектов
Рис. 2 . 1 . Объекты
как регистрационный номер судна, количество перевозимых им пассажиров или имя владельца. Эта информация, скорее всего, будет собрана в программе в одном месте в виде связной совокупности, и эта совокупность информации для конкретного судна может рассматриваться, как объект. Объект судна в программе является проекцией объекта реального мира, и данные в этом объекте отражают лишь незначительную долю всех деталей, которые мы обычно связываем с реальным судном. Манипуляции с объектом судна в программе могут включать в себя, например, смену владельца судна в случае его продажи. Очевидно, что объект в компьютерной программе не является объектом в физическом смысле слова. В самом деле, некоторые объекты в программе могут и не соответствовать никаким объектам реального мира, а лишь некоторым неосязаемым или абстрактным концепциям. Например, мы можем создать объект, который будет помнить, как индивидуальный пользователь предпочитает организовать изображение на компьютерном экране: будут ли значки программ расположены столбиком с левой стороны экрана или вдоль его верхней кромки, какие значки следует вывести на экран, и какой вид будет иметь курсор? Возвращаясь к рис. 2.1, представление изображенной на рисунке фотографии может храниться в памяти компьютера в виде единиц и нолей (их будет очень много!). Самой фотографии в компьютере не будет; только ее представление и некоторая дополнительная информация, необходимая для обеспечения возможности манипуляций с изображением. Эта дополнительная информация обычно включает в себя ширину, высоту и разрешение. Совокупность такой информации образует объект
2.1 Введение в объекты
35
изображения. Программа может осуществлять манипуляции с изображением, изменяя его размер, поворачивая или отражая зеркально, а также посылая его на принтер для вывода на бумагу.
Формализм объектов Рассмотрим более формальное определение объектов, поскольку оно используется в программировании. Объект Объект в программе представляет собой реализацию определенного понятия. Объект представляется совокупностью данных, связанных с понятием, и функций, к которым он имеет доступ с целью установки, изменения или удаления его собственных данных, а также данных, входящих в другие объекты.
Термины данные и функции будут далее рассмотрены во всем объеме. Для нашего объекта изображения данными будут фактические пикселы изображения, плюс его ширина, высота и разрешение, а функции, приложимые к этому объекту, будут включать в себя изменение размеров, зеркальное отражение, вращение и т. д. В качестве другого примера объекта рассмотрим дату рождения. Данными для такого объекта будут значения года, месяца и дня. В качестве функций объекта даты рождения можно предложить следующие действия: • способность вычитать его из другого объекта даты (скажем, из текущей даты), чтобы получить число лет (возраст); • сравнение с другим объектом даты с целью выяснения их равенства; • передача объекта другому объекту, который имеет возможность выводить значения данных и, таким образом, отображать их на экране. Во всех этих трех случаях объект может взаимодействовать с другими объектами через свои функции.
Типы Обычно программы должны манипулировать многими схожими объектами. Например, мы может иметь программу, осуществляющую манипуляции с сотнями разных изображений различающихся размеров. Однако все эти различные изображения схожи в том отношении, что любое из них мы можем поворачивать, зеркально отражать, изменять размеры или выводить на печать. Объекты изображений оказываются почти взаимозаменяемыми. В этом случае естественно рассматривать все эти объекты изображений, как принадлежащие типу изображения. В программировании каждый объект должен принадлежать определенному типу. В программе может быть определено несколько типов, и такая программа может содержать несколько разных видов объектов.
36
Глава 2. Использование объектов
Тип Тип предоставляет определение данных и функций, требуемых для: •
описания некоторой группы объектов или
•
выполнения специфической задачи.
Типы, содержащие и данные, и функции, могут описывать группы объектов. Примеры таких типов: • тип даты (Date на рис. 2.2), данные которого содержат значения дня (day), месяца (month) и года (year) конкретной даты, и который предоставляет функции для сложения дней (AddDays), сравнения дат (Compare) и т. д.; • тип числа (Number на рис. 2.2), данные которого содержат требуемое значение (Value), и который предоставляет обычные функции для действий над числами: сложения, вычитания, умножения, сравнения и т. д. Некоторые типы предоставляют только функции, и тогда мы не может объявлять объекты таких типов, а должны лишь использовать их функции. Примерами могут служить типы, осуществляющие: • математические операции (Math на рис. 2.2), например, извлечения квадратного корня или вычисления синуса или косинуса; • организацию ввода и вывода данных (InputOutput на рис. 2.2) для взаимодействия программы с внешним миром. Рис. 2.2 показывает в общем виде, как могут выглядеть эти типы. Каждая рамка изображает определенный тип и начинается с его имени, за которым следуют сначала данные, а затем функции. В следующем разделе мы рассмотрим, как такие типы определяются и используются конкретно в С#. Мы также увидим, почему данное «value» для типа Number выделено курсивом. Date
Number
day month year
Value
AddDays Compare
*
И
+
И
Т.Д.
Math
InputOutput
Sgrt Sin Cos
Read Write
И
И
Т.Д.
Т.Д.
Т.Д.
Рис. 2.2. Примеры типов
Теперь, чтобы получить всю картину, посмотрим, как могут выглядеть объекты, созданные для первых двух типов (рис. 2.3). Объекты типа Date имеют имена — birth [дата рождения] и marriage [дата брако-
2.1 Введение в объекты
37
сочетания], а также значения каждого данного, указанного в определении типа. Функции в объектах не показаны, потому что они используются совместно всеми объектами данного типа; следовательно, их уместнее показать в самих типах. Созданные таким образом объекты носят название экземпляров данного типа. В С# обычно имена типов начинаются с прописных букв, а имена объектов — со строчных. Date
Number
day month year
Value +
*
AddDays Compare и т.д. /
ч
я* :birth 1970 11 14
и
\
/ :marriage
-19
т.д.
4 3.14159
2000 2 25
Рис. 2.З. Объекты, созданные из типов
Объекты Number выглядят несколько иначе. У них нет имен, и на рисунке показаны лишь их значения. Дело в том, что в большинстве языков (и в С#) предусмотрены встроенные типы данных, соответствующие типам самого компьютера, в частности, числовые типы, например, целые. Такие типы называют простыми типами. Поэтому, например, тип для числа позволяет указать одно числовое значение (скажем, -19); этот тип соответствует одному из числовых типов компьютера, однако его значение может быть указано в обычной форме десятичного целого числа. Функции простого типа представлены встроенными операторами + , — и др. В гл. 3 мы рассмотрим числа в С# более подробно. Простой тип Простой тип обычно: • • • •
представляет данное, имеющее одно значение; непосредственно соответствует встроенному типу компьютера; имеет предопределенный набор значений в языке; использует встроенные операторы языка в качестве своих функций.
38
Глава 2. Использование объектов
К другим простым типам, имеющим эквиваленты в самом компьютере, относятся: • булев (или логический) тип, который принимает два значения, true (истина) и false (ложь), записываемые в виде слов, а не чисел, и которому соответствуют логические операторы вида and (И), or (ИЛИ) и др. Внутри компьютера булевы значения могут представляться одним битом; • символьный тип, который позволяет представить буквы, цифры и другие символы, в основном те, которые имеются на клавиатуре. Большая часть этих символов вводится в программу в апострофах, например, ' К ' или '8'. Внутри компьютера каждый символ занимает 1 байт, как это уже отмечалось в гл. 1. Вернувшись к рис. 2.3, мы можем заметить, что Date, очевидно, не является простым типом, поскольку содержит три значения, а также и потому, что нет предопределенного способа записи даты; в самом деле, одну и ту же дату можно записать множеством разных способов. Например, значения 14 November 1970 14/11/70 November 14, 1970 1970.11.14 являются одинаково правильными способами представления даты в зависимости от ваших потребностей или страны проживания. Поэтому в противоположность простым типам дату можно назвать структурным типом. Простые типы включены в язык, и для них предусматриваются специальные наборы символических обозначений, однако в С# имеется также и целый ряд структурных типов, например, для записи дат. Другим структурным типом является строка символов, заключаемая в двойные кавычки, например, «London» или «John Smith». Этот тип входит в группу структурных типов, потому что он включает не одно, а несколько простых значений. Однако языки обычно предоставляют операторы для объединения строки, например, + или &. Значения строк могут иметь различные представления, однако символы строки всегда хранятся в памяти в одном месте. Программисты имеют возможность определять новые структурные типы, не соответствующие ни компьютерным типам, ни каким-либо предопределенным символическим обозначениям. При этом структурный тип может иметь несколько значений, в противоположность простому типу, для которого допустимо только одно значение. Подытожим сказанное, приведя определение структурного типа.
Использование объектов
39
Структурный тип Структурный тип •
содержит данные для одного или нескольких значений, каждое из которых соответствует своему типу;
•
включает определенные пользователем данные и функции.
Приведенные выше определения типов умышленно несколько расширены по сравнению с теми, что используются в С#, поскольку важно изучать принципы программирования, а не просто язык.
2.2 Члены объектов Как мы уже видели, типы, а, следовательно, и их объекты, включают данные и функции, причем функции иногда выступают в виде операторов. Как они описываются? В этом разделе мы начнем использовать истинные обозначения С# и введем понятие формы, которая показывает, как записываются в С# те или иные конструкции, и что они обозначают. То, как они записываются, называется синтаксисом, а что они обозначают — семантикой. Форма, следовательно, будет выглядеть приблизительно таким образом: Форма для конструкции С# Синтаксис конструкции Семантика конструкции
Шрифты, используемые в формах, уже обсуждались в гл. 1.
Обращение к членам Данные и функции, входящие в тип, носят название членов, или элементов типа. Для обращения к членам типов или объектов, имеющих имена, предусмотрены специальные обозначения. К членам, обозначаемым символами операторов (например, +), обращение осуществляется иначе, в соответствии с принятой практикой написания арифметических выражений. Члены типов, используемые всеми объектами данного типа (а также другими объектами) имеют общее название статических членов. Обращение к ним осуществляется с помощью оператора «точка» (.) с указанием имени типа. Члены, которые предназначены для использования в конкретном объекте, представляющем собой экземпляр типа, называются членами экземпляра, и обращение к ним осуществляется с указанием имени объекта. Таким образом, мы имеем следующую форму:
40
Глава 2. Использование объектов
Обращение к членам тип.член объект.член тип. метод (список__параметров) объект.метод(список_параметров) объект оператор объект оператор объект
Два первых варианта приложимы ко всем данным, а также к большинству функций. Если член является членом экземпляра, указывается объект; если член является статическим, указывается тип. Членам, являющимся методами (см. ниже), могут передаваться параметры. Два последних варианта используются в тех случаях, когда операторы (например, + или -) определены для данного типа. Детали семантики для каждого вида обращения обсуждаются ниже.
Воспользовавшись, например, рисунками 2.2 и 2.3, мы можем записать члены таким образом: birth.day marriage.year birth.AddDays(20) Math.Sqrt(x) InputOutput.Read() 6+ 3
//Дата рождения //Год бракосочетания //Добавить 20 дней к дате рождения //Вычислить квадратный корень из х //Операция чтения //Арифметическое действие над числами
Перед точкой мы указываем тип или объект, а после точки пишем имя члена. Теперь перейдем к обсуждению способов описания членов-данных и членов-функций.
Данные Данные д л я типа состоят из одного или более полей. Каждое поле специфицируется своим собственным типом, который называется объявлением поля. Поля могут объявляться по-разному, что определяет способы их использования, но пока мы ограничимся простой формой объявления: Объявление поля (упрощено) тип поле; тип поле = выражение; тип поле!, поле2, . . . Первый вариант объявляет поле данного типа. Второй вариант объявляет поле и инициализирует его выражением, которое должно давать результат того же типа. Третий вариант показывает, что допустимо объявить список полей одного типа.
2.2 Члены объектов
41
(Термин выражение, использованный в приведенной выше форме, будет объяснен позже. Здесь мы полагаем, что выражение включает значения соответствующих типов.) Какие типы можно использовать? В предыдущих разделах мы упоминали, среди прочих, типы для чисел, дат, изображений и строк. В С# эти типы являются встроенными, и им присвоены имена int, DateTime, Image и string, соответственно. Поэтому примеры объявлений могут выглядеть таким образом: int temperature; Image photol; DateTime birth, graduation, marriage; int century = 100; string taxEnd = "February";
Принято имена простых типов вроде int начинать со строчной буквы, а имена структурных типов, таких как Image — с прописной. Исключением является предопределенный тип string, хотя для него имеется синоним String. Программисты на С# предпочитают использовать обозначение string. Структуры и классы. В С# используются структурные типы двух видов — структуры (struct) и классы (class). DateTime является структурой, a Image — классом. На первых порах мы будем иметь дело в основном со структурами. Во многих отношениях обе конструкции схожи, однако классам свойственны дополнительные черты, расширяющие их возможности, именно, наследование и полиморфизм. Эти черты не используются в простых программах, и о них можно не думать при изучении основ объектно-ориентированного программирования, поэтому мы отложим знакомство с ними до следующих глав. Здесь же мы остановимся на различиях между структурами и классами, проявляющихся в способах их инициализации. Инициализация. В приведенном выше примере полей данных двум последним данным, century и taxEnd, присвоены определенные значения. А что можно сказать об остальных полях? В С# все целочисленные поля при их объявлении инициализируются нолями. Когда объявляется структура, например, DateTime, создаются ее поля, и все они инициализируются в соответствии с их типами. Поэтому если DateTime включает в себя поля дня, месяца и года (а, скорее всего, это так и есть), и все эти поля представляют собой целые числа (а это точно так), то все они будут инициализированы нулевыми значениями. Поля Image, какими бы они ни были, не могут иметь разумных значений до тех пор, пока не будет загружено конкретное изображение. Image, будучи классом, а не структурой, инициализирует свои объекты специальным значением, называемым null. Именно таким будет значение photol, пока мы не предпримем шаги к его изменению.
42
Глава 2. Использование объектов
Функции В С# используются несколько видов функций; пока мы ограничимся рассмотрением конструкторов, методов, свойств и операторов. В соответствии с темой этой главы, мы здесь остановимся на том, как эти функции используются в Сопрограммах; в гл. 3 мы обсудим, как они определяются. Конструкторы. Конструктор представляет собой функцию специального вида, которая используется для создания объекта данного структурного типа. Конструктору передаются некоторые значения, которые он вставляет в поля нового объекта. Таким образом, если мы хотим создать несколько объектов даты, мы можем написать: D a t e T i m e b i r t h = new D a t e T i m e ( 1 9 7 0 , D a t e T i m e m a r r i a g e = new D a t e T i m e ( 2 0 0 0 ,
11, 14),-//Дата 2, 2 5 ) ; / / Д а т а
рождения бракосочетания
Значения в скобках называются параметрами, и они должны перечисляться в том порядке, в каком они ожидаются конструктором. В случае DateTime этот порядок таков: год, месяц, день. Форма создания объекта выглядит таким образом: Создание объекта ТИП obj
= new ТИП (список_параметров)
Создается объект данного типа с именем, указанным полем obj и полями, инициализированными в соответствии со списком параметров.
Использование знака равенства указывает, что содержимое правой части назначается имени объекта, указанного в левой части. Назначение является очень важным понятием программирования, и мы еще им займемся. Объекты можно также создавать посредством их методов, как будет показано ниже для класса Image. Методы. Метод типа может быть использован для изменения значений полей, для выполнения внешних действий, например, вывода чего-то на экран, а также для вычисления результата. Например, в типе DateTime предусмотрен метод для сложения числа лет. В качестве примера использования этого метода мы можем написать: marriage.AddYears(25);
//Прибавить
25 лет
к дате
бракосочетания
Метод носит имя AddYears. Он использован применительно к объекту marriage, и ему передается значение 25, с которым он может работать. Число 25 является параметром метода AddYears. To, что этот метод применяется к объекту, указывается с помощью оператора «точка». В результате этой операции будет изменено значение поля, принадлежащего объекту marriage.
2.2 Члены объектов
43
В разделе 2.1 и на рис. 2.2 было упоминание типа для ввода и вывода. В С# этот тип представляет собой класс с именем Console. Одним из его методов является WriteLine; он используется для вывода значений на экран. Для вывода на экран значения объекта birth мы можем написать Console.WriteLine (birth);
Это предложение активизирует метод WriteLine класса Console с параметром birth. Другая категория методов создает объект для объявленного поля. В приведенном ниже предложении метод FromFile считывает изображение из файла в программу. Image photol = Image.FromFile("Photo.jpg"); Построение предложения здесь такое же, как и в предыдущем примере (WriteLine); в нем используется вариант формата обращения тип.член, обсуждавшийся в начале этого раздела. Свойства. Свойства позволяют нам отыскивать значения полей данных управляемым способом и, возможно, настраивать их. В С# поля по умолчанию объявляются частными (закрытыми) по отношению к объекту. Часто нам надо сделать их значения доступными, но в то же время контролировать вносимые изменения. Например, дню месяца нельзя присвоить значение 32. Если, однако, поле связано со свойством, тогда свойство может ограничить действия, выполняемые над полем. Например, тип DateTime не предоставляет нам непосредственный доступ к каким-либо полям, но имеет много свойств, позволяющих нам обращаться к этим полям. Примерами таких свойств являются Day, DayOfWeek, Year и др. Свойства, как и методы, применяются к объектам с помощью оператора «точка», а их имена начинаются с прописной буквы; параметров у них нет, поэтому нет и скобок. Используя приведенный выше пример создания объекта, можно с помощью свойства Year получить значение поля year: Console.WriteLine(birth.Year) ;
которое будет равно 1970. Другим интересным свойством DateTime является Now, которое позволяет получить текущие дату и время. Написав Console.WriteLine(DateTime.Now);
мы выведем на экран текущие дату и время, например, 2002/07/13 08:55:20 РМ
44
Глава 2. Использование объектов
Для изображений предусмотрены свойства, отображающие размеры, и их можно вывести на экран следующим образом: Console.WriteLine(photol.Width); Console.WriteLine(photol.Height) ;
Операторы. Операторы обозначаются с помощью символов, и приводят к вызову метода, который выполняет действие над двумя объектами, указываемое оператором. Для каждого типа может быть определено много операторов, в том числе и с вполне очевидным смыслом. Например, можно написать: temperature + 5 birth.Year - graduaiton.Year graduation - b i r t h photol.Width < photol.Height
+ 1
Здесь мы видим примеры операторов плюс (+), минус (-), меньше чем (<) и точка (.), однако используются они в различных контекстах. В первых двух примерах обрабатываются целые числа; семантика целочисленных операторов нам знакома из арифметики и работы с калькулятором. Если temperature имеет значение, скажем, 25, тогда temperature+5 составляет 30 и т. д. В третьем примере знак минус используется с переменными graduation и birth, которые предварительно были объявлены, как объекты типа DateTime. Что означает вычитание одного объекта из другого? Если оператор вычитания определен для типа DateTime, в его определение будет включена спецификация типа результата. В данном случае вычитание одного объекта DateTime из другого приводит к созданию объекта иного типа, которому присваивается значение разности в часах. Такой результат может нас не устраивать, и его придется подвергнуть дополнительным преобразованиям, чтобы получить значение в годах. В последнем примере сравниваются два целых числа (ширина и высота) и возвращается значение true или false, которое далее можно проанализировать, как это подробно обсуждается в гл. 4. В этом же примере, кстати, снова используется оператор «точка».
Пространства имен Завершая этот раздел, взглянем на то, как типы сгруппированы в С#. Современный язык типа С# предоставляет программисту для использования в программах богатый выбор предопределенных типов. Некоторые из них являются абсолютно необходимыми, например, типы, позволяющие читать и записывать данные, или те, которые предоставляют компоненты для дружественного интерфейса, к которому привыкли пользователи компьютера. Тип Math, который упоминался ранее, также входит в эту категорию. Никому не придет в голову программировать вычисление квадратного корня с помощью числового ряда — мы ожидаем, что язык предоставит нам
2.3 Структура программ
45
более простое средство. Другие типы, например, DateTime, можно рассматривать, как не столь уж необходимые, однако они весьма полезны и позволяют программисту экономить массу времени и усилий. Типов так много (тысячи!), что в целях упорядочивания они логически подразделяются на пространства имен. Пространство имен в С# представляет собой совокупность связанных типов. Некоторые пространства имен, например, System, очень велики и могут включать более 100 разного рода типов; другие пространства имен, например, System.Timers, содержат всего по несколько типов. Типы DateTime и Math входят в пространство имен System. Использование пространств имен (также называемых пакетами, библиотеками или интерфейсами прикладного программирования, API от Application Programming Interface) — это практическая реализация концепции повторного использования, принципа, чрезвычайно поощряемого в современной программной инженерии. Если язык предоставляет некоторый набор типов, имеет прямой смысл использовать их в своих программах. В настоящей главе мы рассмотрим методику программирования с использованием исключительно предопределенных типов. Это даст нам возможность отложить на время рассмотрение вопроса о создании собственных типов, чем мы займемся в гл. 3.
Сводка терминологии На рис. 2.4 собраны термины объектной ориентации в С#, так или иначе затронутые на предыдущих страницах. Такие слова, как «Программа», «Типы» и «Данные» касаются понятий, определенных в настоящем разделе. Линии показывают, как из одних понятий составляются другие, а слова рядом с линиями, набранные курсивом, поясняют, каким образом достигается это объединение. Например, типы включают в себя данные и функции, а сами функции могут быть любого из четырех перечисленных на рисунке видов. Говоря о типах, мы даем примеры из языка С#, поскольку эти термины уже упоминались. Рисунок 2.4 — это только начало; нам еще многому предстоит научиться. По мере движения вперед мы будем возвращаться к этому рисунку и детализировать его содержимое. Рассмотрим теперь программные элементы более подробно на базе реального С# : примера.
2.3 Структура программ Классы Фундаментальным термином объектной ориентации является понятие класса. Класс — это тип, разделяющий многие атрибуты структурного типа, и именно так мы его до сих пор использовали. Класс является наиболее мощной из всех категорий типов в С# и для получения определенных особенностей поведения необходимо использовать именно его. Многие из этих особенностей нам понадобятся лишь позже, но об одной стоит
46
Глава 2. Использование объектов программа состоит из
• простые
например
типы структурные
содержат
например
группируются в данные
struct
пространства имен
функции
находятся в
подразделяются на
поля содержат
class
конструкторы
имеют
параметры
методы —
константы
свойства
переменные
операторы
Р и с . 2 . 4 . Термины, с которыми мы уже сталкивались при описании объектов
сказать уже сейчас. Класс представляет собой структурную единицу программы. Внутри такого класса мы, как и раньше, можем определить данные и функции, но нам также необходимо определить метод Main, который используется программой .NET runtime для запуска программы. Класс, используемый в качестве программы, должен иметь форму, приведенную ниже. При описании структуры программы мы будем пользоваться такими соглашениями: фигурная скобка, открывающая описание класса, ставится на той же строке программы, что и имя класса; завершающая скобка выровнена на первую букву слова class. Встречается и другое соглашение, когда открывающая скобка ставится на отдельной строке и также под словом class. Вам может понадобиться настроить вашу среду программирования под принятое вами соглашение, если ее редактор автоматически включает в программу отступы не так, как вам хочется. Класс в качестве простой программы using System; class имя_класса { s t a t i c void Main() { . . . предложения
Программе назначено имя имякласса. Ее класс определен между двумя фигурными скобками { }. Ее поведение описывается в предложениях метода Main.
2.3 Структура программ
47
Пример 2.1. Первая программа Рассмотрим первую законченную программу, хранящуюся в файле с именем Welcome.cs. Суффикс «cs» обозначает программу С#, и именно в таком качестве он воспринимается средами разработки и компиляторами. Для удобства ссылок мы снабдили каждую строку номером; эти номера не являются частями С#-программ. О результате работы программы можно судить по строкам 5 и 6. На экран выводится приветственное сообщение, а следующее предложение программы позволяет получить и вывести на экран текущее время. При реальном запуске программы вы увидите: Welcome t o C# I t i s now 2 0 0 2 / 0 7 / 1 3 0 8 : 5 5 : 2 0 PM Файл Welcome.cs
1 2
using System;
3 4 5 6
class Welcome { static void Main() { Console.WriteLine("Welcome to C#"); Console.WriteLine("It is now " + DateTime.Now);
7 8
} }
Анализируя программу, мы обнаруживаем следующие важные моменты: 1. Программа начинается с заявления, что она собирается использовать пространство имен System, конкретно классы Console и DateTime, как это видно в строках 5 и 6. 2. В строке 3 объявляется класс Welcome вместе с его содержимым вслед за открывающей фигурной скобкой. Имена файла и программы (т. е. имя класса) не обязательно должны совпадать, хотя обычно они или одинаковы, или схожи. 3. В строке 4 вводится метод Main, являющийся стартовой точкой (точкой входа) нашей программы. Когда мы потребуем от системы С# запустить программу, управление будет передано именно в эту точку. 4. Содержательные действия программы ограничиваются двумя выводами на экран некоторого текста. Вывод осуществляется с помощью метода Console.WriteLine; в качестве параметра используются текстовые строки и свойство DateTime (для получения текущего времени). 5. В строке 6 мы используем оператор + для указания на то, что наша строка и текущее время должны быть выведены на одной строке экрана. В следующем разделе мы вернемся к этому вопросу. 6. И класс Welcome, и метод Main начинаются с фигурных скобок, поэтому мы завершаем их ответными фигурными скобками в строках 7 и 8.
48
Глава 2. Использование объектов
Пример 2.2. Включение в программу метода Go Чтобы сделать нашу программу более интересной, мы можем добавить предложения к методу Main. Однако программа будет более структурированной, если мы оставим метод Main настолько простым, насколько это возможно, и добавим второй метод, назвав его Go. Этот метод активизируется в методе Main, и именно в него мы поместим новые предложения. Получившаяся структура приведена ниже. Изменения программы заключаются в следующем: 1. Предложения WriteLine перемещаются в новый метод Go (теперь строки 6 и 7). 2. В метод Main включено предложение, WelcomeFromGo и запускающее метод Go.
создающее
объект
Файл WelcomeFromGo.cs I 2 3 4 5 6 7 8 9 10 II 12 13
using
System;
class
WelcomeFromGo {
void
Go() { Console.WriteLine("Welcome to C# from Go"); Console.WriteLine("It is now " + DateTime.Now);
До самого конца настоящей книги мы будем использовать для программ структуру примера 2.2. Заметим, что вывод этой программы почти не будет отличаться от предыдущего: Welcome t o C# from Go I t i s now 2 0 0 2 / 0 7 / 1 3 0 8 : 5 5 : 2 0 PM
Заметьте, что метод Go является произвольным добавлением, не конструкцией С # . Мы используем комбинацию Main — Go потому, что помещение объявления полей и вызовов методов в статический метод Main приводит к некоторым ограничениям их использования. Этот вопрос подробно будет рассмотрен в гл. 8.
Структура программы Так же, как документ на любом языке состоит из предложений этого языка, С#-программа состоит из предложений языка программирования. Эти предложения могут быть разных типов, и они будут рассматриваться
2.3 Структура программ
49
нами по мере углубления в возможности языка. В С# предложение заканчивается точкой с запятой, и обычно для каждого предложения выделяется отдельная строка текста программы. Наличие в каждом предложении завершающего символа точки с запятой позволяет помещать на одну строку текста несколько предложений языка, но такая программа будет выглядеть очень странно. Часто наоборот, предложение не помещается на одной строке и мы вынуждены переносить его часть на следующую строку. В дальнейшем мы увидим много примеров такой ситуации. В этих случаях точка с запятой обозначает конец многострочного предложения. Легко заметить, что все приведенные до сих пор программы использовали для наглядности отступы. Когда в тексте программы начинается описание языковой конструкции, например, класса или метода, эта строка выступает в качестве заголовка, а все следующее далее содержимое класса или метода сдвигается вправо включением нескольких пробелов. Когда конструкция заканчивается (обычно с помощью закрывающей фигурной скобки) выравнивание строк возвращается к предыдущему уровню. Многие среды программирования помогут вам писать программу, автоматически выравнивая ее строки таким образом. Отступы существенно повышают наглядность программы и облегчают ее чтение, по существу нисколько ее не усложняя. Еще одна форма структурирования программы — пустая строка; такие строки обычно включаются в текст программы между методами или для выделения важных частей внутри метода. Структурное оформление программы очень важно, и к нему следует привыкать с самого начала обучения.
Ключевые слова и идентификаторы Программа состоит из слов разного рода. Среди них есть слова, которые по правилам языка С# имеют вполне определенный смысл и не могут использоваться иначе. Такие слова называются ключевыми; в наших программах можно найти такие ключевые слова: using new
class static
void -
В С# определены около 80 ключевых слов, выполняющих в программе разнообразные функции. Эти слова перечислены в Приложении Б. Каждая программа определяет свои собственные слова для используемых в ней данных, методов и свойств; эти слова называются идентификаторами. В наших программах встречались такие идентификаторы: WelcomeFromGo
Go
Кроме идентификаторов, введенных нами, в наших программах использовались идентификаторы, определенные в языке С # : System DateTime
Console Now
WriteLine Main
50
Глава 2. Использование объектов
Идентификаторы имеют специфический формат. Они должны представлять собой слитные слова, начинаться с буквы и не совпадать с какими-либо ключевыми словами. Если в идентификатор входит более одного слова, принято внутренние слова начинать с прописных букв, что облегчает чтение программы. Имеются и другие соглашения относительно использования прописных букв. Как правило, все идентификаторы, за исключением имен полей, начинаются с прописных букв. В приведенной выше программе полей не было, но ранее мы с ними встречались: birth temperature
graduation century
marriage photol
Заметьте, что часто свойству дается то же имя, что и полю, но начинают его с прописной буквы. Так, Year является свойством для поля year (к которому можно обратиться только посредством его свойства).
Операторы и символы Наконец, можно заметить, что в программе используются различные специальные символы. По мере изложения материала число таких символов будет увеличиваться. В программе примера 2.2 мы видим: • фигурные скобки { } для того чтобы начать и закончить описание класса или метода (три пары); •
кавычки " " для определения строк;
•
символы-операторы, например + (строка 7);
•
точка с запятой для завершения предложения;
•
точка для выбора члена объекта или типа;
•
круглые скобки ( ) вокруг параметров для методов, как в строках 6 и 7 (даже если параметры отсутствуют, как в строках 5 и 10).
Пример 2.3. Число дней до конца года Пусть нам надо определить число дней, оставшихся от настоящего момента до конца года. Мы знаем, что существует предопределенный тип для дат, и мы можем создать два объекта этого типа, один для текущей даты, а второй — для последнего дня года. Дата для конца года создается с помощью свойства Year для поля year текущей даты с указанием 12 для месяца и 31 для дня. ' Ключевой частью программы является вычисление, которое выполняется путем вычитания из одного объекта DateTimes другого, а затем воздействия свойством Days на результат вычитания. Файл TimeDifference.cs
using System; class TimeDifference
2.3 Структура программ ///<summary> ///Программа Time Difference
51_ Bishop & Horspool June 2002
///Вычисляет число дней, оставшихся до конца года ///Демонстрирует вывод нескольких строк текста, ///а также использование свойств и операторов /// void Go () { Console.WriteLine("The Time Difference Program"); DateTime now=DateTime.Now; DateTime endOfYear = new DateTime(now.Year, 12, 31); Console.WriteLine(endOfYear) ; Console.WriteLine (now); Console .WriteLine ("Days till the end of the year are " ) ; Console.WriteLine((endOfYear - now).Days); } static void Main() { new TimeDifference().Go();
Вывод программы может выглядеть следующим образом: The Time Difference Program 2002/12/31 08:55:20 PM 2002/08/04 08:55:20 PM Days till the end of the year are 149
В следующем разделе будет показано, как сделать этот вывод более наглядным. Мы сделали предположение (правильное), что значения DateTime и int можно вывести на экран. В следующей главе, в разделе 3.6, мы рассмотрим вопрос о форматировании выводимых значений, чтобы они удовлетворяли правилам, принятым в данной стране или просто нашим предпочтениям.
Комментарии Если программа содержит более нескольких строк, то ее необходимо снабдить пояснениями на простом английском (или любом другом) языке. Даже если текст программы вполне ясен, в начале программы должны быть описаны ее назначение и способ использования. С# предоставляет три различных способа включения в программу комментариев: 1. Комментарии XML. Такие комментарии начинаются с трех знаков /// и тега (как, например, <summary> в примере 2.3), далее следуют строки комментария, начинающиеся теми же знаками ///, и, наконец, последняя строка содержит тот же тег со знаком / (). Комментарии XML позволяют вставлять пояснения почти во что угодно, и в последующих главах мы еще столк-
52
Глава 2. Использование объектов
немея с такими примерами. Пока что отметим, что большинство наших программ и классов будут начинаться с такого рода тегов с кратким изложением существа комментируемой программы (summary в переводе с английского — краткое изложение, резюме). 2. Многострочные комментарии. Такие комментарии начинаются и заканчиваются парами знаков /* и */• Любой текст между этими знаками игнорируется, независимо от количества строк в нем. 3. Комментарии к той же строке. Такие комментарии начинаются с двух знаков // и продолжаются до конца строки. Их можно использовать для пояснения данного предложения одним-двумя словами: DateTime
now = DateTime.Today;//Today — это
свойство
Комментируя программу, мы стараемся достичь оптимального соотношения объема и содержания комментариев, чтобы они были полезны и не повторяли программные предложения. Комментарии, так же, как и сама программа, нуждаются в обновлении: если текст программы претерпел изменения, возможно, надо изменить и комментарии к нему. Пример 2.4. Анализ программы
Мы уже рассмотрели значительное число понятий С# и можем приступить к выделению в программе составляющих ее элементов. Целью этого упражнения будет проверка того, насколько мы усвоили разницу между типами и объектами, данными и функциями и т. д. Итак, вот небольшая, но достаточно любопытная программа. Файл Fetchlmage.cs using System; using System.Drawing; using System.Windows.Forms; public class Fetchlmage : Form { ///<summary> ///Программа Fetch Image // /
Bishop & Horspool June 2002
ill
///Демонстрирует простой способ вывода изображений /// Image pic; protected override void OnPaint(PaintEventArgs e) { this.Width = 500; this.Height = 400; e.Graphics.Drawlmage(pic,30,30); } public Fetchlmage (){ pic = new Bitmap("photo.jpg"); } static void Main() {
2.3 Структура программ
53
Application.Run(new Fetchlmage());
Если файл с именем «photo.jpg» находится в нужном месте, программа выведет на экран изображение, содержащееся в этом файле. Наша задача — рассмотреть каждые слово и символ в программе и классифицировать их в соответствии с материалом, изложенным в этом и предыдущем разделах. Заметим, что в программе определены не все идентификаторы. Это нормальная практика в С#: смысл идентификаторов должен быть определен по контексту. Вывод программы показан на рис. 2.5, а понятия перечислены в табл. 2.1. Однако откуда мы узнали, как анализировать программу, работающую с графикой, когда об этом еще ничего не говорилось? Очень просто: синтаксис позволяет нам понять, что происходит в программе. Например, в предложении е.Graphics.Drawlmage(pic, 30,30) ;
е и Graphics должны быть объектами или классами, потому что к ним применен оператор «точка». Использование прописных или строчных букв в начале идентификаторов дает нам ключ к пониманию того, что е является объектом, a Graphics типом. Обозначение Drawlmage, сопровож-
Рис. 2.5. Вывод программы Fetchlmage
54
Глава 2. Использование объектов
даемое параметрами в скобках, должно быть методом. Точно также мы можем заключить, что в предложении Application.Run(new
Fetchlmage());
Application является классом, Run методом, a Fetchlmage не метод, а конструктор, потому что перед ним стоит ключевое слово new. В качестве упражнения вы можете выполнить такой же анализ примера 2.3. Таблица 2 . 1 . Элементы в программе Fetchlmage Понятие
Соответствующие элементы в программе
Ключевые слова
using, p u b l i c , c l a s s , void, s t a t i c , new
2.4 Строки, вывод и форматирование В этом разделе мы рассмотрим вопросы вывода из программы ее результатов. Этот процесс называется выводом, хотя часто используются термины «запись», «печать» или «отображение на. экране». В примере 2.2 первым предложением вывода было Console.WriteLine("Welcome
to
C# f r o m G o " ) ;
Класс Console предоставляет различные методы для ввода и вывода, и наиболее удобным для вывода является метод WriteLine. To, что мы хотим вывести, указывается в скобках в качестве параметра. В приведенном предложении параметр является строкой. Когда мы посылаем строку методу WriteLine, она появляется на экране, или «консоли». Слово консоль является устаревшим названием экрана, и оно сохранилось, в частности, как имя этого класса С#.
2.4 Строки, вывод и форматирование
55
Строки В С# для описания строковых переменных имеется класс с именем string. Строковые переменные, или просто строки, состоят из символов, заключенных в кавычки, что позволяет использовать всю совокупность символов, как одну переменную, вроде, например, переменной типа int. Однако строки отличаются тем, что в них нетрудно получить доступ к индивидуальным символам. Работа со строками будет подробно освещена в гл. 4. Здесь строки будут нас интересовать только с точки зрения возможностей вывода. Значения строковых переменных не могут занимать в программе более одной строки текста, поэтому, если строка должна быть длинной, ее приходится разбивать на части и делать из нее две или больше строк. Эти строки могут быть снова объединены в одну переменную с помощью оператора плюс, который называют оператором сцепления (конкатенации). Рассмотрим, например, следующее предложение: Console.WriteLine
("Welcome to C# " + "from Go") ;
Здесь указаны две строки, которые будут сцеплены вместе перед вызовом метода WriteLine. Результат будет в точности таким же, как и раньше: Welcome
t o C# from Go
В С# предусмотрена возможность вывода на экран специальных символов, которые нельзя ввести с клавиатуры. Имеются два вида таких символов: escape-символы и символы Unicode, которые будут рассмотрены в гл. 4.
Предложения вывода Мы уже сталкивались с методом WriteLine. Строго говоря, WriteLine не является предложением: это метод, и когда мы вызываем этот метод, мы используем предложение вызова метода. Однако часто, когда имеют в виду этот вызов, говорят о «предложении вывода». Вывод является обобщающим термином, обозначающим как то, что мы отображаем на экране, так и процесс этого отображения. Класс Console имеет еще один метод, Write, который схож с WriteLine, но отличается тем, что не завершает строку вывода; он предназначен для использования после него еще одного вызова Write или WriteLine. В качестве примера модифицируем программу 2.3, где были строки C o n s o l e . W r i t e L i n e ("Days t i l l Console.WriteLine((endOfYear
t h e end of t h e year - now).Days);
are" ) ;
Если их записать в форме C o n s o l e . W r i t e ("Days t i l l t h e e n d of t h e y e a r Console.WriteLine((endOfYear - now).Days);
are " ) ;
56
Глава 2. Использование объектов
то вывод этого фрагмента программы преобразуется следующим образом: Days t i l l
the end of
the year are 149
В основном, однако, метод Write используется, когда имеется целая группа предложений вывода и они включают в себя не только строки, но и объекты.
Вывод объектов Теперь рассмотрим вывод объектов. Мы уже видели во второй из приведенных выше программ, что для вывода можно использовать предложения вида Console.WriteLine(endOfYear) ; Console.WriteLine(now) ; Console .WriteLine ("Days till the end of the year Console.WriteLine((endOfYear - now).Days);
are
");
где endOfYear и now являются объектами DateTime. В С# в каждом типе определен специальный метод с именем ToString. Метод ToString преобразует значение объекта в формат строки разумным образом. Этот метод вызывается автоматически, когда объекты указываются в качестве параметров вызовов WriteLine. Мы можем видоизменить приведенные выше строки, добавив поясняющие надписи и сделав вывод более наглядным: Console .WriteLine ("The end of t h i s year i s " + endOfYear); Console.WriteLine("Today's date i s " + now); Console.WriteLine("There are " + (endOfYear - now).Days) + " days u n t i l l the end of the year");
Этот фрагмент выведет на экран следующее: The e n d o f t h i s y e a r i s 2 0 0 2 / 1 2 / 3 1 0 8 : 5 5 : 2 0 PM T o d a y ' s d a t e i s 2 0 0 2 / 0 8 / 0 4 0 8 : 5 5 : 2 0 PM There a r e 14 9 d a y s u n t i l l t h e e n d o f t h e y e a r
Класс Console организует вывод строка за строкой. Как мы уже видели в примере 2.4, на экран можно выводить не только строки, но и изображения. В этом случае надо использовать другой класс, по имени Graphics, и метод Drawlmage. Вывод на экран фотографии мы выполнили с помощью такого предложения: е.Graphics.Drawlmage(pic,30,30);
Вывод выражений Одним из важнейших достоинств программирования является возможность без труда выполнять различные вычисления. В языках программирования предусматриваются те же арифметические операции, как и в каль-
2,5 Переменные, присваивание и ввод
57
куляторах, так что плюс обозначается знаком +, минус знаком —, умножение знаком *, а деление знаком /. Для того, чтобы вычисления выполнялись в требуемом порядке, можно использовать круглые скобки. В С# такие вычисления носят название выражений. Примеры выражений: 120 * 1.6
Перевод 120 миль в километры
74.5 / 80.0 * 100
Отношение величины к числу 80 в процентах
(12 + 15 + 17) / 3
Среднее из трех чисел
2 4 * 60
Число минут в сутках
39.95 * .1.14
Сумма плюс 14% налога
2002 - 1970 + 1
Число лет между двумя годами
Выражения будут обсуждаться подробнее в следующей главе. Здесь мы только хотим заметить, что их можно использовать в предложениях вывода. В С# предусмотрено средство, которое автоматически преобразует числа в строки; эти строки перед выводом на экран можно сцепить с поясняющими фразами. Таким образом, допустимо использовать такое предложение (ниже приведен его вывод): Console.WriteLine("Graduated
in
"
+ (2003
+ 3) ) ;
Graduated in 2006 Здесь необходимо поместить выражение в скобки, чтобы С# мог различить знак +, используемый для указания сцепления, и арифметический знак +. Без скобок мы получили бы следующий вывод: Graduated in 20033 Другими словами, символьные изображения каждого числа (2003 и 3) последовательно добавляются к строке "Graduated in". Это, разумеется, не то, что нам нужно; круглые скобки заставляют программу в первую очередь выполнить арифметическое сложение.
2.5 Переменные, присваивание и ввод Недостатком приведенных выше вычислений является то, что они абсолютно фиксированы. Все значения непосредственно включены в программу, и сколько бы раз программа не выполнялась, мы всегда будем иметь один и тот же результат. Важнейшей чертой большинства программ, с которой сталкиваются даже люди, далекие от программирования, является возможность вводить в программу некоторые данные и, тем самым, изменять вывод программы. Другими словами, в дополнение к выводу значений мы хотим иметь возможность их ввода. Возникает вопрос: ввода куда?
58
Глава 2. Использование объектов
Переменные Если мы собираемся вводить в программу данные, мы должны прочитать их и сохранить в каком-то известном месте памяти, чтобы в дальнейшем можно было ссылаться на это место в выражениях и предложениях вывода. В процессе составления программы происходит выделение памяти под поля данных. Эти поля могут содержать переменные или константы. Последние отличаются лишь тем, что после присвоения им определенных значений эти значения нельзя изменять. На рис. 2.6 показано расположение в памяти нескольких переменных. Мы предположили, что целые числа (5, 1970 и др.) выражены в десятичной система, а строка ("The time in") состоит из символов, как это обычно и бывает. Размеры рамок вокруг переменных роли не играют. hoursDifference messagel birth
5 The time in 1970 11 25
Рис. 2.6. Несколько объявленных переменных
Переменные перед их использованием должны быть объявлены. Инициализировать переменные, т. е. присвоить им начальные значения, можно одновременно с объявлением. Ниже приведены несколько примеров объявлений вместе с инициализацией:. int temperature = 30; int age = 21; int secondsInHour = 60*60; string resulttext = "";//Инициализация string placeOfBirth = "London, UK";
пустой
строкой
Назначение Знак равенства в приведенных выше примерах инициализации называется оператором присваивания. Его действие заключается в том, что значение с правой стороны оператора присваивается переменной, указанной с левой стороны. Смысл переменных заключается в том, что их значения могут изменяться в процессе выполнения программы. Для того чтобы изменить значение переменной, ей надо присвоить новое значение. Старое значение при этом затирается, и переменная приобретает новое значение. Например, предложения i n t mass = 63; Console.WriteLine(mass);
2.5 Переменные, присваивание и ввод int mass = 65; Console.WriteLine(mass);
приведут к выводу 63 65
Более интересна ситуация, когда с правой стороны знака равенства написано выражение: double tempC, tempF;//Температура tempC = 30; tempF = (tempC*9.0/5.0)+32;
по Цельсию и Фаренгейту
В результате выполнения этого фрагмента в tempF будет записано число 86. Более детально выражения и некоторые специальные операторы присваивания будут рассмотрены в гл. 3.
Присваивание строкам Операция присваивания применима и к строкам; со строками можно также использовать оператор +. Таким образом, для образования полного имени человека можно написать: string
fullName
= "James
"
+ "Smith";
Основное правило при использовании присваиваний заключается в том, что тип переменной с левой стороны должен соответствовать типу значения с правой стороны. Отсюда следует, что строкам нельзя присваивать числовые значения и наоборот. Позже мы узнаем, каким образом выполняются некоторые преобразования, однако приведенное выше фундаментальное правило действует во всех случаях.
Присваивание значений объектам Нашей основной целью является освоение методов работы в объектно-ориентированный среде. Ранее мы уже выполняли присваивание значений объектам в процессе их создания, как, например, в этом предложении из примера 2.3: DateTime
endOfYear
= new DateTime(now.Year,12,31);
Здесь создается новый объект (в правой части предложения), который сохраняется в поле памяти с именем endOfYear. В качестве другого примера рассмотрим предложение Console.WriteLine((endOfYear
-
now).Days;
60
Глава 2. Использование объектов
Тот же результат можно получить, введя дополнительную переменную и операцию присваивания: int days = (endOfYear - now).Days; Console.WriteLine (days); Вторая форма может оказаться удобнее, если мы собираемся использовать переменную days еще и в другом месте программы. Рассмотренные понятия проиллюстрированы в следующем примере. Пример 2.5. Время начала совещания
Компания имеет офисы в Лондоне и Нью-Йорке. Разница во времени составляет летом 5 часов, причем в Лондоне заданный час наступает раньше. Лондонские менеджеры хотят проводить телефонную конференцию каждый вторник в 14.00. Нам надо узнать, в какое время мероприятие начнется в Нью-Йорке; наша программа вычислит это время. Как и в предыдущей программе примера 2.3 для количества дней до конца года, мы предусматриваем две переменные для времени. У нас также есть название города с головным офисом (пусть это будет Нью-Йорк) и временная разница (равная -5). Для определения времени начала совещания мы используем свойство UtcNow типа DateTime, которое дает нам время в Лондоне (находящемся на Гринвичском меридиане). Для удобства мы копируем лондонское время в переменную. С#-программа для вывода результата этих вычислений использует четыре предложения вывода. Структура программы содержит внешний метод Go; его использование обосновывалось в разделе 2.3. Файл TimeMeeting.cs using System; class TimeMeeting { ///<summary> ///Программа Time Meeting
Bishop & Horspool April 2002
///Вычисляет время в конкретном городе ///по заданной разнице во времени по отношению к другому городу /// ///Демонстрирует использование переменных типов string и int /// void Go () { string city = "New York";//Город int offset = -5;//Разница во времени относительно Лондона //(Гринвич, Utc) int startTime = 14;//14.00 или 2 часа дня Console.WriteLine("The Time Meeting Program"); DateTime now = DateTime.UtcNow; DateTime meeting = new DateTime (now.Year,now.Month,now.Day,startTime,0,0); Console.WriteLine("The meeting in London is "
2.5 Переменные, присваивание и ввод
61_
+ " scheduled for " + meeting) ; Console.WriteLine(city + " is " + offset + " hours different");
DateTime offsetMeeting = meeting.AddHours(offset); Console.WriteLine("The meeting in "+city+" will be at " + offsetMeeting); } static void Main() { new TimeMeeting().Go();
После компиляции и запуска программы в консольном окне появятся следующие строки: The The New The
Time Meeting Program m e e t i n g i n London i s s c h e d u l e d f o r 2002/08/05 1 4 : 0 0 : 0 0 York i s -5 hours different m e e t i n g i n New York w i l l be a t 2002/08/05 0 9 : 0 0 : 0 0
Вывод информации о времени получился не очень привлекательным, и мы теперь рассмотрим альтернативные методы вывода для типов вроде DateTime.
Форматирование вывода В С# предусмотрены специальные средства для форматирования чисел и других простых значений с целью их вывода. Эти средства будут рассмотрены в следующей главе. При выводе структурированных объектов могут иметься альтернативные способы представления полей. Например, формат даты и времени 2002/12/31 08:55:20 РМ может нас не устраивать. Поэтому типы часто предоставляют несколько различных методов ToString. В тип DateTime включены альтернативы для длинных и коротких дат. В этом классе также имеются расширенные средства передачи параметра методу ToString, который в этом случае имеет возможность управления форматом даты. Эти средства проиллюстрированы в примере 2.6. Итак, мы видим, что вывод объектов может осуществляться по-разному, но решение о том, каким должен быть состав средств вывода, остается за типом. Пример 2.6. Даты в различных форматах В этом примере мы рассмотрим некоторые возможности альтернативного форматирования даты и времени, предоставляемые типом DateTime. Первый способ управления выводом заключается в использовании одной из альтернатив методу ToString. Этот метод вызывается автоматически, если объект типа DateTime комбинируется со строкой и передается методам Write или WriteLine. Дополнительные методы приведены ниже; их следует вызывать явным образом, используя обычный оператор «точка».
62
Глава 2. Использование объектов
ToShortDateString() ToLongDateString()
//Короткая дата без времени //Длинная дата без времени
Второй способ предполагает использование одного из многих символических параметров для метода ToString. Например, ToString("f") ToString("g")
//Длинная дата и короткое время //Короткая дата и короткое время
Эти вызовы дадут тот же вывод даты, но вторая группа выведет также и время. М ы повторяем программу TimeMeeting и добавляем новые предложения вывода для демонстрации использования этих методов и форматирующих параметров. Результаты этой модификации можно увидеть, сравнив выводы обеих программ. Файл TimeFormat.cs using System; class TimeFormat { ///<summary> ///Программа Time Format ///
Bishop & Horspool June 2002
ill
///Выводит время в различных форматах ///Демонстрирует использование различных форм ToString /// void Go () { Console.WriteLine("The Time Format Program"); DateTime now = DateTime.Now; DateTime endOfYear = new DateTime(now.Year, 12, 31) ; Console.WriteLine("Today is " + now"); Console.WriteLine("End of year is " + endOfYear); Console.WriteLine("As a. short date " + now.ToShortDateString() ) ; Console.WriteLine("As a long date " + now.ToLongDateString() ) ; Console. WriteLine ("As a long date and short time " + now.ToString("f")); Console.WriteLine("As a short date and short time " + now.ToString("g")); Console .Write ("Days till the end of the year are " ) ; Console.WriteLine((endOfYear - now).Days); } static void Main() { new TimeFormat (). Go ();
2.5 Переменные, присваивание и ввод
63
Вывод программы выглядит следующим образом: The Time Format Program Today i s 2002/08/05 13:41:17 End of Year i s 2002/12/31 00:00:00 As a short date 2002/08/05 [Короткая дата] As a long date 05 August 2002 [Длинная дата] As a long date and short time 05 August 2002 01:41 PM [Длинная дата и короткое время] As a short date and short time 2002/08/05 01:41 PM [Короткая дата и короткое время] Days t i l l the end of the year are 147 Написав какую-либо программу, даже такую примитивную, как в предыдущем примере, мы должны оценить, что же у нас получилось, и найти как ограничения, так и способы совершенствования программы. Очевидно, нашу программу можно сделать гораздо более универсальной. Лондонское время совещания может измениться, а разница во времени летом и зимой может быть различной. Даже названия городов могут варьироваться, если программу использует другое отделение компании или вообще другая компания. В этом случае и разница во времени будет иной. К счастью, используя тип DateTime, мы можем работать с интервалами времени, переходящими чрез границу суток, хотя можно спорить, есть ли в этом практический смысл в деловых приложениях. Итак, в целом мы можем предложить массу расширений нашей маленькой программы. Для реализации этих расширений мы должны познакомиться с другой конструкцией С# — вводом.
Предложения ввода Рассмотрим теперь вопрос о том, как можно считать в программу данные, находящиеся вне ее. Взаимодействие пользователя с программой является фундаментальным действием при ее использовании, и вполне оправданно рассмотреть этот вопрос как можно раньше. Предложения ввода являются в некотором роде двойниками предложений вывода, однако их действие решительно отличается: они образуют значения, которые затем могут быть сохранены в переменных. Поэтому, в отличие от предложений вывода, предложения ввода используются в конструкциях присваивания. Типы с левой и правой сторон операции присваивания должны совпадать, поэтому строки можно прочитать только в строковые переменные, а целые числа — в целочисленные переменные. Сложность заключается в том, что класс Console предоставляет лишь метод для чтения строк с именем ReadLine, и любые целые числа необходимо преобразовывать с помощью специального метода, предусмотренного в типе int и имеющего имя Parse. Этот процесс показан в следующей форме.
64
Глава 2. Использование объектов
Простые предложения ввода =
строковая_переменная другая
переменная
=
Console.ReadLine(); Parse(Console.ReadLineО);
тип.
В первом варианте данные читаются до конца строки и копируются в строковую переменную stringvar. Второй вариант используется для других типов переменных; данные читаются до конца строки, после чего к строке применяется метод Parse для указанного типа, и строка, если это возможно, преобразуется в значение этого типа.
Оба способа чтения строки используют ReadLine, из чего следует, что вводимая информация должна умещаться на одной строке текста. На рис.2.7 изображен этот процесс в действии для двух переменных предыдущего примера. string city = Console.ReadLine(); London
int offset
= int.Parse( Console.ReadLine()
Поток данных L
о
n
d
о
n
5
Рис. 2.7. Процесс чтения
На рис. 2.7 показано, как программа управляет процессом чтения. Вводимые строки преобразуются в значения предоставляемых программой переменных. Первый вызов ReadLine получает строку «London» и отправляет ее в переменную city. Второй вызов ReadLine получает строку «5». Эта строка затем преобразуется1 в целое число и сохраняется в переменной offset. Преобразование может выполняться и над другими типами переменных, не только над целыми числами. Например, мы можем прочитать и преобразовать дату. Строка даты должна быть в одном из стандартных форматов, например 2002/8/15. Если предоставленные для чтения данные оказываются не того типа, программа сигнализирует об ошибке и остановится. Этот эффект будет проявляться главным образом при вводе чисел, поскольку строки могут включать что угодно и не имеют каких-либо ограничений по содержимому. Если при чтении второй строки данных рис. 2.7 мы ввели бы с клавиатуры четыре буквы «five» вместо одной цифры «5», возникла бы ошибка. Извлечение из предложения языка отдельных элементов и преобразование их при необходимости в требуемую форму («синтаксический анализ») обозначается в английском языке словом parse, от чего и получил имя соответствующий метод. — Прим. перев.
2.5 Переменные, присваивание и ввод
65
В гл. 6 мы остановимся на том, как можно восстановить выполнение программы при такого рода ошибках и дать возможность пользователю повторить ввод. Пока же наши действия при ошибке будут заключаться в повторном запуске программы и вводе всех данных заново. Данные, прочитанные в память, стираются, когда программа прекращает свое выполнение (так называемая «сборка мусора»). При повторном запуске программы ее переменные заново инициализируются указанными в программе начальными значениями.
Пример 2.7. Определение времени с чтением Мы хотим модифицировать программу примера 2.5 определения времени начала совещания в Нью-Йорке и Лондоне, чтобы она могла использоваться и другими отделениями компании. Пока что мы даже не уверены, что наше совещание можно провести всюду в 14.00 лондонского времени, но мы хотели бы посмотреть, возможно ли это для Берлина (один час вперед), Сан-Франциско (восемь часов назад) и Перта (семь часов вперед). Изменим программу так, чтобы она могла прочитать название города и разницу во времени, и затем будем запускать программу для каждого нового отделения. Заметьте, что в случае отставания времени от лондонского, оно вводится в виде отрицательного значения. Программа очень похожа на предыдущую, но в ней используются некоторые из рассмотренных выше возможностей. Предложения ввода уже обсуждались; здесь они позволяют получить исходные данные, которые используются при вычислениях и выводе результата. Особо надо отметить, что для создания дружественной пользователю программы ввод должен всегда предваряться тем или иным запросом в форме предложения Write (но не WriteLine). В этом случае ответ пользователя будет вводиться на той же строке. Файл TimeReading.cs
using System; class TimeReading { ///<summary> ///Программа Time Reading
Bishop & Horspool April 2002
/ // ill " ///По-прежнему вычисляет время в конкретном городе по ///заданной разнице во времени по отношению к другому ///городу, но использует операцию ввода, что позволяет задать ///второй город во время выполнения ///Демонстрирует ввод переменных string и int /// void Go () { Console.WriteLine ("The Time Reading Program"); 3—2047
66
Глава 2. Использование объектов string city;//Город int offset;//Разница во времени int startTime • 14;г//14.00 или 2 часа дня DateTime now = DateTime.UtcNow; //Введем характеристики второго города Console.Write("City? " ) ; city = Console.ReadLine(); Console.Write("Offset from London? " ) ; offset = int.Parse(Console.ReadLine()); DateTime meeting = new DateTime (now.Year,now.Month,now.Day,startTime, 0,0) ; Console.WriteLine("The meeting in London is " + " scheduled for " + meeting) ; Console.WriteLine(city + " is " + offset + " hours different"); DateTime offsetMeeting = meeting.AddHours(offset); Console.WriteLine("The meeting in "+city+" will be at " + offsetMeeting.ToShortTimeString() ) ; } static void Main() { new TimeReading().Go();
Для демонстрации работы программы ее следует запустить несколько раз. В приведенном ниже выводе данные, введенные пользователем, показаны более светлым шрифтом. The Time Reading Program City? NewYork Offset from London? -5 The meeting in London is scheduled for 2002/08/05 New York is -5 hours different The meeting in New York will be at 09:00 AM The Time Reading Program City? Berlin Offset from London? 1 The meeting in London is scheduled for 2002/08/05 Berlin 1 hours different The meeting in Berlin will be at 03:00 PM The Time Reading Program City? Perth Offset from London? 7 The meeting in London is scheduled for 2002/08/05 Perth is 7 hours different The meeting in Perth will be at 09:00 PM
14:00:00
14:00:00
14:00:00
2.6 Основы API C#
67
The Time Reading Program City? San Francisco Offset from London? -8 The meeting in London i s scheduled for 2002/08/05 14:00:00 San Francisco i s -8 hours d i f f e r e n t The meeting in San Francisco w i l l be a t 06:00 AM Программа, безусловно, полезна, но запускать программу заново для получения каждого нового набора данных представляется довольно неудобным. Хотелось бы сделать так, чтобы программа сама могла воспринимать и обрабатывать многократные запросы. Как этого добиться, будет показано в последующих главах.
2.6 Основы API С# Аббревиатура API является сокращением от Application Programming Interface, интерфейс прикладного программирования, и используется, как и эквивалентный ему термин «пространство имен», для описания группы связанных классов, структур и т. д. В этой главе мы имели дело со следующими API: System System.Drawing System.Windows.Forms
Два последних интерфейса использовались только в примере 2.4 для вывода фотографии, и мы ими заниматься пока не будем. Однако мы использовали ряд важных классов, включенных в System, и теперь мы рассмотрим некоторые из них, не всесторонне, но достаточно детально, чтобы ими можно было пользоваться в следующих главах. DateTime Форма для структуры DateTime приведена ниже. Из нее можно получить более точную информацию о данных и функциях, которые мы уже использовали. Весьма полезными могут быть методы DaysInMonth и IsLeapYear. Далее мы видим, что в DateTime описаны два оператора вычитания; второй определяет разность двух дат и возвращает значение типа TimeSpan. Мы с этим типом пока не сталкивались. TimeSpan хранит время в днях, часах и еще меньших единицах. Если нам нужен ответ в годах, нам придется самим разделить результат на 365 (или 366).
Глава 2. Использование объектов
68 Структура DateTime (сокращено) //******* ***конструкторы** **********
DateTime(int год, int месяц, int день); DateTime (int год, int месяц, int день, int час, int минута, int секунда) ; //**********Статические свойства********** static DateTime Now;
//Текущие дата и время
static DateTime Today;
//Текущие дата и время //00:00:00)
(время равно
static DateTime UtcNow; //Текущие дата и время по Гринвичу (UTC) //***********Свойства экземпляра************ int Day int Month int Year int Hour int Minute int Second int Millisecond int DayOfYear //************Статические методы************* static
int
static
bool
static
int
Compare(DateTime tl,
DateTime t2)
Equals(DateTime tl,
DateTime t2)
DaysInMonth(int год,
static
DateTime Parse(string
static
bool
IsLeapYear(int
int месяц)
s)
год)
static DateTime operator - (DateTime d, TimeSpan t) static TimeSpan operator -(DateTime dl, DateTime o\2) static DateTime operator +( DateTime d, TimeSpan t) static bool operator ==(DateTime dl, DateTime d2) //************* *Методы экземпляра* * ************ int CompareTo(object значение) override bool Equals(object значение) string ToStringO string ToString(string формат) string ToShortDateString() ; string ToLongDateString() ; Во всех перечисленных категориях имеются много других членов. Для получения полной информации обратитесь к интерактивному справочнику или полному учебнику по API.
2.6 Основы API C #
69
В приведенной форме указаны четыре метода, выполняющие сравнение в общем виде и сравнение на равенство. Если d l и d2 являются экземплярами типа DateTime, эти методы можно вызвать следующим образом: DateTime.Equals(dl,d2) ; dl.Equals(d2); d2.Equals(dl);
//Вызов статического метода " //Вызов метода для экземпляра dl //Вызов метода для экземпляра d2
Здесь все три выражения дают один и тот же результат. Однако в типе DateTime определены также операторы для сравнения на равенство, которые облегчают чтение программы. Таким образом, вместо любого из приведенных выше выражений можно просто написать dl
== d2
Тип DateTime предоставляет также операторы сравнения, и мы можем использовать выражение вида dl
< d2
которое вернет true или false в зависимости от соотношения величин d l и d2, или применить один из методов Compare или CompareTo, действие которых слегка различается. Они возвращают отрицательное целое число, если первый операнд меньше второго, 0, если они равны, и положительное целое число, если первый операнд больше. Например, можно написать if
(dl.CompareTo(d2) < 0) { //dl описывает более раннюю дату,
чем d2
Очевидно, что операторы использовать проще, если, конечно, они определены. Операторы сравнения будут рассмотрены подробнее в разделах 3.4 и 4.1.
string и int В API отсутствует определение string. Это связано с тем, что почти все типы в С#, имена которых начинаются со строчных букв, имеют псевдонимы, т. е. другие типы с теми же именами. Реальный класс для string называется String; поскольку он входит в пространство имен System, он также называется System.String. Форма этого типа будет приведена в гл. 4. Если, однако, мы поищем в API определение int, то окажется, что, в отличие от string, типу int соответствует структура-псевдоним с другим именем — Int32. При этом Int32 (или int) предоставляет поля и методы, перечисленные в приведенной ниже форме.
Глава 2. Использование объектов
70
Структура int или int32 (сокращено) //Статические поля const i n t MinValue //-2 147 483 648 const i n t MaxValue // 2 147 483 647 //Статические методы s t a t i c i n t Parse(string s ) ; //Методы экземпляра s t r i n g ToStringO; Во всех перечисленных категориях имеется много других членов. Для получения полной информации обратитесь к интерактивному справочнику или полному учебнику по API. В дополнение к перечисленному, для целых чисел определены все обычные операторы. Random
Мы сначала ввели понятие типов неформальным образом, а затем, в этом разделе, начали описывать их спецификацию. Сделаем наоборот — сначала специфицируем тип, а затем приведем пример его использования. Рассмотрим класс случайных чисел. Его форма приведена ниже. Мы можем представить себе использование класса Random для моделирования выбрасывания игральной кости: Random throw = new Random();//Создаем экземпляр throw i n t d i c e = throw.Next(1,б);//Бросаем кость и получаем число //очков в dice Класс Random (сокращено) //Конструкторы Random () Random(int затравка); //Методы экземпляра int Next () ; i n t Next(int макс_значение) ; i n t Next(int мин_значение, int макс_значение) ; double NextDouble() ; Конструкторы возвращают случайные объекты, которые образуют последовательность псевдослучайных чисел. Методы Next возвращают следующее число в последовательности, возможно, между заданными значениями. NextDouble возвращает число в диапазоне от 0.0 до 1.0.
В приводимом ниже примере случайные числа типа Random используются в качестве объектов вместе с всеми остальными рассмотренными к настоящему моменту типами API.
2.6 Основы API C#
7f
Пример 2.8. Ваш счастливый день Предположим, что мы хотим получить от программы дату счастливого дня. Чтобы выбрать этот день случайным образом, мы просим пользователя ввести счастливое число. Введенное число дает начало индивидуальной последовательности случайных чисел. Затем с помощью этой последовательности мы генерируем номера дня и месяца. На первый взгляд, это можно сделать следующим образом: int int
Однако в этом случае мы можем получить неправильную пару чисел, например, 31 и 4 (31 апреля) или 29 и 2 (29 февраля). Последнее иногда допустимо, а иногда — нет. Поскольку все это просто игра, мы можем устранить ошибки, начав с марта и получая даты, не превышающие 30. Позже, в гл. 4, мы рассмотрим способы обработки специальных случаев и сможем модифицировать эту программу, чтобы она покрывала весь год. Итак, программа Lucky Day [Счастливый день]. Файл Lucky.cs using System; class Lucky { ///<summary> ///Программа Lucky Day
Bishop & Horspool Aug 2002
iii
///Определяет счастливый день ///Демонстрирует использование случайных чисел /// void Go () { Console.WriteLine("The Lucky Day Program\n"); Console.Write("What is your lucky number? " ) ; int luckyNumber = int.Parse(Console.ReadLine()); Random r = new Random(luckyNumber); int luckyDay = r.Next(1,30); int luckyMonth = r.Next(3,12); DateTime luckyDate = new DateTime (DateTime.Now.Year,luckyMonth,luckyDay); Console.WriteLine("Your lucky day will be " + luckyDate.DayOfWeek + " " + luckyDate.ToString("M" ) ) ; } static void Main() { new Lucky () .Go () ;
Возможный вывод программы будет выглядеть следующим образом: The Lucky Day Program What is your lucky number? 7 6 Your lucky day will be Saturday 13 April
72
Глава 2. Использование объектов
Весьма полезно проанализировать программу, обращая внимание на типы и данные. Мы используем пять различных типов: Lucky (класс, образующий программу), Console, int, Random и DateTime. В программу входят, как обычно, два метода Go и Main, а также ряд общедоступных методов, именно, WriteLine, Write, ReadLine, Parse, Next и ToString. К полям относятся luckyNumber, luckyDay, luckyMonth, luckyDate и г. Поля объявляются по мере возникновения в них необходимости; сможете ли вы переписать программу, перенеся объявление полей в начало класса? В программе также используются несколько свойств и конструкторов. Сможете ли вы их найти? Наконец, следует заметить, что работу программы нельзя проверить при однократном запуске. Запустите программу несколько раз и убедитесь в том, что она генерирует различные даты, хотя, разумеется, не в январе и феврале, поскольку мы исключили эти месяцы из рассмотрения.
Основные понятия, рассмотренные в гл. 2 В этой главе были рассмотрены следующие понятия (некоторые из них повторно): программа
структура программы
класс
метод
параметр
предложение
комментарий
сцепление (конкатенация)
ввод и вывод
структура
свойство
простое выражение
идентификатор
тип
переменная
объявление переменной
инициализация
оператор
присваивание (назначение)
член (элемент)
Рассмотрены следующие определения, синтаксические формы и API: объект
тип
простой тип
структурный тип
обращение к членам
объявление поля (простое)
создание экземпляра объекта
класс простой программы
простые предложения ввода
структура DateTime (сокращенно)
структуры int или Int32 (сокращенно)
класс Random (сокращенно)
Контрольные вопросы
73
Рассмотрены следующие операторы и знаки пунктуации:
Явным образом использовались следующие ключевые слова С # : using string int
.
class new
Демонстрировались следующие формы комментариев в программе:
Контрольные вопросы 2.1. Вспомните определения, данные в этой главе. Тип: (а) определяет данные (б) представляет числовое и функции значение (в) состоит из объектов (г) является либо структурой, либо классом 2.2. Вспомнив определения и принятые правила использования строчных и прописных букв, приведенные в этой главе, ответьте, обращением к чему является конструкция Part.Member: (а) к свойству объекта (б) к методу класса (в) к свойству класса (г) к полю объекта 2.3. Для сцепления двух строк мы используем: (a) concat (в) &
(б) + (г) ничего
2.4. Прочитать целое число с клавиатуры можно с помощью: (а) Console.ReadLine() (б) int.Parse(Console.ReadLine()) (в) int.Parse.Console.ReadLine() (г) Parse(Console.ReadLine())
2.5. Если требуется прочитать с клавиатуры две строки, они должны: (а) разделяться точкой с запятой (в) быть заключены в кавычки
(б) разделяться пробелами (г) вводиться на отдельных строках
74
Глава 2. Использование объектов
2.6. В конструкции DateTime.Today Today является: (а) свойством
(б) методом
(в) конструктором
(г) полем
2.7. Если endOfYear endOfYear-now?
и now являются переменными, каков тип
(a) DateTime (в) TimeSpan
(б) int (г) невозможно определить
2.8. Если test является объектом Random, какое выражение вернет значение между 0 и 100? (a) Next.test (100) (в) t e s t (0,100)
(б) test.Next(100) (г) t e s t (next (100) )
2.9. Чтобы получить доступ к классу Console, с какой директивы мы начинаем программу? (a) access Console; (в) using Console;
(б) import Console; (r) using System;
2.10. Для создания и запуска объекта, представляющего собой программу с именем Test, как описано в этой книге, какое предложение мы используем? (a) new Test (). Go ();
(б) new Test();
(в) Go (new Test ());
(г) Go .Test () .Main () ;
Упражнения Выполните эти упражнения на основе подхода, описанного в разделе 2.2, т. е. используя не конкретные операторы и конструкции С # , а общие принципы объектного ориентирования. Представьте свои ответы в виде связного текста или простых блок-схем. 2.1. Игра «Морские гонки». Фирма UPUV Games Inc. решила разработать компьютерную игру с использованием в качестве рабочего поля рис. 2.1, где игроки выступают в качестве капитанов морских судов. Составьте список типов, которые необходимо определить в игре, вместе с входящими в них данными и функциями. Для некоторых типов покажите, как будут выглядеть о дин-два объекта этих типов. 2.2. Даты. Считая, что у нас есть один структурный тип Date и два простых типа Integer и String, сконструируйте новый структурный тип для студента, записавшегося на прослушивание четырех курсов в университете. Продумайте состав данных этого типа с произвольной степенью подробности и включите в состав типа по крайней мере две функции, обслуживающие объекты студентов. Начертите диаграмму типа и изобразите два объекта времени, заполнив их значениями.
Упражнения
75
2.3. Видеокамеры. Разработайте тип для объекта киносъемки, включающий полезную информацию, которую способны фиксировать современные видеокамеры, например дату и скорость фильма. 2.4. Детский сад. У ребенка в детском саду имеются такие характеристики, как имя, фамилия, группа (например, кролики, собачки, рыбки и голуби), а также дата рождения. Создайте тип ребенка Child, содержащий всю эту информацию о ребенке и разработайте методы, полезные для обслуживания этой информации. Одним из методов должна быть функция, возвращающая год выпуска ребенка из детского сада, считая, например, что ребенок находится в детском саду до возраста 6 лет. Начертите диаграмму типа и изобразите два объекта этого типа с их значениями. 2.5. Главы книги. В следующих нескольких главах мы займемся разработкой программы, которая содержит информацию об учебнике. Начните с определения типа главы книги Chapter, в котором хранятся название главы и число страниц в ней. Включите в тип функцию, возвращающую первую страницу главы. Можете ли вы на основании проработанного материала придумать такую функцию? Используя настоящую книгу в качестве тестовых данных, создайте объекты для первых четырех частей. Следующие упражнения требуют анализа и модификации программ этой главы. 2.6. Анализ программы Lucky. Рассмотрев программу примера 2.8, выделите в ней составляющие элементы, как это было сделано для примера 2.4. 2.7. Правильное время. Программа из примера 2.3 выводит дату конца года с временем, соответствующим моменту запуска программы. Модифицируйте ее так, чтобы переменная endOfYear содержала в качестве времени 23:59. Подсказка: вам придется создать дополнительный конструктор, показанный в форме DateTime (раздел 2.6). 2.8. Расширение возможностей программы вычисления времени начала совещания. На основании перечня недостатков программы, приведенного в конце примера 2.6, разработайте модификацию этого примера. Опишите новые возможности в комментариях к программе и измените текст программы соответственно. 2.9. Другое изображение. Найдите какое-нибудь изображение и запустите программу Fetchlmage для вывода его на экран. Подгоняется ли размер окна на экране под размер вашего изображения? А теперь напишите с самого начала несколько новых программ. 2.10. Получение книг из библиотеки. Книги можно брать из библиотеки на 14 дней. Составьте программу, которая считывает имя абонента и выводит короткую квитанцию с указанием текущей даты и даты,
76
Глава 2. Использование объектов
когда книга должна быть возвращена. Текущую дату получите с помощью соответствующего свойства DateTime. 2.11. Проверка программы получения книг из библиотеки. Измените программу из упражнения 2.10, чтобы она вводила текущую дату (вместо того, чтобы получать ее с помощью свойства DateTime), а затем выполните тестирование вашей программы, вводя различные даты и проверяя, дает ли программа правильный ответ. Проверить следует те даты, которые повлекут переход в следующий месяц и даже год. 2.12. Конечный срок. Конечный срок выполнения задания указывается в виде даты и времени. Составьте программу, которая вводит все элементы конечного срока и выводит информацию о том, сколько часов осталось для выполнения задания. Вашей программе понадобится использование класса TimeSpan, который был упомянут в спецификации DateTime в разделе 2.6. 2.13. Часы зала заседания. Банк установил четверо часов в своем зале заседаний в Лондоне, которые показывают время в местах нахождения крупнейших бирж, именно, в Лондоне, Нью-Йорке, Токио и Гонконге. Разница зимнего времени в этих точках по отношению к времени по Гринвичу составляет 0, 5, -9 и - 8 . Составьте программу, которая читает время в Лондоне (в любом удобном для вас формате), создает по одному объекту времени для каждого города и выводит их вместе с названиями соответствующих городов. Объясните, почему вы не можете выполнить эти действия в таком порядке: создать объекты, прочитать время, изменить время в объектах, вывести результаты. 2.14. Часы зала заседания для любого момента времени. Измените программу из упражнения 2.13 так, чтобы время в Лондоне выбиралось с помощью датчика случайных чисел.
Внутри объектов 3
В главе 2 мы знакомились с основами программирования на примерах использования объектов, а теперь займемся вопросами определения новых типов объектов. Мы рассмотрим последовательно каждый компонент типа, начав с полей, конструкторов и свойств, и перейдя затем к предложениям, выражениям и простым типам, из которых составляются методы и операторы. Использование нового для нас вида предложения — простого цикла позволит привести и обсудить интересные примеры с большим объемом вывода.
3.1 Структура типа Объекты объявляются с помощью типов. В гл. 2 мы использовали предопределенные типы, такие как int, а также типы, принадлежащие пространствам имен, например, DateTime. В этой главе мы научимся создавать собственные структурные типы. При наличии такой возможности язык программирования становится открытым, приобретая способность бесконечно наращиваться. Действительно, в дополнение к сотням встроенных в API типов (вроде DateTime, Console и Random), мы можем определить неограниченное количество собственных типов. Давайте посмотрим на элементы, составляющие тип, с точки зрения создания нового типа.
Данные Данные новых типов состоят из полей существующих типов. Эти типы могут быть как типами API, например, int или string, так и ранее объявленными нами типами. Предложим, например, тип для описания информации об экзаменах, включая их даты. Состав данных такого типа может выглядеть следующим образом: struct Exam {//Экзамен s t r i n g course;//Предмет int weight;//Процентный вклад в суммарную оценку s t r i n g lecturer;//Лектор DateTime date;//Дата экзамена s t r i n g venue;//Место проведения экзамена s t a t i c int totalNoOfExams=0;//Общее число экзаменов
78
Глава 3. Внутри объектов
Мы здесь показали данные для простоты в виде полей; ниже мы увидим, как можно сделать их доступными через свойства. Теперь переменную типа Exam можно объявить следующим образом: Exam COSExam = new Exam( "COS110", 10, "Dr B r a i n " , new D a t a T i m e ( 2 0 0 3 , 4 / 2 0 ) , "ELB4.2");
Одно из полей типа Exam объявлено с атрибутом static, что означает, что оно будет существовать в единственном экземпляре для всех объектов типа Exam. В переменной totalNoOfExams мы записываем общее число экзаменов в системе с учетом всех объектов. Более того, мы в процессе создания не назначаем этому полю какого-либо значения, поскольку его вычисляет сам тип.
Функции Функции-члены новых типов значительно более интересны, так как с их помощью мы задаем фактические правила поведения объектов. Поведение описывается посредством предложений, и до сих пор мы использовали лишь очень ограниченный набор предложений. Наиболее важными были следующие типы предложений, с которыми мы имели дело: •
ввода;
•
вывода;
• вызова метода; • присваивания. В этой главе мы детально обсудим перечисленные предложения, а также познакомимся с предложением простого цикла. Остальные предложения будут рассмотрены в гл. 4. Теперь обратимся к тому, каким образом функции определяются, а также как проектировать и группировать функции с целью получения связного и практически полезного набора при разработке нового типа. Ранее мы определили четыре-вида функций — конструкторы, свойства, методы и операторы. Каждый из этих видов играет определенную роль в конструировании и использовании типа.
Конструкторы Каждый тип должен иметь конструктор. Конструктор представляет собой последовательность предложений с тем же именем, что и сам тип; конструктор вызывается, когда создаются объекты этого типа. Например, предложение
3.1 Структура типа DateTime
meeting
= new
DateTime(now.Year, now.Month, now.Day, s t a r t T i m e ,
0,
0);
вызывает конструктор DateTime. Тип может иметь несколько конструкторов, однако у них обязательно должны различаться списки параметров (список параметров функции называется ее сигнатурой). Другой конструктор DateTime используется в предложении DateTime
endOfYear
= new
DateTime(now.Year,
12,
31);
Для того чтобы познакомиться с внутренним устройством конструктора, рассмотрим конструктор для типа Exam, определенного выше. Exam(string с, i n t w, string course = c; weight = w; l e c t u r e r = 1; date = d; venue = v; t o t a l += 1;
1, DateTime d, string
v)
{
Для конструктора типично копирование предоставляемых ему параметров в поля объекта; этим могут ограничиваться его функции. Поскольку параметры и поля относятся к одним и тем же данным, принято именовать параметры начальными буквами названий полей (если, конечно, при этом не возникает конфликтов имен). Если конструкторы инициализируют поля объекта, то как это соотносится с инициализацией, которая может иметь место при объявлении полей? Оказывается, существуют определенные правила относительно структур и инициализации полей, сведенные в табл. 3.1. Табл. 3 . 1 . Инициализация полей структуры Понятие
Структуры
Поля экземпляра
Инициализировать при объявлении нельзя
Статические поля
Инициализировать при объявлении можно
Если есть конструктор
Должны инициализироваться все поля (а не некоторые)
Если конструктора нет
Полям придаются значения по умолчанию для типа, например, 0 для чисел и null для строк
Другими словами, значения полей экземпляров структуры должны устанавливаться внутри конструктора, а не там, где эти поля объявляются. Продолжая наш пример, предположим, что имеется, ряд экзаменов, место проведения которых (venue) к моменту истечения срока неизвестно. Тогда можно предусмотреть второй конструктор такого рода:
80
Глава 3. Внутри объектов
Exam (string с, i n t w, string 1, DateTime d) { course = c; weight = w; lecturer = 1; date = d; venue = "To be announced";//Будет объявлено позже t o t a l += I ;
Между конструкторами не будет конфликта, так как один требует пять параметров, а другой только четыре. Соответствующие предложения создания объектов могут выглядеть таким образом: Exam c o m p u t e r s Exam m a t h s
= new Exam("COS110", 5 0 , " P r o f Watson", new D a t e T i m e ( 2 0 0 3 , 1 1 , 1 3 ) ) ; = new Exam("MATH152", 8 0 , " D r J a m e s " , new D a t e T i m e ( 2 0 0 3 , 1 1 , 1 8 ) , "OldHall");
Другой структурный тип С#, класс, предоставляет конструктор по умолчанию для классов, который также оставляет все поля в неинициализированном виде. Это по-прежнему означает 0 или 0.0 для чисел и null для большинства других объектов.
Методы и свойства Рассматривая методы в принципе, мы можем завершить наш пример Exam, добавив несколько разумных методов пока без их содержимого: public TimeSpan DaysToExam() {...} public .override string ToStringO {...}
Первый метод прочитает поле date объекта и вычислит, сколько дней осталось до даты экзамена. Второй метод будет использоваться для вывода значений на печать. Он замещает другой метод с тем же именем, предоставляемый классом System по умолчанию, и вызывается непосредственно из предложений Console.WriteLine. В методе требуется указать модификатор overide [заместить], потому что он замещает существующий метод. Модификатор public требуется для обоих методов, чтобы они были видны и, соответственно, могли быть вызваны из программных строк, находящихся вне данной структуры. Почему только два метода? Дело в том, что большинство полезных функций в этом типе, как и во многих других, будут предоставлены свойствами. Свойства в первую очередь возвращают значения полей, однако они могут также использоваться для их установки. В результате мы можем постулировать наличие в нашем типе следующих свойств, хотя мы еще даже не умеем объявлять свойства: public public
string Course int Weight
3.2 Поля и свойства public public public public
81
string Lecturer DateTime Date string Venue s t a t i c int Total
Доступность. Заметьте, что для обоих методов, а также для всех свойств мы использовали модификатор public. Этот модификатор обеспечивает видимость определяемых элементов вне типа. Если не указать public при объявлении поля, оно будет рассматриваться как private для данного типа, т. е. закрытое для внешнего мира. Данные, объявленные в структуре (course, weight и т. д.) не имеют модификатора public и, следовательно, будут по умолчанию закрыты. Обычно так и поступают: данные объявляют закрытыми (private), а методы и свойства — открытыми (public).
План объявления типа Все элементы типа могут быть объявлены в любом порядке, однако по этому поводу имеются некоторые соглашения. Обычно мы сначала описываем данные, затем конструкторы, далее методы и операторы. Поэтому общий план структуры выглядит так, как это показано в приведенной ниже форме. Форма для определения класса будет такой же, только ключевое слово struct следует заменить на class. Определение структуры модификаторы struct идентификатор_класса { объявления_полей конструкторы объявления_свойств объявления_методов объявления_ опера торов другие_объявления
Элементы типа могут следовать в любом порядке, хотя обычно следуют плану, приведенному в этой форме. Другие объявления включают события и индексаторы, о которых речь будет идти позже.
3.2 Поля и свойства Важнейшей частью любого типа являются его данные, которые представляются в виде объявленных для них полей. Как было видно из общего обсуждения типов в гл. 2, поля могут содержать переменные (чаще всего) или константы (довольно редко). Отличием языка С# от большинства других языков является наличие понятия свойства. Свойство отражает
82
Глава 3. Внутри объектов
ту или иную черту (аспект) класса. Часто, хотя не всегда, это просто значение поля с почти идентичным именем. Если бы мы сделали поле доступным для всех остальных объектов, тогда они могли бы читать это поле и записывать в него. Введя в класс свойство, мы защищаем поле, контролируя доступ к нему извне. Такая методика носит название инкапсуляции. Синтаксис определения свойств приведен ниже в виде формы. Определение свойства public
ТИП идентификатор_свойства {
get
{предложения,
включающие
return
имя; }
set
{предложения,
включающие присваивание для члена
имя;}
Тело метода get определяет, что происходит, когда используется идентификатор свойства, например, с правой стороны операции присваивания или в качестве аргумента метода. Тело метода set определяет, что происходит, когда используется идентификатор свойства, например, с левой стороны операции присваивания. В определении свойства должен присутствовать хотя бы один из методов (или оба).
Ключевые слова get и set показывают, что для этого поля возможна операция присваивания. После того, как свойство объявлено, его можно использовать вместо поля, которое оно защищает. Мы видели уже много примеров таких действий в гл. 2 применительно к встроенным типам. Теперь рассмотрим пример в предположении, что мы уже определили тип Exam. Свойство для изменения поля venue будет определено таким образом: public get set
s t r i n g Venue {//Место проведения экзамена { return venue; }//Получить место проведения экзамена { venue = value; }//Установить место проведения экзамена
} •
Слово value [значение] имеет в этом контексте вполне определенный смысл. Оно соответствует значению выражения с правой стороны операции присваивания для свойства Venue. Примеры использования этого свойства: //Распечатаем место проведения экзамена по компьютерам Console.WriteLine("The exam i s in " + COSExam.Venue); //Теперь изменим место проведения экзамена venue COSExam.Venue - "HSB3.10";
Если мы определяем свойство без компонента set, то изменения соответствующей этому свойству переменной запрещаются. Пусть, например, мы желаем предотвратить изменение названия экзамена. Тогда мы определим соответствующее свойство таким образом: public string Course { get {return course;}//Получить название курса
3.2 Поля и свойства
83
Теперь воспользуемся всеми полученными нами знаниями относительно данных и функций для создания нового типа. Пример 3.1. Тип StarLord
Важнейшим этапом составления компьютерной игры является определение персонажей, которые будут бороться друг с другом за верховную власть. Предположим, что мы пишем программу для такой игры. Мы начнем с определения типа ее персонажей. Назовем этот тип StarLord [Повелитель звезд] и представим его в виде такой структуры С#: Файл Star Lords, cs using System; ///<summary> ///Персонажи StarLord // /
Bishop & Horspool May 2002
til
///Персонажам свойственны определенные характеристики ///и возможность бороться с другими Повелителями звезд /// public struct StarLord { string name; //Имя int strength, //Сила int points; //Баллы static Random r = new Random();//Случайное число public StarLord(string n, int s) { name = n; strength = s; points = s; } public string Name { get {return name;} } public int Points { get {return points;} set {points = value;} } public int Strength { get {return strength;} } public void Attack(StarLord opponent) { int damage - r.Next(strength/3+points/2);//Повреждение opponent.Points -= damage;//Уменьшение баллов оппонента } public override string ToStringO { return name + " at " + points;
Мы уже достаточно изучили типы, чтобы понимать, что можно объявить два объекта StarLord следующим образом:
84
Глава 3. Внутри объектов
StarLord
lordl,
Iord2;
после чего инициализировать их предложениями: lordl Iord2
= new S t a r L o r d ( " D a r t h " , 1 4 ) ; = new S t a r L o r d ( " B i l b o " , 1 0 ) ;
Заметьте, что поле points инициализируется конструктором, хотя для этого поля в конструкторе нет отдельного параметра. Два объекта могут затем взаимодействовать посредством определенного в StarLord метода Attack: lordl.Attack(Iord2);
В методе Attack выполняются следующие предложения: int damage = r.Next (strength/3+points/2); opponent.Points -= damage;
Степень повреждения вычисляется исходя из текущей силы персонажа и его текущего числа баллов. Полученная величина затем вычитается из числа баллов оппонента. В нашем примере объектом является lordl (с именем Darth), а оппонентом, указанным в качестве параметра метода Attack — Iord2 (с именем Bilbo). В классе StarLord предусмотрены три свойства: Name, Points и Strength. Свойства защищают переменные, так что name и strength можно только прочитать (get), однако points можно как читать, так и изменять (get и set).
Статические свойства и свойства экземпляра Мы уже видели в описаниях API в разделе 2.6, что некоторые свойства объявляются, как статические (static). Это означает, что они приложимы ко всем объектам данного типа. В типе Exam поле totalNoOfExams объявлено статическим и, скорее всего, будет иметь соответствующее ему статическое свойство. Другой пример: предположим, что мы хотим вести учет количества Повелителей звезд во вселенной. Тогда мы добавим к типу StarLord следующую переменную вместе с соответствующим ей свойством: static public get
int count = 0; static int Count { {return count;}
}
которое будет вызываться так:
Внутри объектов
85
Console.WriteLine("There are "+StarLord.Count+" StarLords now"); Разумеется, и конструктор следует изменить так, чтобы при создании каждого нового Повелителя звезд выполнялся инкремент переменной count. Пример 3.2. Тип Time Хотя мы в гл. 2 интенсивно использовали тип DateTime, нетрудно предугадать, что в ряде случаев более удобным может оказаться облегченный вариант этого типа, например, для работы только со временем (без даты). Получив представление о конструкторах и свойствах и проработав пример StarLord, мы вполне способны определить такой тип. Этот пример показывает, что могут существовать различные модели, приводящие к нескольким типам с различными способами обращения к данным. Прежде всего, определим данные: int
hour,
min;//4ac,
минута
По умолчанию эти поля закрыты (pivate). Чтобы открыть к ним доступ, их следовало объявить таким образом: public
int
hour, min;
Однако это разрешит неконтролируемый и, возможно, противоречивый доступ к переменным hour и min. Альтернативой будет объявление объекта постоянным, чтобы после создания объекта его нельзя было изменить. Результатом изменений будут новые объекты. Это приводит к такому конструктору: public Time (int h, i n t m) { hour = h; min = m; и двум свойствам: public Hour { get {return hour; } public Min { get {return min;}
которые допускают доступ к соответствующим полям только для чтения. В чем еще нуждается тип? Ему нужна описывающая его структура и почти всегда метод ToString. В результате мы имеем такой полный вариант простого типа для времени:
86
Глава 3. Внутри объектов
Файл Times.cs struct Time { ///<summary> ///Простой класс Time // / ///Сохраняет часы и минуты ///Не проверяет задаваемое время ///Объекты, будучи созданы, не могут быть /// int hour, min //Час, минута public Time (int h, int m) { hour = h; min = m; } public int Hour { get {return hour;} } public int Min { get {return min;) } public override string ToStringO { return hour + ":" + min;
изменены
Пусть мы хотим создать время, изменить час на 12 и вывести оба времени. Нельзя написать: Time a = new Time (someHour, Console.WriteLine(a); a.Hour = 12; Console.WriteLine(a);
someMin);
потому что Hour — это свойство без компонента set. Вместо этого напишем так: Time a = new Time (someHour, someMin); Console.WriteLine(a); a = new Time(12, a.Min); Console.WriteLine(a);
или даже еще лучше так: Time a = new Time(someHour, someMin); Console.WriteLine (a); Time b = new Time (12, a.Min); Console.WriteLine(b);
где а и b теперь существуют независимо и могут изменяться как угодно.
3.3 Числовые типы
87
3.3 Числовые типы Из семейства языков С язык С# унаследовал 11 числовых типов. Их можно сгруппировать следующим образом: • целые со знаком (sbyte, short, int, long), • целые без знака (byte, ushort, uint, ulong), • действительные с плавающей точкой (float, double), • действительные с фиксированной точкой (decimal). Из всех этих типов в каждодневном программировании чаще других используются int и double, наиболее удобные представители целых и действительных чисел. Числовые типы имеют некоторые общие характеристики: 1. Каждая из четырех групп допускает различные способы представления чисел в компьютере, в смысле расположения битов. Расположение битов влияет на значения, которые мы можем или, наоборот, не можем использовать, и на ошибки, возникающие из-за недостаточной точности представления числа. 2. Каждый тип характеризуется размером и использует для хранения числа определенное количество битов от 8 до 96. Размер влияет также на диапазон хранимых чисел. 3. Каждый тип прямо соответствует структурному типу в пространстве имен System; следствия этого обсуждаются в гл. 8. 4. Значения одного типа обычно могут быть преобразованы в другой, если только второй тип может содержать в себе все значения первого. Так, короткое (short) значение может сохраняться в длинной (long) переменной, но не наоборот, если только не использовать явно указанное преобразование; в последнем случае возможны ошибки. См. ниже о преобразованиях. В табл. 3.2 приведены характеристики пяти типов с примерами. В таблице содержится значительный объем информации. Рассмотрим ее более подробно.
Целочисленные типы Мы обсудим здесь три целочисленных типа — byte, int и long. Тип byte описывает числа без знака, а типы int и long — со знаком. Число типа byte сохраняется в 8 битах и не имеет знака (т. е. все его Q возможные значения считаются положительными), давая 2 = 2 5 6 комбинаций нулей и единиц. Поскольку одна из этих комбинаций характеризует 0, максимальное значение равно 255. Значения типа byte используются в тех случаях, когда мы пишем системную программу, манипулирующую с числами в компьютере на уровне отдельных байтов.
88
Глава 3. Внутри объектов
Таблица 3.2. Характеристики некоторых числовых типов
В большинстве программ, работающих с целыми числами, используется тип int. Этот тип позволяет задать в целом числе до 10 десятичных разрядов, что довольно много. Из-за того, что числа могут иметь как положительные, так и отрицательные значения, максимальное абсолютное О
31
значение числа составляет I , что как раз и соответствует значениям, приведенным в табл. 3.2. Количество положительных значений на 1 меньше количества отрицательных, так как одна из комбинаций битов отводится под 0. Если сравнить тип int с типом sbyte (signed byte, байт со знаком), то мы увидим, в типе sbyte используется та же система представления. Его 8 бит дают 256 различных комбинаций, и они используются для представления чисел в диапазоне от —128 до 127 плюс еще 0. Тип long находится в той же группе целых чисел со знаком, что и int, но в таблице для наглядности диапазон этих чисел указан приблизительно в виде степени десяти. Наличие в числе 64 битов дает 2 различных значений (помните, что половина этих чисел отрицательна, а другая половина — положительна, включая 0), а это приблизительно составляет 10 1 8 .
Типы с плавающей точкой Мы включили в нашу таблицу тип long, потому что поучительно его сравнение с типом double. Оба эти типа содержат одно и то же число битов, однако дают сильно различающиеся диапазоны значений и число десятичных разрядов. Так происходит потому, что числа типа double представляются в памяти совершенно не так, как long. Их представление состоит из двух компонентов со знаком, мантиссы и показателя степени (порядка). Мантиссой называется число, начинающееся с цифры, за которой идет десятичная точка, а за ней — оставшаяся часть числа. Показатель степени указывает, где в действительности среди представленных цифр числа должна стоять десятичная точка. Такое представление носит название представления с плавающей точкой. Хотя в компьютере все числа записываются
3.3
Числовые типы
89
в двоичной форме, формат с плавающей точкой в огромной степени расширяет возможный диапазон представляемых чисел. Это можно увидеть из табл. 3.3, где приведено несколько примеров таких чисел с указанием их десятичного представления. Т а б л и ц а 3 . 3 . Примеры чисел с плавающей точкой Число (а) 5.0 (b) 0.005 (с) -0.005 (d) 5000.0 (е) 500300.0 (f) 50030000000000000000.0 (g) 0.00000000000000000005 (h) 5003000000000.0000077 (i) 5.0E60 (i) 5.0E-60
Пример (а) совсем простой, однако заметьте, что если вы хотите записать число типа double, оно должно содержать десятичную точку; числа без десятичной точки рассматриваются, как целые. Примеры (b)-(g) таблицы иллюстрируют преимущества представления с плавающей точкой: нули в начале или в конце числа не записываются в мантиссу, а учитываются в значении порядка. В результате в мантиссе оказывается больше места для значащих цифр, независимо от того, каков порядок всего числа. Это проиллюстрировано в примере (f), где показано число с 20 десятичными цифрами. В табл. 3.2 указано, что числа double могут содержать лишь 15-16 разрядов, но это относится только к значащим цифрам. Такое же число, как в примере (f), представлено в примере (е), но с другим значением порядка. В примере (g) единственная значащая цифра 5 не смогла бы попасть в 16-разрядное число, но в формате с плавающей точкой она прекрасно помещается. Тем не менее, и плавающая точка не всесильна, и иногда число теряет в точности. В примере (h) все 20 разрядов числа являются значащими, однако реально в памяти могут быть представлены лишь 15 или 16 разрядов. Последние несколько разрядов пропадают, и все число попадет к пользователю с погрешностью по отношению к тому числу, которое было реально введено. Примеры (i) и (j) демонстрируют формат Е (сокращение от слова exponent, показатель степени), с помощью которого мы можем записать число с указанием его порядка. Этот формат удобен в тех случаях, когда справа или слева от десятичной точки имеется много нолей. Например, число из примера (f) можно записать в программе, как 5.003Е20. Его же можно записать и иначе, например, 50.03Е19, если такая запись для пользователя более наглядна. В конце главы читатель найдет несколько контрольных вопросов по поводу формата с плавающей точкой.
90
Глава 3. Внутри объектов
Типы с фиксированной точкой Последний тип, показанный в табл. 3.2, decimal, интересен в том отношении, что, как и типы с плавающей точкой, он представляет действительные числа, но, в отличие от них, позволяет делать это абсолютно точно, без погрешности. В этом типе всегда 28 десятичных разрядов. Однако диапазон возможных значений значительно уже. Числа типа decimal должны записываться в программах с завершающей буквой «М». Такие числа используются в основном в финансовых вычислениях. Например, можно написать decimal taxRate = 12.50М;//Ставка налога
Некоторые другие типы также требуют указания при числе различных букв, чтобы отличить их от более распространенных типов. Например, при записи числа, которое должно иметь тип float (а не double), необходимо использовать букву F вместо Е, например, 3.145F0. С другой стороны, буква L, обозначающая тип long, не является обязательной.
3.4 Выражения Мы подошли к одному из наиболее существенных разделов программирования: как записывать выражения. Выражения уже упоминались вскользь в конце раздела 2.4. Здесь мы рассмотрим этот вопрос более подробно. Выражения являются представлением на компьютерном языке математических формул, однако между ними имеются несколько различий. Правила, касающиеся того, какие типы переменных можно комбинировать, и какие при этом получаются результаты, сильнее связаны с фактическим представлением данных в компьютере, чем с математикой, и эти правила необходимо понимать и помнить. Далее, в дополнение к числовым выражениям, имеются и другие их виды, например, выражения отношения, строковые и битовые. Наконец, имеются предложения выражений, к которым, в частности, принадлежит операция присваивания.
Числовые операторы Операторы используются для построения выражений, в которых участвуют числа, поля и функции. С# предоставляет как операторы, соответствующие обычным арифметическим обозначениям, так и некоторые новые. К первой группе относятся операторы -\— * и /. Оператор * используется для обозначения умножения. Поскольку выражения всегда пишутся на одной строке, оператор / используется для обозначения деления. Часто для обеспечения правильного порядка выполнения действий в выражении используются скобки. Примеры выражений: c o s t * exchangeRate temp * 9.0 / 5 - 32 (b * b - 4 * а * с)
/
(2
* a)
3.4 Выражения
Для каждого из типов данных можно использовать каждый из перечисленных выше операторов. Что произойдет, если мы смешаем в одном выражении данные разного типа? Простой ответ заключается в том, что результат повышается до типа с большим диапазоном значений. Для типов int, long и double правила повышения сведены в следующую таблицу: Повышение типа (сокращено) Если один из операндов имеет тип double, результат будет типа double, в противном случае, если один из операндов имеет тип long, результат будет типа long, в противном случае результат будет типа int.
Для операций над типом byte результат всегда преобразуется в int. Для данных типа decimal результат будет иметь тип decimal, за исключением того, что числа с плавающей точкой не могут смешиваться с числами типа decimal без преобразования (преобразование будет описано ниже/. Важный сопутствующий результат этих правил заключается в том, что деление целых чисел дает целый результат. Поэтому, например, получается: 7 / 3 3 / 4
= 2 = 0
Таким образом, если мы включим в программу дробь, равную одной второй, в виде 1/2, фактически будет использовано значение 0. Наконец, оператор % вычисляет остаток от деления (операция деления по модулю). Предположим, что нам надо преобразов'ать число минут в часы и минуты. Это можно сделать таким образом: int totalMins, hours, mins;//Полное число минут, часы, минуты totalMins = int.Parse(Console.ReadLine());//Введем полное число //минут hours = totalMins / 60;//Целочисленное деление mins = totalMins % 60;//Получим остаток Console.WriteLine(totalMins + " minutes = " + hours + " hours, " + mins
+ " minutes");
Введя значение 429, м ы получим: 429 minutes
= 7 hours,
9 minutes
а введя значение -429, получим 1
Полный перечень этих правил приведен в спецификации ЕСМА С# Language Specification, разд. 14.2.6.
92
Глава 3. Внутри объектов
-429 minutes = -7 hours, -9 minutes Заметьте, что 7x60+9 = 429, а -7x60-9 = -429. В общем виде, если х и у — два числа, тогда операции деления и получения остатка удовлетворяют условию: (х/у) ху +
(х%у) =
х
Завершая обсуждение оператора %, заметим, что 7% -2 дает 1, а -7%-2 дает - 1 . Другими словами, остаток всегда имеет знак левого операнда. Обратите внимание на то, что оператор % можно использовать со всеми числовыми типами, не только с целыми. Пример 3.3. Автобус-челнок Автобус-челнок, перевозящий пассажиров между городом и аэропортом, покидает аэропорт каждые полчаса. Указав время в часах и минутах, мы хотели бы узнать время отправления следующего автобуса. Наша привычка оперировать с часами и минутами настолько развита, что эту задачу ничего не стоит решить в уме, однако для компьютера она потребует некоторого объема вычислений. Задача разбивается на две части: представление времени и выполнение вычислений для следующего автобуса. Поскольку в примере 3.2 мы уже разработали простой тип для описания времени, начнем с него. Мы можем создать объект типа времени: Time
now
= new
Time (h,
m) ;
где h и m вводятся с клавиатуры и соответствуют свойствам now.Hour и now.Min. Вычисления оказываются довольно запутанными: int nextBusInMins = (now.TimeInMins+30)/30*30;
Здесь требуются некоторые объяснения. Если N — целое число, то N/30 дает целочисленный результат деления на 30. Тогда N/30*30 дает другое целое число, кратное 30, но, вероятно, меньшее, чем N, поскольку при делении обычно выполняется округление в меньшую сторону. Выражение будет равно N только когда N кратно 30. Выражение (N+30)/30*30 даст следующее число, кратное 30. Теперь рассмотрим пример с единицами времени. Пусть время равно 9:45. Мы можем получить от объекта это время, выраженное в минутах (для выполнения этой операции к типу Time было добавлено новое свойство), т. е. 9x60+45 = 585. Добавление к числу минут числа 30 даст 615. Целочисленное деление на 30 даст 20. Умножение этого результата на 30 даст 600 минут, или 10 часов, т. е. время 10:00. Из этого времени мы конструируем новый объект с помощью второго конструктора:
3.4
Выражения
93
public Time(int t) { hour = t / 60; min = t % 60;
Тогда следующим предложением программы будет: Time nextBus = new Time(nextBusInMins); Мы можем внести в программу усовершенствование, позволяющее обобщить ее на любой интервал движения автобуса, не только 30 минут. Предположим, что летом автобусы отправляются с интервалами 20 минут. Нам не хотелось бы просматривать весь текст программы и отыскивать места, в которых встречается число 30. Вместо этого мы заменяем константу 30 на имя поля, объявляя это поле константой. В приводимом ниже примере эта константа имеет имя interval. Имея два объекта времени, мы можем вывести на экран требуемую информацию. Текст программы приведен ниже. Файл Shuttle.cs using System; class ShuttleBus { ///<summary> ///Программа Shuttle Bus Bishop & Horspool Aug 2002 /// == ========== ///Вычисляет время подхода следующего автобуса в начале или ///середине часа ///Демонстрирует определение структур, а также использование ///свойств и арифметических операторов, включая деление по ///модулю /// const int interval = 30; void Go() { Console.WriteLine("What is the time? " ) ; Console.Write("Hour: " ) ; int hours = int.Parse(Console.ReadLineО);//Введем часы Console.Write("Min: " ); int mins » int.Parse(Console.ReadLine());//Введем минуты Time now = new Time(hours,mins) ; int nextBusInMins = (now.TimelnMins + interval) /interval*interval; Time nextBus = new Time(nextBusInMins); Console.WriteLine("Time is " + now + " and the next bus is at " + nextBus) ; } static void Main() { new ShuttleBus () .Go () ;
94
Глава 3. Внутри объектов
struct Time { ///<summary> ///Простой класс Time III
///Сохраняет часы и минуты; не проверяет введенное время ///Объекты, будучи созданы, не могут быть изменены /// int hour, min;//4acbi, минуты public Time (int h, int m) { hour = h; min • m; } public Time(int t) { hour = t / 60; min = t % 60; } public int Hour { get {return hour;} } public int Min { get {return min;} } public int TimelnMins { get {return hour * 60 + min;} } public override string ToStringO { return hour + ":" + min;
Чтобы проверить программу, мы вводим, например, 9:14. Вывод программы будет таким: What i s t h e time? Hour: 9 Min: 14 Time i s 9:14 and t h e
next bus
is
at
9:30
Для исчерпывающего тестирования программы мы должны вводить разные значения времени, и среди них те, которые приведут к пересечению характерных временных границ. Набор тестовых данных может иметь следующий состав: 9 9 9 9 9 10 23
14 29 30 45 59 00 35
3.4 Выражения
95
Что случится в последнем случае? Если мы прочитаем описание типа Time, мы увидим, что в нем фактически не выполняется никаких проверок значений, поэтому в этом случае мы получим результат 24:00.
Выражения отношения Следующей полезной операцией над числами является их сравнение. С этой целью большинство языков предоставляют целый набор операторов сравнения, именно, = = != < > <= >=
равно не равно меньше чем больше чем меньше чем или равно больше чем или равно
Результатом такого сравнения двух значения будет значение типа bool, причем это значение может быть либо true [истина], либо false [ложь]. Подробное обсуждение типа bool, имеющего собственный набор операторов, проводится в гл. 4. Здесь мы только отметим, что с помощью перечисленных выше операций можно сравнивать числовые значения. Такого рода сравнения будут использованы ниже для управления циклами. Для каждого из числовых типов существует каждый из перечисленных операторов. Если в одном предложении сравнения смешиваются разные числовые типы, тогда используется оператор более «высокого» типа: Типы результата сравнения Если один из операндов имеет тип double, выполняется сравнение типов double, в противном случае, если один из операндов имеет тип long, выполняется сравнение типов long, в противном случае выполняется сравнение типов int.
Учитывая, что типы с плавающей точкой могут представлять данные неточно, лучше избегать сравнения чисел этих типов на равенство. Обычно оказывается достаточным применить сравнение на неравенство. Примеры условных выражений: i < 10 t.Year == 2003 DateTime.Now != first x >= у Для числовых типов предусмотрены два специальных оператора, ++ и — . Их можно использовать для прибавления или вычитания единицы из переменной, участвующей в выражении. Очень часто, однако, они исполь-
96
Глава 3. Внутри объектов
зуются не внутри выражений, а в качестве самостоятельных предложений, например, так:
mass--;
Результат таких записей заключается в том, что единица вычитается из значения переменной (или прибавляется к нему). Приведенные выше предложения эквивалентны следующим: i = i+1; mass = mass-1; Фактически имеются две формы операторов ++ и — . Можно написать как х++ (постфиксная форма), так и ++х (префиксная форма), и то же самое для оператора — . Если эти операторы используются в качестве самостоятельных предложений, нет никакой разницы между префиксной и постфиксной формой этих операторов. (В программах этой книги операторы инкремента и декремента всегда используются в виде самостоятельных предложений.) Ниже мы увидим примеры использования этих операторов в циклах.
Операторы присваивания Наряду с простым оператором присваивания =, имеется возможность комбинирования этого оператора с каждым из других арифметических операторов с образованием составных операторов присваивания. Для чисел это будут следующие операторы: +=
Во всех случаях смысл такой записи будет в выполнении указанной арифметической операции над числами, стоящими в правой и левой частях выражения и в присваивании результата этой операции левому операнду выражения. Таким образом, в предложениях i += 2; mass -= 5;
выполняется прибавление 2 к i и вычитание 5 из mass, соответственно. Эти операторы являются просто сокращенными записями, но они экономят время на ввод программы и весьма популярны среди программистов на С#.
3.4 Выражения
97
Другим расширением присваивания является множественное присваивание. Допустимо написать в одной строке September = a p r i l
= June = november = 30;
Оператор присваивания, как будет объяснено ниже, является правоассоциативным, поэтому указанные в примере операции присваивания будут выполняться справа налево.
Приоритет и ассоциативность Наконец, мы должны рассмотреть понятие приоритета (старшинства) применительно к рассмотренным операторам. Приоритет определяет, в каком порядке выполняются операторы. В обычных математических выражениях умножение всегда выполняется перед вычитанием, в каком бы порядке эти действия не были указаны в выражении, поэтому 10-3*2 дает в результате 4, а не 14. К счастью, в С# операторы действуют точно таким же образом. В табл. 3.4 перечислены операторы формально принятых в С# групп. Первая группа имеет наивысший приоритет; далее приоритет снижается. Т а б л и ц а 3 . 4 . Приоритет операторов и ассоциативность Группа
Операторы 1 х.у
f(x)
Ассоциативность
Первичные
(х)
++ —
new
Справа налево
Унарный
-
Мультипликативные
*
Аддитивные
+ -
Слева направо
Отношения
< > < = > =
Слева направо
Равенства
==
Слева направо
Присваивания
= *=
Справа налево /
%
Слева направо
!• /=
%= += - =
Справа налево
Здесь х обозначает переменную или выражение; у обозначает поле или функцию; f обозначает функцию.
Ассоциативность определяет порядок выполнения операций по отношению к операторам одной группы. Все бинарные операторы левоассоциативные (за исключением присваивания). Унарные операторы, например, точка или new, правоассоциативные. Поэтому выражения, включающие несколько операторов плюс, выполняются слева направо (левоассоциативный оператор). То же относится и к оператору минус, что соответствует нашим привычкам. Например, 1 0 - 4 - 1 + 3 4—2047
98
Глава 3. Внутри объектов
даст 8. Мы и не ожидаем, что это выражение будет вычисляться, как 10
-
(4
-
(1
+ 3)
что даст 10. На практике арифметические операторы на вызывают особых затруднений, однако следует проявлять осторожность, когда эти операторы взаимодействуют с группами первичных операторов или операторов отношения. В табл. 3.5 приведены некоторые примеры, которые помогут яснее представить себе рассмотренные понятия. Таблица 3.5. Примеры действия приоритетов Вычисляется, как
Выражение 1 + 2 / 3 + 4
1
х+у > p+q
(х
today.Hour
+ (2 + у)
/ 3) >
(р + q)
(today.Hour)
+ 10
+ 4
+ 10
Преобразования Мы уже несколько раз упоминали преобразования типов. Понять принцип преобразований в С# очень легко. Значение может быть неявным образом повышено к следующему старшему типу. Такое неявное преобразование выполняется при смешивании в выражении различных значений. Правила преобразования были перечислены в разделе 3.4. Желая выполнить обратное преобразование, старшего типа в младший, мы должны использовать явное преобразование. Правила для него таковы: Явное преобразование (идентификатор_ типа) checked
выражение
({идентификатор типа)
выражение)
Выражение вычисляется и преобразуется в указанный тип, возможно, с потерей информации. Действительные числа при преобразовании в целые значения округляются вниз (в сторону ноля). Если тип не может содержать результирующее значение и включена проверка (модификатор checked), возникает ошибка, называемая OverflowException [исключение переполнения]. Помещение модификатора checked перед преобразованием приводит к возникновению исключения даже если режим проверки выключен в компиляторе.
Поэтому если в программе описаны следующие переменные: i n t i = 6; double x e 3.14; double у - 5.003000000008Е+15; double z;
3.5 Простые циклы
99
то приведенные ниже операции присваивания приведут к результатам, описанным в комментариях: z i i i i
= = = = =
i;//Всегда правильно — значение равно б. О х;//Ошибка компиляции — требуется явное преобразование у;//Ошибка компиляции — требуется явное преобразование (int) х;//Правильно — значение равно 3 (int) у;//Ошибка выполнения, если включен режим проверки //(checked), неопределенное значение в противном //случае
Значение i в последнем случае при выключенной проверке будет равно —1244672188, что не имеет никакого отношения к значению у, за исключением того, что некоторые биты величины у будут рассматриваться, как целое число. Такого же рода результат получится, например, при смешивании целых и байтов, или любых двух числовых типов, требующих указания явного преобразования . Очевидно, что проверка преобразований числовых типов является важной операцией, и вам следует убедиться, что режим проверки включен в той среде, в которой вы работаете. Альтернативой этому является явное указание модификатора checked, как это показано в приведенной выше форме. Операция явного преобразования и модификатор checked используют круглые скобки вокруг выражения, к которому они применяются и, как было показано в табл. 3.4, рассматриваются как первичные операторы.
3.5 Простые циклы Выражения составляют основную часть любой программы, однако они становятся особенно полезны, если повторяются многократно. Именно в этом случае особенно ярко проявляется истинное могущество такой машины, как компьютер. Люди не любят выполнять нудные повторяющиеся операции, компьютер справляется с ними гораздо лучше. Поэтому мы приступаем к рассмотрению очень простой структуры, выполняющей циклическую работу. Простой цикл управляется переменной-счетчиком, начальное значение которой показывает, сколько раз нам надо выполнить заданную операцию. Есть и более сложные циклы; мы рассмотрим их в следующей главе. Ключевом словом, позволяющим организовать циклическое выполнение, является слово for, поэтому такие циклы известны под названием «циклы for». Ниже приводится форма для таких циклов.
Полный перечень этих правил приведен в спецификации ЕСМАС# Language Specification, разд. 14.2.6.
100
Глава 3. Внутри объектов
Цикл for (простой)
for
(int счетчик = начало; счетчик <= предел; тело цикла
счетчик ++) {
Переменная счетчик пробегает значения от начало до предел, а предложения, формирующие тело цикла, повторяются предел-начало+'\ раз. Выражение счетчик++ может быть заменено любым другим выражением, которое изменяет значение переменной счетчик, например, счетчик+=2 или счетчик—.
Например, цикл for
( i n t 1 = 1 ; i<=10;
i++){...}
будет повторяться 10 раз, а переменная i будет последовательно принимать значения от 1 до 10. Для того чтобы вывести строку три раза, можно воспользоваться следующим циклом: for
(int i = l ; i<=3; Console.WriteLine("Hip
Hip Hooray"
Вывод этого фрагмента: Hip Hip Hooray Hip Hip Hooray Hip Hip Hooray
Первый класс программ, естественным образом использующих циклы — это программы вывода таблиц. Воспользуемся в качестве основы примером из гл. 2. Пример 3.4. Таблица значений времени начала совещания Нам хотелось бы представить в форме таблицы все возможные значения времени начала совещания в течение дня. Воспользовавшись примером 2.7, мы можем модифицировать его так, чтобы на экран выводилась таблица вроде следующей: London 08:00 AM 09:00 AM 10:00 AM 11:00 AM 12:00 PM 01:00 PM 02:00 PM 03:00 PM 04:00 PM 05:00 PM 06:00 PM
New York 03:00 AM 04:00 AM 05:00 AM 06:00 AM 07:00 AM 08:00 AM 09:00 AM 10:00 AM 11:00 AM 12:00 PM 01:00 PM
3.5 Простые циклы
101
Вспомним, что в исходной программе были два предложения для создания двух объектов после ввода значений, но до вывода результата. Предложения имели следующий вид: DateTime meeting = new DateTime(now.Year, now.Month, now.Day, s t a r t T i m e , 0, 0 ) ; DateTime offsetMeeting - meeting.AddHours(offset); При разнице во времени, равной —5, созданные объекты имели состав, показанный на рис. 3.1. Обратите внимание на то, что мы показали на рисунке поля объектов, эквивалентные свойствам, к которым у нас есть доступ, но не сами свойства. meeting
offsetMeeting
year = 2003 month = 10 day = 7 hour = 14 min = 0 sec = 0
year = 2003 month = 10 day = 7 hour = 9 min = 0 sec = 0
Рис. З . 1 . Объекты DateTime Для образования таблицы нам требуется несколько таких пар значений. Но нужно ли иметь несколько объектов? Ответ будет отрицательным. Мы можем объявить объекты вне цикла, а затем изменять их значения в каждом шаге цикла. Цикл for, соответствующий приведенной выше таблице, будет иметь следующий вид: for
(int hour=8; hour<=18; hour++)
Другими словами, часы будут изменяться от 8 до 18, что соответствует интервалу от 8am до 6pm в 12-часовом формате времени. Создание объекта времени начала совещания изменится: meeting
= new
DateTime(now.Year,
now.Month,
now.Day,
hour,
0,
0);
Легко сообразить, что значения полей объектов, изображенных на рис. 3.1, в каждом шаге цикла будут изменяться, получая новое значение для переменной hour. Данные для второго столбца таблицы извлекаются из объекта offsetMeeting
=
meeting.AddHours(offset);
который уже имеет информацию о текущем времени, в которой следует модифицировать лишь значение часов.
102
Глава 3. Внутри объектов Полный текст программы приведен ниже.
Файл TimeLoop.cs using System; class TimeLoop { ///<summary> ///Программа Time Looping / // ill"
Bishop & Horspool August 2002
•
///Предоставляет таблицу перевода времени в одном городе ///в значение времени в другом ///Демонстрирует простой цикл loop /// void Go () { string city;//Город int offset;//Разница во времени DateTime now = DateTime.UtcNow; //Введем характеристики второго города Console.WriteLine("Table of times in London and " ) ; Console.Write("City? " ) ; city = Console.ReadLine(); Console.Write ("Offset from London? ") ; offset = int.Parse(Console.ReadLine()); Console.WriteLine("London "+city); DateTime meeting, offsetMeeting;//Время в Лондоне, время в //другом городе for(int hour=8; hour<=18; hour++) { meeting = new DateTime (now.Year, now.Month, now.Day, hour, 0, 0 ) ; offset.Meeting = meeting.AddHours(offset); Console.WriteLine(meeting.ToShortTimesString()+" "+ offsetMeeting.ToShortTimeString()); static void Main() { new TimeLoop (). Go ()
Полный вывод программы: Table of times in London and City? New York [Город?] Offset from London? -5 London New York 08:00 AM 03:00 AM 04:00 AM 09:00 AM 05:00 AM 10:00 AM 06:00 AM 11:00 AM 07:00 AM 12:00 PM 08:00 AM 01:00 PM
3.5 Простые циклы 02:00 РМ
09:00 AM
03:00 04:00
10:00 AM 1 1 : 0 0 AM
РМ РМ
05:00 РМ
12:00 РМ
06:00
01:00 РМ
РМ
103
Пример 3.5. Главы книги Нас попросили найти полное количество страниц в книге из четырех глав, а также вывести ее оглавление. В качестве входных данных нам дадут названия глав и число страниц в каждой. Этот пример потребует от нас более серьезной проработки формы и использования цикла. Для того чтобы сконцентрировать свое внимание на организации цикла, мы не объявляем тип для глав, хотя это следовало бы сделать. Хотя на первый взгляд вывод содержания не имеет никакого отношения к вычислению времени начала совещания, все же воспользуемся примером 3.4 в качестве основы для нашего алгоритма. В обоих случаях организация цикла потребует наличия следующих секций: •
начальные установки;
•
цикл;
•
тело цикла — ввод, процесс, вывод, модификация;
•
завершающие действия.
Если мы хотим определить число страниц, мы должны начать с того, что инициализировать текущее число страниц числом 1. Следует также присвоить 1 номеру главы. Правило по умолчанию С#, назначающее нулевое значение, здесь не годится. Эти действия составят секцию начальных установок. Далее следует цикл по всем главам. Внутри цикла мы должны считывать название главы и число страниц в ней. Эти действия составят секцию ввода в теле цикла. В примере 3.4 в качестве ввода фактически выступало создание нового значения объекта времени. Очевидно, должна быть также секция вывода в теле цикла, потому что мы должны напечатать название главы и номер ее первой страницы. Начальная страница для данной главы у нас уже будет, но в конце каждого шага цикла мы должны вычислить начальную страницу следующей главы или номер последней страницы книги, если текущая глава оказывается последней. Наконец, нам надо вывести результаты этого процесса, т. е. объем всей книги. Вооруженные этой разработкой, мы можем теперь без труда написать всю программу по секциям. Помимо номера первой страницы, который мы знаем, мы должны предусмотреть номер текущей главы. Начальные установки: int int int int
chapNumber;//Номер главы pages;//Число страниц startingPage = 1;//Начальная страница LastChapter = 4;//Последняя глава
104
Глава 3. Внутри объектов
Цикл: for
(int
i - 1 ; i<=lastChapter; i++) {
Ввод: //Для chapNumber прочитать chapName (см. ниже) и pages Вывод: //Вывести название главы и номер ее первой страницы Модификация: startingPage
+= pages;
Завершающие действия: Console.WriteLine(Total
pages = " + (startingPage-1);
Мы здесь опускаем детали объявлений и структуру класса и методов, чтобы сконцентрировать внимание на логике всего процесса. Мы также сократили предложения ввода и вывода до комментариев, потому что они могут на практике оказаться весьма запутанными, а здесь нам надо только осознать суть этих секций. Будет ли такая программа работать правильно? Попробуем протестировать ее с некоторыми предварительными данными: Introduction Basics Advanced Concepts Conclusion
18 30 43 5
Для тестирования программы мы выписываем все переменные, с которыми мы имеем дело, и последовательно модифицируем их, моделируя выполнение программы, как если бы мы были компьютером. Переменные: startingPage chapNumber
1 19 49 92 97 I
Вывод: Chapter I Introduction Chapter 1 Basics Chapter 1 Advanced Concepts Chapter 1 Conclusion Total pages is 96
1 19 49 92
3.5 Простые циклы
105
Очевидно, что программа работает неверно. В выводе каждой главе присваивается номер 1. Где же ошибка? М ы установили значение chapNumber в секции начальных установок, полагая, что эта переменная будет модифицироваться после обработки каждой главы, как и переменная startingPage. Однако соответствующее предложение отсутствует в секции модификации. Легко сообразить, что номер главы у нас совпадает со значением переменной цикла. Таким образом, м ы можем исключить переменную i, использованную нами в качестве счетчика цикла, и воспользоваться вместо нее переменной chapNumber: for
внеся соответствующие изменения в другие места программы. Если после этого протестировать программу, все будет правильно, а программа выдаст результат, показанный ниже. Теперь м ы готовы к рассмотрению полного текста программы. Файл BookChapters.cs using System; class BookChapters { ///<summary> ///Программа Book Chapters
Bishop
& Horspool May 2002
///Формирует оглавление книги и ///определяет полное число страниц ///Демонстрирует использование циклов /// void Go () { string chapName;//Название главы int pages;//Число страниц в ней int startingPage = 1;//Начальная страница int LastChapter = 4;//Номер последней главы for (int chapNumber = 1; chapNumber <=lastChapter; chapNumber + + ) { Console.Write("Chapter "+chapNumber+" name: " ) ; chapName=Console.ReadLine() ; Console.WriteLine("Chapter "+chapNumber+" pages: " ) ; pages = int.Parse(Console.ReadLine()) Console.WriteLine("\t\t\tChapter "+ chapNumber+"\t"+chapName+"\t"+startingPage); startingPage +=pages; } Console.WriteLine("Total pages = "+startingPage-l); } static void Main() { new BookChapters () .Go ();
Глава 3. Внутри объектов
106
Типичный прогон программы с приведенными ранее данными Introduction Basics Advanced Concepts Conclusion
Мы предусмотрели в программе, чтобы ее вывод был сдвинут вправо и отделен тем самым от вводимых данных. Для этого использовались символы табуляции. В следующем разделе мы увидим, как можно более изящно выровнять вывод. Позже, в гл. 4 мы рассмотрим вопрос о вводе данных не с клавиатуры, а из файла, что сделает вывод программы еще более аккуратным. Существуют множество вариантов организации циклов, как и много приемов, позволяющих составлять эффективные программы с повторениями. Мы вернемся к этому вопросу в следующем разделе, хотя детальное рассмотрение циклов мы отложим до гл. 4. Обратимся теперь к форматированию вывода чисел и строк.
3.6 Форматирование вывода Хотя организовать цикл в программе предыдущего раздела было достаточно просто, с выводом ее результатов определенно возникают проблемы. Нам хотелось бы, чтобы выводимые числа аккуратно выравнивались. Может также возникнуть необходимость вывода значения типа double, и тогда нам нужно средство контроля числа выводимых разрядов после десятичной точки. К счастью, С# предоставляет простой и мощный способ управления форматом вывода. Вместо того, чтобы сцеплять все элементы, выводимые в одну строку экрана, в единую символьную строку, мы можем использовать их, как отдельные элементы, но в начало вызова WriteLine вклю-
3.6 Форматирование вывода
107
чить форматирующую строку. Форматирующая строка содержит всю информацию о выводимых элементах текста и плюс к этому шаблон для каждой указываемой далее переменной, причем каждый шаблон может содержать спецификацию формата. Например, форматированный вывод аккуратно выровненной таблицы значений корней из целых чисел можно выполнить таким предложением вывода: Console.WriteLine("{О,4}{1,8:f2}",
n , Math.Sqrt(n));
которое означает «взять нулевой выводимый элемент и записать его с выравниванием вправо в четырех столбцах, после чего взять первый выводимый элемент и записать его с выравниванием вправо в следующих восьми столбцах в формате с фиксированной точкой и двумя десятичными знаками». В результате получится красивая ровная таблица, приведенная ниже. Видно, что десятичные точки выровнялись, и каждое значение корня включает два десятичных знака после точки, даже если они равны 0. Table 0 10 20 30 40 50 60 70 80 90 100
Форматирующий параметр Форматирующий параметр является первым параметром вызова Write или WriteLine, которая имеет, таким образом, альтернативную форму: Предложение вывода с форматированием Console.WriteLine
Элементами вывода могут быть любые переменные С#, для которых может существовать спецификация формата. Спецификации формата форматО, формат1,... описаны ниже. Текстовые вставки текст не обязательны. В качестве формата может также выступать отдельная символьная строка.
Эта форма WriteLine отличается от предыдущей тем, что она имеет более одного параметра, именно, формат, за которым следует список выводимых элементов. Предыдущая форма WriteLine имела только один параметр, который представлял собой строку, составленную путем сцеп-
108
Глава 3. Внутри объектов
ления всех выводимых элементов, которые явно или неявно преобразовывались в строки. Для правильной работы метод WriteLine должен содержать столько же спецификаций формата, сколько затем указано выводимых элементов. Спецификации формата заключаются в фигурные скобки и нумеруются от ноля. Форматы и элементы должны соответствовать друг другу. А что будет, если они не соответствуют? Ниже описаны возможные случаи при выполнении такого предложения: Console.WriteLine(format,a,b,с,d); 1. Все форматы соответствуют элементам — выводятся четыре элемента. string
format
=
"{0}
{1}
{2}
{3}"
2. Указан другой порядок — в строке формата спецификации не обязательно должны следовать по порядкутгомеров элементов вывода; изменение порядка спецификаций иногда оказывается удобным. Предложение string
format
=
"{0}
{2}
{1}
{3}"
выведет элементы в таком порядке: а, с, b, d. 3. Слишком мало спецификаций — будут выведены только элементы, отвечающие имеющимся спецификациям, поэтому e n d напечатаны не будут. string
format
= "{0}
{1}"
4. Повторяющиеся номера — если мы повторяем номер элемента, он будет выведен повторно. Предложение string
format
-
"{0}
{1}
{1}
{3}"
выведет элементы a, b, b и d, а элемент с будет опущен. 5. Слишком много спецификаций — будет возбуждена ошибка. s t r i n g
format
=
"{0}
{1}
{2}
{3}
{4}"
Поскольку в списке элементов нет элемента, соответствующего {4}, эта спецификация не может быть реализована, и С# остановит программу.
Спецификации формата Каждая спецификация, заключенная в фигурные скобки, может содержать значительный объем информации о том, каким образом должен быть напечатан данный элемент. Для спецификаций предусмотрен собственный синтаксис, описанный в приведенной ниже форме.
3.6 Форматирование вывода
109
Спецификация формата {N,M:s} N указывает на позицию элемента в списке выводимых переменных после того, как строка формата будет передана в метод Write; позиции элементов нумеруются от 0. М задает ширину области, в которую будет помещено форматированное значение. Если М отсутствует или отрицательно, значение будет выровнено влево, в противном случае — вправо. s является необязательной строкой форматирующих кодов, которые используются для управления форматированием чисел, даты и времени, денежных знаков и т. д.
Целое число N задает позицию печатаемого элемента в списке этих элементов, причем 0 относится к первому элементу списка, и всегда должен присутствовать. При выводе целых чисел и строк нас интересует только параметр ширины М. Последний описатель s часто отсутствует; если он указан, в нем задается тип форматирования и число. Так, в нашем примере с квадратными корнями мы использовали такой формат: {I,8:f2} Другой тип форматирования обсуждается в конце этого раздела; полный список приведен в Приложении В.
Выравнивание строк и чисел Параметр М, определяющий ширину поля вывода, помогает повысить наглядность выводимой информации. Посмотрим, как можно выровнять числа в таблице, отражающей оглавление книги. Мы использовали для вывода следующее предложение: Console.WriteLine("\t\t\tChapter "+chapNumber+"\t" +chapName+"\t"+startingPage); Перепишем его с включением форматирующей строки: Console.WriteLine(format,chapNumber,cnapName,startingPage); В такой форме легче понять, что именно будет выведено на экран. Если форматирующую строку объявить таким образом: string
format
= "\t\t\tChapter
{0,2}
на экране появится следующая таблица: Chapter Chapter Chapter Chapter
I 2 3 4
Introduction Basics Advanced Concepts Conclusion
I 19 49 92
{l}\t\t{2,3}
Глава 3. Внутри объектов
110
Используя выравнивание вправо для двух числовых элементов, мы смогли вывести их столбиком. Если число страниц в книге достигает тысячи, последний формат надо изменить на {2,4}.
Форматирование других типов данных Мы видели в гл. 2, что с помощью класса DateTime можно выполнять форматирование дат. В дополнение к формату {N,M} для целых чисел и {N,M:Fd} для действительных, имеется удобный формат для денежных единиц. При форматировании денежных единиц следует использовать {N,M:C}; сумма будет напечатана с двумя десятичными позициями после точки (автоматически), а перед суммой будет поставлен знак денежной единицы той страны, где расположен компьютер. Если, например, компьютер так настроен, что знаком денежной единицы считается знак доллара $, мы можем написать: Console.WriteLine("Your
new salary
is
{0:C}",salary*l.04);
что при зарплате 10000 долларов выведет: Your
new s a l a r y
is
$10,400.00
Если настройки компьютера отличаются, то же предложение могло вывести Your
new s a l a r y
is
R10 400,00//Сумма
в южноафриканских
рэндах
Другими словами, форматирование денежных единиц, как и форматирование дат, приспосабливается к местоположению компьютера в соответствии с настройками операционной системы. В С#, если требуется вывести денежные документы определенным образом, мы можем заместить настройки компьютера, и тогда вывод не будет зависеть от того, на каком компьютере выполняется программа. Способ вывода денежных документов определяется классом с именем NumberFormatlnfo. Создав объект этого класса, мы можем установить его свойства, например те, которые перечислены в приводимой ниже форме. Класс NumberFormatlnfo (сокращено) //Конструктор NumberFormatlnfo() //Свойства CurrencyDecimalSeparator CurrencyGroupSeparator CurrencySymbol и много других свойств Для изменения формата денежных единиц различных стран объект NumberFormatlnfo используется с методом String.Format.
3.7
Методы и параметры
щ
Например, NumberFormatlnfo f = new NumberFormatlnfо(); f. CurrencyDecimalSeparator = ", " ; f.CurrencyGroupSeparator = " "; f.CurrencySymbol = "R";
Далее с помощью метода Format класса String м ы создаем строку из формата, спецификатора формата и значения: Console.WriteLine( String.Format(f,"Your
new s a l a r y
is
{0:C}",salary*l.04));
Эта строка выведет: Your
new s a l a r y
is
RIO 4 0 0 , 0 0
даже на компьютере долларовой зоны. Метод String.Format, входящий в класс String, подключает указанный формат (в данном случае формат f) к спецификации формата и списку переменных.
Выбор форматов Ваше право выбрать, будете ли вы пользоваться форматами или сцеплением строк в предложениях вывода. Форматы необходимы в случаях выравнивания вправо, управления числом десятичных знаков после точки, а также вывода денежных единиц и дат. Однако для форматов есть и еще одна область применения. Иногда нам требуется вывести данные единообразно из разных частей программы. В этом случае можно определить единый формат строки, поместив его в такое место, где он будет доступен отовсюду. Изменение этого формата сразу изменит вывод из всех мест программы. В разделе 3.8 мы покажем, как это делается.
3.7 Методы и параметры Мы продолжаем развивать тему содержимого объекта. Если взглянуть еще раз на форму с определением структуры, приведенную в разделе 3.1, то мы увидим, что нам еще надо выяснить, как определяются методы, операторы и другие функции. В этой главе мы будем иметь дело с методами; операторы и функции отложим до гл. 9. Мы уже широко использовали методы, определенные в типах, предоставляемых различными пространствами имен С#. К этим типам относятся int, string, Console, DateTime, Math и Random. В качестве примеров использования этих методов можно привести следующие предложения: offsetMeeting
В каждом случае вызов метода сопровождается скобками даже при отсутствии параметров, как в случае ReadLine(). Именно таким способом мы отличаем вызов метода от использования свойства.
Объявление метода Теперь мы можем обсудить, каким образом объявляется наш собственный метод. Рассмотрим общую форму объявления метода: Объявление метода (упрощено) модификатор_доступа тип_метода идентификатор_метода (параметры) { предложения и объявления переменных return выражение //Если типизированный метод //Параметры тип идентификатор, тип идентификатор, Метод объявляется с указанием своего типа. В этом плане метод может быть типизированным (с именем некоторого типа) или нетипизированным (типа void). Как и все члены, метод может быть закрытым (private) или открытым (public). По умолчанию назначается атрибут private. Методы могут содержать только переменные, но не константы. Если метод типизирован, в нем должно быть хотя бы одно предложение .return с выражением, результат вычисления которого имеет тот же тип, что и тип самого метода. Нетипизированный метод может иметь предложения return без выражений.
В качестве примера рассмотрим метод, который мы включили в тип StarLord в примере 3.1. public void Attack(StarLord opponent) { int damage = r.Next(strength/3+points/2) ; opponent.Points -= damage; Метод объявлен с атрибутом public и является открытым. Он имеет тип void, т. е. не4 возвращает значения, и требует один параметр. Внутри себя метод Attack объявляет переменную с именем damage и выполняет одно предложение присваивания. Другим примером метода того же рода мог бы быть метод, выполняющий начальную установку значений баллов и силы: public void Renew (int strength = s;
s,
int p) {
3.7
Методы и параметры points
113
= р;
Этот метод очень похож на конструктор, но инициализирует лишь некоторые поля объекта. Переменные в методе не инициализированы и им следует явным образом присвоить значения перед их использованием. Переменные имеют область видимости, которая действует только внутри метода; каждое объявленное в методе имя имеет смысл только внутри этого метода.
Типизированные методы Типизированные методы широко используются в С#; их отличительной чертой является то, что они возвращают значение. Хорошим примером типизированного метода будет метод для преобразования температуры из одной шкалы в другую (например, из градусов по Цельсию в градусы по Фаренгейту): public double Fahrenheit(double r e t u r n c*9.0/5 + 32;
с)
{
Типизированный метод обязательно должен содержать предложение return, следовательно, перед завершением он создает значение. Это требование соответствует правилам его вызова. Теперь формализуем вызов метода.
Вызов метода 3 Мы уже знакомы с вызовом методов . Форма вызова такова: Вызов метода (упрощено) идентмфикатор_метода (список_аргументов) Значения аргументов передаются параметрам метода; осуществляется вход в метод, и его предложения выполняются до конца метода или до предложения return.
Нетипизированные (типа void) методы вызываются, как предложения: они не могут быть частью выражений. Типизированные методы вызываются, как часть выражения, но их можно также вызывать и как предложения, однако в этом случае их возвращаемое значение теряется. Примеры вызова методов: lordl.Attack(Iord2) ; lord2.Renew(10, 10) ; f = Fahrenheit(tempToday+5) ;
В С# для вызова метода официально используется термин invoke [возбуждать, активизировать], а само действие именуется invocation [возбуждение, активизация]. Термины calling и call [вызов] имеют тот же смысл.
114
Глава 3. Внутри объектов
В каждом случае выполняется вычисление параметров, и их значения передаются параметрам метода. Формальные параметры внутри метода всегда выступают, как переменные. Их значения могут изменяться, как и значения других переменных, но все эти изменения не возвращаются из метода в программу, когда метод завершает свое выполнение (за исключением параметров out и ref, см. ниже). Вы можете задать вопрос, почему Attack и Renew были объявлены, как члены объектов, в то время как Fahrenheit существует сам по себе. Ответ на этот вопрос будет дан в следующем подразделе.
Разработка метода В объектно-ориентированном программировании методы используются в нескольких различных контекстах. 1. Функции объекта. Открытые методы обеспечивают видимые снаружи функциональные возможности объекта. Обычно они выполняют вычисления, основанные на параметрах и собственных полях объекта, а также и посредством того или иного взаимодействия с внешним миром. В С# типы обычно содержат не так много методов такого рода, как в других языках, потому что все простые функции, осуществляющие чтение или модификацию данных, реализуются в виде свойств. Если вы посмотрите состав объектов любого API C#, то вы увидите, что свойств у класса обычно оказывается больше, чем методов. 2. Структурирование программы. Методы внутри программы позволяют раздробить логическую структуру программы на меньшие по размеру и более обозримые фрагменты. Разделение на методы может быть линейным, если программа должна выполнить последовательный ряд действий, каждое из которых инкапсулируется в отдельный метод. Каждый из этих методов вызывается только однажды из главной программы Main или метода Go. Если внимательно подойти к выбору имен методов, главная программа становится более наглядной, и ее легче расширять и обслуживать. Типичная структуризация такого рода может выглядеть так: AcquireData() ; //получение данных Analyze(); //анализ данных ProduceReport() ; //подготовка отчета 3. Обеспечение функциональных возможностей. Часто используемые программные фрагменты внутри объекта (например, вычисления типа преобразования из шкалы Цельсия в шкалу Фаренгейта) можно оформить в виде метода, и тогда их внутреннее функционирование молено протестировать только однажды, после чего быть уверенным, что при вызове они будут работать правильно. Все эти принципы будут проиллюстрированы в последующих примерах.
3.7 Методы и параметры
115
Статические методы Мы в этой книге построили изложение С# на примерах использования существующих типов, что дало нам возможность продемонстрировать большое количество методов разного рода в различных контекстах. Еще одна классификация методов заключается в разделении всех методов на статические и методы экземпляра. Как и статические поля, статические методы объявляются с ключевым словом static и существуют для класса только в единственном экземпляре. Эти методы вызываются указанием их имени после имени типа и точки. Методы экземпляра относятся к конкретному экземпляру класса; их имена при вызове предваряются именем объекта и точкой. В следующих двух предложениях totalMins = int.Parse(Console.ReadLine()); i n t damage = r . N e x t ( s t r e n g t h / 3 + p o i n t s / 2 ) ;
Parse и ReadLine являются статическими методами, в то время как Next — это метод экземпляра. Как узнать, надо ли использовать статический метод? Хороший стиль программирования требует, чтобы вы использовали статические методы как можно реже. В приводимых ниже примерах случаи, когда статические методы должны или, наоборот, не должны использоваться, будут особо оговариваться.
Параметры out и ret В С# параметры обычно «передаются по значению». Это означает, что после вычисления значения выражения, представляющего параметр, это значение присваивается параметру. После того, как метод начал выполняться, переменные, указанные в списке параметров метода, уже не взаимодействуют друг с другом и вообще не затрагиваются. Значения этих переменных остаются неизменными. Такие параметры называются параметрами-значениями. Другая возможность заключается в использовании ref-параметра, где ref является сокращением от reference, ссылка. В этом случае аргумент и параметр относятся к одной и той же переменной. Любые изменения ref- параметра в процессе выполнения метода влияют на аргумент — параметр и аргумент здесь одно и то же. Для того чтобы подчеркнуть другой механизм передачи параметра, в вызове метода, как и при его объявлении, следует использовать ключевое слово ref. Заметьте, что каждый ref-аргумент перед вызовом метода должен получить какое-либо значение. Третья возможность предоставляется для тех случаев, когда мы хотим получить из метода несколько значений. Типизированные методы позволяют возвращать только одно значение, если же необходимо вернуть несколько значений, мы можем объявить некоторые параметры с ключевым словом out. Такой out-параметр схож с ref-параметром; отли-
116
Глава 3. Внутри объектов
чие заключается в том, что при вызове метода аргумент может и не иметь определенного значения, но метод обязан назначить out-параметру какое-либо значение перед тем, как управление будет передано из метода вызывающей программе. Ключевое слово out, как и в случае ref, должно указываться перед соответствующим параметром и при вызове метода, и при его объявлении. Заметьте, что в языке С# существует ключевое слово in, которое не имеет никакого отношения к передаче параметров и не является противоположностью ключевому слову out. Ключевые слова out и ref используются при программировании не очень часто, однако иногда они оказываются необходимы. В качестве примера можно рассмотреть обмен двух значений. Предположим, волшебник объявляет, что два персонажа типа StarLord должны заменить друг друга (т. е. обменяться именами, баллами и вообще всем). Эту операцию можно реализовать в виде метода: public void SwapLords(ref Starlord one, ref StarLord temp = one; one=two; two=temp;
StarLord two) {
который будет вызываться таким образом: StarLord.SwapLords(ref
lordl,
ref
Iord2);
Сравнение объектов Можно ли использовать операторы отношения со структурированными объектами? Ответ таков: только в том случае, если они будут реализованы в виде методов. Мы сами должны определить, что именно мы имеем в виду под операцией сравнения этих типов. Структура DateTime, входящая в API (см. раздел 2.6), основывает операцию сравнения на закономерностях реального мира дат. Однако рассмотрим структуру Exam, которую мы использовали в начале этой главы. struct Exam {//Экзамен string course;//Предмет int weight;//Процентный вклад в суммарную оценку string lecturer;//Лектор DateTime date;//Дата экзамена s t r i n g venue;//Место проведения экзамена s t a t i c i n t totalNoOfExams=0;//Общее число экзаменов Что может означать сравнение двух объектов Exam, например, использование выражения el<e2? Мы можем задать порядок объектов по номеру курса, а затем использовать еще и дату экзамена, если считать, что по
3.8 Проект 1 — сравнение телефонных счетов
117
каждому курсу может быть несколько экзаменов. Но компилятор языка С# никаким образом не может предугадать наш выбор. Сравнение на равенство может по умолчанию предполагать равные значения всех полей объекта, но даже и здесь могут возникнуть неоднозначности. Для предоставления имеющей смысл операции сравнения на равенство, мы должны заместить метод Equals. Для других форм сравнения мы должны заместить метод Compare, как уже обсуждалось выше применительно к DateTime. Для Exam следует добавить: public override bool Equals(object e) { return course == ((Exam) e) .course && date == ((Exam) e ) . d a t e ;
Этот вариант метода Equals основывается на определении равенства строк (эта операция определена) и равенства объектов DateTime (эту операцию мы видели в спецификации API). В этом случае мы игнорируем остальные три поля, включенные в объявление класса, но при желании мы можем учесть и их значения. Тогда наш метод Equals можно использовать следующим образом: el.Equals(e2)
Однако создание такого метода, к сожалению, не позволяет написать el == e2 Чтобы получить возможность использовать эту операцию, мы должны перегрузить оператор ==, как это будет объяснено в гл. 9. Аналогично можно объявить метод CompareTo. Эффективное использование этих методов в действительности требует создания дополнительных управляющих структур, поэтому мы вернемся еще к этому вопросу в гл. 4 и ее примерах.
3.8 Проект 1 — сравнение телефонных счетов В нашем первом проекте мы собираемся рассмотреть различные конструкции типов и методов и остановиться на разных путях решения поставленной задачи.
Постановка задачи Различные телефонные компании предлагают месячные контракты различной стоимости. Часто оказывается весьма затруднительно обоснованно определить, какая же компания предлагает наиболее выгодную сделку. Например, компания может предложить более низкий базовый тариф, но брать с клиента немного больше за каждый телефонный разговор. Мы хотим смоделировать типичный семейный месячный счет и посмотреть,
118
Глава 3. Внутри объектов
как будет отличаться итоговая стоимость обслуживания в различных компаниях.
Данные Для моделирования нам потребуются два вида данных. Во-первых, размер поминутной оплаты для каждой компании. Предположим, что компании, которые мы сравниваем, имеют одну и ту же структуру оценки стоимости переговоров, отличаются только численные величины. Позже мы сможем обрабатывать и более сложные контракты. Пусть оплата формируется из следующих четырех составляющих: • помесячная плата, например, 29.95; • базовая поминутная плата (базовый тариф), например, 0.55; • снижение платы для льготной категории разговоров, например, 0.20; • количество предоставляемых бесплатных минут в месяц, например, 60. Если мы составим тип, описывающий телефонную компанию, эти значения будут передаваться конструктору каждого объекта. Второй аспект данных — это сами телефонные разговоры. Мы должны иметь тестовый набор разговоров для того чтобы «проиграть» его с каждой компанией и получить достоверные результаты сравнения. Данные для каждого телефонного разговора включают его продолжительность в минутах, а также категорию разговора — обычный или льготный. Мы еще не настолько хорошо изучили С#, чтобы автоматически определять наличие льготы, исходя из времени суток, поэтому будем использовать 0 или 1 для указания категории разговора.
Вычисления Держа в уме данные нашей модели, рассмотрим выполняемые в ней вычисления. Базовая стоимость разговора callCost вычисляется, как произведение продолжительности разговора на величину базовой поминутной платы. Предположив, что максимальная продолжительность разговора maxCallLength является константой, получим при базовом тарифе basicRate: callLength = r.Next(maxCallLength);//Длительность разговора double callCost = callLength*basicRate;//Стоимость разговора
где г является генератором случайных чисел. Создавая объекты г при определенном значении начального числа-затравки, мы обеспечиваем одинаковые последовательности продолжительностей телефонных разговоров для всех компаний. Для льготных разговоров мы задали льготу cheapRateReduction таким образом, что она показывает величину уменьшения платы, а не ее новое значение. Так, приведенное выше число 0.20 означает, что льгот-
3.8 Проект 1 — сравнение телефонных счетов
1_1_9
ный тариф составляет 0.55 - 0.20 — 0.35. Если теперь мы будем генерировать случайные числа callRate, принимающие значения 0 для нормального тарифа и 1 для льготного, наши вычисления преобразуются следующим образом: callLength - г.Next(maxCallLength); int callRate = s.Next (2);//0 для нормального тарифа, 1 для //льготного double callCost = callLength*(basicRate - callRate*cheapRateReduction) ; В табл. 3.6 показаны несколько типичных значений; помните, что 1 означает дешевый разговор. Таблица 3.6. Случайный набор телефонных разговоров и их стоимость Продолжительность
Код льготы
Поминутная плата
Стоимость разговора
10
1 1
0.35 0.35
3.50
1
11
0.35
0.35
9
1
0.35
3.15
0 1
0.55
4.40
0.35
2.80
Следующий фактор — бесплатное время разговоров. Эта величина уменьшается до исчерпания, после чего начинается начисление платы. Удобный способ учета этого фактора заключается в организации двух циклов — первого для учета свободного времени, и второго, в который попадут все оставшиеся телефонные разговоры. for (int m=freeMinutes; m>0; m -= callLength) { ...Вычислим стоимость разговоров, но не будем их учитывать }
int restCalls = 8; //Или другая величина for (int c=restCalls; с>0; с — ) { . . . Вычислим стоимость разговоров и добавим их к итоговой сумме
Разработка типа В программе должен быть ведущий класс для главной функции, а также структура, описывающая телефонную компанию. Помимо характеристик стоимости, мы можем хранить название компании, а в качестве локальных данных будут выступать генераторы случайных чисел (один для продолжительности разговора и другой для его категории). Поместив генераторы в структуру, мы дублируем их для каждого объекта, обеспечивая тем самым одинаковые последовательности значений, как это и требуется для достоверного сравнения.
Глава 3. Внутри объектов
120
Форматирование вывода Вывод программы можно представить в форме таблицы. Поскольку некоторые данные исчисляются в денежных единицах, мы пользуемся форматом С, который обсуждался в разделе 3.6. Таблица может иметь следующий вид: Telco Monthly R 29.95 Free minutes 60 [Помесячно 29.95 Бесплатно 60 минут] Basic rate R 0.55 Reduction R 0.20 [Базовый тариф 0.55 Скидка 0.20] Contract Free Free Free Free Free Charged Charged Charged Charged Charged Charged Charged Charged
Предложения, создающие объект телефонной компании и выводящие на печать результаты, выглядят так: s t r i n g format = "{0,23}Total {1:С}"; PhoneCompany p e l = new PhoneCompany("Telco",29.95,0.55,0.20,60) ; Console.WriteLine(format,"",pel.PhoneBill() ) ; Внутри структуры PhoneCompany имеются следующие предложения форматированного вывода: Console.Write("{0,8}{1,б}{2,б}{3,8:f2}", type, callLength, callRate, callCost); Console.WriteLine("{0,8:f2}",callCharge); В этом случае м ы не используем формат С, поскольку не хотим печатать символ национальной валюты.
Разработка метода Последнее, что нам осталось обсудить, это как проектируются методы в структуре. Мы уже решили, что у нас будет один метод, который вычислит и напечатает весь телефонный счет. Однако мы видели, что в нем тре-
3.8 Проект 1 — сравнение телефонных счетов буется организовать два цикла, в которых будут выполняться одни и те же вычисления. Разница будет только в том, будет ли выведено слово «Free» [Бесплатно] или «Charged» [С оплатой]. Тело метода может быть таким: double Calculate(string type, out int callLength) { callLength = r.Next(maxCallLength); int callRate = s.Next(2);//0 для нормального тарифа, 1 для //льготного double callCost = callLength* (basicRate - callRate*cheapRateReduction); Console.Write("{0,8}{1,б}{2,б}{3,8:f2}", type, callLength, callRate, callCost); return callCost;
Метод вычисляет стоимость телефонного разговора и выводит все его характеристики. Поэтому метод вполне самодостаточен и все его собственные переменные объявляются в нем же. Если, однако, данный телефонный разговор бесплатный, нам необходимо знать его продолжительность, которая будет вычтена из оставшегося числа бесплатных минут. Для возврата этого значения мы используем out-параметр.
Программа Наконец, м ы можем привести полный текст программы. Файл PhoneComparison.cs using System; class PhoneComparison ///<summary> ///Программа PhoneComparison
Bishop & Horspool Dec 2002
ill
///Сравнивает телефонные счета от двух компаний ///Демонстрирует разработку типа и метода, параметры и ///форматирование вывода /// //Параметры моделирования const int maxCallLength = 30; const int maxCallsPerMonth = 10; void Go() { string format = "{0,23}Total {1:C}"; PhoneCompany pel » new PhoneCompany("Telco",29.95,0.55, 0.20, 60) ; Console.WriteLine(format,"",pcl.PhoneBill()); ConsoleReadLine(); PhoneCompany pc2 = new PhoneCompany("Cellco",17.95,0.45,0.10,30); Console.WriteLine(format,"",pc2.PhoneBill() ) ; } static void Main() {
122
Глава 3. Внутри объектов
new PhoneComparison() .Go(); } public struct PhoneCompany { string name;//Название компании double monthlyFee;//Помесячная плата double basicRate;//Базовый тариф за минуту double cheapRateReduction;//Скидка для льготного тарифа int freeMinutes;//Число бесплатных минут Random r,s;//Случайные числа public PhoneCompany(string n, double m, double b, double c, int f) { name = n; monthlyFee = m; basicRate = b; cheapRateReduction = c; freeMinutes = f; r = new Random (19); s = new Random (13); } double Calculate(string type, out int callLength) { callLength = r.Next(maxCallLength); //О для нормального тарифа, 1 для льготного int callRate • s.Next(2); double callCost = callLength* (basicRate - callRate*cheapRateReduction); Console.Write("{0,8}{1,б}{2,6}{3,8:f2}", type, callLength, callRate, callCost); return callCost; } public double PhoneBill() { Console.WriteLine(name) ; Console.WriteLine("Monthly {0:C} "+ "Free minutes {l}\n"+ "Basic rate {2:C} Reduction {3:C}\n"+ monthlyFee, freeMinutes, basicRate, cheapRateReduction) Console.WriteLine("Contract Mins Rate Cost Charged"); Console.WriteLine("Monthly fee{0,-20}{I:f2}","",monthlyFee) ; double cost = monthlyFee;//Месячный счет с начальным //значением int callLength;//Продолжительность разговора double callCharge;//Начисление за платный разговор for (int m=freeMinutes; m > 0 ; m -= callLength) { Calculate("Free", out callLength); Console.WriteLine(" { 0, 8 : f 2 } " , 0) ; } int restCalls =8;//r.Next(maxCallsPerMonth); for (int c=restCalls; c>0; с — ) { callCharge = Calculate("Charged", out callLength); Console.WriteLine("{0,8:f2}",callCharge); cost+=callCharge; } return cost;
3.8 Проект 1 — сравнение телефонных счетов
123
Вывод Ниже приведен полный вывод программы. Telco Monthly R 29.95 Free minutes 60 B a s i c r a t e R 0.55 Reduction R 0.20 Contract Mins Monthly fee Free 19 11 Free 1 Free Free 18 Free 23 4 Charged 12 Charged Charged 29 Charged 9 Charged 8 Charged 8 Charged 9 Charged 20
Total R 66.20 Cellco Monthly R 17.95 Free minutes 30 Basic r a t e R 0.45 Reduction R 0.10 Contract Mins Monthly fee Free 19 11 Free 1 Charged Charged 18 Charged 23 4 Charged Charged 12 Charged 29 9 Charged 8 Charged
Как и можно было ожидать, вторая компания предлагает более выгодный контракт.
Оценки В рассмотренную программу можно внести (и мы это сделаем) многочисленные усовершенствования. Сейчас речь о другом. Нет ли в ней ошибок?
124
Глава 3. Внутри объектов
Ошибок, возможно, и нет, но одна несуразность имеется. Идея программы заключается в том, что моделирование позволит справедливо сравнить компании. С этой целью в каждом счете сначала будут отсчитываться бесплатные минуты, а затем вычисляться плата за дополнительные разговоры. Чтобы не делать счет слишком длинным, мы ограничились восемью разговорами. Однако фактическое число смоделированных разговоров для двух компаний оказывается различным, потому что различается число бесплатных разговоров. Для компании Telco платные разговоры имеют продолжительность 12
29
20
в то время как для компании Cellco эта последовательность иная: 1
18
23
4
12
29
9
8
У нас получилось, что первые три платные разговора для Cellco оказались бесплатными для Telco. Как можно уравнять компании? Если мы определим число разговоров для первой компании, а затем вставим соответствующее число фиктивных разговоров между бесплатными и платными разговорами для компании Cellco, мы решим эту проблему. Ее реализацию мы оставляем для самостоятельной работы читателя.
Основные понятия, рассмотренные в гл. 3 В этой главе были рассмотрены следующие понятия (некоторые из них повторно): конструкторы
поля экземпляра
статические поля
доступ
свойства представление с плавающей точкой
числовые типы
повышение типа
деление по модулю
выражения отношения
операторы присваивания
старшинство
ассоциативность
преобразование
простые циклы
форматирование вывода
объявление метода
объявление структуры
методы voj.d и типизированные
вызов метода
статические методы
виды параметров
числовые выражения
Контрольные вопросы
_ ^ _ _ _ _
125
Введенные и использованные ключевые слова: static
public
int
long
double
float
decimal
for
void
out
ref
value
get
set
В настоящей главе были приведены формы для следующих конструкций: определение структуры
определение свойства
повышение типа (сокращено)
типы результатов сравнения
явное преобразование
цикл for (упрощение)
предложения вывода с форматированием
спецификация формата
класс NumberFormatlnfo (сокращено)
объявление метода (упрощение)
вызов метода (упрощение)
Контрольные вопросы 3.1. Важной чертой, отличающей свойство от поля, является (а) правила использования (б) скобки прописных букв (в) модификаторы доступа
(г) способ объявления
3.2. Если а является объектом, а р свойством, и мы выполняем присваивание а.р=х, х представляется в р как (a) value (б) х (в) р.х (г) р 3.3. Для типа может быть предусмотрено несколько конструкторов при условии, что (а) все они имеют различающиеся имена (б) все они имеют различающиеся списки параметров (сигнатуры) (в) по крайней мере один из них является конструктором по умолчанию
126
Глава 3. Внутри объектов
(г) один конструктор инициализирует все локально объявленные поля 3.4. Какое из предложений справедливо? (а) свойство должно иметь то же имя, что и поле в этом типе, но начинаться с прописной буквы (б) свойство всегда должно быть открытым (public) (в) свойство определяет поведение операций get и set (г) свойство определяет поведение операций get или set или обеих 3.5. Сложение значений double и int всегда даст в результате (а) значение double (б) значение int (в) значение int, если значение double не имеет дробной части О 1
(г) значение long, если значение double превышает 2 3.6. Правильным выражением для часа для нового объекта DateTime является (а) new
DateTime(2003,9,25).Hour
(б) new
DateTime(2003,9,25).Hour()
(в) new
DateTime(2003,9,25).getHour()
(r) Hour(new
DateTime (2003,9,25))
3.7. Циклом вывода первых 10 нечетных чисел является (а) for
(int i=l; i<=10;
i+=2)
Console.WriteLine(i); (б) for
(int i = l;
i<19; i+=2)
Console.WriteLine(i); (в) for
(int i=l; i<10; i++) Console.WriteLine(i*2+l);
(r) for
(int i=0; i<=10;
i+ + )
Console.WriteLine (i*
3.8. Для того чтобы вывести целое число, действительное число с двумя десятичными знаками и денежную величину в указанном порядке и разделенные четырьмя пробелами, следует использовать предложение Console.WriteLine(s,i,r,с):
Упражнения
127
где s обозначает (а) "{0,4}{l,4:f2}{2,4:C}" (б) "{0}{l,f:4,2} (в) "{0}
{2,с:4,2}"
{1:f2}
{2:С}"
(г) "{0}\t{l:f2}\t{2:C2}"
3.9. Важным отличием типизированного метода от метода void является (а) метод void не может быть вызван в выражении (б) типизированный метод не может быть вызван без выражения или присваивания (в) типизированный метод может иметь только in-параметры (г) метод void должен быть открытым (public) 3.10. Если мы объявили метод void Update (x,out у) какое из перечисленных выражений совершенно правильно? (a) U p d a t e
(w,z)
(в) U p d a t e
(in
(б) U p d a t e w,
out
z)
(г) о б а
(w,
out
варианта:
z) (б)
и
(в)
Упражнения 3.1. Экзамены. Воспользовавшись типом Exam, обсуждавшимся в разделе 3.1, составьте программу, которая будет вводить характеристики четырех экзаменов и выводить их с указанием дат, когда они состоялись. В дополнение к этому, выведите число дней между каждыми двумя экзаменами. 3.2. Расписание автобуса-челнока. Воспользовавшись примером 3.3 в качестве отправной точки, составьте программу, выводящую расписание для автобуса-челнока, начиная с 6am и до 11.45pm. 3.3. Сокращенное расписание. Между 12 (полночь) и 5.30 автобус также ходит с 30-минутным интервалом. Включите эту информацию в расписание. Полезно воспользоваться подходом, примененным в Проекте 1 (раздел 3.8). 3.4. Расширенный тип Time. Воспользовавшись типом Time из примера 3.2, добавьте методы, реализующие прибавление одного часа и сравнение двух значений времени. Затем измените пример 3.4, чтобы в качестве счетчика цикла использовалось не целое число, а значения типа Time. 3.5. Преобразование температуры. Напишите программу, выводящую таблицу преобразования градусов по Цельсию от 0 до 100 в градусы Фаренгейта. Отформатируйте таблицу так, чтобы на каждой строке помещались три значения.
128
Глава 3. Внутри объектов
3.6. Преобразование валют. Модифицируйте упражнение 3.5 для преобразования разумного диапазона денежных сумм из валюты вашего компьютера в некоторую другую. Используйте класс NumberFormatlnfo, обсуждавшийся в разделе 3.6. 3.7. Рукопись. Измените программу BookChapters примера 3.5 так, чтобы в ней использовалась структура, объект которой создается для каждой главы, и которая содержит название и число страниц. Добавьте свойство, которое вычисляет и возвращает окончательное число страниц с учетом уменьшения объема рукописи на 80% в процессе набора. Оттестируйте программу, генерируя 15 глав со случайным числом страниц между 20 и 50. 3.8. Доставка пиццы. Фирма Pizza Delivery 2U хотела бы сообщать клиентам более точное время доставки пиццы по их заказам. С момента получения заказа до момента доставки пиццы следует принять во внимание два фактора: длину очереди к духовке для приготовления пиццы (т. е. число уже имеющихся в системе заказов) и район проживания клиента. Клиенты, проживающие поблизости от фирмы, будут получать заказы быстрее, чем живущие в отдаленных районах. Pizza 2U доставляет заказы в четыре определенных района. Фирме нужно иметь рядом с телефоном таблицу, в которой можно быстро найти время доставки заказа для всех этих четырех районов от 9am до 11pm при длине очереди от 0 до 5. Разработайте систему, использующую структуры и циклы для вывода на печать требуемых таблиц.
Управление и массивы 4
Главной темой этой главы является изучение того, как программы могут повторять свои действия и выбирать их, основываясь на выполненной проверке. В большинстве случаев проверка включает в себя сравнение значений объектов, а сравнение основывается на операторах и значениях булева типа, с изучения которых и начнется эта глава. В случае повторных действий программы очень часто должны сохранять большое количество результатов, и тогда сохраняемые данные оформляются в виде массива. В этой главе мы рассмотрим простые массивы чисел и объектов, а также классические способы взаимодействия циклов с массивами. Завершит наше изучение базовых типов данных языка С# особый случай строк, составленных из отдельных символов.
4.1 Булевы выражения Целый ряд предложений С# использует условные операции. Условная операция — это выражение, которое после вычисления дает true или false. Например, выражение 1<2 всегда дает true, в то время как противоположное выражение 1>=2 всегда дает false. Более полезным для программы будет выражение вида п<10, которое дает true только если переменная п содержит значение, меньшее 10; в противном случае выражение дает false. Операторы отношения (сравнения) перечислены в приведенной ниже форме. Эти операторы можно использовать для сравнения двух значений любого базового типа, включая int, double, char и string. Они также годятся для сравнения многих структурных типов, включая DateTime, при условии, что реализация типа заключает в себе методы, поддерживающие сравнения.
5—2047
130
Глава 4. Управление и массивы
Операторы отношения а < b
//Проверить: а меньше Ь?
а <= b
//Проверить: а меньше или
равно Ь?
а >= b
//Проверить: а больше или
равно Ь?
а > b
//Проверить: а больше Ь?
а == b
//Проверить: а равно Ь?
а
//Проверить: а не равно Ь?
!= b
Результат сравнения является значением типа bool. Этот тип состоит всего лишь из двух значений, которые записываются как true [истина] и false [ложь]. Сравнивать допустимо числа, символы, строки, булевы величины и любые другие типы, для которых определены операции сравнения.
Название типа bool дано в честь Джорджа Була (George Boole), который изобрел ветвь математики, называемую сейчас булевой алгеброй. Эта наука тесно связана с исчислением предикатов. В Сопрограммах мы можем объявлять переменные типа bool. Ниже приведены несколько примеров, в которых результат сравнения сохраняется в переменной типа bool. bool bl, Ь2, ЬЗ, Ь4, Ь5; int a = 19; int b = -27; char с = ' с' ; char d = ' d ' ; s t r i n g hello = " h e l l o " ; s t r i n g there = " t h e r e " ; //Присваивается bl = a < b; //Присваивается Ь2 = с != d; //Присваивается ЬЗ = hello < there; //Присваивается b4 = there > " t h e " ; b5 = bl < b2; //Присваивается
false true true true true
Операции присваивания значений переменным ЬЗ и Ь4, возможно, требуют дополнительных разъяснений. Сравнение и упорядочение строк использует лексикографический порядок, основанный на кодировке Unicode. Практически достаточно знать, что в этой кодировке '0' располагается перед ' 1 ' , которая, в свою очередь, размещается перед '2' и т. д. до '9'. Аналогично, 'а' располагается перед *Ь' и т. д. до 'z\ 'А' находится перед 'В' и т. д. до 'Z'. Если одна строка длиннее другой, но символы более короткой строки совпадают с началом более длинной, более короткая строка размещается перед более длинной; отсюда следует результат присваивания переменной Ь4. В Приложении Г можно найти таблицу некоторых употребительных символов Unicode с указанием порядка их следования. Наконец, допустимо сравнение двух булевых значений. Хотя булева алгебра не дает определения порядка следования true и false, язык С# реализует false целым числом 0, a true — целым числом 1. Поэтому false располагается перед true.
4.1
131
Булевы выражения
Булевы логические операторы Мы можем комбинировать условия или булевы значения с помощью булевых логических операторов. Булевы логические операторы testl & test2
//testl И test2
testl | test2
//testl ИЛИ test2
!
//HE
(testl)
testl
testl & teast2 равно true, только если оба операнда равны true; в противном случае результат равен false, testl \ teast2 равно true, если любой из операндов равен true; в противном случае результат равен false, \test1 равно true, если операнд равен false и наоборот.
Поскольку у булева выражения может быть только два значения, возможные результаты операции легко перечислить и показать в форме таблиц истинности, приведенных ниже. В табл. 4.1, 4.2 и 4.3 показано действие операторов &, | и !. Таблица 4 . 1 . Оператор И Правый операнд
Оператор & Левый операнд
false
true
false
false
false
true
false
true
Таблица 4.2 Оператор ИЛИ Правый операнд
Оператор | Левый операнд
false
true
false
false
true
true
true
true
Таблица 4.З. Оператор НЕ Операнд Оператор !
false
true
true
false
Если, например, переменная m содержит значение 7, тогда булево выражение т>0
& т<10
132
Глава 4. Управление и массивы
будет равно true. В этом можно убедиться, найдя в табл. 4.1 клетку на пересечении строки для true (потому что т > 0 равно true) и столбца для true (потому что т < 1 0 тоже равно true). Два из приведенных выше операторов имеют близких родственников, булевы условные операторы: Булевы условные операторы test! && test2
//testl УСЛОВНОЕ И test2
testl I I test2
//testl УСЛОВНОЕ ИЛИ test2
Результат testl && test2 равен false, если testl равно false; в противном случае результат равен значению test2. Результат testl [ | test2 равен true, если testl равно true; в противном случае результат равен значению test2.
Эти операторы не только выглядят схоже; они имеют одинаковые таблицы истинности. Оператор && имеет ту же таблицу истинности, что и оператор &, а оператор || имеет ту же таблицу истинности, что и оператор |. Значит ли это, что эти операторы вполне эквивалентны? Ответ будет «не совсем». Однако следует всегда стремиться использовать условные операторы && и ||, а не & и |, соответственно, потому что программа с ними будет работать быстрее. Поскольку им не приходится анализировать правосторонний операнд, они требуют меньше процессорного времени.
Булевы выражения С помощью булевых операторов можно комбинировать несколько проверок. Однако считается хорошим стилем использовать в сложных выражениях круглые скобки, чтобы любой, читающий программу, мог сразу увидеть, как она должна работать. Попробуйте прочитать следующее предложение: bool
test2
= а
> 0
&& b
<
100
а
<= 0
&& b
>=
100;
А теперь быстро ответьте, будет ли переменной test2 присвоено значение true, если а равно 0 и b равно 99? Ответом будет «нет», потому что оператор ИЛИ (||) имеет более низкий приоритет, чем оператор И (&&). Однако лучше для придания операциям большей наглядности заключить их в скобки: bool
test2
•
(а
> 0
&& b
<
100)
(а
<= 0
&& b
>= 100) ;
Оператор отрицания! имеет более высокий приоритет, чем ИЛИ и И. Поэтому выражение (а
==
0)
(Ь == 0)
эквивалентно по значению
4.2
Предложения выбора
(а ! = 0 )
(Ь
133
== 0)
Оператор отрицания, между прочим, имеет очень высокий приоритет (как и все унарные операторы). Если бы мы написали выражение таким образом: !
а == 0
||
b == 0
то компилятор С# посчитал бы его эквивалентным следующему: (!а)
== 0
| |
b == 0
и сообщил бы об ошибке. Если переменная а имеет тип int, в сообщении об ошибке будет сказано, что оператор! не приложим к значениям типа int.
4.2 Предложения выбора Мы подошли, наконец, к первому предложению, использующему условия. Предложение if можно описать с помощью следующей формы: Предложение if if
(условие)
{
предложения! ; } else
{
предложения! ;
Если условие после его вычисления равно true, тогда выполняются предложения^ в противном случае выполняются предложений. Часть else предложения if не является обязательной; все от ключевого слова else до конца может быть опущено.
Предположим, что нам нужна программа, которая будет вычислять значение квадратного корня из числа. Если мы хотим, чтобы программа работала надежно, мы должны заставить ее сообщать об ошибке, если число отрицательно, поскольку из отрицательного числа нельзя извлечь квадратный корень. Соответствующая часть программы может выглядеть следующим образом: C o n s o l e . W r i t e ( " E n t e r a number: " ) ; double x - double.Parse(Console.ReadLineО); double result; if (x < 0 . 0 ) { Console.WriteLine("Error: the number cannt be negative"); [Ошибка: результат не может быть отрицательным] result • 0.0;//Присвоим нейтральное значение } else {
134
Глава 4. Управление и массивы
result = Math.Sqrt(x); Предложение if управляется условием, которое дает true или false. Условие х<0.0 использует оператор сравнения меныпе-чем. Если это условие оказывается истинным (true), тогда выполняются предложения, заключенные между первой парой фигурных скобок. Если условие оказывается ложным (false), тогда вместо этого выполняются предложения, заключенные между парой фигурных скобок, расположенных после ключевого слова else. Предложение if, использованное в этом простом примере, иногда называют предложением if-then-else [если-то-иначе], потому что в нем заключены два варианта выполнения. Мы проверяем условие, и если оно дает true, (то) мы выполняем одно действие, в противном же случае (иначе) мы выполняем другое действие. Очень часто нам надо, чтобы в качестве одной из возможностей программа вообще ничего не выполняла бы. В такой ситуации мы можем опустить часть else предложения. Ниже приведен небольшой пример, где мы снова вычисляем квадратный корень, но игнорируем знак минус, если он нам встречается, рассматривая только абсолютную часть числа. Console.Write("Enter a number: " ) ; double x = d o u b l e . P a r s e ( C o n s o l e . R e a d L i n e ( ) ) ; i f (x < 0.0) {//Если введено отрицательное число х = -х;//Сделаем число положительным }
double
result
• Math.Sqrt(x);
Пример 4.1. Вычисление буквенного обозначения оценки, заданной в процентах Предположим, что у нас есть экзаменационная оценка сданного курса, заданная в процентах, и мы хотим преобразовать ее в буквенную шкалу. В простейшем случае, если мы знаем, что оценка менее 50% соответствует уровню F, будет достаточно предложения if
(m < 5 0 . 0 )
grade
m ' F' ;
Если, однако, m больше или равно 50%, уровень придется дифференцировать. Пусть, например, граничные значения оценки в процентах между уровнями F, D, С, В и А равны 50.0, 65.0, 80.0 и 90.0; тогда мы можем использовать последовательность предложений if-then-else, с помощью которых постепенно установить, какому уровню соответствует наша конкретная оценка. В этом случае уровню F будет соответствовать оценка, которая оказалась меньше всех остальных граничных значений.
4.3 Предложения повторения
135
Файл ComputeGrade.cs using System; class ComputeGrade { ///<summary> Bishop & Horspool 2002 ///Программа ComputeGrade ///========«============ ///Преобразует оценки в процентах в буквенную шкалу ///Демонстрирует использование предложений if-else /// string DetermineGrade(double m) { string result = "F"; if(m >= 90.0) result = "A"; else if(m >= 80.0) result = "B"; else if(m >= 65.0) result = "C"; else if(m >= 50.0) result = "D"; return result; void Go () { Console.Write ("Enter the mark: " ) ; double mark = double.Parse(Console.ReadLine()); Console.WriteLine("The grade is " + DetermineGrade(mark)) static void Main() { new ComputeGrade().Go() ;
Для проверки работы программы мы будем вводить различные оценки. Вот пример одного из тестовых прогонов: Enter the mark: 7 7 The Grade is С
4.3 Предложения повторения Компьютеры особенно удобны для выполнения периодических действий. Все языки программирования предоставляют способы повторного выполнения предложений до тех пор, пока не будет достигнут желаемый результат. В гл. 3 был описан цикл for, который использовался для повторения действия фиксированное число раз, что позволило сделать программу более универсальной. В действительности структура цикла for довольно сложна. Как мы вскоре увидим, в гл. 3 были упомянуты лишь некоторые его возможности. Пожалуй, простейшим способом повторения группы предложений С# является цикл while. Формат цикла while дан в приведенной ниже форме.
Глава 4. Управление и массивы
136 Цикл while while (условие) { тело цикла
Вычислить условие, если результат равен true, выполнить предложения в теле цикла. Далее снова вычислить условие и, если результат равен true, снова выполнить тело цикла. Повторять, пока вычисление условия не даст false.
В качестве простого примера цикла while приведем фрагмент программы, которая продолжает запрашивать у пользователя число от 1 до 10 до тех пор, пока не будет введено число из этого диапазона. i n t number; b o o l ok = f a l s e ; w h i l e (!ok) { C o n s o l e . W r i t e ( " E n t e r a number from 1 t o number = i n t . P a r s e ( C o n s o l e . R e a d L i n e ( ) ) ; ok = (number >= 1) && (number <= 10) ;
10:
");
}
Console.WriteLine ("the number is
"+number);
Теперь мы можем использовать циклы while в законченной программе. Пример 4.2. Программа несчастливых дней Суеверные люди полагают, что 13-й день месяца является несчастливым днем, если он попадает на пятницу. Мы хотим написать программу, которая создаст список таких несчастливых дней, начиная от января того года, который вводит пользователь. При решении этой проблемы следует ответить на ряд вопросов. 1. Как формируется список дней? Мы хотим вывести этот список на экран, и естественно включить в программу цикл, который, после нахождения каждого несчастливого дня, повторяется снова и снова в поисках следующего. Естественно, этот процесс не должен продолжаться вечно, поэтому мы должны иметь способ его остановки. Мы решили включить в цикл запрос пользователю Find another? у или п) ]
(enter у or n) [Искать следующий?
(введите
Таким образом, цикл управляется булевой переменной, значение которой определяется каждый раз исходя из ответа пользователя. Это выглядит таким образом: bool more = true; while (more) { //Выполняем требуемые действия Console.Write (" Find another? more = ConsoleReadLine() == "y";
(enter у or n) " ) ;
4.3 Предложения повторения
137
Поэтому, как только пользователь введет что-либо, отличное от у, переменная more приобретает значение false и цикл разрывается. 2. Как мы будем искать пятницы и 13-е числа? Разумеется, в каждом месяце только одно 13-е число, поэтому нам нужен цикл, который просматривает дни месяца, проверяя дату. Цикл этот опять является циклом while. Он начинается в январе заданного года (пользователь вводит это значение) и затем просматривает каждый месяц в поисках нужного соответствия. В общих чертах цикл будет выглядеть следующим образом: //Сконструируем объект "13 января данного года" DateTime = new DateTime(year,1,13); //Просматриваем месяцы, пока не попадем на пятницу while ( !Fridaythel3th) { date = date.AddMonths (1); Метод AddMonth определен в классе DateTime. Мы не упомянули этот метод в разделе 2.6, но его наличие в тщательно разработанном классе совершенно необходимо. 3. Как мы отыскиваем 13-е числа, попадающие на пятницу? Условие, использованное в приведенном выше фрагменте, следует уточнить и расширить. Нам необходимо знать день недели, на который выпадает конкретная дата. Оказывается, в классе DateTime не предусмотрено метода, непосредственно возвращающего эту информацию. Однако методу ToString может быть передана форматная строка, в которой указан способ преобразования даты в форму, удобную для печати. Так же, как форматная строка "D" формирует строку с полной датой, имеются другие форматные строки, которые позволяют сконструировать полную дату из меньших блоков. Один из таких форматов, "d", формирует сокращенное название дня (например, "Fri" для Friday, пятница), а другой, "dddd" формирует полное название дня (например, "Friday"). Мы в программе используем второй формат. Теперь мы можем объединить все отрывки нашей программы. Заметьте, что второй цикл помещен внутрь первого. При этом мы не создаем заново объект времени для нового месяца, а продолжаем работать с уже имеющимся. Поэтому инициализация второго цикла помещена снаружи обоих циклов. Файл Unlucky.cs using System; class Unlucky { ///<summary> ///Программа Unlucky Days
(вариант 1) Bishop & Horspool 2002
///Находит несчастливые дни (пятницы, попадающие на 13-е числа) ///Демонстрирует использование циклов while ///
138
Глава 4. Управление и массивы void Go () { Console.Write("Enter starting year: " ) ; int year = int.Parse(Console.ReadLine()); bool more = true; //Сконструируем объект "13 января данного года" DateTime date = new DateTime(year,1,13); while (more) { //Просматриваем месяцы, пока не попадем на пятницу while(date.ToString("dddd") != "Friday") { date = date.AddMonths(1); } //Напечатаем полностью дату несчастливого дня Console.WriteLine("{0:D}", date); date = date.AddMonths(1); Console.Write(" Find another? (enter у or n) more = Console.ReadLine () == "y";
" ) ;
s t a t i c v o i d Main() { new Unlucky () .Go ( ) ;
Если м ы запустим программу Unlucky, ее вывод будет выглядеть приблизительно так: Enter starting year: 2003 13 June 2003 Find another? (enter 13 February 2004 Find another? (enter 13 August 2004 Find another? (enter 13 May 2005 Find another? (enter
у or n) ") у у or n) ") у у or n) ") у у or n) ") n
Структуру программы Unlucky можно улучшить, использовав в ней некоторые конструкции, описанные далее в этой главе. Так что позже м ы к этой программе еще вернемся.
Цикл do-while Цикл do-while do { тело цикла } while {условие) Выполняется тело цикла; затем вычисляется условие. Если результат равен true, снова выполняется тело цикла, затем опять вычисляется условие и т д. , пока условие не даст false.
4.3 Предложения повторения
139
Используя цикл while, возможно выполнить тело цикла ноль раз. Однако можно представить себе ситуацию, когда тело цикла обязательно должно быть выполнено хотя бы один раз. С такой ситуацией мы столкнулись при выводе запроса в приведенном ранее примере ввода числа от i до 10. Ниже приведен вариант этого фрагмента с использованием цикла do-while. int do
}
num; { C o n s o l e . W r i t e ( " E n t e r a number i n t h e num = i n t . P a r s e ( C o n s o l e . R e a d L i n e ( ) ) ; w h i l e (! (num >= 1 && num <= 1 0 ) ) ;
range
1 to
10:
");
Новый вариант цикла проще, так как не требует введения специальной булевой переменной, а также слегка более эффективен. В цикле do-while сначала выполняется тело цикла, а затем проверка условия показывает, следует ли тело цикла выполнять повторно. Цикл do-while обычно используется в тех случаях, когда по условиям задачи тело цикла обязательно должно быть выполнено хотя бы один раз, как в нашем примере. В качестве второго примера приведем измененный вариант главной части программы Unlucky, которую мы недавно рассматривали. Поскольку мы хотим найти по меньшей мере одну пятницу, попадающую на 13-е число, внешний цикл while более естественно оформить в форме do-while. bool more; do { while(date.ToString("dddd") != "Friday") { date = date.AddMonths(1); } Console.WriteLine("{0:D}'\ date); date = date.AddMonths(1); Console.Write(" Find another? (enter у or n) " ) ; more = Console.ReadLine() == "y"; } while (more);
Цикл for, подробное рассмотрение В программах часто приходится выполнять одни и те же действия над последовательностью значений. Хотя для этого можно воспользоваться циклами while или do-while, цикл for предоставляет больше возможностей и позволяет писать более лаконичные программы. Ниже приводятся варианты программы для вывода значений квадратного корня из последовательности целых чисел от 1 до 10 с помощью цикла while и эквивалентного ему цикла for. int i = 1; while (i <=10) { Console.WriteLine(Math.Sqrt(i)); i++;
for (int i=l; i<=10; i Console.WriteLine( Math.Sqrt(i));
140
Глава 4. Управление и массивы
Вариант с циклом while содержит три важных компонента: 1. Инициализация управляющей переменной i (переменной, которая должна обеспечить перебор элементов последовательности), затем цикл while, управляемый посредством... 2. Проверки на повторение, которая позволяет установить, все ли повторения выполнены, после чего следует тело цикла, завершаемое... 3. Предложением, которое продвигает управляющую переменную к следующему значению в последовательности. Конструкция цикла for помещает все эти три компонента в заголовок цикла. Первый компонент заголовка определяет инициализацию; второй компонент реализует проверку, показывающую, требуется ли еще повторение тела цикла; третий компонент определяет продвижение переменной к следующему значению в последовательности. В качестве дополнительного необязательного средства в компоненте инициализации можно объявить переменную. Обычно эта переменная и является управляющей переменной для цикла. С простым вариантом цикла for мы уже сталкивались в разделе 3.5; полный синтаксис цикла for приведен ниже. Цикл for for
(инициализация;
...//тело
условие_продолжения;
продвижение)
{
цикла
Выполняется фрагмент инициализации. Далее вычисляется условие продолжения, и если результат равен true, выполняется тело цикла. После выполнения тела цикла выполняется фрагмент продвижение, после чего снова вычисляется условие продолжения, чтобы выяснить, следует ли повторять тело цикла. Цикл повторяется до тех пор, пока условие продолжения не даст значение false. Заметьте, что в предложении инициализации можно объявить одну или несколько переменных для использования в качестве переменных управления циклом.
Цикл for имеет настолько общий характер, что его можно использовать для получения самых разнообразных эффектов. В дополнение, любая из трех частей заголовка может быть опущена, так как не всегда заранее известно, на сколько, например, надо смещаться по последовательности или когда завершить цикл. Ниже рассмотрены некоторые варианты цикла for. Цикл вперед. Это самая распространенная форма, как показывает приведенный ниже пример. for
(int i - 0; i < 10; a [ i ] - 0;
Здесь массив из 10 элементов инициализируется нолями.
4.3 Предложения повторения
141
Цикл назад. Иногда оказывается удобнее перемещаться по массиву элементов в обратном порядке или иметь счетчик, который в каждой итерации уменьшает свое значение. Например: for (int i = 10; i >= 0; i — ) { Console.Write("{0} seconds to lift-off
...",i);
Бесконечный цикл. Иногда требуется цикл, который не имеет условия завершения. Обычно в таких случаях предусматривается какой-либо способ разрыва цикла где-то в самом теле цикла; одной из возможностей будет предложение return. Такого рода пример имеется в программе ShowBinary (см. пример 4.7 ниже). Как и в этой программе, мы можем оформить цикл следующим образом: while (true) { //Тело цикла опущено } Однако многие программисты предпочтут такой вариант for ( ; ; ) { //Тело цикла опущено
Согласно правилам, если в заголовке цикла for опущены части инициализации и продвижения, то не выполняется никаких действий. Если же опущена часть условия продолжения, то цикл продолжается вечно, как если бы это условие всегда было равно true. В такой форме цикла нет особых преимуществ, это просто выражение определенного стиля программирования. Вложенные циклы. В программах очень часто встречаются циклы, вложенные в другие циклы. Если такие циклы имеют управляющие переменные, то эти переменные должны быть различными для внешних и внутренних циклов, иначе возникнет путаница. Вложенные циклы использованы в программе Primes (пример 4.4). Пустые циклы. В циклах while и for возможна ситуация, когда тело цикла полностью пропускается. Например, приведенный ниже цикл не выполнится ни разу, если переменная п будет иметь нулевое значение. for
(int
= 0;
n;
Двойные циклы. Нетрудно сделать так, чтобы в цикле существовали две управляющих переменных, которые изменяются синхронно в процессе повторения тела цикла. Приведенный ниже небольшой пример int for
i, j ; (i = 0, j = 5; i <= Console .WriteLine( " { 0
j;
i-••+/ i.
j ]
142
Глава 4. Управление и массивы
выводит последовательность пар (0,5) (1,4) (2,3). Такого рода циклы довольно необычны, но могут быть полезны при обработке комплексных структур данных. Циклы с неоднозначным выходом. Если условие продолжения цикла содержит булево выражение с оператором И, тогда цикл может завершиться, если одно из условий в выражении станет равным false. Обычно необходимо знать, какое это было условие, поэтому непосредственно после цикла мы снова проверяем условия. Например, предположим, что мы хотим ввести 10 чисел, при этом цикл ввода надо завершить либо после ввода отрицательного числа, либо после ввода всех 10 чисел. Такой цикл будет иметь следующий вид: int i = 0; int num; do
{
num • int.Parse(Console.ReadLine()); } while (!(num<0) && i<10); if (num < 0) Console.WriteLine("Number {0} found", num); else //Цикл завершился на 10-м такте, и отрицательных чисел не было Console.WriteLine("No negative found"); Порядок, в котором условия проверяются после выхода из цикла, существенен: если десятый (последний) элемент отрицателен, оба условия дадут false одновременно. Нам же надо обнаружить отрицательное число, поэтому это условие надо проверять первым. Такие циклы с неоднозначным выходом использованы в программе Primes (пример 4.4). Пример 4.3. Несчастливые дни, вариант 2. Мы теперь можем еще раз изменить программу Unlucky и использовать в ней цикл for. В этом варианте все, связанное с просмотром месяцев, собрано в одно месте, именно, в самом предложении for: for
(DateTime date = new DateTime(year,1,13); more; d a t e = date.AddMonths(1))
Управляющая переменная цикла теперь представляет собой объект date. Условием повторения является, как и раньше, булева переменная more, a смещение к следующему шагу, ранее выполнявшееся посредством отдельного предложения где-то в теле цикла, теперь выступает совершенно отчетливо. Новая программа носит название Unlucky2.cs.
4.3 Предложения повторения
143
Файл Unlucky2.cs
using System; class Unlucky2 { ///<summary> ///Программа Unlucky Days / // /// '
(вариант 2) Bishop & Horspool 2002
///Находит несчастливые дни (пятницы, попадающие на 13-е числа) ///Демонстрирует использование циклов for, позволяющих ///улучшить структуру программы /// void Go () { Console.Write("Enter starting year: " ) ; int year = int.Parse(Console.ReadLine()); bool more = true; for (DateTime date 4 new DateTime(year,1,13); more; date « date.AddMonths (1)) { while(date.ToString("dddd") != "Friday") { date = date.AddMonths(1); Console.WriteLine("{0:D}", date); Console.Write (" Find another? (enter у or n) " ) ; more = Console.ReadLine() == "y";
s t a t i c void Main() { new Unlucky2 () . Go () ;
Вывод программы не изменился (см. пример 4.2). Пример 4.4. Простые числа Приведем пример программы, иллюстрирующей использование вложенных циклов и циклов с неоднозначным выходом. Программа вычисляет (и выводит) простые числа от 0 до 100 посредством проверки, делится ли каждое число на какое-либо положительное целое, отличное от 1 и самого себя. С целью оптимизации работы программы в нее введены два упрощения: •
она проверяет только нечетные числа (после 2);
•
она проверяет делители до (и включая) квадратного корня из числа.
(Замечание: если вам нужна программа, генерирующая большое число простых чисел, то для этого существует гораздо более быстрый алгоритм.) Во внешнем цикле переменная i проходит последовательность значений 3, 5, 7, ..., 99. Это сформулировано следующим образом: for
(int i = 3; i <= max; i += 2) {
Внутренний цикл использует более изощренную проверку условия, приводящую к завершению цикла, как только мы обнаруживаем, что i не есть простое число, или когда мы перебрали все делители вплоть до квадратного корня из i. Этот цикл выглядит следующим образом:
144
Глава 4. Управление и массивы
bool prime = true; int l a s t = (int) Math.Sqrt(i); for (int divisor • 3; prime && divisor <= l a s t ;
divisor += 2)
Когда цикл разрывается, мы должны проверить, почему он завершился — потому что мы нашли или, наоборот, не нашли простое число. Полный текст программы Primes.cs приведен ниже. Файл Primes.cs
using System; class Primes { ///<summary> ///Программа Primes
Bishop & Horspool 2002
/// | ill ///Выводит все простые числа между 1 и 100 ///Демонстрирует использование вложенных циклов for ///и циклов for с неоднозначным выходом /// int max = 100; void Go () { Console.Write (" 2");//Выведем первое простое число for (int i = 3; i . <= max; i +=2) { //Предположим, что число простое bool prime = true; //Когда прекратить проверку int last = (int) Math.Sqrt(i); for (int divisor = 3; prime && divisor <= last; divisor +=2) { prime = (i % divisor != 0) ; . } if (prime) Console.Write(" "+i); if (i % 19 == 0) Console.WriteLine(); } Console.WriteLine(); } static void Main() { new Primes().Go();
Как и можно было ожидать, вывод программы выглядит следующим образом: 2 23 59 97
3
5 29 61
7 31 67
11 37 71
13 41 73
17 43 79
19 47 83
53 89
4.4 Простые массивы
145
Мы добавили управляющее предложение if, чтобы выводить на одну строку определенное количество (не всегда одинаковое) результирующих простых чисел.
4.4 Простые массивы Массив содержит в себе список значений объектов, обращение к которым осуществляется соответственно их позиции в списке. Каждая позиция соответствует определенному значению индекса. Индекс первой позиции, т. е. индекс первого элемента в списке, всегда равен 0. Второй элемент имеет индекс 1 и т.д. Правила объявления простого массива и его семантика показаны в приведенной ниже форме: Объявление массива (упрощено) тип_элементов [размер]
] идентификатор массива
= new тип элементов
идентификатор массива создается из размер объектов типа тип элементов, обращение к которым осуществляется посредством индексов, принимающих значения от 0 до размер — 1.
Примеры массивов: int [ ] markCounts doublet] rainfall s t r i n g [] monthName • DateTime[] myExams People [] t e c h n i c i a n s
= new i n t f101]; : new double[13]; new s t r i n g [ 1 3 ] new DateTime[noOfExams]; new People[noOfTechnicians]
Во всех этих примерах прежде всего указывается тип элементов, затем этот тип снова упоминается, когда мы конструируем массив с помощью оператора new. Позже, когда мы будем обсуждать наследование, мы увидим, что эти два типа не обязательно должны совпадать. Пара символов [ ] в этом контексте обозначает массив. В объявлении указывается также размер массива. Размер может быть представлен константой, а также выражением, вычисление которого дает целое число. Заметьте, что размер, представляющий собой число элементов в массиве, всегда на 1 больше последнего допустимого индекса из-за того, что индекс первого элемента равен 0. Это очевидное несоответствие часто бывает источником ошибок в программах. Для того чтобы наша программа выглядела более наглядной, мы объявили переменные rainfall и monthName массивами из 13 элементов. Это дает нам возможность использовать выражение monthName[5] для имени «Май», и monthName[12] для имени «Декабрь». Поскольку в году нет месяца с номером 0, мы в обоих массивах оставляем нулевой элемент неиспользуемым. В программах часто требуется выполнить одни и те же вычисления над всеми элементами массива. Цикл for, который мы недавно описыва-
Глава 4. Управление и массивы
146
ли, предоставляет возможность с помощью целочисленной переменной просмотреть все значения индексов массива. Несколько примеров таких действий мы увидим в последующих программах. Пример 4.5. Распределение оценок Мы хотим подсчитать, сколько раз каждая из десяти оценок встречается в классе из 250 студентов. Например, может оказаться, что оценка 0 встретилась среди всех 250 оценок 13 раз. Мы строим массив с индексами от 0 до 10 (заданный диапазон возможных оценок) и сохраняем в каждом элементе массива суммарное число студентов с данной оценкой. Этот массив изображен на рис. 4.1. markCount
13
15
О
1
18 2
21
30
31
20
3
4
5
6
25
27
33
17
ю
Рис. 4.1. Массив с распределением оценок Для упрощения программы, а также для облегчения ее отладки, м ы будем генерировать оценки с помощью датчика случайных чисел, а не вводить их с клавиатуры. Текст программы приведен ниже. Файл Frequencies.cs using System; class Frequencies { ///<summary> ///Программа Frequencies Bishop & Horspool 2002 ///===================== ///Подсчитывает частоты оценок между 0 и 10. ///Проверка программы выполняется с помощью датчика ///случайных чисел ///Демонстрирует простую обработку массивов /// const int limit « 11;//Пусть таблица будет небольшой const int classSize = 250; •void Go () { int [ ] markCount • new i n t [ l i m i t ] ; int score; Random r • new Random () ; //Генерируем 250 оценок for (int i = 0; i < classSize; i++) { //генерируем числа от 0 до l i m i t - 1 score = r.Next(limit) ; markcount[score]++;
4.4 Простые массивы
147
Console.WriteLine("Table
of mark counts\n"+ '\n\n+ "
int for
Mark
Occured");
s t u d e n t s = 0; ( i n t i = 0; i < l i m i t ; i++) { Console.WriteLine( " {0,4} {1.6}", i , markCount[i]); s t u d e n t s +« m a r k c o u n t [ i ] ;
}
Console.WriteLine("Total{0,7}",
students) ;
}
static new
void Main() { Frequencies().Go();
Типичный вывод выглядит следующим образом: Table
Mark 0 1 2 3 4 5 6 7 8 9 10 Total
of
mark counts
Occured 24 26 26 17 21 22 16 24 28 27 19 250
В отличие от результата, изображенного на рис. 4.1, полученный вывод представляет равномерное распределение оценок (маловероятная ситуация), потому что генератор случайных чисел формирует именно такое распределение.
Работа с индексами массива Рассмотрим массив rainfall, который мы хотим заполнить значениями количества месячных осадков в миллиметрах. Заполненный массив может выглядеть так, как показано на рис. 4.2, а количество осадков, например, в мае (9.8 мм) будет выражаться следующей величиной: rainfall[5]
//Количество
осадков
за май
Глава 4. Управление и массивы
148 rainfall
0
С
1
16 7
2
17 2
3
13 4
4
10 1
5
9. 8
6
0. 2
7
0
8
0
9
4. 5
10
7. 0
11
12 .4
12
22 . 1
Рис. 4.2. Массив со значениями количества месячных осадков с нулевым неиспользуемым элементом
Для печати содержимого массива мы можем использовать такой фрагмент: Console.WriteLine(" Month mm"); for ( i n t month= 1; month <= 12; month++) { C o n s o l e . W r i t e L i n e ("{0,4}{ l , l l : f l } " , month,
который выведет на экран Month 1 2 3 4 5 6 7 8 9 10
Многие программисты сочтут ненужным и даже вредным с точки зрения расходования памяти создание массива с одним неиспользуемым элементом. Не составляет никакого труда избавиться от этого лишнего элемента. Если неиспользуемый начальный элемент опустить, то массив надо будет объявить с размером 12: stringt] monthName = new string[12];
Схема такого массива приведена на рис. 4.3. Теперь для вывода количества осадков по месяцам придется воспользоваться циклом, в котором индекс массива будет пробегать значения не от 1 до 12, а от 0 до 11: Console.WriteLine(" Month mm"); for ( i n t month= 0; month < 12; month+ + ) { Console.WriteLine("{0,4}{1,11:f1}", month+1,
rainfall[month]);
Профессиональный программист почти наверняка использует второй вариант цикла и не будет резервировать в массиве лишний элемент. С другой стороны, если в программе используются всего несколько массивов, вопрос об излишней трате памяти не стоит очень остро. Какую форму цикла использовать — дело вкуса; более важным критерием является удобство чтения программы. rainfall
0
16. 7
1
17. 2
2
13 4
3
10 1
4
9. 8
5
0. 2
6
0
7
0
8
4. 5
9
7. 0
10
12 4
11
22 . 1
Рис. 4.3. Массив со значениями количества месячных осадков со всеми используемыми элементами
150
Глава 4. Управление и массивы
Размер и длина массива Как уже говорилось, размер объявленного массива не может изменяться, а индексы элементов массива всегда представляют собой целые числа. Эти два ограничения могут быть сняты использованием специальных типов С# — коллекций, которые будут описаны в гл. 8. Однако мы можем настроить размер создаваемого массива путем ввода его размера с клавиатуры или вычисления в программе, вместо того, чтобы задавать его в виде константы. В примере 4.5, если мы хотим расширить диапазон обрабатываемых оценок с 11 до, скажем, 50 или 75, или 100, то объявление массива следует выполнить таким образом: Console . W r i t e L i n e (What i s t h e maximum mark? int limit = int.Parse(Console.ReadLine()); int c l a s s S i z e = 250; i n t [ ] markCount = new i n t [ l i m i t ] ;
"
).;
Остальные участки программы останутся без изменения, но программа сможет вывести таблицу распределения любого количества оценок в соответствии с данными, введенными с клавиатуры. Length является свойством массива, которое можно использовать внутри метода, чтобы определить длину массива, передаваемого в качестве параметра. Например, мы можем объявить метод Print для вывода массива marks таким образом: void P r i n t ( i n t [ ] а) { for ( i n t i = 0; i < a . L e n g t h ; i + + ) { Console.WriteLine("{0}: {1}", i, a[i]);
Методу Print можно передать массив целых чисел любой длины, и этот массив будет напечатан. Именно поэтому мы дали формальному параметру вполне анонимное имя а. Пример 4.6. Вычисление буквенного обозначения оценки, заданной в процентах, вариант 2 В качестве дополнительного примера рассмотрим еще раз программу для вычисления буквенного обозначения оценки по значению оценки, заданной в процентах (пример 4.1). На этот раз мы используем массив, содержащий значения границ между соседними буквенными оценками, и второй массив, содержащий буквенные обозначения оценок. Мы также используем метод определения оценки, который основан на цикле с оператором return в нем. Внешний цикл обработки вводимых оценок имеет любопытную структуру. Вместо того, чтобы каждый раз задавать вопрос, будет ли вводиться следующая оценка, как мы делали в программе Primes (пример 4.4), мы используем «неправильную» оценку (-1) для оповещения программы об
4.4 Простые массивы
151
окончании ввода данных. В результате м ы вынуждены проверять оценку дважды — один раз перед определением ее буквы (поскольку м ы не можем оценивать отрицательные числа) и второй раз для завершения цикла. Файл ComputeGrade2.csJ using System; class ComputeGrade2 { ///<summary> ///Программа ComputeGrade
(вариант 2) Bishop
/ I /
=
=
& Horspool
2002
=
ill
///Преобразует оценку в буквенное обозначение. ///Демонстрирует использование массивов и выход из цикла ///посредством оператора return /// doublet] boundary = { 90.0, 85.0, 80.0, 75.0, 70.0, 65.0, 60.0, 50.0, 40.0 }; string[] grade - { "A+", "A", "A-", "B+", "В", "В-", "C", "D", "E"}; string DetermineGrade(double m) { for (int i = 0; i< boundary .Length; i++) { if (m >= boundary[i]) return grade[i]; } return "F"; } void Go() { double mark; Console.WriteLine("Enter -1 to quit"); Console.WriteLine("Mark Grade"); do { mark = double.Parse(Console.ReadLine()); if (mark >= 0) {0}", DetermineGrade(mark)); Console.WriteLine } while (mark >= 0 ) ; static void Main() { new ComputeGrade2() .Go()
Типичный прогон программы дает следующий результат: Enter Mark
-1 t o q u i t Grade
77
B+
65
B-
64
c
90
A+
-1
152
Глава 4. Управление и массивы
Цикл foreach В дополнение к формам циклов, рассмотренным уже в разделе 4.3, в С# имеется еще одна особая конструкция, называемая циклом foreach. Этот цикл разработан специально для коллекций, собраний данных, которые будут рассмотрены в гл. 8. С другой стороны, массив является простым видом коллекции значений, и цикл foreach вполне можно использовать с массивами. Если, например, у нас есть массив с перечислением названий месяцев string[]
то следующий цикл выведет список месяцев в том же порядке, как они указаны в объявлении массива и по одному имени на строке: foreach ( s t r i n g s i n MonthNames) Console.WriteLine ( s ) ;
{
}
Приведем форму для цикла foreach. Цикл foreach foreach
(тип идентификатор in идентификатор-массива {
. . . тело цикла, может ссылаться на идентификатор
Идентификатору присваиваются последовательные значения из массива. Для каждого значения тело цикла повторяется.
4.5 Строки и символы Хотя эта глава посвящена, главным образом, управлению программой, мы ввели в нее описание массивов, что дало нам возможность предложить более наглядные примеры на циклы. Строки обладают многими характеристиками массивов, а символы представляют собой элементы строк. Рассмотрим теперь эти понятия более детально.
Тип string [строка] На протяжении всей книги мы использовали строковые константы. Встречались нам также и операции сцепления строк. Например, предложение Console.WriteLine("Result
= "+n);
4.5 Строки и символы
153
преобразует значение п в строку (если только тип п не является уже строкой) и сцепляет эту строку со строковой константой "Result = ", в результате чего образуется новая более длинная строка, передаваемая затем методу WriteLine класса Console. Строковый тип string является структурным типом, имеющим много общего с другими типами С#, но имеющим и присущие только ему особенности поведения. Со строками можно выполнять следующие операции: • назначение строковых значений строковым переменным, • сравнение двух строк, • сцепление (конкатенацию) двух строк, • доступ к свойствам строки и, • вызов различных методов, определенных для типа string. Строка схожа с массивом элементов, которые мы можем читать, но не изменять. Предположим, что мы объявляем и инициализируем строку следующим образом: string
s = "abcdefghij";
Мы можем получить доступ к каждому символу строки и напечатать его с помощью такого цикла: for
( i n t i = 0 ; i < s .L e n g t h ; i + + ) { C o n s o l e . W r i t e L i n e ( " E l e m e n t {0} = { 1 } " , i ,
s[i]);
который выводит 10 строк текста вроде следующих: Element 0 = а Element 1 = Ь Element
9 = j
Однако строки не считаются массивами и назначение s[3]
= ' х ' ; //Неверная
попытка
заменить
букву
d на букву х
является неверным, потому что переменные строкового типа являются неизменяемыми — их нельзя модифицировать. Для того чтобы создать новое строковое значение, необходимо создавать новый экземпляр строкового типа .
Изменяемые строки должны реализовываться, как экземпляры класса StringBuilder.
154
Глава 4. Управление и массивы
Операции со строками Базовые операции со строками перечислены в приведенной ниже форме. Объявление и использование строк string svarl; string svar2 = "строка"; svarl = svar2; svarl = "строка"; svarl = svar2 + "строка"; int len = svarl.Length;
//Объявляем строковую переменную //Объявляем и инициализируем строку //Присваиваем значение строке //Присваиваем значение строке //Сцепляем две строки //Получаем длину строки
Переменная svarl создается с начальным значением null, в то время как переменная svar2 создается и инициализируется значением "строка".
Важно подчеркнуть разницу между понятиями null (т. е. отсутствие значения) и нулевая, или пустая строка. Здесь может помочь пример. Приведенные ниже строки С# приведут к ошибке времени выполнения: string s i ; si += "abc";
//Создается строка со значением null //Неверно, выполнено не будет!!
потому что левый операнд в операции сцепления строк не имеет значения. Если, однако, переписать строки следующим образом: string s2 • " " ; s2 += "abc"; то они будут успешно выполнены и переменная s2 получит в итоге значение "abc". Строковые константы начинаются и заканчиваются символами двойных кавычек « " », причем эти константы могут иметь любую длину, начиная от 0. Иногда нам нужно включить в строковую константу символ двойной кавычки. Для этого символ кавычки необходимо предварить символом обратной косой черты «\». Однако иногда требуется включить в строку сам символ «\». Эта проблема решается тем же способом — включением перед выводимым символом «\» еще одного символа «\». Ниже приведено несколько примеров такого рода. Console.WriteLine ( " " ) ; Console.WriteLine ( " \ " " ) ; Console.WriteLine("\\\\"); Console.WriteLine ( " \ \ \ " a \ \ \ " " ) ;
Вот еще полезный пример: string s • " \ \ \ " " ; Console.WriteLine(s.Length) ; //Выводит 2 Важно отметить, что иногда включение в текст программы двух символов приводит к тому, что только один (специальный) символ помещается в
4.5 Строки и символы
155
строковую константу. Все эти специальные комбинации символов в С# имеют в качестве первого символа пары символ обратной косой черты. Некоторые дополнительные специальные комбинации символов перечислены в следующем разделе, посвященном символьному типу char. Все комбинации, допустимые в качестве символьных констант, могут быть использованы и внутри строковых констант.
Методы типа string Тип string включает целый ряд предопределенных методов. Выборка наиболее полезных методов приведена в форме для класса string. Полную информацию о методах string можно найти в описании класса System.String (потому что string в языке С# является синонимом System. String). Класс string (сокращено) bool si.StartsWith(string s2) //true, если si начинается с s2 bool si.EndsWith(string s2) //true, если si заканчивается строкой s2 int si.IndexOf(char ch) //Ищет символ ch внутри si int si. IndexOf (char ch, int pos) //To же, но начиная с позиции pos int si.IndexOf(string s2) //Ищет строку s2 внутри si int si. IndexOf (string s2, int pos) //To же, но начиная с //позиции pos string si.Substring(int pos) //Извлекает подстроку из si string si. Substring (int pos, int len) //To же string si.ToLowerf) //Копирует si, преобразуя буквы в строчные string si.ToUpper () //Копирует si, преобразуя буквы в прописные string si.Trim () //Копирует si, убирая лидирующие и //завершающие пробелы string si.TrimStart() //Копирует si, убирая лидирующие пробелы string si.TrimEnd() //Копирует si, убирая завершающие пробелы string [] si.Split () //Расщепляет si на слова, по одному на //элемент массива char si. [int pos]; //Обращается к элементу в позиции pos StartsWith возвращает true только если строка s1 начинается со строки s2; EndWith выполняет аналогичную проверку в конце строки. IndexOf возвращает позицию первого вхождения в строку s1, начиная от ее левого конца, символа ch или подстроки s2. Первая позиция строки имеет номер 0. Если поиск не дает результата, возвращается - 1 . Два варианта с аргументом pos начинают поиск от позиции pos. Методы Substring возвращают подстроки строки s1. Первый вариант возвращает всю подстроку, начинающуюся с позиции pos; второй вариант возвращает подстроку длиной len, начинающуюся с позиции pos. ToUpper и ToLower преобразуют все алфавитные символы в строке s1 в прописные или строчные, соответственно. TrimStart возвращает копию строки s1, из которой убраны лидирующие пробелы; TrimEnd возвращает ту же строку, из которой убраны завершающие пробелы; Trim возвращает копию без тех и других пробелов. Split разбивает строку s/ на массив подстрок, где в качестве подстрок рассматриваются части строки s1, ограниченные пробельными символами (пробелами или символами табуляции). s1[pos] выполняет обращение к символу в позиции pos.
156
i
Глава 4. Управление и массивы
Тип char Если символьная строка схожа с массивом, то к какому типу принадлежит элемент этого массива? Ответ — к типу char. Переменная типа char может содержать значение, представляющее собой числовой код одного символа. Каждый символ, например, буква 'а' или знак '&', имеет закрепленный за ним числовой код. Соответствие кодов и символов до некоторой степени произвольно, однако все классы и методы в библиотеке С#, работающие со строками и символами, должны использовать одну и ту же кодировку. Библиотека С# использует метод кодирования, носящий название Unicode. Каждое значение Unicode лежит в диапазоне от 0 до 65535 и занимает, следовательно, 16 бит памяти. В Unicode 26 строчных букв латинского алфавита от 'а' до V нумеруются по порядку, так же, как и 26 прописных букв от 'А' до Z' и 10 цифровых символов от '0' до '9'. Составляя программу, вы всегда можете считать, что символы в этих группах нумеруются последовательно (см. Приложение Г, где приведены дополнительные сведения относительно кодировки Unicode). Объявление и использование символов char char
chl;
//Объявление
ch2 = ' X ' ;
символьной переменной
//Объявление и инициализация символьной переменной
chl
= ch2;
//Присваивание символьного
значения
chl
= 'У;
//Присваивание символьного
значения
s
= s+chl;
//Сцепление
строки и символа
s
= chl+s;
//Сцепление
символа и строки
Объект c h l создается и инициализируется символом с кодом 0, a ch2 создается и инициализируется значением 'X'.
В качестве примера приведем фрагмент программы, который позволяет увидеть числовые значения некоторого диапазона символов: for
( c h a r ch = ' A ' ; ch <= ' Z ' ; ch++) ; { Console.WriteLine("Code f o r {0} i s {1}",
ch,
(int)ch);
Для получения числового кода символа достаточно выполнить преобразование char в int. Выводом предыдущего примера будут 26 строк такого рода: Code Code
for for
A is B is
65 бб
Code
for
Z is
90
Символьное значение можно также присвоить переменной типа int. Приведенный ниже цикл выведет все символы символьной таблицы: for (int i=0; i<256; Console.Write((char)i + "
") ;
4.5 Строки и символы
157
Вывод может иметь такой вид:
0 C Z u x ? ?
1 2 3 4 5 6 7 8 9 : ; < = > ? @ А В D E F G H I J K L M N O P Q R S T U V W X Y [ \ ] _ a b c d e f g h i j k l m n o p q r s t v w y z { | } - | ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 7 ? ? ? ? ? ? ? ? ? ? ? ? i Ф £ a V | S " с
I a u
I D f t 0 0 0 0 6 x 0 U U U D Y _ f i & i a ft 4 Q u
* у
9
ft у
i
ft
ft
£
£
£
£
d
fi
d
б
б
о
б
• о
ft
Полученный вывод не вполне точен, так как не все коды могут быть отображены в виде символов на экране.
Escape-последовательности Символьная константа записывается в форме 'X', где одиночные кавычки (апострофы) используются в качестве ограничительных символов как слева, так и справа. Учитывая, что в кодировке Unicode могут существовать 65535 различных 16-битовых кодов, а типичная клавиатура компьютера имеет лишь около 50 алфавитно-цифровых клавиш, и принимая еще во внимание, что некоторые символы выводятся специфическим образом (например, символ табуляции), необходимо предусмотреть специальные обозначения для записи большинства символов типа char. Эти специальные обозначения начинаются с обратной косой черты; некоторые из них перечислены в приводимой ниже форме. Они носят название Escape-последовательностей (escape — переход), а символ обратной черты в данном контексте называют строковым escape-символом (не путать с клавишей Esc на компьютерной клавиатуре). Этот символ указывает на переход к альтернативным обозначениям. Символьные escape-последовательности (сокращено) \п \t \0 V \" \\ \uXXXX
Символ Символ Символ Символ Символ Символ Символ
перевода строки горизонтальной табуляции null одиночного апострофа кавычки обратной косой черты UNICODE с кодом ХХХХ (шестнадцатеричное)
Все комбинации могут использоваться внутри строковой константы или константы типа char и обозначают один 16-битовый символ. В комбинации \uXXXX символ X обозначает одну шестнадцатеричную цифру (от 0 до 9 или от А до F); так, \uOO41 обозначает символ с кодом 4x16+1, или 65, а это есть код буквы 'А'
158
Глава 4. Управление и массивы
Символ перевода строки осуществляет переход на следующую строку; символ табуляции смещает вывод к следующей предопределенной позиции в текущей строке. Ноль-символ часто используется для обозначения отсутствия символа — он обычно игнорируется при выводе. Поэтому его можно использовать как способ удаления символов из строки. Таким образом, мы имеем целый ряд символьных и строковых значений со схожими названиями, но имеющими разный смысл: •
ноль-символ ' \ 0 ' ;
•
null;
•
пустая строка " " ;
•
строка, содержащая ноль-символ " \ 0 " .
Эти значения могут быть созданы следующим образом: char cl = string si string s2 s t r i n g s3
'\0'; = n u l l ; //si не имеет значения = " " ; //s2 имеет строковое значения; длина строки = О = "\0"; //s3 имеет строковое значения с длиной = 1
Программа может выполнять лишь ограниченный набор операций над символьными переменными, именно, задавать им новые значения, сравнивать их и сцеплять со строками. Однако эти значения исключительно полезны при вводе и выводе текста. Unicode Имеется много тысяч символов, отсутствующих на клавиатуре. Это акцентированные символы, символы-умляуты, специальные символы денежных единиц, символы алфавитов других языков (иврита, греческого) и т. д. Однако, как уже отмечалось, имеется международное соглашение относительно числового представления всех этих символов, носящее название Unicode. Информацию об этой кодировке можно найти по адресу http://www.unicode.org. Некоторые наиболее полезные коды приведены в табл. 4.4. Таблица 4.4. Некоторые символы Unicode Символ
Для того чтобы вставить символ Unicode в строку, мы используем escape-код \ucccc, где сссс — код Unicode. Необходимо указывать полный 16-битовый код. Например, можно написать Console.WriteLine("Dr
M\uOOFCller's salary \u00A5200.000");
i n Japan w i l l
be " +
что приведет к выводу на экран строки: Dr Muller's salary in Japan will be ¥200.000
Если символы Unicode выводятся неправильно, вы должны установить на вашем компьютере необходимые шрифты. Инструкции, касающиеся этих действий, содержаться на Web-сайте Unicode. Возможности консольного окна по части вывода символов Unicode могут оказаться ограниченными (не все символы будут отображаться на экране), но все-таки попробуйте выполнить приведенный выше фрагмент.
Методы типа char Тип char, являющийся синонимом типа System.Char, предоставляет ряд статических методов, в основном связанных с классификацией символьных значений и распределением их по различным категориям символов. Некоторые методы анализа символов приведены ниже. Тип char (сокращено) bool
char.IsDigit{ch)
//true,
если
ch
цифра
bool
char.IsLetter(ch)
//true,
если
ch
буква
bool
char.IsLetterOrDigit(ch)
//true,
если ch
bool
char.IsLower(ch)
буква или цифра
//true,
если ch
строчная буква
bool char.IsUpper(ch)
//true,
если ch
прописная буква
bool
//true, если ch //табуляция
char.IsWhiteSpace (ch)
пробел или
Под ch подразумевается значение типа char. Если проверка дает отрицательный результат, метод возвращает значение false.
Помимо букв, цифр и пробельных символов, имеются много других категорий. Все детали можно найти в документации к типу System.Globalization. UnicodeCategory. Проверки на категорию обычно используются для просмотра вводимого текста и поиска в нем определенных частей. Например, для нахождения следующего идентификатора (имени переменной) в строке исходного текста программы, можно использовать такой метод: string GetNextldentifier(string line, int idStart = - 1 ;
int pos) {
160
Глава 4. Управление и массивы for
(int i=pos; i
if (idStart < 0) return null; //Идентификатор не найден int j ; for (j = idStart+1; j < line.Length & & char.IsLetterOrDigit(line[j] ) ; j++) ;//Ничего не делать return line.Substring(idStart, j-idStart);
Первый параметр представляет собой строку, в которой осуществляется поиск; второй параметр указывает, где следует начать поиск (если мы ищем второй или последующие идентификаторы в строке). Обратите внимание на второй цикл приведенного фрагмента. В этом цикле нет никаких предложений, потому что он служит просто для нахождения позиции в строке, которая прекращает действие этого цикла. Замечание: Если вводимая строка представляет собой предложение языка С#, следует рассмотреть особые случаи. В алгоритм программы следует ввести пропуск слов, находящихся внутри строковых констант и комментариев. Возможно также, потребуется игнорировать ключевые слова языка С # . Обсуждаемая ниже программа иллюстрирует использование строковых переменных, переменных типа char, а также циклов while и for. Пример 4.7. Отображение двоичных чисел Целые числа хранятся внутри компьютера в двоичной форме. В большинстве современных компьютеров тип int использует 32 двоичных разряда, которые иначе называются битами. Используемое нами программное обеспечение обычно скрывает двоичное представление чисел в памяти компьютера преобразованием их по мере необходимости в десятичную систему или наоборот. Если, однако, нам надо увидеть, как выглядит двоичное представление целого числа, мы можем воспользоваться следующим алгоритмом: многократно делить число на 2 и запоминать остаток. Например, десятичное число 13 может быть преобразовано в его двоичное представление 1101 путем следующих операций деления: •
13 -f 2 = 6 с остатком 1;
•
6 т 2 = 3 с остатком 0;
•
3 -е- 2 = 1 с остатком 1;
•
1 и- 2 = 0 с остатком 1.
4.5 Строки и символы
161
На этом шаге мы можем остановиться, так как число, которое надо делить, стало нолем. Последовательность остатков, написанная в порядке, обратном их получению, дает 1101. Действительно, 1101 является двоичным представлением числа 13, так как 13 = 1х2 3 + 1х22 + 0х2 х + 1x2° Программа ShowBinary, приведенная ниже, многократно запрашивает у пользователя ввод числа и затем выводит на экран его двоичное представление. Программа использует два различных цикла while, один — для многократного получения числа с клавиатуры и преобразования его в двоичную форму, а второй — для многократного деления на 2 и получения остатка. Мы можем обратить порядок следования остатков путем сцепления каждого остатка с началом строковой переменной. Первый цикл while в программе ShowBinary использует условное выражение к > 0. Если это условие выполняется, тело цикла (предложения, заключенные в фигурные скобки) выполняется. Затем снова вычисляется условное выражение. Если результат опять равен true, тело цикла снова выполняется, и этот процесс повторяется до тех пор, пока вычисление условного выражения не даст false. Поскольку тело цикла в каждой итерации делит к на 2, значение к должно становиться все меньше и меньше. В конце концов к должно стать равным нолю, и условное выражение дает false. Второй цикл в программе, расположенный внутри метода Go, написан как бесконечный цикл. Обычно следует избегать бесконечных циклов. Конкретно в этой программе цикл for содержит условное выражение, которое приводит к выходу из цикла не как обычно, а с помощью предложения return. Файл ShowBinary.cs
using System; class ShowBinary { ///<summary> ///Программа ShowBinary / //
Bishop & Horspool 2002
ill
///Выводит число в двоичной системе обозначений ///Демонстрирует использование цикла while /// void PrintBinary(int k) { if (k — 0) { Console.WriteLine("0"); return; } string s = ""; while (k > 0) { if (k%2 == 0) s = ' 0' + s ; 6—2047
Глава 4. Управление и массивы
162 else s = ' 1' + s ;
к /= 2; } Console.WriteLine(s); } void Go () { Console.WriteLine("Note: enter -1 to exit the program."); for ( ; ; ) { Console.Write("Enter number to display in binary: " ) ; int num = int.Parse(Console.ReadLine()); if (num < 0) return; PrintBinary(num);
static void Main() { new ShowBunary() .Go();
Типичный прогон программы может выглядеть следующим образом: Note: enter -I to exit Enter nubmer to display 1101 Enter nubmer to display 100 Enter nubmer to display 1100100 Enter nubmer to display
the program. in binary: 13 in binary: 4 in binary: 100 in binary: -1
4.6 Дополнительные предложения выбора Для реализации требуемого алгоритма передачи управления в программе от одного предложения к другому достаточно предложений if и циклов while. Однако программы становятся более наглядными, если в них использовать более выразительные конструкции передачи управления. К таким конструкциям можно причислить предложения break и continue. Предложение break может быть использовано для внеочередного выхода из цикла, а предложение continue позволяет вне очереди начать выполнение следующей итерации цикла.
Предложение break Рассмотрим внутренний цикл в программе Primes.cs (пример 4.4). Как только мы находим делитель для переменной i, мы должны завершить цикл. Другой способ программирования такого цикла будет выглядеть следующим образом:
4.6 Дополнительные предложения выбора prime
=
true;
163
#*~
for (int j = 3; j*j <= 1; j +=2) { if (i % j == 0) { prime = false; break;
С одной стороны, такой цикл проще, так как заголовок цикла осуществляет только продвижение переменной j по последовательности значений. С другой стороны, структура управления ходом программы оказывается более сложной, потому что когда выполняется предложение break, цикл немедленно завершается и выполнение продолжается со следующего предложения после тела цикла. Внутри тела цикла может быть любое количество предложений break. Эти предложения можно использовать внутри циклов while, do-while и for, а также предложений switch (см. ниже) для выхода из этих конструкций.
Предложение continue Предложение continue является в некотором роде противоположностью предложению break. Если оно выполняется, то осуществляется немедленный возврат в начало цикла для выполнения следующей итерации. Если предложение continue выполняется внутри цикла while, то снова вычисляется условное предложение и, если результат оказывается равным true, тело цикла выполняется еще раз; в противном случае цикл завершается. Если предложение continue выполняется внутри цикла do-while, тело цикла выполняется снова. Если же оно выполняется внутри цикла for, то вслед за ним прежде всего выполняются действия по переходу к следующей итерации (третий компонент заголовка цикла), после чего выполняется вычисление условия и, если результат равен true, выполняется тело цикла в рамках следующей итерации. Ниже приведен простой фрагмент программы, в которым используются оба предложения, и break, и continue. В цикле for осуществляется ввод с клавиатуры по одному символу до тех пор, пока не будет введена пустая строка или символ завершения. После завершения ввода программа сообщает, сколько было введено символов и строк. int for
numChars = 0, numLines = 0, l i n e L e n = 0; ( i n t k = Console.Read() ; k ! = - l ; k = Console.Read()) char с = (char)k; numChars++; if (c ==' ' || с == ' \ t ' ) continue; if (c == ' \ n ' ) { numLines++; i f ( l i n e L e n == 0) b r e a k ; l i n e L e n = 0; } else
Предложение switch Предложением if удобно пользоваться в тех случаях, когда в программе может возникнуть лишь несколько ситуаций, приводящих к различающимся действиям. В таких случаях мы можем просто написать последовательность предложений if, образующих каскад, как это показано в следующем примере: //Анализ команды выбора того или иного действия по редактированию if (с — 'X') DeleteMode();//Удаление else if (с == ' I ' ) InsertMode();//Вставка e l s e if (с - - ' R ' ) ReplaceMode();//Замена else Console.WriteLine("Control code {0} unknown", с);//Код режима //неопознан
Когда число отрабатываемых ситуаций велико, программирование в таком стиле становится утомительным и не очень наглядным. Оно также приводит к неэффективной программе, так как программа должна выполнять последовательный анализ всех условных предложений, пока не дойдет до положительного результата. Предложение switch предоставляет удобный способ программирования последовательности проверок различных условий, отличающийся краткостью и большей эффективностью, чем каскад предложений if. Приведенный выше фрагмент можно написать с помощью предложения switch таким образом: //Анализ команды выбора того или иного действия по редактированию switch (с) { case 'X' : DeleteMode(); break; case ' I ' : InsertMode(); break; case 'R' : ReplaceMode(); break; default: Console.WriteLine("Control code {0} unknown", c) ;
4.6 Дополнительные предложения выбора
165
Формат предложения switch приведен ниже. Предложение switch switch (выбор) { case метка1: действие! break; case метка2: действие2 break; default: действия по умолчанию break; Если значение выражения выбор равно значению метка1, то выполняются предложения, обозначенные как действие 1 (ветвь выбора с меткой метка1); в противном случае если выражение выбор равно метка2, то выполняются предложения, обозначенные как действие2 и т. д. Если выражение выбор не равно ни одной из указанных меток, выполняются предложения, следующие за ключевым словом "default".
Правила использования предложения switch заключаются в следующем. •
Выражение выбора (которое указывается в скобках после ключевого слова switch) должно после вычисления давать значение типа int, bool, char или string.
•
Каждое значение, используемое в качестве метки после ключевого слова case, должно быть совместимым с типом выражения выбора.
•
Одно и то же значение не может использоваться в качестве метки выбора более одного раза.
•
Метки выбора могут следовать в любом порядке.
•
Несколько меток выбора могут указывать на одно и то же действие.
•
Предложение после метки выбора всегда должно завершаться предложением break или каким-либо другим предложением (например, предложением return), которое передает управление за пределы предложения switch.
•
Предложение break, использованное внутри предложения switch, передает управление на предложение, непосредственно следующее за концом предложения switch.
•
Ветвь по умолчанию (помеченная ключевым словом default), выполняется, если выражение выбора не равно ни одной из указанных меток выбора.
166
•
Глава 4. Управление и массивы
Ветвь по умолчанию не является обязательной. Если ее нет, а выражение выбора не равно ни одной из указанных меток выбора, не выполняется никаких действий; выполнение программы продолжается с предложения, непосредственно следующего за концом предложения switch.
Пример 4.8. Преобразование латинских чисел Рассмотрим пример законченной программы, которая использует все возможности предложения switch. Программа преобразует латинские числа в десятичные; например, число XIV будет преобразовано в 14. Перед тем, как мы перейдем к программе, рассмотрим кратко латинские числа. Римляне, возможно, начали считать, используя систему счисления с основанием единица, поэтому единица писалась, как I, два как II, а три как III. Однако такая система по мере увеличения числа быстро становится слишком громоздкой, и большинству людей было бы трудно отличить, например, 12 от 13, если их записывать, как последовательность единиц. В результате римляне расширили свою систему, начав использовать дополнительные буквы: I для обозначения единицы, V для обозначения пяти, X для обозначения десяти, L для обозначения пятидесяти, С для обозначения сотни, D для обозначения пяти сотен, М для обозначения тысячи. Числа, находящиеся между этими узловыми точками, можно записывать с помощью комбинаций перечисленных букв. Так, XI обозначает одиннадцать, XII — двенадцать, XIII — тринадцать и т. д. Аналогично, XV — это пятнадцать, а XVI — шестнадцать. Когда значения букв должны складываться, как в случае числа XV, они должны указываться в нисходящем порядке своих значений. Поэтому запись VX неправильна. Для уменьшения длины латинских чисел неудобное число вроде XVIIII (которое представляет 19) можно записать в виде XIX. В этом случае действует следующее правило: единичное включение буквенного обозначения числа должно вычитаться из последующего числа, если последующее число больше предыдущего. При этом мы должны использовать такое вычитание в самой последней позиции, так что XIX является правильным числом, a IXX — неправильным. Учитывая все эти правила, мы имеем:
4.6 Дополнительные предложения выбора
1(37
VIII — это 8, XXVI — это 26, XXXIV — это 34, XLI — это 41, MCMLXXXIX — это 1989, MMIV — это 2004. Программа Roman2Arabic многократно запрашивает у пользователя ввод латинского числа и затем выводит его эквивалент в десятичном виде. Программа не сообщает об ошибке, если латинские цифры появляются в неправильном порядке. Файл Roman2Arabic.cs using System; class Roman2Arabic { ///<summary> ///Программа Roman2Arabic
Bishop & Horspool May 2002
///Преобразует в десятичную систему и выводит на экран числа, ///вводимые пользователем в латинской системе счисления ///Выполняется лишь минимальная проверка правильности ///вводимых латинских чисел ///Демонстрирует использование предложений switch /// void Arabic(string s) { int units, tens, hundreds, thousands; units = tens = hundreds = thousands = 0; for (int = 0; i < s. Length; i++) { char с = s[i]; switch (c) { case ' I' : case ' i' : units++; break; case ' V : case ' v' : units = 5 - units; break; case ' X' : case ' x' : tens += 10 - units; units = 0; break; case ' I/ : case ' 1' : tens = 50 - tens - units; units = 0; break; case ' С : case ' c' : hundreds += 100 - tens - units; tens = units =0;
168
Глава 4. Управление и массивы break; case ' D' : case ' d' : hundreds • 500 - hundreds - tens - units; tens = units =0; break; case ' M' : case ' m' : thousands += 1000 - hundreds - tens - units; hundreds = tens = units = 0; break; default: Console.WriteLine( "Error: {0} is not a Roman digit", c) ; break;
return thousands + hundreds + tens + units; } void Go() { for ( ; ; ) { Console.Write ( "Enter Roman number (or empty line to exit) : ") ; string s = Console.ReadLine() ; s = s .Trim() ; if (s. Length == 0) break; Console.WriteLine ( "{0} is the Roman notation for {1}", s, Arabic (s.))
static void Main() { new Roman2Arabic ().Go();
Обратите внимание на то, что конструкции case 'метка1 используются парами, 'X' вместе с 'х' и т. д. Таким простым способом м ы расширили программу на случаи использования как строчных, так и прописных латинских букв. После запуска программы вы получите результат вроде следующего: Enter Roman XIX is the Enter Roman XIX is the Enter Roman XIX is the Enter Roman XIX is the Enter Roman
number (or empty line to Roman notation for 19 number (or empty line to Roman notation for 1900 number (or empty line to Roman notation for 1919 number (or empty line to Roman notation for 1977 number (or empty line to
4.7 Проект 2. Игра «Камень—ножницы—бумага» Введение В игру «Камень—ножницы—бумага» обычно играют два человека в несколько раундов. В каждом раунде оба игрока прячут руки за спиной. Затем, по счету три, оба игрока быстро выбрасывают руки вперед. Если рука сжата в кулак, она обозначает камень. Если выбрасывается ладонь со сжатыми вместе пальцами, это обозначает бумагу. Наконец, если указательный и средний палец образуют букву V, это обозначает ножницы. Если оба игрока выбрасывают одинаковые фигуры, раунд считается закончившимся вничью. В противном случае ножницы побеждают бумагу (потому что ножницы могут резать бумагу), бумага побеждает камень (потому что лист бумаги можно обернуть вокруг камня), а камень побеждает ножницы (потому что ударив камнем по ножницам, можно их повредить). Введя слова «rock paper scissors» в поисковой машине Web, вы найдете многие тысячи Web-сайтов посвященных этой игре . (Если даже вы не относитесь к этой игре серьезно, многие не разделяют вашего мнения.) В табл. 4.5 приведена матрица правил этой игры, где +1 обозначает победу игрока А, - 1 победу игрока Б, а 0 — ничью. Таблица 4.5. Возможные исходы игры «Камень—ножницы—бумага» Фигура игрока В Rock [Камень]
Фигура игрока А
Paper [Бумага]
Scissors [Ножницы]
Rock
0
-1
1
Paper
1
0
-1
Scissors
-1
1
0
Когда в эту игру играют два человека, она является борьбой психологии — каждый игрок старается предугадать, какую фигуру выкинет его соперник. Когда человек играет с компьютером, стратегия уж точно не основана на психологии, особенно, если игроку известен исходный текст программы. Наша программа, чтобы затруднить предугадывание, использует случайные числа.
Структура программы Программа разбита на два класса, сохраняемые в двух разных файлах. Один, класс DriveRPSGameConsole, отвечает за все взаимодействия с пользователем. Он также ведет учет для игрока-человека числа выигранОсобенно рекомендуем сайт http://www.worldrps.com
170
Глава 4. Управление и массивы
ных, проигранных и ничейных раундов. Другой класс, RPSGame, фактически ведет игру — выбирает фигуру, выбрасываемую компьютером, и, получив информацию о фигуре оппонента, сообщает, кто выиграл данный раунд. Исходный текст программы приведен ниже. Управляющая структура программы DriveRPSGameConsole использует внешний цикл, который в каждой итерации запрашивает у пользователя его фигуру и отвечает выбором своей. Для удобства пользователя задание фигуры осуществляется вводом одной буквы: «г» для камня, «р» для бумаги и «s» для ножниц. Для завершения игры пользователь должен ввести «q» (сокращение от quit). Разделение программы на два класса потребовало добавления ключевого слова public [открытый] к конструктору и двум методам класса RPSGame. Без ключевого слова public и конструктор, и оба метода были бы по умолчанию закрытыми (private). Другими словами, они были бы скрыты и их нельзя было бы вызвать из другого класса DriveRPSGameConsole. В противоположность этому, метод, названный Result, вызывается только из фрагмента внутри класса и, следовательно, не нуждается в объявлении его открытым. Подробное обсуждение всех модификаторов доступа С# проводится в гл. 9. Файл DriveRPSGameConsole.cs using System; c l a s s DriveRPSGameConsole ///<summary> ///Программа RPSGame
{ Bishop
& Horspool
August
2002
///Играет с пользователем в игру "Камень—ножницы—бумага", ///используя для ввода-вывода консоль /// void Go () { ///<summary> ///Управляет игрой "Камень—ножницы—бумага" /// RPSGame game = new RPSGame ( ) ; int noOfWins, noOfDraws, noOfLosses, round; noOfWins = noOfDraws = noOfLosses = round = 0; string computersChoice; string result; for ( ; ; ) {
computersChoice = game.ComputersChoice;//Ход компьютера string players.Choice = null; do { Console.Write( "Enter R (Rock), P (Paper, "+ "S (Scissors), or Q (Quit): " ) ; string b = Console.ReadLineO .ToLower () ;//Ход пользователя switch (b[0]) {
4.7 Проект 2. Игра «Камень—ножницы—бумага» case ' г' : playersChoice case ' p' : playersChoice case ' s' : playersChoice case ' q' : playersChoice
= "Quit"; break; } } while (playersChoice == null); if (players.Choice == "Quit") break; result = game.ComparePlays(playersChoice); round++; Console.WriteLine("Round "+round); Console.WriteLine("The computer's choice = "+computersChoice); Console.WriteLine ("The player's choice = "+playersChoice); switch (result) { case "draw": Console.WriteLine( " This round is drawn");//Ничья в этом раунде noOfDraws++; break; case "lose": Console.WriteLine( " Sorry,you lose this round");//Вы проиграли //этот раунд noOfLosses++; break; case ""win": Console.WriteLine( 11 Well done, you win this round");//Вы //выиграли этот раунд noOfWins++; break; } Console.WriteLine("Status: {0} wins, {1} draws,"+ "{2} losses", noOfWins, noOfDraws, noOfLosses);
static void Main() { new DriveRPSGameConsole() .Go ();
Второй файл называется RPSGame.cs и содержит программную реализацию класса RPSGame. В нем определяется свойство, обеспечивающее доступ к следующей фигуре, выбранной компьютером. Метод ComparePlay вычисляет и выводит результат раунда.
172
Глава 4. Управление и массивы
Файл RPSGame.cs using System; class RPSGame { ///<summary> ///Ведет учет состояния игры "Камень—ножницы—бумага" /// string[] MoveNames = {"Rock", "Paper", "Scissors"}; string computersChoice; Random r; public RPSGame() { r = new Random () ; public string ComputersChoice { get { //генерирует случайные числа 0, 1 или 2, преобразует //их в строку //и сохраняет для дальнейшего использования на этапе //сравнения computersChoice = MoveNames[r.Next(3) ]; return computersChoice; } public string ComparePlays(string playersChoice) { ///<summary> ///Определяет, выиграл ли игрок у компьютера ///Вызывает функцию Result, конструирующую сообщение ///Функция Result вызывается со следующими параметрами: /// — ход игрока /// - ход компьютера, при котором игрок выигрывает /// - ход компьютера, при котором выигрывает компьютер /// switch (playersChoice) { case "Rock" // игрок выигрывает проигрывает return Result("Rock" , "Scissors", "Paper"); case "Paper" : // игрок выигрывает проигрывает return Result("Paper", "Rock", "Scissors"); case "Scissors" : // игрок выигрывает проигрывает return Result("Scissors", "Paper", "Rock",); default: return null; string Result(string player, string Pwin, string Plose) { if computersChoice == Pwin) return "win"; else if computersChoice == Plose) return "lose"; else return "draw";
Основные понятия, рассмотренные в гл. 4
173
Если вы запустите программу, ее вывод может выглядеть примерно так: Enter R (Rock) , P (Paper) , S (Scissors) , or Q (Quit) : R Round 1 The computer's choice = Scissors The player's coice = Rock Well done, you win this round Status: 1 wins, 0 draws, 0 losses Enter R (Rock) , P (Paper) , S (Scissors) , or Q Round 2 The computer's choice = Rock The player's coice = Rock This round is drawn Status: 1 wins, 1 draws, 0 losses Enter R (Rock) , P (Paper) , S (Scissors) , or Q Round 3 The computer's choice = Paper The player's coice = Rock sorry, you lose this round Status: 1 wins, 1 draws, 1 losses Enter R (Rock) , P (Paper) , S (Scissors) , or Q
(Quit) : R
(Quit) : R
(Quit) : Q
Поскольку компьютер выбирает свои фигуры, используя генератор случайных чисел, доли выигрышей, проигрышей и ничьих должны составлять приблизительно по 3 3 % в достаточно долгом сеансе. Хотя интерфейс ввода весьма прост (что может быть проще для игрока, чем ввести одну букву в каждом раунде), выход программы нельзя назвать наглядным. Вариант этой игры, использующий графический интерфейс (GUI), представлен в гл. 5.
Основные понятия, рассмотренные в гл. 4 Понятия, рассмотренные в этой главе: массивы
индексация массивов
различные циклы
булевы выражения
предложение break
предложение continue
предложение if
предложение switch
цикл while
цикл do-while
тип bool
тип string
тип char
escape-последовательности и символы Unicode
Глава 4. Управление и массивы
174
Рассмотренные операторы: & &&
I!
Ключевые слова: for
while
do
if
else
break
continue
switch
case
default
foreach
char
string
bool
Синтаксические формы: операторы сравнения
булевы логические операторы
булевы условные операторы
предложение if
цикл while
цикл do-while
цикл for
объявление массива (упрощено)
цикл foreach для массива
объявление и использование типа string
класс string (сокращено)
объявление и использование типа char
символьные escapeпоследовательности (выборка) предложение switch
тип char (сокращено)
Контрольные вопросы 4.1. Если мы хотим установить булеву переменную freePass [бесплатный билет] для детей [children] до 15 лет или для студентов [student] до 25 лет (возраст содержится в переменной age), какое из приведенных ниже предложений будет правильным?
Контрольные вопросы
175
(а) freePass
= age < 15
(б) freePass
= age
(в) freePass (r) freePass
= age < 15 | | (age < 25 && s t u d e n t ) ; = student | | age < 15;
| |
age < 25
| |
student;
< 25 && student;
4.2. Какой из приведенных ниже фрагментов соответствует такому алгоритму: цикл повторяется до тех пор, пока пользователь на введет «q»? (а) bool more = false; while (!more) { ...предложения more = Console.ReadLine()
(в) bool quit = true; while (!quit) { ...предложения more = Console.ReadLine () ==
"q");
(r) bool more • true; while (more) { ...предложения more = Console.ReadLine()
"q");
!=
4.3. Какую последовательность значений выведет приведенный ниже фрагмент? int i while
= 17; (i !=
1)
{
Console.Write("{0}", i ) ; i = 3*i + 1; while (i%2 == 0) i /= 2;
(a) 17 13 5 1 (в) 17 13 5
(6) 17 15 13 11 9 7 5 3 1 (г) 17 13 5 4 1
4.4. Что важно включить в цикл с неоднозначным выходом? (а) предложение break (б) предложение if в конце цикла (в) предложение continue
(г) предложение return
176
Глава 4. Управление и массивы
4.5. Что выведут приведенные предложения? string s = "V'hello! \"\n" ; Console.WriteLine("{0},s.Length
(a) 9
(6) 8
(в) 6
(г) 12
4.6. Если массив объявлен следующим образом: DateTime[] myExamDays = {new DateTime(2003,11,4), new DateTime(2003,11,7), new DateTime(2003,11,12), new DateTime(2003,11,19)}
тогда день моего второго экзамена выражается: (a) myExamDays[2]
(б) myExamDays[I].Day
(в) myExamDays.Day[1]
(г) myExamDays[2].Day
4.7. Если предложение switch не включает выбор по умолчанию, результатом этого будет: (а) ошибка компиляции (б) если значение выражения выбора не соответствует ни одной из меток, то выбирается ветвь case с меткой, наиболее близкой по значению к значению выражения выбора (в) если значение выражения выбора не соответствует ни одной из меток, то выполнение продолжается с предложения, стоящего после конструкции switch (г) ошибка выполнения 4.8. Правильным является следующее предложение switch для задания числа дней в месяцах года: (a)
int
Daysln(int month) switch case
(month) 9:
{
{
case
4:
case
break; case
2:
return
break; else
return
31;
break; }
else }
return
31;
28;
6:
case
11:
return
30;
Контрольные вопросы (б) int
177
Daysln(int
month) {
switch (month) { case 9, 4, 6, 11: return break; case 2: return 28;
30;
break; default: return 31; break;
(в) int Daysln(int month) { switch (month) { case 9: case 4: case break; case 2: return 28; default: return 3 1 ;
(r) int Daysln(int month) { int days; switch (month) { case 9: case 4: case break; case 2: days = 28; break; else days = 3 1 ; break;
6: case
11: return 3 0 ;
6: case
11: days
= 30;
4.9. В программе ComputeGrade2 (пример 4.6) как наилучшим образом вывести два массива? Требуемый вывод должен выглядеть следующим образом: 90.0 85.0
А+ А
и т. д. (а) foreach
(double
mark
in boundary)
Console.WriteLine(mark+" (б) for
"+grade[mark]);
(int i=0; K b o u n d a r y . Length; i++) Console.WriteLine(boundary[i]+"
(в) foreach
(double
mark
"+grade[i]);
in boundary)
Console .WriteLine (mark+If
"+grade [boundary [mark] ] ) ;
178
Глава 4. Управление и массивы (г) fоreach
(double mark in boundary,
Console.WriteLine(mark+"
s t r i n g symbol in grade) "+symbol);
4.10. Если у нас есть объявление s=" by A A Milne ", и мы хотим вывести просто имя "A A Milne", какой из приведенных ниже фрагментов выполнит эту операцию? (а) s .TrimStart () . Substring (s . IndexOf ('A' ) ) . T r i m E n d O ; (б) s . Substring (s. IndexOf ('A' ) ) . Trim() ; (в) S.Substring(s.IndexOf('A')); (r) s.TrimStart () .Substring (s . IndexOf ('A') ,s. Length) .TrimEndO ;
Упражнения 4.1. Разнообразие циклов. Сравнительно легко преобразовать цикл for в эквивалентный ему цикл loop и наоборот. Приведенный ниже фрагмент должен вводить строки в массив и затем выводить их на экран: string s; int lineNum = 0; string[] line = new string[10]; for (s=Console.ReadLine(); s!=null; s=Console.ReadLine()) { line[lineNum] = s; lineNum++; for (int i = 0; i < lineNum; Console.WriteLine{"{0,4}: {1}", i+1, lineNum); }
Включите этот фрагмент в законченную программу и проверьте ее работу. После этого замените циклы for циклами while и убедитесь, что программа по-прежнему дает правильные результаты. Еще раз модифицируйте программу, использовав циклы do-while. 4.2. Любые города. Модифицируйте пример 2.7 (Определение времени с чтением), чтобы программа вводила данные до тех пор, пока пользователь не сигнализирует об окончании ввода. Придумайте, каким должен быть этот сигнал. 4.3. Гистограммы. Если у нас есть массив значений вроде того, что создавался в программе распределения оценок (пример 4.5) или схожий с массивом количества осадков (рис. 4.2), мы можем организовать более наглядный вывод, представляя значение в виде строки звездочек. Мы получим, например, следующее (для нескольких первых строк вывода): mark 0 1
Разработайте метод, который, получив в качестве параметра значение, выводит соответствующее количество звездочек. Включите этот метод в программу Frequencies. 4.4. Гистограмма количества осадков. Если пользоваться методом вывода из упражнения 4.3, то для получения разумного изображения иногда потребуется изменять масштаб данных при определении количества выводимых звездочек. Например, значения из рис. 4.2 можно удвоить перед тем, как передать их методу Histogram [гистограмма]. Разработайте способ определения необходимости и численных характеристик масштабирования и включите соответствующий фрагмент в программу вывода количества осадков. 4.5. Числа различных систем счисления. Модифицируйте программу ShowBinary (пример 4.7), чтобы она выводила число в любой системе счисления до 10 после ввода значения основания системы. 4.6. Шестнадцатеричные числа. Шестнадцатеричная система счисления, использующая основание 16, является общепринятой в вычислительной технике. В ней используются 16 цифр от 0 до 9, а далее буквы от А до F. Так, число 15 записывается как F, а 16 как 10. Модифицируйте программу ShowBinary, чтобы она выводила числа в шестнадцатеричной системе. Подсказка: для выбора старших цифр можно использовать предложение switch, или преобразование сначала из char в int, а затем из int в char. Отладьте оба варианта. 4.7. Преобразование арабских чисел в латинские. Значительно более интересной задачей, хотя и более простой в некоторых отношения, является преобразование арабских чисел (т. е. с основанием 10) в латинские. Принципы преобразования вы найдете в примерах 4.7 и 4.8. 4.8. Печать текста. Текстовые сообщения (например, службы телефонных сообщений) часто печатаются строчными буквами, но многие сотовые телефоны имеют встроенные средства преобразования в прописную первой буквы после такого символа пунктуации, как точка или знак вопроса. Составьте программу, которая будет вводить сообщение в переменную string (на одной строке), а затем обрабатывать его с получением новой строки с прописными буквами в соответствующих местах. 4.9. Символы валюты. Компания учитывает расходы своих служащих в долларах. После того, как они возвращаются из деловой поездки, они могут представить свои расходы в одной из следующих валют: • • • • • •
фунтах стерлингов, например, £2050; евро, например, €5196; канадских долларах, например, C$4987; долларах США, например, $5000; йенах, например, ¥200000; шведских кронах, например, 7000кг.
180
Глава 4. Управление и массивы
Составьте программу, которая читает одну из этих статей расходов и преобразует ее в доллары США. Курсы валют можно непосредственно записать в программу. Получите их с Web-сайта www.xe.net или другого схожего. Обратите внимание на обозначения денежных сумм в валютах, в которых символ валюты пишется после суммы. 4.10. Марафонский забег. В марафонском забеге участвуют пять основных соперников из Англии, Германии, Италии, Швеции и Норвегии. Организаторы забега получили следующую информацию о каждом бегуне: имя, страна, возраст, лучшее достигнутое им марафонское время (в часах и минутах). •
Разработайте класс, представляющий марафонского бегуна. Для записи лучшего достигнутого им времени используйте класс Time. Обычно марафонское время имеет порядок двух часов.
• Напишите небольшую программу, которая позволит проверить правильность создания вами пяти бегунов. •
Теперь напишите программу забега Race, которая выполнит следующие операции: создание бегунов, генерацию случайного финального времени в забеге для каждого бегуна, вывод на экран списка бегунов с указанием их характеристик вместе с финальным временем и лучшим достигнутым временем.
•
Определите победителя.
4.11. Худеющие приятели. Два приятеля решили попробовать похудеть за несколько недель. Первый из них вначале имел вес 100 кг и талию 98 см в окружности, второй — вес 85 кг и талию 95 см. Каждую неделю они записывают новые значения веса и окружности талии и определяют свои успехи по каждому из этих параметров. • Разработайте и запрограммируйте класс для записи хода похудания с данными о текущем весе и окружности талии. •
Протестируйте класс, создавая объекты для нескольких людей, вплоть до шести.
• Теперь составьте программу, которая отобразит четырехнедельный сеанс похудания. Каждую неделю генерируются случайные значения нового веса и окружности талии так, чтобы они грубо соответствовали предыдущим (например, текущий вес ± до 1.5 кг и окружность талии ± до 2 см). Выводите на экран новые значения и сохраняйте их снова в объектах, а также выводите данные о том, кто похудел больше в каждом случае. •
Ведите учет потерям веса и полноты, а в конце сеанса выведите суммарные потери для каждого участника.
•
Определите участника, потерявшего в весе больше других.
Упражнения
181
4.12. Голосование. Совет директоров состоит из трех членов, у каждого из которых имеется переключатель с двумя надписями: «за» и «против». При голосовании в случае большинства голосов «за» зажигается лампочка. Цепь, которая реализует включение лампочки, представляется булевым выражением L
= a & ( b | c )
| b & c
Составьте программу, которая выводит таблицу возможных значений «за» и «против» для участников a, b и с, а также соответствующие значения L. Для определения L разработайте метод Boolean. 4.13. Дни рождения. Вероятность того, что два человека в группе людей будут иметь один и тот же день рождения, вычисляется по следующей формуле: , . , pin) = 1
365 365
х
364 365
х
363 365
х ... х
364 - п + 1 365
Составьте программу, которая оценивает и выводит эту вероятность для групп от 2 до 60 человек. Нарисуйте таблицу значений пжр(п) для значений п от 10 до 50. Если вы справились с упражнением 4.3, выведите значения для 10, 20, ..., 50 в виде гистограммы. 4.14. Ряд Фибоначчи состоит из последовательности чисел, в которых каждое следующее число представляет собой сумму двух предыдущих, например, 1
1
2
3
5
8
13
21
34
5 5
...
Составьте программу для вывода первых 50 членов ряда Фибоначчи. Используя вложенный цикл for, модифицируйте программу так, чтобы она выводила только каждое третье число. Что можно заметить в этих числах? 4.15. Телефонные компании. Усовершенствуйте проект из гл. 3, включив в него данные об интервалах времени, в течение которых действует льгота, а для каждого телефонного разговора — время его начала (для представления времени можете воспользоваться структурой Time из примера 3.2). Замените генерацию случайного числа, указывающего, является ли разговор льготным, на анализ вхождения времени начала разговора в льготный интервал. Завершите программу выводом на экран заключения о том, какая компания в данном сеансе моделирования предоставляет более выгодные условия.
Графические интерфейсы пользователя с применением системы Views
5
В обсуждавшихся выше программных примерах мы использовали текстовый ввод и вывод. В этой главе мы познакомимся с тем, как создавать на экране окна (формы) Windows, посредством которых пользователь может взаимодействовать с программой. Это взаимодействие реализуется с помощью многочисленных стандартных элементов управления, таких как кнопки и окна редактирования, и включает даже средства вывода изображений. Взаимодействие осуществляется через настраиваемое пользователем пространство имен Views, написанное на языке С# и доступное читателю этой книги. Views использует язык XML, сходный с HTML, для определения содержимого формы Windows. Взаимодействие с формой во время выполнения требует использования в программе различных управляющих структур, в частности, циклов и предложений switch, которые были рассмотрены в гл. 4.
5.1 Графические интерфейсы пользователя Программы, рассматриваемые в предыдущих главах этой книги, управлялись текстовым вводом, получаемым либо с клавиатуры, либо из текстового файла, и выводили результаты своей работы тоже в виде текста — либо в файл, либо в консольное окно на экране. Хотя такая форма ввода-вывода вполне достаточна при проведении работ по настройке компьютера, однако для обычного компьютерного пользователя вывод в виде текста представляется малопривлекательным. И не следует рассматривать этот вопрос только с эстетической точки зрения. Можно привести много примеров, когда выбор вариантов с помощью мыши будет значительно удобнее для пользователя, чем альтернативный ввод путем набора текста на клавиатуре. Итак, мы хотели бы, чтобы наши программы создавали на экране окна для взаимодействия с пользователем, и чтобы внешний вид наших программ был бы столь же приятным, как у коммерческих продуктов. На языке С# вполне можно написать программу, которая будет работать не в консольном текстовом окне, а создаст собственное окно, способное выводить различные сообщения, и содержащее различные элементы управления: кнопки, по которым пользователь сможет щелкать мышью,
5.1 Графические интерфейсы пользователя
183
текстовые поля, в которые можно вводить требуемый текст и проч. Такого рода окно, содержащее нетекстовые элементы для выбора режимов и отображения другой информации, и взаимодействие с которым осуществляется главным образом посредством курсора, перемещаемого с помощью движения мыши, называется графическим интерфейсом пользователя, или, для краткости, GUI (от Graphical User Interface) (произносится gooey). Сопрограмма, выполняемая на компьютере с платформой Windows, может использовать классы из пространства имен System.Windows. Forms для отображения на экране элементов GUI и для управления вводом от мыши и клавиатуры. На рис. 5.1 показана простая форма GUI, созданная таким образом.
Рис. 5 . 1 . Простой графический интерфейс, созданный с помощью форм Windows
Возможности этой формы совершенно очевидны: в окно выводится изображение, а две кнопки используются для показа (Show) и сокрытия (Hide) изображения. Пример был разработан с помощью средства Windows, называемого Visual Studio; программа содержала 124 строки, из которых 15 были написаны программистом, а остальные сгенерированы средой Visual Studio. Обучение программированию с использованием классов и различных инструментальных средств является непростым делом в силу большого количества классов, каждого с солидным набором методов и свойств, которые к тому же взаимодействуют друг с другом неочевидным образом. В этой книге мы знакомим читателя прежде всего с языком С#. Поэтому мы стараемся описывать программные примеры и пространства имен С# по возможности независимым от Windows (операционной сие-
184
Глава 5. Графические интерфейсы с применением системы Views
темы) образом. При таком подходе мы можем уделить внимание также проектированию GUI — т. е. наилучшему расположению элементов управления. Например, на рис. 5.1 кнопки расположены с левой стороны окна, хотя внешний вид кадра был бы лучше, если бы кнопки были помещены над изображением в центре кадра. Чтобы не быть зависимым от платформы и одновременно обеспечить простые средства экспериментирования с GUI, мы сначала разработаем программы, которые будут создавать графический интерфейс для ввода-вывода на основе пространства имен Views, которое было специально разработано для этой книги. Это пространство имен можно переписать по сети как в форме исходных текстов, так и в откомпилированной форме и использовать затем в операционной системе Windows. Файлы находятся на Web-сайте, созданном для поддержки этой книги. Справочные данные по Views приводятся в Приложении Е. Система Views соотносится с формами Windows в том смысле, что она использует в точности те же термины и понятия, что и класс System. Windows.Forms. Будучи, однако, подмножеством, Views обеспечивает более удобные средства работы с GUI. В результате то, чему мы научимся, изучая Views, можно будет перенести на формы Windows без всякого труда.
5.2 Элементы GUI Давайте сначала посмотрим на GUI в целом. Мы затронем следующие термины: •
управление;
• форма (т. е. форма Windows); • линейка меню; • планировка интерфейса; •
взаимодействие.
Графический интерфейс пользователя представляет собой окно на экране, содержащее ряд различных компонентов, или элементов управления. В качестве элементов управления могут выступать, например, надписи, кнопки и текстовые поля. В программном отношении графический интерфейс реализуется в виде формы Windows. Интерфейс чаще всего имеет в верхней части формы строку меню с именем программы или окна, а также несколько кнопок. Обычно таких кнопок три — для свертывания окна (со знаком минус), для его развертывания и изменения размеров (два перекрывающихся прямоугольника) и для уничтожения (крестик). Расположение этих кнопок зависит от платформы; например, на рис. 5.1, который получен в системе Windows, кнопки размещены в правой части строки меню.
5.2 Элементы GUI
185
Что именно расположено под строкой меню, всецело определяется программой, создавшей это окно. Говорят, что элементы управления распланированы определенным образом относительно друг друга и границ окна. Выбор как самих элементов управления, так и их планировки определяется программистом. После того, как мы рассмотрим пример и познакомимся поближе с некоторыми компонентами, мы вернемся к вопросу о планировке интерфейса. Получив интерфейс с элементами управления, мы взаимодействуем с некоторыми из них с помощью мыши и клавиатуры. Поместив курсор мыши на такой элемент управления, как кнопка, и нажав клавишу мыши («щелкнув» по экранной кнопке), мы можем инициировать какие-либо действия. В другом случае мы можем находиться в элементе управления, который позволяет вводить текст с клавиатуры с отображением его в элементе управления. Другие элементы управления определяют области для выводимых данных, которые, как мы уже видели, могут иметь текстовую или графическую форму.
Введение в простые элементы управления Составление и использование игровых компьютерных программ — весьма увлекательное занятие, и мы теперь подошли к такому этапу, когда можем создать для игры удобный графический интерфейс. На рис. 5.2 показано окно GUI для рассмотренной ранее игры «Камень—ножницы—бумага» (см. проект 2 в гл. 4). С правилами игры мы уже знакомы; здесь мы рассмотрим процесс создания для нее графического интерфейса. Прежде всего рассмотрим внимательно рис. 5.2. Область окна под строкой меню содержит разнообразные элементы управления, которые можно сгруппировать следующим образом. Кнопки. В нижней части окна расположены три элемента управления типа Button [кнопка] ; на один из них, с надписью «Scissors», указывает стрелка. Пользователь может помесить курсор мыши на кнопку и щелкнуть по ней. Это действие фиксируется операционной системой, которая, в свою очередь, сообщает программе о событии. В окне имеются еще две управляющие кнопки, названные «Rock» и «Paper», которые точно также посылают в программу события. Надписи. Некоторые элементы управления являются просто строками текста; например тот, где написано «Choose your selection...» [Сделайте свой выбор...]. Эти элементы управления относятся к типу Label [надпись] и являются пассивными — если пользователь помещает на них курсор мыши и щелкает по ним, или вводит что-то с клавиатуры, ничего не происходит. Список. В правой части окна имеется большой прямоугольник, в который программа выводит данные о выборе пользователя и последующих собы-
Глава 5. Графические интерфейсы с применением системы Views
186
СПИСОК
надпись
| Bound 1 I The computer's choice • Paper j The player's choice • Paper 1 This round is drawn окно ввода текста
I Round 2 j The computer's choice • Scissors I The player's choice • Rock I Wei! done, you win this round I I Round 3 j T he computer's choice • Paper j The player's choice = Paper 1 This round is drawn
кнопка
Рис. 5.2. Графический интерфейс для игры «Камень—ножницы—бумага
тиях. Этот элемент управления содержит вертикальный список строк и носит название ListBox [список] . Текстовое поле. На рис. 5.2 мы видим три прямоугольные белые рамки, содержащие числа; эти элементы управления представляют собой объекты типа TextBox [текстовое поле]. В игре «Камень-ножницы-бумага» они используются для вывода некоторых результатов игры: в них отображается суммарное число выигрышей, проигрышей и ничьих для игрока. В дальнейшем везде, где это возможно, мы используем обычные слова (кнопка, окно ввода текста) для обозначения элементов управления. Если же речь будет идти о конкретных экземплярах объектов С#, мы будем пользоваться программными обозначениями (Button, TextBox).
5.2 Элементы GUI
187
Программу можно написать таким образом, что она будет принимать данные, вводимые пользователем в этих полях, но в рассматриваемой программе эта возможность не реализована. Программа, создающая графический интерфейс, должна указать, какие ей требуются элементы управления, какой они должны быть величины и в каких местах окна они будут расположены. Эти детали являются обязательной составляющей разработки интерфейса, и явное указание их в программе может оказаться весьма утомительным делом. Если rockButton является экземпляром класса, который создает и выводит изображение кнопки, нам хотелось бы избежать включения в программу предложений вроде rockButton.Location
= new System.Drawing.Point(55, 275);
чтобы поместить кнопку на расстоянии 55 пикселов от левого края и 275 пикселов от верхнего края окна. Такое планирование интерфейса отнимет массу времени, будет приводить к ошибкам и потребует многочисленных пробных прогонов.
Планирование GUI Разрабатывая графический интерфейс, мы можем просто добавлять в окно элементы управления по мере того, как в них возникает нужда, однако такой подход не даст удовлетворительных результатов. Интерфейс будет аккуратно выглядеть лишь в том случае, если схожие элементы управления будут сгруппированы вместе. Например, все кнопки естественно расположить в верхней части окна или, наоборот, в нижней. Мы можем разделить экран пополам, расположив окна ввода с левой стороны, а окна вывода с правой и т. д. Различные варианты планировки интерфейса будут проиллюстрированы в последующих примерах. Вопрос заключается в том, каким образом мы будем манипулировать элементами интерфейса? Здесь имеются три возможности. «Перетащить и оставить». Этот способ предполагает наличие специального инструментального средства, с помощью которого составляется текст программы. Мы перетаскиваем мышью требуемый элемент управления из списка, в котором перечислены все возможности, и оставляем его на том месте окна, где, на наш взгляд, он должен быть. В дальнейшем мы можем изменить расположение элементов управления, перетаскивая их мышью по экрану, и так же просто изменять их размеры. Такие инструментальные средства представляют собой весьма сложные программы, занимающие в компьютере много места. Примером такой среды является Visual Studio, пакет, разработанный специально для С# и других языков на платформе Windows. Visual Studio обеспечивает массу возможностей, и пользоваться им очень непросто, если вы только начинаете изучать программирование. Visual Studio генерирует програм-
188
Глава 5. Графические интерфейсы с применением системы Views
мный код, использующий различные средства повышенной сложности. Однако есть и другие, более «легковесные» инструментальные среды, которые, к тому же, обладают тем преимуществом, что не зависят от платформы. Одну из них можно использовать и для Views, о чем речь будет идти ниже. Абсолютное позиционирование. Не используя метод «Перетащить и оставить», мы можем включать в программу вызовы методов форм Windows, которые с точностью до пиксела позволят расположить в окне требуемые элементы управления. Типичный экран дисплея содержит пикселы, нумеруемые от 0 до по меньшей мере 800 по горизонтальной оси х и от 0 до по меньшей мере 600 по вертикальной оси у, причем оси расположены так, как это показано на рис. 5.3. Там же изображена кнопка, которая будет расположена в позиции х=300, z/=150, т. е. чуть меньше, чем на полдороги по каждой оси.
о0 300Д50
О Рис. 5.3. Оси координат на компьютерном экране
Программные строки, помещающие кнопку в позицию #=300, г/=150, будут выглядеть приблизительно так: b u t t o n = new B u t t o n ( ) ; b u t t o n . L o c a t i o n = new P o i n t ( 3 0 0 ,
150);
что само по себе не сложно для понимания, но потребует тщательного выбора координат для каждого элемента управления с учетом их размеров и будет довольно трудоемким и скучным делом. Другая сложность заключается в том, что компьютерный экран может быть и больше, чем 600x800, и в этом случае абсолютное позиционирование надо выполнять очень тщательно, чтобы результаты выглядели одинаково удовлетворительно на различных компьютерах. Относительное позиционирование. Если мы позволим располагать элементы управления самой системе, передав ей некоторые базовые указания, дело окажется более простым. При таком подходе обычно создаются горизонтальный и вертикальный списки, вложенные друг в друга, как вам заблагорассудится. Например, интерфейс рис. 5.1 соответствует диаграмме, приведенной на рис. 5.4.
5,3 Введение в систему Views
189
два элемента горизонтального списка
два элемента вертикального •* списка
Рис. 5.4. Относительная планировка элементов управления
Спецификация подобной планировки будет примерно следующей (не будем обращать внимание на синтаксис) вертикальная горизонтальная кнопка кнопка конец горизонтальной изображение конец вертикальной Другими словами, мы начинаем вертикальный список. Первым элементом выступает горизонтальный список, состоящий из двух кнопок. Этот элемент завершается; за ним следует изображение, после чего завершается и вертикальный список. Пространство имен Views, используемое в этой книге, рассчитано на относительную планировку, однако имеет и некоторые возможности абсолютной планировки для более точного позиционирования. Разрабатывать интерфейс с помощью Views гораздо проще, чем посредством Visual Studio, и, тем не менее, Views предоставляет достаточные возможности для управления внешним видом и функционированием интерфейса.
5.3 Введение в систему Views Программирование графического интерфейса с помощью Views включает следующие шаги: 1. Задание расположения элементов управления. 2. Создание объекта графического интерфейса. 3. Обеспечение реакции на действия над элементами управления. 4. Взаимодействие с элементами управления. С точки зрения графического интерфейса под реакцией понимается отклик программы на такие действия пользователя («события»), как щелчок по кнопке. Взаимодействие — это более широкий термин; он, в част-
190
Глава 5. Графические интерфейсы с применением системы Views
ности, подразумевает, что мы можем помещать что-то в элемент управления, например, вводить текст в текстовое поле.
Задание расположения элементов управления В системе Views спецификация планировки выполняется в XML. Эта аббревиатура является сокращением от extensible Markup Language (расширенный язык разметки); синтаксис XML основан на HTML (HyperText Markup Language, язык разметки гипертекста), который используется для задания внешнего вида и содержимого Web-страниц. Однако язык XML имеет более упорядоченную структуру и допускает расширения, что делает его чрезвычайно удобным средством для решения самых разнообразных задач. Система обозначений XML основана на тегах и атрибутах. Каждый тег имеет имя и заключается в квадратные скобки или завершается соответствующим ему тегом закрытия. Теги закрытия предваряются символом наклонной черты (деления). Атрибуты являются идентификаторами с присвоенными им значениями. В каждом домене, к которому применяется XML, определяется набор тегов и атрибутов. Так, для Views мы определим набор из приблизительно 15 тегов, каждый с 5-6 необязательными атрибутами. Элементы обозначений XML, используемых в системе Views, показаны в приводимой ниже форме. Обозначения XML для Views < тег
атрибуты>
другие теги тег > или • < тел а трибуты /> где атрибуты — это список определений атрибутов, записанный в виде идентификатор = значение Теги и идентификаторы могут содержать только буквы; значения атрибутов могут принимать разнообразные формы (числа, строки и т. д., как того требует конкретный атрибут). Если заданный тег идентифицирует элемент управления, который может быть отображен на экране средствами Views и используется интерактивно, тогда для него требуется атрибут Name.
Рассмотрим некоторые теги Views XML, которые могут быть полезны для программы Show-Hide, иллюстрируемой рисунками 5.1 и 5.4. <Button Name=Show/> <Button Name=Hide/>
5.3 Введение в систему Views
191
В этом примере — это тег без атрибутов, который содержит другие теги, поэтому его закрывающий тег указан на отдельной строке с повторением имени тега. Теги для кнопок просты, но поскольку они идентифицируют интерактивные элементы управления в графической интерфейсной форме Windows, они должны определять атрибут Name. В качестве пояснения укажем, что спецификация <Button
Name=Show/>
эквивалентна следующей: <Button
Name=Show>
Заметьте, что теги открытия и закрытия в нашем примере должным образом вложены. При обнаружении несоответствия Views сообщит об ошибке. Другим примером из того же интерфейса будет предложение
Name=pic Image='Jacarandas.jpg'
Height=4cm/>
Здесь атрибутов больше, для имени файла с изображением и для задания высоты. Если мы указываем либо высоту, либо ширину для отображения рисунка, Views автоматически настраивает другое измерение, что очень удобно. Дополнительные детали измерений в атрибутах Views будут затронуты в разделе 5.5.
Создание объекта графического интерфейса Язык XML для GUI системы Views ничего на значит для самого С#. Он имеет смысл только для определенного нами класса Views.Form. Следовательно, для создания GUI мы должны создать объект Views.Form и передать его XML в качестве инициализирующего параметра. В приведенной ниже форме подытожен этот процесс. Создание объекта Views.Form Views.Form
GUIname
спецификация
- new Views.Form(@'
Views XML
Во время выполнения создается объект с именем GUIname. Символ @ начинает многострочный текст, состоящий из спецификаций Views XML.
В любой программе мы можем иметь несколько выполняемых графических интерфейсов, как один после другого, так и действующих одновременно. Например, с помощью одного интерфейса пользователь входит в систему, после чего активизируется другой интерфейс, организующий содержательное взаимодействие с программой. Строка спецификации фор-
192
Глава 5. Графические интерфейсы с применением системы Views
мы Views не обязательно должна указываться непосредственно в качестве параметра конструктора Views.Form; она может быть инициализирована или прочитана из файла в другом месте программы. Например, вполне правильный фрагмент создания GUI может выглядеть таким образом: string spec = @""; Views.Form f = new Views.Form(spec);
Спецификацию можно сохранить и в отдельном файле. В этом случае аргументом конструктора будет имя файла. (Хотя в обоих случаях аргумент будет представлять собой строку, конструктор легко отличит спецификацию XML от имени файла.)
Правила использования в Views прописных букв и кавычек Класс Views.Form не различает строчные и прописные буквы в именах элементов управления; слово «Button» вполне можно написать, как «bUtTon», хотя это вряд ли разумно. Текст «Show», однако, появится в качестве надписи на элементе управления точно с теми же буквами, которые использованы в спецификации. Хотя Views игнорирует форму написания имен элементов управления, мы тщательно следили в этой главе, чтобы имена тегов в точности соответствовали (в плане использования прописных и строчных букв) соответствующим именам классов в пространстве имен System.Windows. Form. Аналогично, имена атрибутов в точности совпадают с именами свойств этих классов. Теги без прописных букв (например, ) не соответствуют именам классов Windows, и то же справедливо для имен атрибутов, написанных строчными буквами (мы с ними столкнемся позже). Заметьте, что кавычки вокруг названий кнопок могут опускаться, если название не включает в себя пробелы или специальные символы. Это правило специально введено в Views.Form для удобства программиста. В стандартном языке XML двойные кавычки обязательно ставятся вокруг названий и любых других текстовых значений; включение символов двойных кавычек внутрь строк С# менее удобно, чем использование символов одиночных кавычек (и уж точно менее удобно, чем полное отсутствие кавычек).
Обеспечение реакции на действия над элементами управления Создание экземпляра класса Views.Form приводит к появлению на экране компьютера изображения графического интерфейса пользователя. После этого программа может ожидать, пока пользователь не сделает что-нибудь, например, щелкнет по кнопке или введет какой-то текст. Эти воздействия обнаруживаются операционной системой и передаются той части
5.3 Введение в систему Views
193
программы, которой они предназначены, в данном случае созданному нами объекту Views.Form. Часть программы, которая принимает и обрабатывает внешнее воздействие, носит общее название обработчика события. Обработчики событий обычно представляют собой методы, которым передается в качестве параметра имя конкретного активизированного элемента управления. Далее обработчик может решить, как он должен отозваться на это воздействие. Активные элементы управления вроде кнопок несомненно должны иметь собственные обработчики событий, в то время как потенциально пассивные, например, надписи или изображения, в обработчиках событий не нуждаются. В системе Views мы ждем поступления события в цикле ожидания события, основанном на специальном методе с именем GetControl. Этот метод передает в программу имя активизированного элемента управления, после чего происходит переход в обработчик события. Стиль программирования показан в приведенной ниже форме. Взаимодействие с элементами управления в системе Views void Go () { SetUp (); //Создание и инициализация GUI for
case "controll": предложения; break; case "control2": предложения; break; и т. д . default: break;
f.GetControl возвращает имя активизированного элемента управления. Щелчок по кнопке закрытия программы в верхнем правом углу интерфейсного окна приводит к возврату из интерфейса значения null; это завершает цикл и закрывает программу. Метод ActionPerformed использует предложение switch для выяснения того, какой именно элемент управления был активизирован, и выполняет затем соответствующие предложения.
7—2047
194
Глава 5. Графические интерфейсы с применением системы Views
Взаимодействие с элементами управления Находясь в обработчике события, мы можем взаимодействовать с элементом управления двумя способами. Первый и предпочтительный способ заключается в вызове одного из методов Views, специально разработанных с этой целью. В разделе 5.6 мы подробно обсудим эти методы, но о двух из них мы скажем здесь, потому что они будут использоваться в последующих примерах. Это методы GetText и PutText, которые позволяют прочитать текст из текстового поля или из списка либо записать текст в эти элементы управления. Эти методы описаны в приведенной ниже форме. Взаимодействие с текстовыми полями и списками в системе Views s t r = GetText(идентификатор_элемента) PutText{идентификатор элемента, str) Для данного элемента управления с именем, хранящемся в виде строки в параметре идентификатор_элемента, GetText вернет строку, выведенную в данный момент элемент управления. PutText, наоборот, поместит в него данную строку. В случае элемента TextBox PutText затирает текущее значение; в случае элемента ListBox к содержимому списка добавляется следующая строчка и в нее заносится данная строка.
Например, в игре «Камень—ножницы—бумага», проиллюстрированной выше на рис. 5.2, мы выводим сообщение в список с именем «results» в конце каждого раунда. Одним из таких сообщений будет form.PutText("results",
"This round is
drawn");
Мы также выводим число в текстовое поле в левой части кадра с помощью следующего предложения: form.PutText("draws", noOfDraws.ToString());
Как мы увидим позже, текстовое поле имеет имя «draws»; целочисленная переменная noOf Draws ведет учет количества ничьих. Поскольку метод PutText принимает только строки, мы сначала преобразуем целое число в строку с помощью ToString. Поскольку элементы управления Views фактически являются действительными элементами управления, поддерживаемыми той частью операционной системы, которая рисует их на экране и следит за приходом от них сигналов в результате, например, щелчка мышью, второй способ взаимодействия с элементами управления заключается в непосредственной установке их атрибутов. Для этого вы должны знать, какие у них атрибуты, и как мы уже упоминали в начале этой главы, приобретение таких детальных знаний требует много времени, а их использование чревато ошибками. Поэтому можно рекомендовать оставаться, насколько это возможно, в мире Views. Однако технически возможно получить до-
5.3 Введение в систему Views
195
ступ к богатому набору классов, предоставляемому Windows.Forms или другими системами, поверх которых выполняется система Views (которые будут очень похожи); как это делается, будет описано в разделе 5.7. Пример 5.1. Игра «Камень—ножницы—бумага» с графическим интерфейсом Игра «Камень—ножницы—бумага» из гл. 4 предоставляет идеальную возможность изучения программирования графического интерфейса. Игры просто органически нуждаются в графических средствах. На рис. 5.2 мы уже видели, что именно нам хотелось бы получить. Но как мы этого достигли? Мы руководствовались последовательностью тех же шагов, которые были описаны выше. Рассмотрим их подробнее. Определение состава элементов управления. Для ввода трех возможных действий нужны три кнопки. На рис. 5.2 показаны кнопки с рисунками, и в дальнейшем мы увидим, как создаются такие кнопки. Однако можно было использовать и пустые кнопки. В качестве результатов мы решили выводить количество выигрышей, проигрышей и ничьих, отсюда и три текстовых поля. Для сообщений о ходе игры мы использовали список с надписью над ним. Разумное группирование элементов управления. Группирование в данном случае очевидно. Нужно собрать вместе все кнопки и все текстовые поля. В остальном расположение элементов может быть каким угодно. Составление эскиза графического интерфейса. Расположить наши элементы в окне интерфейса можно разными способами. На рис. 5.5 показаны два варианта, отличающиеся от рис. 5.2. C D C D C D )
C D C D i C D ;
: б
Рис. 5.5. Варианты интерфейса для игры «Камень—ножницы—бумага»
Перевод эскиза интерфейса в обозначения Views. Какой бы мы не выбрали формат, концепция вертикального и горизонтального списков остается в силе. Для расположения, показанного на рис. 5.5, а, спецификация будет выглядеть следующим образом:
196
Глава 5. Графические интерфейсы с применением системы Views
три кнопки список три текстовых поля Рассматривая эту спецификацию сверху, мы видим, что интерфейс состоит из вертикального списка с тремя элементами — горизонтального списка, элемента управления-списка и второго горизонтального списка. Каждый из двух горизонтальных списков содержит по три элемента. Другое расположение элементов, показанное на рис. 5.5, б, потребует такой спецификации: три кнопки список три текстовых поля Здесь имеется один горизонтальный ряд с тремя вертикальными элементами. Они представляют собой вертикальный список, элемент управления-список и второй вертикальный список. Если теперь вернуться к рис. 5.2, то расположение элементов на нем будет описываться следующ и м образом: три текстовых поля список три кнопки Приведенные варианты спецификаций показывают гибкость техники проектирования, предоставляемой системой Views, в том отношении, что размещение элементов управления отделяется от их программной реализации.
5.3 Введение в систему Views
197
Размещение элементов является, однако, лишь одной стороной разработки внешнего вида интерфейса. Мы должны также принять во внимание расстояния между элементами. Views выберет эти расстояния достаточно разумным образом, но если мы хотим вмешаться в этот процесс, мы можем ввести две настройки. Во-первых, каждый элемент управления имеет атрибуты Width [ширина] и Height [высота]. Эти величины могут быть заданы, в частности, в сантиметрах, и тогда типичный тег Views может быть описан таким образом:
Width=2cm/>
что сделает элемент больше, чем он был бы по умолчанию. Другой способ изменения расстояния между элементами заключается в использовании тега <space> [расстояние], который позволяет установить также атрибуты ширины и высоты для (обычно пустого) прямоугольника: <space Height=lcm/> Если мы зададим для высоты совсем маленькое значение, и установим черный цвет элемента, мы можем даже нарисовать линию: <space Width=10cm Height=0.1cm BackColor=Black halign=center/> Добавление обработчиков событий к активным элементам управления. Три элемента управления, которые активизируются пользователем, представляют собой кнопки. Поэтому метод ActionPerformed должен предоставить обработчик для каждой из этих кнопок. В сущности, требуемое действие во всех трех случаях одно и то же — записать имя нажатой кнопки. После этого программа должна поместить выводимые значения в текстовые поля, как уже отмечалось выше. Вызов PutText входит в раздел взаимодействия с интерфейсом. Программа написана в виде класса DriveRPSGameGUI, который замещает DriveRPSGameConsole.cs. Ее следует компилировать вместе с существующим классом RPSGame.cs из гл. 4. Кроме того, необходимо сообщить системе, что в программе будет использоваться пространство имен Views, что мы и делаем в самом начале программы, вместе с обычным предложением using System. Файл DriveRPSGameGUI.cs
using Views; using System; class DriveRPSGameGUI { ///<summary> ///Программа RPSGame с GUI iii
Bishop & Horspool May 2002
198
Глава 5. Графические интерфейсы с применением системы Views
///Играет с пользователем в игру "Камень—ножницы—бумага" ///Демонстрирует взаимодействие с GUI и использование switch ///при обработке строк ///Использует класс RPSGame из гл. 4 /// string fspec = @""; string playersChoice; void Go() { ///<summary> ///Управляет игрой "Камень—ножницы—бумага" /// RPSGame game = new RPSGame (); int noOfWins, noOfDraws, noOfLosses, round; Views.Form form = new Views.Form(fspec); noOfWins = noOfDraws = noOfLosses = 0; round = 0; form.PutText("drawBox", " 0 " ) ; form.PutText("winBox", " 0 " ) ; form.PutText("lossBox", " 0 " ) ; string computersChoice, c, result;
5.3 Введение в систему Views for
(
;
;
)
199
{
computersChoice = game.ComputersChoice; с = form.GetControl() ; if (с == null) break; ActionPerformed(с) ; //Устанавливает ход игрока result = game.ComparePlays(playersChoice); round++; form.PutText("history", "Round "+round); form.PutText("history", "The computer's choice = "+computersChoice); form.PutText("history", "The player's choice = "+playersChoice); switch (result) { case "draw": form.PutText("history"," This round is drawn"); noOfDraws++; form.PutText("drawBox", noOfDraws.ToString()); break; case "lose": form.PutText("history"," Sorry, you lose this round"); noOfLosses++; form.PutText("lossBox", noOfLosses.ToString() ) ; break; case ""win": form.PutText("history"," Well done, you win this round"); noOfWins++; form.PutText("winBox" , noOfWins.ToString() ) ; break; } form.PutText(history", " " ) ; } form.CloseGUI() ; } void ActionPerformed(string c) { switch (c) { case "Rock": case "Paper": case Scissors": playersChoice = c; break; default: throw new Exception("Unhandled control
" + c) ;
static void Main() { new DriverRPGGameGUI().Go();
Перед запуском этой программы ее следует откомпилировать вместе с уже знакомым нам файлом RPSGame.cs, а также с классами Views. При запуске выполнимого файла на экране компьютера появляется окно, схожее с тем, что изображено на рис. 5.2.
200
Глава 5. Графические интерфейсы с применением системы Views
Сравнивая текст DriveRPSGameGUI.cs с рис. 5.2, мы можем заметить, что строковая константа, используемая для инициализации fspec, содержит спецификацию, описывающую элементы управления. Конструктор класса Views.Form, вызываемый в методе Go, получает строку fspec в качестве своего аргумента и использует ее для определения состава и расположения элементов управления в окне интерфейса. Интерфейс игры «Камень—ножницы—бумага» содержит несколько примеров пар вертикальных и горизонтальных вложений. Ряд кнопок с изображениями трех вариантов руки игрока создается путем заключения их в пару ... , но эта группа сама является элементом вертикального списка. В интерфейсе имеются и другие вложенные группы. Создав объект Views.Form, метод Go входит в цикл и, вызывая метод GetControl, ожидает, когда пользователь щелкнет по кнопке. Программа отображает результаты своей работы выводом текста в одно из текстовых полей, а также в элемент управления-список. В гл. 7 мы рассмотрим дополнения к этой программе, которые сделают ее более интеллектуальной.
5.4 Планировка элементов с помощью системы Views В этом разделе мы рассмотрим более подробно возможности планировки элементов с помощью системы Views. Заметьте, что использованные нами теги не соответствуют элементам управления Windows.Form и поэтому, в соответствии с нашим соглашением, пишутся без прописных букв в именах.
Форма GUI Спецификация для всей формы GUI должна начинаться с тега . Эти два тега охватывают группу элементов управления, как будет объяснено ниже. Тег
Text='RPS
Game'>
...
Группы элементов управления Задать расположение групп элементов управления в Views можно тремя способами — с помощью вертикального или горизонтального списков, а также посредством абсолютного размещения элементов с указанием их координат. Вертикальные списки. Вертикальный список элементов управления использует пару тегов ... , окружающих спецификации элементов. В качестве простого примера предположим, что мы хо-
5.4 Планировка элементов с помощью системы Views
201
тим отобразить три кнопки различных размеров. Для этого можно использовать следующую спецификацию: @"";
Эта спецификация создает интерфейсную форму, изображенную на рис. 5.6, а. Vertical L .
Рис. 5.6. Вертикальный (а) и горизонтальный (б) списки элементов
Горизонтальные списки. Горизонтальный список формируется схожим образом, но с помощью пары тегов ... . Горизонтальный список изображен на рис. 5.6, б. Заметьте, что по умолчанию элементы управления вертикального списка выравниваются по левым сторонам, а элементы горизонтального списка выравниваются по верхним сторонам. Для задания способа горизонтального выравнивания элементов вертикального списка используется атрибут halign, который может принимать значения left [лево], right [право] или centre [центр] (или center для предпочитающих американское написание этого слова); вертикальное выравнивание элементов горизонтального списка осуществляется с помощью атрибута valign, принимающего значения top [верх], bottom [низ] и middle [середина]. Интервалы. Для образования интервалов между элементами, а также для вывода линий используется тег <space>. Величина интервала определяется значениями атрибутов Height [высота] и Width [ширина]. В последнем случае с помощью атрибута BackColor можно установить требуемый цвет линии.
202
Глава 5. Графические интерфейсы с применением системы Views
Абсолютное размещение. Абсолютное позиционирование элементов достигается с помощью панелей, которые будут описаны в конце этой главы. Шрифты Помимо возможности произвольно устанавливать расположение элементов управления, Views позволяет также изменять вид выводимых текстов, повышая тем самым наглядность и привлекательность интерфейсной формы. Вид отображаемых на экране букв носит название шрифта. Шрифты могут относиться к разным семействам, а также иметь различные стили и размеры. По умолчанию, Views выводит все тексты одним из шрифтов sans serif (например, Helvetica) с прямым начертанием букв и размером 10. Размер шрифта задается в пунктах: 8 пунктов соответствуют самому мелкому шрифту, а крупный шрифт размером 24 пункта и больше можно использовать, например, для заголовков. Для элементов управления с текстом, например, надписей и кнопок, можно указать атрибут Font [шрифт], значение которого состоит из различных сцепленных друг с другом дескрипторов (описателей) шрифта. Каждый дескриптор, за исключением размера шрифта, представляет собой слово или короткую аббревиатуру. Некоторые из этих слов перечислены в табл. 5.1, а полный перечень приведен в Приложении Е. Т а б л и ц а 5 . 1 . Характеристики шрифтов По умолчанию
Варианты
Семейство
SansSerif
Serif
Жирность
тесПит[средняя]
bold [жирный]
Стиль
upright [прямой]
italic [курсив]
Размер
10 pt
Monospace [равноширинный]
И м я Serif обозначает обобщенный тип шрифтов с засечками (serif), обычно Times Roman; SansSerif обычно обозначает шрифты Arial или Helvetica; Monospace — это обобщенный тип шрифтов с равной шириной всех символов, например, Courier. Размер шрифта задается указанием его высоты в пунктах в виде десятичного числа; число может содержать дробную часть. Дескрипторы шрифтов могут комбинироваться в любом порядке. Некоторые примеры показаны в табл. 5.2. Таблица 5.2. Примеры шрифтов
Описание шрифта Font=Boidi4
Пример Hello there
Pont=ItalicSansSerif 18 Font=courier
Hello t h e r e
5.5 Элементы управления Views
203
5.5 Элементы управления Views Views поддерживает многие элементы управления, включенные в Windows.Form. Упорядоченный по алфавиту список элементов управления представлен в табл. 5.3. Таблица 5.3. Элементы управления Views.Form Элемент управления Views. Form
Краткое описание
<Button/>
Нажимаемая кнопка
Кнопка с флажком, который можно поставить или убрать
Выпадающий список кнопок с флажками
Выпадающий список, из которого можно извлечь отдельный элемент в виде значения
Прямоугольная область, содержащая группу селективных кнопок